From 12098cb3d34c60b3f56e5966b7854ca75aea47bf Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 18 Jul 2026 20:54:28 -0700 Subject: [PATCH 1/5] =?UTF-8?q?docs(reader-arc):=20increment-1=20BAND=20pl?= =?UTF-8?q?an=20=E2=80=94=20{seed=20+=20comparison(#78)=20+=20question-ari?= =?UTF-8?q?thmetic}=20for=20ruling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design-first plan, ruling before any build (same gate as #76). Band = foundational spine core: seed-simple + comparison + question-arithmetic (simple/summation/difference), on the shared #78 polarity substrate. Bakes in Josh's four refinements: (1) deliberate ~30 smaller-band rationale (first end-to-end reader conversion ever + two reusable foundations, zero new hard capabilities beyond #78); (2) done-when reset to band-solve>0 wrong=0 + foundations validated, ~40 bar cumulative across increments 1-3; (3) haircut factor (real/upper-bound) as a named output; (4) TWO wrong=0 direction drivers — #78 statement polarity AND q:difference question direction, both positively determined + fail-closed. Grounds design on what EXISTS (question layer partly built: summation works, q:difference partial). Schedules q:complex decomposition next (may be corridor-tractable, could move the ~48% ceiling). --- .../increment-1-band-plan-2026-07-19.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/research/increment-1-band-plan-2026-07-19.md diff --git a/docs/research/increment-1-band-plan-2026-07-19.md b/docs/research/increment-1-band-plan-2026-07-19.md new file mode 100644 index 00000000..39c5e8dc --- /dev/null +++ b/docs/research/increment-1-band-plan-2026-07-19.md @@ -0,0 +1,135 @@ +# Increment-1 Band Plan — reader arc (DESIGN-FIRST, awaits ruling) + +**Status**: DESIGN-FIRST — ruling before any build (same gate as PR #76). +**Date**: 2026-07-19 +**Base**: `forgejo/main @ e1eb2a5c` (worktree `core-wt-seed`, branch `feat/seeding-injection` → to be +repurposed to the band). +**Model context**: unit of work = case-BAND (coverage is a per-case conjunction; see +`compare-increment-funnel-2026-07-18.md` MEASURED OUTCOME + the band map). Done-when = band-solve wrong=0, +NOT injection coverage. + +--- + +## §1 The band (approved) + +**`{ seed-simple + comparison (#78 polarity) + question-arithmetic (simple / summation / difference) }`** + +The tractable spine's *foundational core* — the two capabilities every future band reuses +(question-arithmetic + #78 polarity) plus the two most common statement types (seed, comparison), adding +**zero new hard capabilities beyond #78** (which is already scoped). Band-map upper bound ≈ **30 cases** +(size 3–4). Deliberately the ~30 band, not the 42 band — see §5. + +## §2 What EXISTS today (grounding — increment 1 EXTENDS, does not rebuild) + +- **Compare COMPILER**: merged (PR #77) — `compare_multiplicative` executes wrong=0 (27/27). The reader + frame was reverted to the `_comparison_anchor_verb()` whitelist (issue #78 open). +- **Question layer, PARTIALLY built** (`generate/math_candidate_parser.py`, `CandidateUnknown`): + - `q:summation` — aggregate-cue vocab {in total, altogether, combined, together, in all} → `entity=None` + → the 2b summation compiler. **Works today** (0101 solved via compare + summation). + - `q:simple` — "How many `` does `` have?" → single-register decode. Works. + - `q:difference` — PARTIAL: "how many more ``" Pattern-B sets `peer_count`; **direction is not yet a + positively-determined, fail-closed decision** (§4 driver 2). + - `extract_conditional_op_question_candidates` exists (some q:complex conditional forms). +- **Seed injector** (`inject_discrete_count_statement` + `_try_extract_discrete_count_anchor`): whitelists + possession {has/have/had} → `CandidateInitial`, acquisition {collected/received/bought/got} → + `CandidateOperation(add)`; **refuses every other verb and all existential "There are N X"** (fail-closed). + +So the question-arithmetic *foundation largely exists*; increment 1 **completes** it (q:difference +direction) and adds the **seed + comparison** statement readers on the shared **#78 polarity substrate**. + +## §3 The gap increment 1 builds + +1. **#78 polarity substrate (the shared foundation).** Positive polarity determination = a + **neutral-polarity ALLOWLIST**, fail-closed (NOT the reverted fail-open blocklist): + - `_NEUTRAL_SEED_VERBS` (production/possession/acquisition: made/makes, baked, grew, scored, wrote, + caught, taught, built, planted, picked, earned, harvested, found, drew, cooked, produced, + existing + has/have/had/collected/received/bought/got) → seed (`InitialPossession` or from-zero `add`). + - `_DEPLETION_VERBS` (lost/spent/gave/sold/used/donated/ate/dropped/paid/lent) → **refuse** in increment 1 + (a depletion is a delta over prior state, not a seed; modelling it is out of scope — fail-closed). + - **Unknown verb → refuse.** Allowlist, positively determined. This *closes issue #78* for both seed + and comparison, built once. +2. **Seed-simple injection** — extend the base injector via the substrate + a new **existential** path + ("There are `N` `X` [in/on the `C`]" → `InitialPossession(entity=C-or-collective, N X)`; zero polarity risk). +3. **Comparison frame-anchoring** — re-land the verb-free frame (the reverted `c3aed13b`), but the verb + slot is gated by the **same `_NEUTRAL_SEED_VERBS` allowlist** (fail-closed) rather than a blocklist. + Depletion/unknown verb in a frame → refuse. Reuses the merged compare compiler. +4. **q:difference direction (§4 driver 2)** — complete the Pattern-B path: "how many **more** A than B" → + signed `A − B`; "how many **fewer/less**" → `B − A`; **ambiguous/unsupplied direction → refuse**. +5. **q:simple / q:summation** — verify coverage on the band's cases; extend minimally only where a target + case needs it (measured on tune). + +## §4 The TWO wrong=0 direction drivers (Josh point 4 — positively determined, fail-closed) + +- **Driver 1 — statement polarity (#78):** neutral-seed allowlist vs depletion. Seed vs deplete decided + by verb class, fail-closed on unknown. +- **Driver 2 — question q:difference direction:** "more…than" → `A−B`, "fewer…than" → `B−A`, ambiguous → + **refuse**. Same discipline as #78 and the inverse-compare lesson resurfacing in the question layer. A + wrong direction here is a coherent-but-inverted answer on the run-once arbiter — the exact fail-open + hazard we refused to ship in #77. + +## §5 Deliberate smaller-band rationale (Josh point 1 — recorded so it is NOT read as under-ambition) + +The map says a "real bar" needs size-4/~42; this band is the ~30, size-3–4 one, **by design**: + +- **Increment 1's job is the FIRST end-to-end reader conversion the arc has ever landed.** Corridor + real-reach is still 5/500 — *all* from the compiler tier; **no reader increment has converted a single + case.** The bar for increment 1 is crossing zero, on foundations. +- It ships the **two foundations every future band reuses** (question-arithmetic completion + #78 polarity) + and adds **zero new hard capabilities beyond #78** (already scoped). +- The 42 band adds **multi-compound** — a *new* hard capability. Stacking a new hard capability onto the + first-ever conversion attempt raises the odds increment 1 *also* lands ~0. **Smaller-but-foundational + maximizes the probability of the critical first win**, and every capability it ships is reused, never + redone. + +## §6 Done-when (Josh point 2 — reset for the band model; X=15-per-layer retired) + +Increment 1 is DONE when: +1. **band-solve > 0 wrong=0** on the disjoint measure split — the first real reader conversion; AND +2. both foundations **validated and reuse-ready**: #78 polarity allowlist (fail-closed, wrong=0 on the + depletion confusers) + q:difference direction (fail-closed on ambiguity); AND +3. wrong=0 held on tune + measure + smoke; cross-family parses stable-or-explained. + +The **~40 "real bar" is CUMULATIVE across increments 1–3** (foundation → +multi-compound ≈ 42 → ++currency/rate ≈ 63), NOT increment 1 alone. Forcing one increment to clear a bar the map says needs five +capabilities would recreate the unsatisfiable-threshold problem at band scale. + +## §7 Measurement (Josh point 3 + discipline) + +- **Develop-on-tune / measure-once.** wrong=0 verified on tune throughout; measure split run ONCE at the end. +- **HAIRCUT FACTOR = a named increment-1 output**: `real band-solve ÷ ~30 upper bound`. This ratio — the + single most valuable number beyond the conversions themselves — recalibrates the projected yield of every + future band. Reported explicitly with the triple. +- Funnel re-run after each layer; refusal-histogram pinned + asserted stable-or-explained; cross-family + parses unchanged-or-better; the depletion-confuser suite proves the polarity allowlist fail-closed. + +## §8 Out of scope + roadmap boundary + +- **Intractable-now (52%, 135/258):** `q:complex` (101 — conditional/averaging/multi-step), percent, + simultaneous-equations, fraction/partitive. Explicitly OUT. +- **Increment 2–3 (the cumulative bar):** `multi-compound` (→42), `currency/rate` (→63). +- Tractable holdout ceiling ≈ **48%** until the q:complex decomposition (§9) revises it. + +## §9 Scheduled NEXT (not increment-1 scope — Josh's forward directive) + +**Decompose `q:complex` (101 cases) by sub-type BEFORE treating 48% as a fixed ceiling.** Hypothesis: +"averaging" = sum-over-count and "simple multi-step" = a turn program — both may be **corridor-tractable +with the compiler we already have**, hiding inside the "intractable" 101. If even 30–40 are reachable, the +ceiling moves materially. Measure before planning around 48% as fixed. Does NOT touch increment-1 scope. + +Roadmap boundary to rule on in principle: when the tractable bands are exhausted (~48% reached), the +serve / practice / tackle-intractable fork opens. The conjunction structure is a wall for the practice lane +*too* (reward = full-conjunction case-solve is near-zero until a closure happens by luck), so the ceiling +argues for **continued design-first**, not switching. + +## §10 Open questions for the ruling + +- **Q1 — depletion verbs: refuse vs model-as-subtract in increment 1?** Plan says refuse (fail-closed, + smallest scope). Modelling "X had M, lost N" needs prior-state + subtract-operation wiring — proposed + for a later increment. Confirm refuse-only for now. +- **Q2 — existential entity binding.** "There are N X in the C" → entity = C (container). Container-less + ("There are N X") → collective entity from the noun, or refuse? Plan leans refuse when no container/subject + grounds the register (fail-closed), admit only container-bound. Confirm. +- **Q3 — is q:difference in-scope for increment 1, or deferred to keep the band at seed+comparison+q:simple/summation?** + Plan includes it (23 cases, and it completes the question-arithmetic foundation), but it adds the 2nd + polarity driver. If you'd rather minimize the first-conversion risk surface, q:difference can drop to + increment 1.5. Recommend keeping it (foundational, and its cases are otherwise stranded). From 387f0656498cb9742ab0d1e071e804fb33800ded Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 18 Jul 2026 21:18:02 -0700 Subject: [PATCH 2/5] =?UTF-8?q?feat(reader-arc):=20#78=20positive-polarity?= =?UTF-8?q?=20allowlist=20substrate=20(seed=20+=20comparison)=20=E2=80=94?= =?UTF-8?q?=20foundations,=20band-solve=3D0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two reusable foundations of increment 1, both verified wrong=0: - NEUTRAL_COUNT_VERBS (math_roundtrip): the shared positive-polarity ALLOWLIST (production/acquisition/possession-count verbs), fail-closed. Extends ADD_VERBS with curated neutral production verbs (score/write/teach/plant/harvest/produce/ draw/sing). Depletion/transfer (SUBTRACT/TRANSFER_VERBS) excluded by positive determination — 'Alice lost twice as many' refuses, not via a blocklist. - Seed reader: discrete_count matcher acquisition path aligned to the allowlist (made/baked/grew/scored/wrote/taught now inject; depletion refuses). - Comparison reader: _comparison_anchor_verb() allowlist extended to the same NEUTRAL_COUNT_VERBS (keeps fail-closed; captures scored/caught/wrote frames). Verified: production injects, depletion refuses; 71 pinned comparative tests pass (polarity guards now via allowlist); tune wrong=0; smoke 176 green. FINDING (measured 4 ways: yield harness, band map, band oracle, capability histogram): band-1 as scoped {seed + forward-comparison + q:simple/summation} converts 0 tune cases. Its otherwise-in-band cases STRAND on multi-compound (25) and compare-additive (17) — capabilities outside the ~30 band. The conjunction recurs at sub-capability granularity; the band-map taxonomy was too coarse. Foundations validated + reuse-ready (done-when part 2); band-solve>0 (part 1) needs the band to grow to +compare-additive(+multi-compound) — a scope/done-when ruling for Josh. No merge until ruled. --- .../increment-1-band-plan-2026-07-19.md | 24 +++++++++++- generate/math_candidate_parser.py | 38 ++++++++++--------- generate/math_roundtrip.py | 36 ++++++++++++++++++ generate/recognizer_match.py | 11 ++++-- 4 files changed, 86 insertions(+), 23 deletions(-) diff --git a/docs/research/increment-1-band-plan-2026-07-19.md b/docs/research/increment-1-band-plan-2026-07-19.md index 39c5e8dc..7abf0a30 100644 --- a/docs/research/increment-1-band-plan-2026-07-19.md +++ b/docs/research/increment-1-band-plan-2026-07-19.md @@ -1,6 +1,26 @@ -# Increment-1 Band Plan — reader arc (DESIGN-FIRST, awaits ruling) +# Increment-1 Band Plan — reader arc (RULED — APPROVED TO BUILD) -**Status**: DESIGN-FIRST — ruling before any build (same gate as PR #76). +## RULING (Josh, PR #79, 2026-07-19) — approved to build under these + +- **Q1 depletion → REFUSE, confirmed.** Count depletion-containing band cases as *expected haircut*, not bugs. +- **Q2 existential → admit CONTAINER-BOUND only, refuse container-less, confirmed.** If tune surfaces a + container-bound case that is actually a partitioned total, refuse that sub-form too. +- **Q3 q:difference → DEFER the capability, CLOSE the hazard now.** Its 23 cases likely strand on + `multi-compound` (increment 2), so building the direction driver now is risk without in-band yield — + defer to where its cases convert, and **verify the co-occurrence to confirm**. BUT make the existing + partial Pattern-B ("how many more") path **fail-closed immediately**: if it can emit a guessed-direction + difference today, that is a latent wrong=0 hazard to close regardless. **Increment 1 then carries EXACTLY + ONE wrong=0 driver (#78).** +- **Sharpening 1** — allowlist-miss refusals are recorded as **curriculum** (unknown-verb refusals feed the + practice/coverage backlog, not treated as failures). +- **Sharpening 2** — the sha256 tune/measure split stays **FIXED across increments 1–3** (the ~40 bar is + cumulative; `evals/gsm8k_math/holdout_dev/v1/split.py` is frozen). +- **Sharpening 3** — the **haircut factor** ships with the triple in the final report. + +The §3/§4/§6 design below is unchanged EXCEPT: q:difference (item 4) becomes *hazard-close only*, not a +built capability; increment 1 carries one wrong=0 driver (#78), not two. + +**Status**: RULED — approved to build (measure-once). Design-first gate cleared (same gate as PR #76). **Date**: 2026-07-19 **Base**: `forgejo/main @ e1eb2a5c` (worktree `core-wt-seed`, branch `feat/seeding-injection` → to be repurposed to the band). diff --git a/generate/math_candidate_parser.py b/generate/math_candidate_parser.py index 6ebf4c1f..856b6814 100644 --- a/generate/math_candidate_parser.py +++ b/generate/math_candidate_parser.py @@ -49,6 +49,7 @@ from generate.math_problem_graph import ( ) from generate.math_roundtrip import ( ADD_VERBS, + NEUTRAL_COUNT_VERBS, SUBTRACT_VERBS, TRANSFER_VERBS, WORD_NUMBERS, @@ -1188,25 +1189,26 @@ def _resolve_reference_token(raw: str) -> tuple[str, str]: return collapsed, collapsed +# ADR-0250 increment 1 (#78) — the comparison-frame verb slot is a POSITIVE +# polarity ALLOWLIST, fail-closed: the shared NEUTRAL_COUNT_VERBS (production / +# acquisition / possession-count verbs — made/baked/grew/scored/wrote/taught/…), +# plus a few activity/possession verbs whose comparison reading is neutral +# (does/takes/gains/studies/reads + the possession lemmas). Depletion/transfer +# verbs (lose/spend/give/sell/win/use) are NOT in the set → the frame refuses +# them by positive determination (never assume-safe-unless-blocklisted). This +# is the same allowlist the seed reader uses — one piece of verb knowledge, +# built once. The round-trip filter + reference grounding remain the wrong=0 net. +_COMPARISON_ANCHOR_VERBS: Final[frozenset[str]] = NEUTRAL_COUNT_VERBS | frozenset({ + "has", "have", "had", "holds", "hold", "held", "owns", "own", "owned", + "does", "do", "did", "takes", "take", "took", + "gains", "gain", "gained", "studies", "study", "studied", "reads", "read", +}) + + def _comparison_anchor_verb() -> str: - # ADR-0131.G.2a — widen the comparison anchor verb beyond 'has'/'have'. - # The verb here only names the action whose *quantity* is being compared - # ("A N more/×-as-many X than/as B"); it does not carry polarity - # the way accumulation verbs do, so a closed set of non-inverting action - # verbs is wrong=0-safe (the round-trip filter still requires the - # comparator anchor + reference actor to ground). The set reuses the - # already-vetted legacy math_parser._COMPARE_VERB lemmas plus the - # production/activity verbs observed in real GSM8K comparative statements - # ('does'/'collected'/'gained'/'studied' …). - # - # Deliberately EXCLUDED (polarity-inverting in a comparison context — - # admitting them could read the comparison backwards → wrong>0): - # lose/lost, win/won, spend/spent, use/used, give/gave, sell/sold. - return ( - r"(?:has|have|had|gets|get|got|takes|take|took|buys|buy|bought|" - r"does|do|did|makes|make|made|collects|collect|collected|" - r"gains|gain|gained|studies|study|studied|reads|read)" - ) + return "(?:" + "|".join( + sorted(_COMPARISON_ANCHOR_VERBS, key=len, reverse=True) + ) + ")" _COMPARE_ADDITIVE_RE: Final[re.Pattern[str]] = re.compile( diff --git a/generate/math_roundtrip.py b/generate/math_roundtrip.py index 2e2ef697..8195e2db 100644 --- a/generate/math_roundtrip.py +++ b/generate/math_roundtrip.py @@ -78,6 +78,42 @@ ADD_VERBS: Final[frozenset[str]] = frozenset({ "pack", "packs", "packed", "build", "builds", "built", "grow", "grows", "grew", + # ADR-0250 increment 1 — curated neutral-gain production verbs the reader + # band needs (each asserts a COUNT of items the actor gains/produces; all + # verified gain-polarity, none depletion/transfer). Refusal-preferring: + # unknown verbs still refuse (curriculum). + "score", "scores", "scored", + "write", "writes", "wrote", + "teach", "teaches", "taught", + "plant", "plants", "planted", + "harvest", "harvests", "harvested", + "produce", "produces", "produced", + "draw", "draws", "drew", + "sing", "sings", "sang", +}) + + +# ADR-0250 increment 1 — the #78 positive-polarity ALLOWLIST (fail-closed). +# Neutral-polarity verbs that assert a COUNT of discrete possessable items the +# actor GAINS or PRODUCES — safe to read as a seed (initial/add) or as the verb +# anchor of a comparison frame ("A N times as many X as B"). This is the +# ONE piece of verb knowledge the seed + comparison readers share, built once. +# DELIBERATELY EXCLUDES: SUBTRACT_VERBS / TRANSFER_VERBS (polarity-inverting → +# the compared/seeded quantity would be a loss, reading backwards → wrong>0); +# and the ambiguous add-verbs save/add/slice/pack (save = "saved 20 minutes" is +# not a possessed count; add/slice/pack take non-count readings). Positive +# determination, never assume-safe-unless-blocklisted; unknown verb → refuse. +NEUTRAL_COUNT_VERBS: Final[frozenset[str]] = frozenset({ + "bake", "bakes", "baked", "make", "makes", "made", "cook", "cooks", "cooked", + "build", "builds", "built", "grow", "grows", "grew", + "buy", "buys", "bought", "get", "gets", "got", "find", "finds", "found", + "receive", "receives", "received", "earn", "earns", "earned", + "pick", "picks", "picked", "collect", "collects", "collected", + "gather", "gathers", "gathered", "catch", "catches", "caught", + "score", "scores", "scored", "write", "writes", "wrote", + "teach", "teaches", "taught", "plant", "plants", "planted", + "harvest", "harvests", "harvested", "produce", "produces", "produced", + "draw", "draws", "drew", "sing", "sings", "sang", }) # Surface verbs that grammatically place the actor as the *loser* of the diff --git a/generate/recognizer_match.py b/generate/recognizer_match.py index 92c29c1c..570106b5 100644 --- a/generate/recognizer_match.py +++ b/generate/recognizer_match.py @@ -32,6 +32,7 @@ from dataclasses import dataclass from typing import Any, Final, Literal, Mapping from evals.refusal_taxonomy.shape_categories import ShapeCategory +from generate.math_roundtrip import NEUTRAL_COUNT_VERBS from generate.recognizer_registry import RatifiedRecognizer @@ -1050,7 +1051,11 @@ def _try_extract_discrete_count_anchor( verb = m.group("verb").lower() if verb in _POSSESSION_VERBS: anchor_kind = "possession" - elif verb in _ACQUISITION_VERBS: + elif verb in NEUTRAL_COUNT_VERBS: + # ADR-0250 increment 1 — #78 positive-polarity allowlist. Production / + # acquisition verbs (made/baked/grew/scored/wrote/taught/…) assert a + # count the actor gains → acquisition (CandidateOperation(add), from + # zero). Depletion/transfer verbs are NOT in the allowlist → refuse. anchor_kind = "acquisition" else: return None @@ -1201,10 +1206,10 @@ def _try_extract_compound_discrete_count_anchors( verb = head_m.group("verb").lower() if verb in _POSSESSION_VERBS: anchor_kind: Literal["possession", "acquisition"] = "possession" - elif verb in _ACQUISITION_VERBS: + elif verb in NEUTRAL_COUNT_VERBS: anchor_kind = "acquisition" else: - return None # head verb not in whitelist — refuse compound + return None # head verb not in the #78 neutral-polarity allowlist — refuse compound def _resolve_count_kind(count_token: str) -> str | None: if count_token.isdigit(): From c217af31ee1dd8b5f387ab1dcbae10df0feba2ce Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 18 Jul 2026 21:37:16 -0700 Subject: [PATCH 3/5] docs(reader-arc): band-solve=0 outcome + minimal-convertible-band measurement Records the increment-1 outcome straight (band-solve=0, ~30-band bet falsified; foundations = banked byproduct not success) + the minimal-band measurement: no small band converts; taxonomy not at bedrock (GSM8K statements are per-sentence multi-capability); multi-compound highest-leverage (70/106 tractable needed-sets) but insufficient alone; q:complex biggest intractable wall (~35 cases). Increment-2 recommendation = CASE-FIRST (target specific closest cases, build their exact end-to-end needs) not capability-first, since the taxonomy fragments. Foundations verified full-500 wrong=0, smoke 176. --- .../increment-1-band-plan-2026-07-19.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/research/increment-1-band-plan-2026-07-19.md b/docs/research/increment-1-band-plan-2026-07-19.md index 7abf0a30..655b33fa 100644 --- a/docs/research/increment-1-band-plan-2026-07-19.md +++ b/docs/research/increment-1-band-plan-2026-07-19.md @@ -21,6 +21,47 @@ The §3/§4/§6 design below is unchanged EXCEPT: q:difference (item 4) becomes built capability; increment 1 carries one wrong=0 driver (#78), not two. **Status**: RULED — approved to build (measure-once). Design-first gate cleared (same gate as PR #76). + +--- + +## BUILD OUTCOME (2026-07-19) — band-solve=0; the ~30-band bet is FALSIFIED + +The headline stays band-solve=0. The two foundations are **built and verified**; they are the banked +byproduct, **not** a success against increment 1's conversion goal. + +### Shipped (verified, wrong=0-safe) +The **#78 positive-polarity allowlist substrate** — `NEUTRAL_COUNT_VERBS` (`math_roundtrip.py`, extends +`ADD_VERBS` with curated neutral production verbs), shared by the seed matcher (acquisition path) and the +comparison `_comparison_anchor_verb()`. Depletion/transfer verbs refuse by **positive determination** +(not a blocklist). Verified: production injects / depletion refuses; **71 pinned comparative tests pass**; +**full-500 wrong=0** (tune 261 + measure 239); PARSED unchanged (tune 3, measure 2 — no regression, no +conversion); **smoke 176 green**. Merge gate met: full-500 wrong=0 + refusal histogram stable-or-explained. + +### band-solve = 0, measured FOUR ways +yield harness, band map, band oracle (rewrite-and-run), capability histogram — all agree: the scoped band +`{seed + forward-comparison + q:simple/summation}` converts **0** tune cases. Its otherwise-in-band cases +**strand on capabilities outside the band**: 25 need multi-compound, 17 need compare-additive. §5 named +this exact risk; it landed. + +### Minimal-convertible-band measurement (Josh's option-3 spec) +- **No small band converts.** The band oracle converts 0 with `{seed + comparison + q}`. +- **Taxonomy is NOT at bedrock.** The refined atomic classifier's "smallest tractable set" `{seed, q:simple}` + (4 cases) dissolved on inspection — every one actually needs rate / compare / currency / copula + (e.g. "James **spends 30 minutes twice a day** on meditation" = rate + temporal + unit-conversion in ONE + sentence). **Real GSM8K statements are individually multi-capability**; the conjunction recurs *within* + a sentence, so a capability list keeps fragmenting. +- **multi-compound is the single highest-leverage capability** — on the critical path for **70/106 (66%)** + of tractable needed-sets — but insufficient alone (its cases also need rate/currency/etc.). +- **q:complex is the biggest intractable wall** (~35+ cases gated on it: 3 size-1, 18+14 size-2 needed-sets). +- **Closest cases** (0000/0001/0148/0082) all need complex compare forms (chained / mass-noun entities / + "than" + aggregate reference) + summation/difference — still multi-capability. + +### Implication for increment 2 (the first-conversion increment) — CASE-FIRST, not capability-first +Because the taxonomy fragments, no small capability band is a real floor. The path to the first conversion +is **case-first**: pick 2–3 specific closest tune cases, hand-enumerate their *exact* end-to-end needs +(however many capabilities), build precisely that, convert them, measure. The "minimal convertible band" is +not a small capability set — it is whatever a chosen handful of real cases need, built end-to-end. This is +the recommendation carried to Josh with the foundations PR. **Date**: 2026-07-19 **Base**: `forgejo/main @ e1eb2a5c` (worktree `core-wt-seed`, branch `feat/seeding-injection` → to be repurposed to the band). From 2da1a338395b804b39ad227b6aa08803420b747d Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 18 Jul 2026 21:44:34 -0700 Subject: [PATCH 4/5] fix(reader-arc): compound extractor refuses no-digit compare clauses (wrong=0 hazard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compound discrete-count extractor's all-or-nothing tail guard is digit- based, so a no-digit compare clause in a conjunctive list ('buys 4 lbs beans, 6 lbs milk, and twice the amount of carrots as beans') was silently DROPPED — injecting 4+6 and losing the carrots (a wrong=0 landmine, masked only by the case refusing elsewhere). The #78 substrate widens the compound extractor's verb reach, so harden it: refuse the whole compound on any ' the amount/number of' / 'twice/thrice/times/half the' / 'double/triple' surface. Verified: 0082 compound now refuses fail-closed; tune+measure wrong=0; PARSED unchanged (3/2); smoke 176 green. --- generate/recognizer_match.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/generate/recognizer_match.py b/generate/recognizer_match.py index 570106b5..1d506294 100644 --- a/generate/recognizer_match.py +++ b/generate/recognizer_match.py @@ -1129,6 +1129,14 @@ _COMPOUND_REFUSE_SUBSTRINGS: Final[tuple[str, ...]] = ( " half as ", " twice as ", " thrice ", "%", " percent", " half of ", " quarter of ", " third of ", + # ADR-0250 increment 1 — a compare clause using the " the + # amount/number of " surface carries NO digit, so the all-or-nothing + # tail-digit guard below cannot see it and would silently DROP it (e.g. + # "buys 4 lbs beans, 6 lbs milk, and twice the amount of carrots as beans" + # injected 4+6 and lost the carrots — a wrong=0 hazard). Refuse the whole + # compound when any such compare surface appears. + " twice the ", " thrice the ", " times the ", " half the ", + " the amount of ", " the number of ", " double ", " triple ", ) # Fraction literal pattern (matched against raw statement, not padded). From 81688465a580d35d5f974f35b3a577e4ffc1eb4b Mon Sep 17 00:00:00 2001 From: Shay Date: Sat, 18 Jul 2026 21:57:36 -0700 Subject: [PATCH 5/5] =?UTF-8?q?test(reader-arc):=20Q3=20hazard-close=20?= =?UTF-8?q?=E2=80=94=20lock=20q:difference=20fail-closed=20(no=20guessed?= =?UTF-8?q?=20direction)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discharges Josh's Q3 ruling: defer the q:difference capability, close the hazard now. _pattern_b_comparative_candidates is inert by construction (returns []), so 'how many more/fewer … than …' questions refuse end-to-end even when every statement injects cleanly. This suite LOCKS that so a future D.5 wiring cannot reopen the guessed-direction hole without positively determining the more/fewer direction. Verified inert-safe by 5 direct tests. --- .../test_adr_0250_q_difference_failclosed.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_adr_0250_q_difference_failclosed.py diff --git a/tests/test_adr_0250_q_difference_failclosed.py b/tests/test_adr_0250_q_difference_failclosed.py new file mode 100644 index 00000000..86003201 --- /dev/null +++ b/tests/test_adr_0250_q_difference_failclosed.py @@ -0,0 +1,39 @@ +"""ADR-0250 increment 1 — Q3 hazard-close: q:difference is fail-closed. + +Josh's Q3 ruling deferred the q:difference *capability* (its cases strand on +multi-compound) but required the hazard be CLOSED now: the partial Pattern-B +"how many more/fewer" path must never emit a guessed-direction answer. The +direction bit ("more…than" → A−B vs "fewer…than" → B−A) is a wrong=0 driver +that must be positively determined; until it is, the question MUST refuse. + +`_pattern_b_comparative_candidates` is inert by construction (returns []), so +these questions refuse end-to-end. This suite LOCKS that so a future D.5 wiring +cannot silently reopen the hazard without the direction determination. The +critical cases are the ones where every STATEMENT injects cleanly — a guessed +answer would be most likely there, and matching gold by luck would be a +sealed-test wrong wearing a success costume. +""" +from __future__ import annotations + +import pytest + +from generate.math_candidate_graph import parse_and_solve + +# Each: statements parse cleanly; only the q:difference question is in play. +# Every one MUST refuse (answer is None) — never a signed-difference answer. +_Q_DIFFERENCE_PROBLEMS = [ + "Alice has 10 apples. Bob has 4 apples. How many more apples does Alice have than Bob?", + "Alice has 10 apples. Bob has 4 apples. How many fewer apples does Bob have than Alice?", + "Whitney bought 9 books and 7 magazines. How many more books than magazines did Whitney buy?", + "Tom made 12 cookies. Sam made 5 cookies. How many more cookies did Tom make than Sam?", + "Mary has a bag holding 20 pounds. She buys 4 pounds of beans. How many more pounds can Mary fit?", +] + + +@pytest.mark.parametrize("problem", _Q_DIFFERENCE_PROBLEMS) +def test_q_difference_refuses_no_guessed_direction(problem: str) -> None: + result = parse_and_solve(problem) + assert result.answer is None, ( + "q:difference must refuse (deferred capability, hazard closed): a " + f"guessed-direction answer {result.answer!r} is a wrong=0 hole. Problem: {problem!r}" + )