diff --git a/docs/research/compare-increment-funnel-2026-07-18.md b/docs/research/compare-increment-funnel-2026-07-18.md index ada1139f..0660b3c6 100644 --- a/docs/research/compare-increment-funnel-2026-07-18.md +++ b/docs/research/compare-increment-funnel-2026-07-18.md @@ -111,3 +111,71 @@ Compare is the **first slice of a much larger reader project**: the reader parse (3/261 tune), and "no injection" (75%) is a shared wall in front of every family. This is not scope creep — it is the arc seeing its true size. The increments stay small and measured *precisely because* the project is large. + +--- + +## MEASURED OUTCOME — the reader half (frame-anchored recognizer) + +### What shipped +Per the construction constraint (anchor on the comparative FRAME, not verb-whitelist growth), the two +multiplicative-frame regexes (`_COMPARE_MULT_ANCHOR_RE`, `_COMPARE_MULT_NTIMES_RE`) were re-anchored: +the closed-class frame — ` as many/much as ` — is the gate, the verb slot became a +verb-free bounded connector, and `as much` joined `as many`. `_comparison_anchor_verb()` (the whitelist) +is left in place for the ADDITIVE / NESTED frames (out of scope). Both frame regexes remain `$`-anchored, +so multi-clause / nested surfaces stay refused by construction. + +### The polarity refutation (and the sanctioned fallback) +Pure verb-free anchoring is **not** wrong=0-safe: a polarity-inverting / depletion verb inside the frame +("Alice **lost** twice as many apples as Bob") makes the compared quantity a loss, so +`actor = factor × reference` reads the relation backwards → wrong>0. The frame does NOT disambiguate this +(pinned by `test_adr_0131_G2a…::test_polarity_inverting_verb_not_admitted`, `…spend…`, +`test_recognizer_comparative_inject…[Alice lost…]`). Resolution = the fallback the ruling anticipated: +frame-anchored **plus a closed-class polarity blocklist** (`_COMPARE_POLARITY_BLOCK`: +lose/spend/use/give/sell/win/donate/pay/eat/drop/lend) the connector may not span. Admission stays +open-class for every *safe* verb (scored/wrote/caught/taught/baked/grew/…); the blocklist is the one +piece of verb knowledge wrong=0 genuinely requires — a small closed semantic class, not a whitelist. +**The frame is the gate either way.** + +### Footprint — provable, tiny, safe +Develop-on-tune / measure-once discipline held. A new-vs-old frame-fire diff over all 500 official cases: + +| Split | cases | divergent frame-fires | wrong | PARSED delta | +| :--- | ---: | ---: | ---: | ---: | +| tune | 261 | **0** (byte-identical histogram) | 0 | 0 | +| measure | 239 | **1** — `0441` "Zig **wrote** four times as many books as Flo." | 0 | 0 | + +`0441` is the exact archetype (verb-blocked-only, single-clause, frame-at-end). Post-change its frame +fires correctly — and the case **still refuses**, one layer downstream: *"no admissible candidate for +question: 'how many books did they write altogether?'"*. It advanced frame-refusal → **question-refusal** +(the pre-registered sanctioned delta), confirming the frame now reads but the binding wall is the +**shared summation-question** layer. Same wall killed `0228` ("…how many boxes in both warehouses +combined?"). Smoke gate: **176 passed**. + +### X = 15 — MISSED, delta = 0 (and why it's a shared-layer miss, not a family-exhaustion miss) +The 38 tune compare-family cases, classified by their **actual** structural blocker (not the noisy +surface histogram): 16 multiclause / nested (`…as Emily, who has…`, `…as Shawna, but 3 fewer…`), +10 single-clause-frame-no-match (multi-word "the X Y" references, `than` vs `as`, decimal factors, +unit-ellipsis "as much as", subject-is-comparison), 5 "times the number/amount of" (self-referential / +possessive references), 5 "as ADJ as" (attribute register), 2 "double what / triple". End-to-end traces +of the cleanest candidates (`0228`, `0274`, `0491`, `0165`, `0299`) show **every one dies at a +non-compare layer**: summation-question reading, seeding-sentence injection (shared discrete-count), +additive-compare, attribute registers, simultaneous equations. Even the cleanest compare-family frame +extension (multi-word reference) converts **0** cases end-to-end — and introduces an actor-misbinding +hazard — so it was reverted. + +**Ruling per the pre-committed funnel-conditioned escalation rule:** the mass is dying in **shared +layers**, not compare-specific logic ⇒ the next increment is a **shared-layer design increment** +(highest-leverage: the summation-question reader — evidenced by `0228` + `0441` both dying there), NOT +a practice-lane escalation, and NOT more compare-family frame surgery. X is re-applied *after* the +shared layer lands. + +### Triple report +- **correct**: measure new-correct = **0**. The compiler tier solves the compare cases the reader can + already reach (`0101` "double what", `0108` "times the number of", `0411`) — all **pre-existing**, not + new from the frame-anchoring. +- **refused-by-reason (traps broken out)**: polarity-inverting (lost/spent) → refused by + `_COMPARE_POLARITY_BLOCK` (absent from the real splits; pinned by synthetic confusers). Family funnel: + 16 multiclause/nested, 10 frame-no-match, 5 number/amount-of, 5 as-ADJ, 2 double-what — all now + refuse-and-record. +- **wrong = 0**: held on tune (261) **and** measure (239) **and** smoke (176). No over-admission from + the verb-free widening; no branch-disagreement regression (0 tune divergences). diff --git a/generate/math_candidate_parser.py b/generate/math_candidate_parser.py index 6ebf4c1f..d6a46989 100644 --- a/generate/math_candidate_parser.py +++ b/generate/math_candidate_parser.py @@ -1217,25 +1217,76 @@ _COMPARE_ADDITIVE_RE: Final[re.Pattern[str]] = re.compile( rf"(?P{_COMPARE_REF})\s*\.?$" ) +# ADR-0250 reader-arc increment 1 — FRAME-ANCHORED admission (verb-free). +# +# The multiplicative comparison FRAME — " as many/much as +# " / " times as many/much as " — is the closed-class +# structure that carries the relation. The verb between the actor and the +# frame is open-class ("scored", "caught", "taught", "has", ...) and does NO +# disambiguating work the frame doesn't already do; growing a verb whitelist +# only adds maintenance and trap surface ("scored twice" = event count, +# "reads twice a day" = frequency — both verb-adjacent traps). So the +# connector below admits ANY 0-3 word tokens in the verb slot and lets the +# COMPLETE frame be the gate. This is safe by construction: +# * both regexes remain ``$``-anchored, so the frame must end the clause — +# multi-clause / nested surfaces ("…, but 3 fewer…", "6 more than three +# times…") do NOT match and stay refused (a later layer / practice lane); +# * the connector is word-only (``\w+``), so it cannot span a comma or +# clause boundary — the actor stays the sentence-initial subject; +# * the frequency/event-count traps lack the "as many/much as " +# tail, so the frame refuses them WITHOUT any verb reasoning; +# * the round-trip filter + branch-disagreement + compiler conservation +# remain the wrong=0 net unchanged. +# ``_comparison_anchor_verb()`` is intentionally left in place for the +# ADDITIVE / NESTED frames (out of scope for this increment). +# +# ONE class of verb is NOT frame-neutral: polarity-inverting / depletion +# verbs. In "A N as many X as REF", a verb like lose/spend/give/sell +# makes the compared quantity a LOSS or transfer delta, not a possession — +# so ``actor = factor × reference`` (what compare_multiplicative asserts) +# reads the relation backwards → wrong>0. The frame does NOT disambiguate +# this, so the connector must refuse to span any member of this closed set +# (the exact set ``_comparison_anchor_verb`` documents as EXCLUDED). This is +# the sanctioned fallback: the frame is still the gate for every OTHER verb; +# this blocklist is the minimal, closed, semantic-class piece of verb +# knowledge wrong=0 requires — NOT an open-class whitelist. Every non-listed +# verb (scored/wrote/caught/taught/baked/grew/…) is admitted verb-free. +_COMPARE_POLARITY_BLOCK: Final[str] = ( + r"lose|lost|loses|losing|" + r"spend|spent|spends|spending|" + r"use|used|uses|using|" + r"give|gave|gives|giving|given|" + r"sell|sold|sells|selling|" + r"win|won|wins|winning|" + r"donate|donated|donates|donating|" + r"pay|paid|pays|paying|" + r"eat|ate|eats|eating|eaten|" + r"drop|dropped|drops|dropping|" + r"lend|lent|lends|lending" +) +_COMPARE_FRAME_CONNECTOR: Final[str] = ( + rf"(?:(?!(?i:{_COMPARE_POLARITY_BLOCK})\b)\w+\s+){{0,3}}" +) + # Multiplicative: anchor-as-value form ("twice"/"thrice"/"half"/"quarter"/ -# "third" carry the factor implicitly). "as many " required; unit +# "third" carry the factor implicitly). "as many/much " required; unit # ellipsis ("twice as many as Bob") is deferred to keep wrong==0 strict. # "quarter" / "third" admit an optional article ("a quarter", "a third") — # the article is not a named group; matched_verb is the anchor word itself, # which is a substring of the source and registered in # COMPARE_MULTIPLICATIVE_ANCHORS, so round-trip checks pass. _COMPARE_MULT_ANCHOR_RE: Final[re.Pattern[str]] = re.compile( - rf"^(?P{_ENTITY})\s+{_comparison_anchor_verb()}\s+" - r"(?:a\s+)?(?Ptwice|thrice|half|quarter|third)\s+as\s+many\s+" + rf"^(?P{_ENTITY})\s+{_COMPARE_FRAME_CONNECTOR}" + r"(?:a\s+)?(?Ptwice|thrice|half|quarter|third)\s+as\s+(?:many|much)\s+" r"(?P\w+(?:\s+\w+)?)\s+as\s+" rf"(?P{_COMPARE_REF})\s*\.?$" ) -# Multiplicative: explicit "N times as many as ". +# Multiplicative: explicit "N times as many/much as ". # ADR-0131.G.2a — unit slot admits an optional second word ("jumping jacks"). _COMPARE_MULT_NTIMES_RE: Final[re.Pattern[str]] = re.compile( - rf"^(?P{_ENTITY})\s+{_comparison_anchor_verb()}\s+" - rf"(?P{_VALUE})\s+times\s+as\s+many\s+" + rf"^(?P{_ENTITY})\s+{_COMPARE_FRAME_CONNECTOR}" + rf"(?P{_VALUE})\s+times\s+as\s+(?:many|much)\s+" r"(?P\w+(?:\s+\w+)?)\s+as\s+" rf"(?P{_COMPARE_REF})\s*\.?$" )