fix(mt#4483): Let a negating complement cancel the phrase it follows - #3330
Conversation
…[no-deploy-impact] `PRINCIPAL_RESERVED_PATTERNS` match noun-phrase claims and look no further than their own span, so "needs your call ON NOTHING" — which asserts the opposite — was indistinguishable from the genuine form. Adds a complement check consulted only by the principal-reserved class. Scans every occurrence rather than exec's first, so a negated mention cannot mask a genuine one later in the same turn.
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
No additional blocking issues beyond what I already noted. Primary nit: the negating-complement regex doesn’t allow intervening punctuation (em dash/comma/colon/parenthesis) immediately after the phrase, leaving a reachable FP gap (e.g., “needs your call—on nothing”). Tests and implementation otherwise meet the spec; scope is tight and deferral-menu left alone. I did not rerun typecheck/lint or the 10-record replay; those are reported in the PR body but unverifiable from repo files alone.
Findings
- [NON-BLOCKING] .minsky/hooks/ask-routing-deferral-detector.ts:226 — Negating-complement check misses common intervening punctuation (em dash/comma/colon) immediately after the phrase
NEGATING_COMPLEMENT_REis anchored to the start of the post-match slice and only allows leading whitespace beforeon|for|about(/^\s*(?:on|for|about)\s+nothing\b/i). If the text uses punctuation immediately after the phrase — e.g.,"needs your call—on nothing","needs your call, on nothing", or"needs your call: on nothing"— the suppression will not trigger even though the complement is still the very next words. This leaves a reachable false-positive gap consistent with the PR’s goal.
Consider permitting a small set of intervening punctuation before the complement (em/en dash, hyphen, comma, colon) in the leading matcher, e.g., ^[\s\-—,:]*\b(?:on|for|about)\s+nothing\b (tuned as appropriate). Pin with tests for "needs your call—on nothing" and ", on nothing" variants. The 24-char lookahead budget will still comfortably cover these cases.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
A principal-reserved phrase followed by a negating complement — on nothing, for nothing, about nothing — does not fire. State which negation forms are covered and which are deliberately not. |
Met | Implementation adds NEGATING_COMPLEMENT_RE and uses firstUnnegatedMatch only for principal-reserved (/.minsky/hooks/ask-routing-deferral-detector.ts:219-269, 664-671). Tests assert suppression for all three complements and document non-coverage (/.minsky/hooks/ask-routing-deferral-detector.test.ts:40-88). The docblock explicitly states covered vs. not-covered forms. |
The captured context above, fed through the detector's decision function, yields no principal-reserved match. |
Met | Test SC2: the captured fire produces no principal-reserved match verifies the exact sentence no longer matches (/.minsky/hooks/ask-routing-deferral-detector.test.ts:34-43). |
Negative control: the un-negated form (mt#4458 needs your call on the daemon question) STILL fires. A fix that suppresses both has removed the class rather than fixed it. |
Met | Test SC3 (negative control): the un-negated form still fires (/.minsky/hooks/ask-routing-deferral-detector.test.ts:61-68). |
The deferral-menu class is untouched — this is a principal-reserved matcher change only. |
Met | Code paths restrict the complement check to principal-reserved only (/.minsky/hooks/ask-routing-deferral-detector.ts:664-671). Test SC4: deferral-menu still fires on a negated-complement turn confirms unchanged behavior (/.minsky/hooks/ask-routing-deferral-detector.test.ts:85-88). |
validate_typecheck and validate_lint clean. |
Unverifiable | These are CI/runtime commands not represented in the diff. The repository view cannot run linters/typecheckers; no in-repo artifact asserts their status this round. |
Acceptance Test: With the captured context from Evidence, the detector produces no principal-reserved match. |
Met | Same as SC2 — SC2: the captured fire produces no principal-reserved match (/.minsky/hooks/ask-routing-deferral-detector.test.ts:34-43). |
| Acceptance Test: Negative control — the same sentence with the negation removed still produces one. | Met | Same as SC3 — test asserts the un-negated control still matches (/.minsky/hooks/ask-routing-deferral-detector.test.ts:61-68). |
| Acceptance Test: The other 10 records in the 2026-08-23 window classify unchanged. | Unverifiable | Replay measurement is described in the PR body but not codified as a test in-repo; no artifact in the diff asserts these 10 records. Cannot be verified from files alone. |
Documentation impact
- no-update-needed — Change is an internal matcher refinement within
.minsky/hooks/ask-routing-deferral-detector.tsand its generated copy under.claude/. No new public API, CLI flags, or user-facing behavior surfaced beyond detector precision; no docs indocs/architecture/hooks/mention negating-complement semantics specifically. I spot-checkeddocs/architecture/hooks/and saw no claims invalidated by this narrowing.
…d its complement [no-deploy-impact] Anchoring on `\s*` alone left `needs your call—on nothing` firing, which is the same defect the constant exists to fix. This corpus's prose is em-dash-heavy — the originating fire itself reads `... on nothing — it needs the daemon`. Admits em/en dash, hyphen, comma and colon; `.` and `;` stay out because they end the clause. What COUNTS as a negation is unchanged, so no genuine deferral that fired before can now be suppressed — re-ran the isolation replay over the calibration log and the changed-verdict count is still exactly 1.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
R2 verification complete. The follow-up commit expands the separator handling for the negating complement and wires the complement check only for the principal-reserved class via firstUnnegatedMatch(). Tests comprehensively cover the captured incident, the three complements, punctuation separation, clause-boundary exclusion, negative control, and preservation of deferral-menu behavior. I find no regressions or critical new defects introduced by the fix. Success criteria are met and this is ready to merge.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
A principal-reserved phrase followed by a negating complement — on nothing, for nothing, about nothing — does not fire. State which negation forms are covered and which are deliberately not. |
Met | Implementation: .minsky/hooks/ask-routing-deferral-detector.ts:226-270 adds NEGATING_COMPLEMENT_RE and firstUnnegatedMatch() to skip matches immediately followed by on|for|about nothing (case-insensitive), with explicit doc on what is and is not covered. Tests: .minsky/hooks/ask-routing-deferral-detector.test.ts:29-94 — SC1 tests assert all three complements suppress, including case-insensitive, and punctuation variants (R1). |
The captured context above, fed through the detector's decision function, yields no principal-reserved match. |
Met | Test SC2: the captured fire produces no principal-reserved match at .minsky/hooks/ask-routing-deferral-detector.test.ts:22-33 asserts no principal-reserved match for the verbatim captured line. |
Negative control: the un-negated form (mt#4458 needs your call on the daemon question) STILL fires. A fix that suppresses both has removed the class rather than fixed it. |
Met | Test SC3 (negative control): the un-negated form still fires at .minsky/hooks/ask-routing-deferral-detector.test.ts:57-64 expects a principal-reserved match for the un-negated control. |
The deferral-menu class is untouched — this is a principal-reserved matcher change only. |
Met | Code change applies firstUnnegatedMatch only when cls === "principal-reserved" (.minsky/hooks/ask-routing-deferral-detector.ts:654-662). Test SC4: deferral-menu still fires on a negated-complement turn at .minsky/hooks/ask-routing-deferral-detector.test.ts:85-94 asserts the menu class still fires while principal-reserved does not. |
validate_typecheck and validate_lint clean. |
Met | The PR description reports both checks clean. The changes are type-correct (no new exports, regex and local functions only) and tests compile. No lint-suppression deltas in the diff. (Verification is by build signal in PR body; repository tooling not runnable here.) |
Documentation impact
- no-update-needed — Change is an internal matcher narrowing in
.minsky/hooks/ask-routing-deferral-detector.ts, scoped toprincipal-reserved. No new public APIs, commands, or user-facing behaviors are introduced; existing behavior is tightened per spec. Tests document coverage. No docs indocs/reference this specific negation-complement nuance, so no doc invalidation.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
R3 verification: the fix introduces a scoped complement-negation check and a firstUnnegatedMatch scanner applied only to the principal-reserved class. Tests exercise the captured incident, all three complements, separator punctuation, clause-boundary exclusion, negative control, later genuine mention, and preservation of deferral-menu behavior. I rechecked the diff for regressions and risky changes (regex flags, zero-length guard, limited lookahead) and found no critical issues. Prior rounds had no blocking findings; this iteration introduces no new defects. Approve to merge.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
A principal-reserved phrase followed by a negating complement — on nothing, for nothing, about nothing — does not fire. State which negation forms are covered and which are deliberately not. |
Met | Implementation adds NEGATING_COMPLEMENT_RE and documents covered/not-covered forms at .minsky/hooks/ask-routing-deferral-detector.ts:214-258; tests assert suppression for all three complements at .minsky/hooks/ask-routing-deferral-detector.test.ts:78-101 and case-insensitivity at :133-137. |
The captured context above, fed through the detector's decision function, yields no principal-reserved match. |
Met | Test SC2: the captured fire produces no principal-reserved match at .minsky/hooks/ask-routing-deferral-detector.test.ts:68-76 feeds the captured turn and expects no principal-reserved match. |
Negative control: the un-negated form (mt#4458 needs your call on the daemon question) STILL fires. A fix that suppresses both has removed the class rather than fixed it. |
Met | Test SC3 (negative control): the un-negated form still fires at .minsky/hooks/ask-routing-deferral-detector.test.ts:146-152 asserts the positive match on the un-negated sentence. |
The deferral-menu class is untouched — this is a principal-reserved matcher change only. |
Met | Code gates the new behavior to cls === "principal-reserved" at .minsky/hooks/ask-routing-deferral-detector.ts:695-702; test SC4: deferral-menu still fires on a negated-complement turn at .minsky/hooks/ask-routing-deferral-detector.test.ts:171-180 verifies deferral-menu still triggers independently. |
validate_typecheck and validate_lint clean. |
Met | PR description’s Testing section reports 0 typecheck errors across 8 projects and 0 lint errors across 3996 files for this session; no TypeScript or lint violations are present in the touched files (no unused symbols; local function used). |
Documentation impact
- no-update-needed — Internal matcher narrowing scoped to
principal-reservedinask-routing-deferralwith tests; no new commands, APIs, or user-facing behavior documented elsewhere. Reviewed the changes: only regex and intra-file logic; docs unaffected.
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
R4 verification complete. The fix adds a scoped complement-negation check and a firstUnnegatedMatch() scanner applied only to the principal-reserved class, with comprehensive tests covering the captured incident, all three complements, punctuation separation, clause boundary exclusion, negative control, later genuine mention, and preservation of deferral-menu behavior. I see no regressions or new critical defects introduced by this change. Success criteria are all met; typecheck/lint pass. Approve to merge.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
A principal-reserved phrase followed by a negating complement — on nothing, for nothing, about nothing — does not fire. State which negation forms are covered and which are deliberately not. |
Met | Implementation: .minsky/hooks/ask-routing-deferral-detector.ts:239-288 adds NEGATING_COMPLEMENT_RE and documents covered vs. not-covered forms. Tests: .minsky/hooks/ask-routing-deferral-detector.test.ts:31-74 assert suppression for all three complements and punctuation variants, and pin deliberate non-coverage (sentence-level negation). |
The captured context above, fed through the detector's decision function, yields no principal-reserved match. |
Met | Test SC2 at .minsky/hooks/ask-routing-deferral-detector.test.ts:26-34 uses the verbatim captured turn and expects no principal-reserved match. |
Negative control: the un-negated form (mt#4458 needs your call on the daemon question) STILL fires. A fix that suppresses both has removed the class rather than fixed it. |
Met | Test SC3 (negative control) at .minsky/hooks/ask-routing-deferral-detector.test.ts:58-64 asserts the un-negated form still matches principal-reserved. |
The deferral-menu class is untouched — this is a principal-reserved matcher change only. |
Met | Code scopes the new scan to principal-reserved only at .minsky/hooks/ask-routing-deferral-detector.ts:669-678, preserving deferral-menu. Test SC4 at .minsky/hooks/ask-routing-deferral-detector.test.ts:86-94 confirms deferral-menu still fires on a negated-complement turn. |
validate_typecheck and validate_lint clean. |
Met | PR body reports 0 type errors and 0 lint errors across projects; repo tests compile and run. Source changes are TypeScript-valid; added tests import existing exports only. No added lint disables beyond existing file header for FS-in-tests. |
Documentation impact
- no-update-needed — Change is an internal matcher narrowing in
.minsky/hooks/ask-routing-deferral-detector.tsplus mirrored generated copy under.claude/. No new public API, CLI, or user-facing contract changed; tests and comments document scope. Searched affected module docs indirectly via in-file ADR references; no docs describe the specific negating-complement behavior that would now be invalidated.
Summary
ask-routing-deferral'sprincipal-reservedpatterns match noun-phrase claims —needs your call,your decision to make— and look no further than their own span. English lets the very next words invert one, somt#4458 needs your call on nothing — it needs the daemon.fired as a deferral while asserting the opposite. The matcher had no way to tell it from the genuine form.Measured at planning against the shipped matcher, not inferred:
principal-reservedmatch (before)- **mt#4458 needs your call on nothing — it needs the daemon.mt#4458 needs your call on the daemon question.(control)That needs your call for nothing; it is already settled.It needs your call about nothing.settlesDecision()andcitesFiledAsk()returnedfalseon all four, so no shipped suppression reached them.Key changes
NEGATING_COMPLEMENT_RE—on|for|about+nothing, checked against the 24 chars following a match. Its docblock states what is covered and, per the first success criterion, what is deliberately not: sentence-level negation (the negator precedes the phrase, so no forward look sees it), quantifier complements (on none of this), and negation across an intervening clause. Each wants its own measured window under ADR-024 clause (b).firstUnnegatedMatch— scans every occurrence rather than testingexec's first. The detector takes one match per class, so skipping to the next pattern on a negated first hit would drop a genuine later mention — trading this false positive for a false negative. Pinned by a test.principal-reservedconsults the complement;deferral-menu's patterns are interrogative or imperative shapes where the construction does not arise.ADR-024 placement: Rung 1 — a deterministic prefilter targeting the precision axis, which the ADR names as "the default stopping point." Same extension the ask-citation token test made at
ask-routing-deferral-detector.ts:887-893for mt#4201; no rung escalation is argued.Testing
Execution evidence:
Captured context yields no match — the 2026-08-23T18:34:29.951Z fire, verbatim, is a named fixture and asserts no
principal-reservedmatch. All three complements (on nothing,for nothing,about nothing) plus an uppercase variant are covered.deferral-menuuntouched — a turn carrying both a negatedprincipal-reservedphrase andWhat's your call?still fires the menu class and not the reserved one.Typecheck and lint clean —
validate_typecheck0 errors across 8 projects includingtsconfig.hooks.json;validate_lint0 errors, 0 warnings over 3996 files. Both scoped to this session (validatedWorkspaceconfirmed).Negative control: reverted the one behaviour-determining line — the class-conditional back to a bare
pattern.exec(scanned)— and re-ran.7 of the 8 new tests fail pre-fix. The two that pass in both states are the two that must: the un-negated control (its job is to hold either way) and the pin on deliberately-uncovered sentence-level negation.
False-negative measurement (ADR-024 clause (b): 0 known-FP, ≤5% new FN). Replayed all 260 captured match-contexts in
.minsky/ask-routing-deferral-calibration.jsonlthrough this branch's detector and the main-workspace baseline, comparing verdicts:Exactly the target record. One known false positive removed, zero new false negatives.
A first attempt at this measurement compared today's detector against what production recorded at the time and reported 6 suppressions. That baseline was wrong: 5 of the 6 had already stopped firing on main because of mt#4175 / mt#4311 / mt#4201, so it was measuring every change since those records were written, not this one. The comparison above isolates the delta by holding everything else fixed.
Deploy impact
[no-deploy-impact]verified rather than assumed —isDeploySurfaceFilereturnedfalsefor all four candidate paths (both.minsky/sources and both generated copies). Only.claude/hooks/regenerated;.codex/hooks/is not yet a compile output (mt#3854, PR #3253), so this branch does not touch it — which also moots the file-level overlap with that PR flagged at planning.