fix(intent): anchor CORRECTION trigger with word boundaries
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.
This commit is contained in:
parent
7ef4ef4546
commit
0dd30b86a7
2 changed files with 69 additions and 1 deletions
|
|
@ -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),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue