From 37c066f95814cc690a8693ca4af23d75ec05b0eb Mon Sep 17 00:00:00 2001 From: abrichr Date: Thu, 5 Nov 2026 05:14:40 -0500 Subject: [PATCH 1/3] fix(identity): treat comma/period as OCR noise in squash Thin identity-band crops make RapidOCR read "Smith," as "Smith." (seen on native Windows). That punctuation mismatch fired affirmative contradiction budgets and returned mismatch instead of the glyph-collapse abstain the homonym fixtures require. Drop comma and period from the squashed comparison form so the existing abstain assertions hold on Windows and Linux without loosening the tests. --- .../flow/openadapt_flow/runtime/identity.py | 12 +++++++-- packages/flow/tests/test_identity.py | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/flow/openadapt_flow/runtime/identity.py b/packages/flow/openadapt_flow/runtime/identity.py index b151351b1d..c26f2df47f 100644 --- a/packages/flow/openadapt_flow/runtime/identity.py +++ b/packages/flow/openadapt_flow/runtime/identity.py @@ -364,8 +364,16 @@ def squash(text: str) -> str: - """Lowercase and remove ALL whitespace (OCR-tolerant comparison form).""" - return "".join(text.lower().split()) + """Lowercase and drop whitespace and OCR-volatile punctuation. + + Comma and period are stripped because thin identity-band crops routinely + make RapidOCR read ``Smith,`` as ``Smith.`` (and the reverse). Leaving + them in the comparison form turned a glyph-collapse ABSTAIN into a false + affirmative MISMATCH on native Windows while Mac stayed consistent. + """ + return "".join( + ch for ch in text.lower() if not ch.isspace() and ch not in ",." + ) def band_region(point: Point, band_height: int, viewport: tuple[int, int]) -> Region: diff --git a/packages/flow/tests/test_identity.py b/packages/flow/tests/test_identity.py index 5d5e1fdc67..aea0aa053e 100644 --- a/packages/flow/tests/test_identity.py +++ b/packages/flow/tests/test_identity.py @@ -347,6 +347,23 @@ def test_tokenize(self): assert tokenize(" Jane Li \n panel ") == ["jane", "li", "panel"] assert tokenize("") == [] + def test_comma_period_name_punctuation_is_ocr_noise(self): + # Recorded thin-band context often stores "Smith." while the live + # band (or another engine) returns "Smith," — same patient row. + match = band_match( + "100512 Smith. John 01/15/1980 Active", + "100512 Smith, John 01/15/1980 Active", + ) + assert match.coverage == 1.0 + assert match.contradicted_chars == 0 + assert match.unexplained_name_tokens == 0 + # A real near-name sibling must still mismatch. + bad = band_match( + "100512 Smith. John 01/15/1980 Active", + "100512 Smith, Joan 01/15/1980 Active", + ) + assert bad.contradicted_chars > 0 or bad.unexplained_name_tokens > 0 + class TestOperatingPoint: """Pin the ROC-chosen decision parameters and their boundaries @@ -456,6 +473,14 @@ def test_coverage_ignores_scattered_short_blocks(self): assert coverage("abcdef", "abcdef") == 1.0 assert coverage("", "x") == 0.0 + def test_squash_drops_comma_period(self): + # Thin identity-band crops make RapidOCR read "Smith," as "Smith." + # (Windows native); both must compare equal so glyph abstain is not + # overwritten by a false punctuation mismatch. + assert squash("Smith,") == squash("Smith.") + assert squash("Smith, John") == "smithjohn" + assert squash("Jr.") == "jr" + def test_band_region_clamps_to_viewport(self): assert band_region((500, 10), 64, (1280, 800)) == (0, 0, 1280, 64) assert band_region((500, 795), 64, (1280, 800)) == (0, 736, 1280, 64) From 3a8805f4a7e31bf81f155297252a3fcd51d444d5 Mon Sep 17 00:00:00 2001 From: abrichr Date: Thu, 5 Nov 2026 05:16:37 -0500 Subject: [PATCH 2/3] style(identity): ruff-format squash join expression --- packages/flow/openadapt_flow/runtime/identity.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flow/openadapt_flow/runtime/identity.py b/packages/flow/openadapt_flow/runtime/identity.py index c26f2df47f..0b3daded1f 100644 --- a/packages/flow/openadapt_flow/runtime/identity.py +++ b/packages/flow/openadapt_flow/runtime/identity.py @@ -371,9 +371,7 @@ def squash(text: str) -> str: them in the comparison form turned a glyph-collapse ABSTAIN into a false affirmative MISMATCH on native Windows while Mac stayed consistent. """ - return "".join( - ch for ch in text.lower() if not ch.isspace() and ch not in ",." - ) + return "".join(ch for ch in text.lower() if not ch.isspace() and ch not in ",.") def band_region(point: Point, band_height: int, viewport: tuple[int, int]) -> Region: From dafa28c7c34b9da230fea939ef69b77c13880757 Mon Sep 17 00:00:00 2001 From: abrichr Date: Thu, 5 Nov 2026 05:44:56 -0500 Subject: [PATCH 3/3] fix(identity): keep digit-separating periods in squash Stripping every period turned DOBs like 01.01.1900 into 01011900, which the glyph-vulnerable-identifier gate then treated as an MRN-like token and spuriously abstained (CI Hypothesis fuzz on shard 1/3). Keep periods between digits; still drop OCR-volatile trailing comma/period on names so Smith,/Smith. compare equal. --- .../flow/openadapt_flow/runtime/identity.py | 29 +++++++++++++++---- packages/flow/tests/test_identity.py | 5 ++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/flow/openadapt_flow/runtime/identity.py b/packages/flow/openadapt_flow/runtime/identity.py index 0b3daded1f..820697261b 100644 --- a/packages/flow/openadapt_flow/runtime/identity.py +++ b/packages/flow/openadapt_flow/runtime/identity.py @@ -364,14 +364,31 @@ def squash(text: str) -> str: - """Lowercase and drop whitespace and OCR-volatile punctuation. + """Lowercase and drop whitespace and OCR-volatile name punctuation. - Comma and period are stripped because thin identity-band crops routinely - make RapidOCR read ``Smith,`` as ``Smith.`` (and the reverse). Leaving - them in the comparison form turned a glyph-collapse ABSTAIN into a false - affirmative MISMATCH on native Windows while Mac stayed consistent. + Comma and period on letter tokens are stripped because thin identity-band + crops routinely make RapidOCR read ``Smith,`` as ``Smith.`` (and the + reverse). Leaving them in the comparison form turned a glyph-collapse + ABSTAIN into a false affirmative MISMATCH on native Windows while Mac + stayed consistent. + + Periods that separate digits (``01.01.1900``) are kept: collapsing them + turns a date into a digit run that the glyph-vulnerable-identifier gate + then mistreats as an MRN-like token and spuriously abstains. """ - return "".join(ch for ch in text.lower() if not ch.isspace() and ch not in ",.") + lowered = text.lower() + out: list[str] = [] + for i, ch in enumerate(lowered): + if ch.isspace(): + continue + if ch in ",.": + prev = lowered[i - 1] if i else "" + nxt = lowered[i + 1] if i + 1 < len(lowered) else "" + if prev.isdigit() and nxt.isdigit(): + out.append(ch) # date / decimal separator + continue + out.append(ch) + return "".join(out) def band_region(point: Point, band_height: int, viewport: tuple[int, int]) -> Region: diff --git a/packages/flow/tests/test_identity.py b/packages/flow/tests/test_identity.py index aea0aa053e..314dca793f 100644 --- a/packages/flow/tests/test_identity.py +++ b/packages/flow/tests/test_identity.py @@ -480,6 +480,11 @@ def test_squash_drops_comma_period(self): assert squash("Smith,") == squash("Smith.") assert squash("Smith, John") == "smithjohn" assert squash("Jr.") == "jr" + # Date / digit separators must survive: collapsing "01.01.1900" into + # "01011900" makes the glyph-vulnerable-identifier gate spuriously + # abstain on a clean MRN + period-separated DOB pair (CI fuzz). + assert squash("01.01.1900") == "01.01.1900" + assert squash("01/01/1900") == "01/01/1900" def test_band_region_clamps_to_viewport(self): assert band_region((500, 10), 64, (1280, 800)) == (0, 0, 1280, 64)