| """ |
| eval/grounded.py β Shared grounded matching primitives for Bureaucat. |
| |
| Provides the no-invention check used by app.py (to drive the verifying mascot |
| state, D2-05) and by eval/run_eval.py (for bake-off accuracy gating). |
| |
| Stdlib-only: only `re` and `unicodedata`. Importing this module never loads |
| model weights and never imports app. |
| """ |
|
|
| |
| |
| |
|
|
| import re |
| import unicodedata |
|
|
|
|
| |
| |
| |
| |
| |
| |
|
|
| _WHITESPACE_RE = re.compile(r"\s+") |
|
|
|
|
| def normalize(text: str) -> str: |
| """ |
| Collapse all Unicode whitespace to single ASCII space; NFC-normalize; lowercase. |
| |
| Preserves digits, commas, periods (Swedish decimal format), and kr suffix. |
| Purpose: makes OCR whitespace/case variance transparent to value_found(). |
| """ |
| |
| text = unicodedata.normalize("NFC", text) |
| |
| text = _WHITESPACE_RE.sub(" ", text) |
| |
| return text.lower().strip() |
|
|
|
|
| |
| |
| |
|
|
| def value_found(gold_value: str, haystack: str) -> bool: |
| """ |
| Check if gold_value appears in haystack using normalized substring match |
| with non-digit boundaries on both sides. |
| |
| The digit boundary (`(?<!\\d)...(?!\\d)`) prevents a gold value from matching |
| INSIDE a longer run of digits β both for short refs (e.g. "123" inside |
| "1234567") AND for long values (e.g. ref "12-345678" must NOT match inside |
| "912-3456789", and ISO date "2026-06-15" must NOT match inside "12026-06-15"). |
| Applying the boundary universally (not only when <6 digits) closes the |
| superstring hole that previously let a garbled longer number satisfy the |
| no-invention / recall gate (WR-01, D-12). |
| |
| Legitimate values are always delimited by whitespace/punctuation in the |
| model's output, so the boundary never rejects a real match. |
| """ |
| norm_gold = normalize(gold_value) |
| norm_hay = normalize(haystack) |
|
|
| pattern = r"(?<!\d)" + re.escape(norm_gold) + r"(?!\d)" |
| return bool(re.search(pattern, norm_hay)) |
|
|
|
|
| |
| |
| |
|
|
| def extract_values_from_section(section_text: str) -> list: |
| """ |
| Extract emitted verbatim values from the "Deadlines & money" section. |
| |
| Exploits the locked prompt's "one item per line, verbatim value first" format: |
| - 15 juni 2026 β last day to file |
| - 1 234 kr - belopp |
| |
| For each line: |
| 1. Strip leading bullet marker ('- ' or '* ' or bare '-' at start) and whitespace. |
| 2. Skip blank lines and any "None found" lines. |
| 3. Split on the first interpretation separator: ' β ' (em-dash) or ' - ' (hyphen |
| with surrounding spaces). The surrounding-spaces requirement prevents the |
| internal hyphens in ISO dates (e.g., 2026-06-15) from being split. |
| 4. The text before the separator is the verbatim value; strip it. |
| |
| Returns a list of verbatim value strings. Empty list if nothing matches. |
| """ |
| values = [] |
| for raw_line in section_text.splitlines(): |
| line = raw_line.strip() |
| if not line: |
| continue |
|
|
| |
| line = re.sub(r"^[-*β’]\s*", "", line).strip() |
| if not line: |
| continue |
|
|
| |
| if re.match(r"none found\.?$", line, re.IGNORECASE): |
| continue |
|
|
| |
| |
| |
| |
| |
| sep_match = re.search(r"\s+[ββ\-]\s+", line) |
| if sep_match: |
| verbatim = line[: sep_match.start()].strip() |
| else: |
| |
| verbatim = line |
|
|
| if verbatim: |
| values.append(verbatim) |
|
|
| return values |
|
|
|
|
| |
| |
| |
|
|
| def check_no_invention(result) -> list: |
| """ |
| Returns list of values from result.deadlines that are NOT found verbatim |
| in result.transcription (the no-invention check, D-12). |
| |
| Called from app.py to drive the verifying mascot state (D2-05). |
| Does NOT require a gold dict β safe to call on any real user letter. |
| Returns empty list = no invention detected. |
| |
| Works on any SimpleNamespace or StructuredResult that has .deadlines |
| and .transcription attributes. |
| """ |
| invented = [] |
| for emitted_val in extract_values_from_section(result.deadlines): |
| if not value_found(emitted_val, result.transcription): |
| invented.append(emitted_val) |
| return invented |
|
|