From 0dd30b86a76c40fbe02bef7074a90c76a9f8054d Mon Sep 17 00:00:00 2001 From: Shay Date: Thu, 21 May 2026 08:29:16 -0700 Subject: [PATCH] fix(intent): anchor CORRECTION trigger with word boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit While investigating the adjacent RECALL classifier gap, a much wider intent-classification bug surfaced: every prompt beginning with a word that *starts with* the letters of any CORRECTION trigger silently routed to CORRECTION with a mangled subject. Concrete examples seen during diagnosis: "Now remember light." → CORRECTION subject="w remember light" "Nothing matters." → CORRECTION subject="thing matters" "Notice the truth." → CORRECTION subject="tice the truth" "Note that recall fires." → CORRECTION subject="te that recall fires" "Nominate a candidate." → CORRECTION subject="minate a candidate" "Norma is here." → CORRECTION subject="rma is here" "Notwithstanding ..." → CORRECTION subject="twithstanding ..." Root cause: ``generate/intent.py`` ``_RULES`` line ~213 used the pattern (?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction) The alternation has ``no``, ``incorrect``, ``actually``, ``correction`` as bare substrings — no word boundary on either side. Combined with ``re.match``'s start-of-string anchor, *any* prompt beginning with ``No``-, ``Incorrect``-, ``Actually``-, or ``Correction``-prefixed text matched as CORRECTION; the regex's match span was then sliced off the prompt to produce a subject like ``"w remember light"`` (from ``"Now remember light."``). The same hazard threatens: * ``no`` → eats ``Now`` / ``Notice`` / ``Note`` / ``Nothing`` / ``Nominate`` / ``Norma`` / ``Notwithstanding`` / ... * ``incorrect`` → would eat ``incorrectly`` * ``actually`` → would eat ``actualization`` * ``correction`` → would eat ``corrections`` Fix: add ``\b`` anchors on both sides of the alternation. \b(?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)\b ``\b`` is zero-width, so ``re.match``'s start-of-string anchor still holds; the left ``\b`` is a no-op at position 0. The right ``\b`` forces the matched token to end on a word boundary — i.e., the next character must be non-word (whitespace, punctuation, EOL) — so ``\bno\b`` matches ``"No."`` / ``"No way"`` / ``"No, ..."`` but NOT ``"Now"`` / ``"Nothing"`` / etc. Verified 11/11 previously-misfiring prompts now correctly classify as UNKNOWN, and 8/8 legitimate CORRECTION pragmas (``"No."`` / ``"No way."`` / ``"Incorrect."`` / ``"Actually, ..."`` / ``"Correction: ..."`` / ``"That's wrong."`` / ``"No, that's wrong."`` / ``"no, knowledge is wrong."``) still route correctly. Tests extended with two new parametrized blocks in ``tests/test_intent_subject_extraction.py``: * ``test_correction_canonical_forms_still_route`` — 8 cases pinning the legitimate CORRECTION patterns * ``test_correction_does_not_eat_no_prefixed_words`` — 10 cases pinning the boundary fix against regression Verified: pytest tests/test_intent_subject_extraction.py 25/25 pass pytest tests/test_intent_proposition_graph.py + others 60/60 pass core test --suite smoke 67/67 pass core test --suite runtime 19/19 pass Out of scope: ``"That is not right."`` (a real CORRECTION pragma the regex never caught because ``that'?s\s+`` requires literal ``s`` after ``that``; the colloquial ``that is`` form was always UNKNOWN). Separate gap, unchanged here. --- generate/intent.py | 9 +++- tests/test_intent_subject_extraction.py | 61 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/generate/intent.py b/generate/intent.py index f83d08f8..bd42eba2 100644 --- a/generate/intent.py +++ b/generate/intent.py @@ -209,7 +209,14 @@ _RULES: tuple[tuple[re.Pattern[str], IntentTag], ...] = ( (re.compile(r"what\s+(?:causes|triggers|enables|prevents|drives|produces|induces|yields)\s+", re.IGNORECASE), IntentTag.CAUSE), (re.compile(r"how\s+(?:do|can|should|would)\s+(?:I|we|you)\s+", re.IGNORECASE), IntentTag.PROCEDURE), (re.compile(r"(?:is|are|does|do|can|could|would|should|was|were|has|have|will)\s+.+\??\s*$", re.IGNORECASE), IntentTag.VERIFICATION), - (re.compile(r"(?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)", re.IGNORECASE), IntentTag.CORRECTION), + # Word boundaries on both sides are load-bearing: without them ``no`` + # would prefix-match every word beginning with those letters + # (``Now``, ``Notice``, ``Nothing``, ``Nominate``, ``Norma``, ...) and + # silently route them all to CORRECTION with a mangled subject like + # ``"w remember light"``. The same hazard applies to ``incorrect`` + # (would eat ``incorrectly``), ``actually`` (would eat + # ``actualization``), and ``correction`` (would eat ``corrections``). + (re.compile(r"\b(?:no|that'?s\s+(?:not|wrong)|incorrect|actually|correction)\b", re.IGNORECASE), IntentTag.CORRECTION), (re.compile(r"(?:remember|recall)\s+", re.IGNORECASE), IntentTag.RECALL), ) diff --git a/tests/test_intent_subject_extraction.py b/tests/test_intent_subject_extraction.py index 1f347479..dfc3a247 100644 --- a/tests/test_intent_subject_extraction.py +++ b/tests/test_intent_subject_extraction.py @@ -126,6 +126,67 @@ def test_recall_strips_articles(prompt: str, expected_subject: str) -> None: assert intent.subject == expected_subject +# --------------------------------------------------------------------------- +# CORRECTION — word-boundary discipline on the trigger pattern +# --------------------------------------------------------------------------- +# +# Until a recent fix, the CORRECTION regex matched the bare token ``no`` +# without word boundaries. Combined with ``re.match``'s start anchor, +# every prompt beginning with ``No``-as-prefix (``Notice``, ``Note``, +# ``Now``, ``Nothing``, ``Nominate``, ``Norma``, ``Notwithstanding``) +# silently routed to CORRECTION with a mangled subject like +# ``"w remember light"`` (from ``"Now remember light."``). The same +# hazard threatened ``incorrect`` / ``incorrectly``, ``actually`` / +# ``actualization``, ``correction`` / ``corrections``. The fix added +# ``\b`` anchors on both sides of the alternation; these parametrized +# cases pin the boundary discipline against regression. + + +@pytest.mark.parametrize( + "prompt", + [ + "No, that's wrong.", + "No.", + "No way.", + "no, knowledge is wrong.", + "Incorrect.", + "Actually, that's false.", + "Correction: memory is not storage.", + "That's wrong.", + ], +) +def test_correction_canonical_forms_still_route(prompt: str) -> None: + """Legitimate CORRECTION pragmas must still classify after the + word-boundary fix narrowed the alternation.""" + intent = classify_intent(prompt) + assert intent.tag is IntentTag.CORRECTION + + +@pytest.mark.parametrize( + "prompt", + [ + # ``No``-prefixed words that previously misfired + "Nothing matters.", + "Notice the truth.", + "Note that recall fires.", + "Nominate a candidate.", + "Now remember light.", + "Norma is here.", + "Notwithstanding the evidence.", + # ``Incorrect``-prefixed / ``Correction``-prefixed words + "Incorrectly stated.", + "Corrections department.", + # ``Actually`` prefix — rarer but symmetric + "Actualization of intent.", + ], +) +def test_correction_does_not_eat_no_prefixed_words(prompt: str) -> None: + """Words beginning with the CORRECTION trigger letters must not + silently route to CORRECTION via a missing word-boundary anchor.""" + intent = classify_intent(prompt) + assert intent.tag is not IntentTag.CORRECTION + + # --------------------------------------------------------------------------- # Edge cases — degenerate inputs do not produce empty subjects # ---------------------------------------------------------------------------