revert(reader-arc): hold frame-anchoring out of increment 1 — defer to rate-frame increment (#77 ruling)

Reverts the c3aed13b parser change (frame-anchored comparative regexes +
verb-free connector + _COMPARE_POLARITY_BLOCK + 'as much'); the reader stays
on the _comparison_anchor_verb() whitelist (fail-closed). Increment 1 ships
the compiler tier + the funnel finding only.

Three reasons on record: (1) frame-anchoring converts 0 of 500 official cases
— zero measured benefit today; (2) its consumer is rate-frame admission, a
later increment — the next increment (seeding-sentence injection) is a shared
layer that doesn't touch it; (3) the polarity blocklist is fail-OPEN — an
unenumerated depletion verb in a complete frame is a coherent-but-inverted
graph = a wrong on the run-once sealed arbiter (round-trip guards parse
consistency, not gold-correctness), a trade the fail-closed doctrine never
takes for zero gain.

Verified before revert-merge: with the whitelist reader, the 5 real compare
parses are non-divergent — test_real_compare_cases_solved_wrong_zero still
sees 5, solves 5, wrong 0 (corridor real-reach 0->5/500 preserved).

Frame-anchoring recorded as an OPEN DESIGN TASK owned by the rate-frame
increment: frame + POSITIVE polarity determination (neutral-polarity allowlist
or small polarity classifier), not assume-safe-unless-blocklisted. Detail in
docs/research/compare-increment-funnel-2026-07-18.md.
This commit is contained in:
Shay 2026-07-18 20:03:38 -07:00
parent c3aed13bbe
commit 6f1ef49018
2 changed files with 64 additions and 72 deletions

View file

@ -116,7 +116,13 @@ the project is large.
## MEASURED OUTCOME — the reader half (frame-anchored recognizer)
### What shipped
> **STATUS: BUILT + MEASURED, then DEFERRED out of increment 1 (PR #77 ruling).** The code change
> (`c3aed13b`, frame-anchored recognizer) was reverted from the merged increment; only the compiler
> tier + this finding land. Frame-anchoring is now an **open design task owned by the rate-frame
> increment** — see *Deferral ruling* below. This section is the on-record measurement that motivated
> the split.
### What was built and measured
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 — `<factor> as many/much <unit> as <REF>` — is the gate, the verb slot became a
@ -124,17 +130,21 @@ verb-free bounded connector, and `as much` joined `as many`. `_comparison_anchor
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)
### The polarity refutation — the real lesson
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.**
`test_recognizer_comparative_inject…[Alice lost…]`). The prototype fix was a closed-class polarity
**blocklist** the connector may not span — but the ruling named the deeper problem this exposes, and it
is decisive: **the verb carries a polarity bit that must be positively determined, not
assumed-safe-unless-blocklisted.** A blocklist is fail-*open* — it admits every unenumerated verb, so a
depletion verb nobody listed reaches a *complete* frame as a coherent-but-inverted graph: a **wrong** on
the run-once sealed arbiter (the round-trip net guards parse consistency, not gold-correctness). The
fail-closed doctrine never takes that trade — least of all for the **zero** measured benefit below. So
the wrong=0-safe resolution is genuine design work: frame **plus positive polarity determination** (a
neutral-polarity *allowlist*, or a small polarity classifier), exercised end-to-end where it earns its
keep — the rate-frame increment — not bolted on here.
### Footprint — provable, tiny, safe
Develop-on-tune / measure-once discipline held. A new-vs-old frame-fire diff over all 500 official cases:
@ -164,18 +174,51 @@ extension (multi-word reference) converts **0** cases end-to-end — and introdu
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
layers**, not compare-specific logic ⇒ the next increment is a **shared-layer design increment**, NOT a
practice-lane escalation, and NOT more compare-family frame surgery. **Shay's ruling (#77): next =
seeding-sentence injection** — the 135/261 "no injection (discrete_count)" wall, the dominant shared
bucket that gates every family (production/possession verbs like *made/baked/grew/wrote* that don't
inject an initial state). The compare-specific unblock (summation-question reader + **inverse compare**
known=actor, solve reference; `0441` needs both) stays queued behind it. 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.
- **refused-by-reason (traps broken out)**: polarity-inverting (lost/spent) → refused fail-closed by the
`_comparison_anchor_verb()` whitelist (the merged state, after the frame-anchoring revert; the
blocklist prototype that would have admitted-then-guarded them is deferred with the frame change).
Family funnel: 16 multiclause/nested, 10 frame-no-match, 5 number/amount-of, 5 as-ADJ, 2 double-what —
all 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).
### Deferral ruling (#77) — the split
Increment 1 merges the **compiler tier + this finding**; the **frame-anchoring reader change is held
out** (`c3aed13b` reverted from the PR). Three compounding reasons, on record:
1. **Zero measured benefit today** — 0 conversions across all 500 official cases.
2. **Its consumer is a later increment** — frame-anchoring exists to serve rate-frame admission
(increment 2). The *next* increment is seeding-sentence injection, a shared layer that does not touch
it. Nothing on the critical path needs it now.
3. **Fail-open surface at the sealed test for that zero benefit** — the polarity blocklist admits every
unenumerated verb; an unlisted depletion verb in a complete frame is a coherent-but-inverted graph =
a wrong on the run-once arbiter. The fail-closed doctrine never risks that, least of all for no gain.
Verified before merge: with the whitelist reader restored, the 5 real compare parses are **non-divergent**
`test_real_compare_cases_solved_wrong_zero` still sees **5, solves 5, wrong 0** (corridor real-reach
`0→5/500` preserved). The tier keeps its win without the frame change.
### OPEN DESIGN TASK (owned by the rate-frame increment)
**Frame-anchored comparative admission with wrong=0-safe polarity determination.** The frame is the right
gate; the open problem is the verb's polarity bit. It must be **positively determined**, not
assumed-safe-unless-blocklisted:
- a **neutral-polarity allowlist** (fail-closed: only verbs affirmatively known non-inverting admit), or
- a small **polarity classifier / lexicon** (possession/production/acquisition = neutral → `factor×ref`;
depletion/transfer/loss = inverting → refuse or model as a delta).
Deliverable when the rate-frame increment lands it: exercised end-to-end on rate frames, measured on the
disjoint split, wrong=0 preserved on the sealed arbiter. Until then the reader stays on the
`_comparison_anchor_verb()` whitelist (fail-closed) for all comparative frames.

View file

@ -1217,76 +1217,25 @@ _COMPARE_ADDITIVE_RE: Final[re.Pattern[str]] = re.compile(
rf"(?P<reference>{_COMPARE_REF})\s*\.?$"
)
# ADR-0250 reader-arc increment 1 — FRAME-ANCHORED admission (verb-free).
#
# The multiplicative comparison FRAME — "<factor> as many/much <unit> as
# <REF>" / "<N> times as many/much <unit> as <REF>" — 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 <unit> as <REF>"
# 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 <verb> 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/much <unit>" required; unit
# "third" carry the factor implicitly). "as many <unit>" 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<actor>{_ENTITY})\s+{_COMPARE_FRAME_CONNECTOR}"
r"(?:a\s+)?(?P<anchor>twice|thrice|half|quarter|third)\s+as\s+(?:many|much)\s+"
rf"^(?P<actor>{_ENTITY})\s+{_comparison_anchor_verb()}\s+"
r"(?:a\s+)?(?P<anchor>twice|thrice|half|quarter|third)\s+as\s+many\s+"
r"(?P<unit>\w+(?:\s+\w+)?)\s+as\s+"
rf"(?P<reference>{_COMPARE_REF})\s*\.?$"
)
# Multiplicative: explicit "N times as many/much <unit> as <REF>".
# Multiplicative: explicit "N times as many <unit> as <REF>".
# 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<actor>{_ENTITY})\s+{_COMPARE_FRAME_CONNECTOR}"
rf"(?P<value>{_VALUE})\s+times\s+as\s+(?:many|much)\s+"
rf"^(?P<actor>{_ENTITY})\s+{_comparison_anchor_verb()}\s+"
rf"(?P<value>{_VALUE})\s+times\s+as\s+many\s+"
r"(?P<unit>\w+(?:\s+\w+)?)\s+as\s+"
rf"(?P<reference>{_COMPARE_REF})\s*\.?$"
)