* chore(ADR-0163.C): land three Phase C pending proposals in live log Phase C (#301) shipped the CLI but its PR dry-run wrote to a tmp log path. This commit moves the three Phase C proposals into the live teaching/proposals/proposals.jsonl so the Phase B→C audit trail is visible in the proposal log and the proposals are ready for the operator to ratify after Phase D ships. Proposals (all state=pending, kind="exemplar_corpus"): - 59223f13722f906a1cf9b65d9b01c990 — descriptive_setup_no_quantity - 46ce297f797ff16da12db5de422ca3c9 — rate_with_currency - a3b892546977c5f0f64c578d6052adbd — temporal_aggregation Produced by `core teaching propose-from-exemplars --all` against the live Phase B corpora. No ratification (ADR-0161 §5 — only the repo owner ratifies). The Phase D admissibility-replay gate confirmed replay_equivalent=true, wrong_count_delta=0 for all three. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(ADR-0163.D): wire ratified RecognizerSpecs into math_candidate_graph admissibility surface Phase D is the first PR to extend the math admission surface. The audit (#294) said the gap was admission, not operators, algebra, substrate, or packs. Phase A measured the refusal taxonomy. Phase B authored seeds. Phase C synthesized recognizers. Phase D wires those recognizers into generate/math_candidate_graph.py. Modules - generate/recognizer_registry.py — pure projection over the proposal log. Only proposals with source.kind="exemplar_corpus" AND review_state="accepted" enter the tuple. Sorted by (review_date, proposal_id). In-process cache keyed on log (mtime, sha256) — no filesystem cache (ADR-0161 §1). Malformed accepted specs raise RegistryLoadError citing the offending proposal_id; silent drops are forbidden. - generate/recognizer_match.py — per-category rules-only matchers (no LLM, no embedding, no learned classifier). Honors the Phase C synthesizer's narrowness rule: out-of-corpus currency symbols, window units, and per-unit values do NOT match. Three matchers: _match_descriptive_setup_no_quantity (zero-quantity surface), _match_temporal_aggregation (event_count_per_window with observed_window_units/quantifiers honored), _match_rate_with_currency (currency_per_unit_rate with observed currency/per-unit/amount-kind honored). - generate/math_candidate_graph.py — narrowest-edit guard at the per-statement choice loop. Before the existing "no admissible candidate for statement" refusal, consult the ratified registry. Recognized statements are dropped from per_sentence_choices (zero math state) so the Cartesian product is identical to "this statement was never there." Empty registry is a no-op — backward compatibility preserved byte-identically. Downstream consumption of parsed_anchors (turning recognized rate/temporal surfaces into solver state that produces concrete answers) is Phase E follow-up. Tests (32 new) - tests/_phase_d_fixture.py — synthetic in-memory ratified registry built from the three Phase C pending proposals' content. Per ADR-0161 §5 the agent does NOT ratify the live log; the synthetic registry round-trips the real RecognizerSpec bytes the operator will ratify after Phase D ships. - tests/test_recognizer_registry.py (9) — empty/pending/wrong-kind filtering, sort order, malformed-spec rejection, cache hit + invalidation, live-log Phase C audit check. - tests/test_recognizer_match.py (14) — per-category positive cases, narrowness (out-of-corpus surface forms rejected), no-LLM import check. - tests/test_candidate_graph_recognizer_wiring.py (7) — empty registry preserves existing refusal; synthetic registry: recognized statements no longer trigger per-statement refusal; wrong_count_delta == 0 on GSM8K train_sample; capability axes G1.. G5+S1 wrong=0 unchanged; per-category admission counts on the refused-set; unrecognized statements still refuse with the existing reason. - tests/test_phase_d_replay_evidence.py (2) — full admissibility replay gate under synthetic registry: replay_equivalent=true, wrong_count_delta=0, every capability axis wrong=0; each ratified recognizer admits >= 1 train_sample statement (wiring is consequential). Per-category fixture-based admission counts (synthetic registry vs GSM8K train_sample refused-set sentences): - descriptive_setup_no_quantity: 40 - rate_with_currency: 2 - temporal_aggregation: 7 Narrowness-invariant negative case results (matcher correctly returns None on out-of-corpus / load-bearing-math surfaces): - rate_with_currency: "She paid $5 for the book." (no per-unit) - temporal_aggregation: "On Saturday she went to the store." (single day token) - descriptive_setup_no_quantity: "There are some kids in camp." (indefinite quantifier) Candidates for Phase B round 2 (3 of 20 temporal seeds match the spec's structural commitment but not my surface regex — author_notes explicitly flagged these as schema-gap edge cases): - ta-v1-0004 "Mark does a gig every other day for 2 weeks." - ta-v1-0012 "Robin walks 4 dogs every other day around the park." - ta-v1-0019 "The pump fills the tank with 80 gallons over 6 hours." Three landed wirings DO NOT shift the GSM8K train_sample baseline counts under fixture (correct=3, wrong=0, refused=47 unchanged) — Phase D's narrow wiring is wrong=0 safe by construction; lift to "correct" requires Phase E's downstream parser-side consumption of parsed_anchors. Capability axes G1..G5+S1 wrong=0 unchanged. Cross-refs: ADR-0163 (Phase D), ADR-0057 (proposal review), ADR-0151 (auto-proposal), ADR-0161 §5 (ratification boundary), Phase A PR #297, Phase B PR #298, Phase C PR #301. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
20 KiB
ADR-0163 — Path to GSM8K mastery: candidate-graph admissibility via the contemplation/HITL corridor
Status: Proposed Date: 2026-05-26 Author: Shay Anchor: thesis-decoding-not-generating Parent: ADR-0114a — Capability Obligations, ADR-0119 — GSM8K eval lane Companions: ADR-0149 — Recognizer pipeline, ADR-0151 — Auto-proposal pipeline, ADR-0152 — Learning-arc proof corridor, ADR-0155 — CI contemplation runner, ADR-0161 — HITL async queue, ADR-0132/0133/0134/0135 — Binding graph
Context — what the audit found
A scoping pass across the unlanded math branches and the actually-shipped
state on main produced a result that reframes the math architecture
question entirely.
State of the math substrate on main
The following components are already landed (worktrees on disk are stale forks of work that landed via other PR paths):
| Component | Status |
|---|---|
generate/binding_graph/ (all 7 modules: model, allocation, adapter, admissibility, units, question_target, __init__) |
✅ landed (ADR-0132/0133/0134/0135) |
generate/math_versor_arithmetic.py (221 lines) |
✅ landed (ADR-0139/0140) |
generate/math_symbolic_equivalence.py (97 lines) + math_symbolic_normalizer.py (371 lines) |
✅ landed (ADR-0131.1) |
generate/math_parser.py (1,106 lines) |
✅ landed |
generate/math_candidate_parser.py (2,232 lines) |
✅ landed |
generate/math_candidate_graph.py (511 lines) |
✅ landed |
generate/math_problem_graph.py (490 lines) |
✅ landed |
generate/math_solver.py (506 lines), math_verifier.py (501 lines), math_realizer.py (422 lines), math_roundtrip.py (484 lines) |
✅ landed |
| Capability axis lanes G1..G5, S1 | ✅ landed with v1 corpora |
Capability axis lane results on main
Every named capability axis passes its controlled lane at 100% with
wrong = 0:
| Lane | Cases | Solved correct | Refused as expected | Wrong | Verdict |
|---|---|---|---|---|---|
| G1 verb classes | 20 | 20 | 0 | 0 | ✅ exit_criterion passed |
| G2 comparatives | 29 | 29 | 0 | 0 | ✅ wrong_count_is_zero |
| G3 numerics v1 | 26 | 20 | 6 | 0 | ✅ overall_pass: true |
| G4 multi-clause | 32 | 32 | 0 | 0 | ✅ wrong_count_is_zero |
| G5 aggregate | 20 | 20 | 0 | 0 | ✅ wrong_count_is_zero |
| S1 rate events | 20 | 20 | 0 | 0 | ✅ wrong_count_is_zero |
GSM8K train-sample result on main (50 cases, ADR-0126)
correct: 0
refused: 50
wrong: 0
exit_criterion: { correct_min: 10, wrong_max: 0, passed: false }
Every refusal reason is identical in shape:
candidate_graph: no admissible candidate for statement: "<STATEMENT>"
Sample refused statements:
"Tina makes $18.00 an hour."— rate with currency"She splits it up into 25-foot sections."— division-into-sections + unit"The student council sells scented erasers in the morning before school starts to help raise money for school dances."— descriptive setup, no extractable quantity"There are some kids in camp."— indefinite quantity ("some")"In one hour, Addison mountain's temperature will decrease to 3/4 of its temperature."— rate of change + fraction
The reframe
The gap is not in operator algebra, not in the binding graph
internals, not in symbolic equivalence, not in the capability
axes themselves. The gap is in generate/math_candidate_graph.py —
the admissibility surface that turns a natural-language statement into
a candidate the downstream pipeline can consume.
The capability axes pass at 100% because they test statement shapes the candidate-graph already admits. GSM8K refuses at 100% because its statements span shapes the candidate-graph has never been taught.
Every downstream component (binding graph, versor arithmetic, symbolic equivalence, multi-clause decomposer, aggregator) is mastered in isolation. The lift to GSM8K is admissibility expansion, not operator development.
This is the most consequential single finding in the math work to date. It reframes the entire roadmap.
Decision — what to build, in what order, under what doctrine
Doctrine
Three non-negotiables:
-
wrong = 0is invariant at every phase. Awronganswer is an architectural regression, not a tuning miss. Arefusedanswer is honest; awronganswer is not. Every exit criterion in this ADR readswrong_max: 0. -
No hand-rolled recognizers. New statement shapes land via the
DerivedRecognizerpipeline that ADR-0149/0154 already wired. The recognizer comes from corpus exemplars, not from operator-written regex. This honors thesis-decoding-not-generating: we teach the engine to find better, not stuff it with more found patterns. -
Every new shape lands through the contemplation → proposal → review corridor. No parallel learning path. Recognizers are proposed by contemplation (ADR-0150/0152), gated by replay-equivalence (ADR-0057), reviewed by the operator via the HITL queue (ADR-0161), and admitted to the active corpus only on ratification.
These three rules, applied consistently, make admissibility expansion a capability of the engine rather than an editing task on the operator.
Phases
Phase A — Refusal taxonomy (measure before building)
Goal: categorize every refused statement in the GSM8K train sample by statement shape, not by content.
Deliverables:
evals/gsm8k_math/refusal_taxonomy/v1/taxonomy.jsonl— one record per refused statement, carryingcase_id,statement,refusal_reason, and a typedshape_categoryenum.- Initial shape categories (extend as the corpus grows):
rate_with_currency— "Tina makes $18.00 an hour."unit_partition— "She splits it up into 25-foot sections."descriptive_setup_no_quantity— pure context with no extractable measurement.indefinite_quantity— "some", "a few", "several".fractional_rate_of_change— "decreases to 3/4 of its temperature".comparative_with_unit— "20% more than", "twice as long as".nested_question_target— "How many more than X did Y have?"temporal_aggregation— "over five days, she earns…"conditional_quantity— "if she had 2 more, she would have…"
- A new eval lane
evals/refusal_taxonomy/that runs the categorizer over an arbitrary refused-statement set and emits the histogram. - Acceptance: every refused statement in the 50-case sample has a
typed
shape_category; "uncategorized" count is reported but non-blocking.
This phase produces no recognizers and no corpus changes. It is the load-bearing measurement that prevents Phase B from chasing the wrong gap.
Phase B — Exemplar corpus per shape category
Goal: for each top-N shape category from Phase A, hand-author a small
exemplar corpus (≤ 20 statements per category) with the expected
MathProblemGraph shape annotated.
Deliverables:
teaching/admissibility_exemplars/<shape_category>_v1.jsonlper category, each line carryingstatement,expected_graph, andprovenance.- The exemplar corpus is reviewed-evidence floor material under ADR-0057 — pack-consistent, boundary-clean, polarity affirms.
- Top-N is chosen by Phase A's histogram. Three categories per round; ratchet rather than scope creep.
This phase is the only place hand-authoring happens. Twenty statements per category, three categories per round — sixty hand- authored statements total per round. Each one is a seed the contemplation loop generalizes.
Phase C — Contemplation ingests exemplars and emits recognizer proposals
Goal: the contemplation runner (ADR-0150/0152/0155) ingests each
exemplar corpus, decomposes the statements, and emits one or more
DerivedRecognizer proposals per shape category.
Deliverables:
- Contemplation runner extended to ingest the exemplar corpus path as
a candidate source (alongside
discovery_candidates.jsonl). - Each proposal carries:
- the shape category it generalizes,
- the recognizer's pattern in canonical form,
- replay-equivalence evidence against the active corpus + the exemplar set,
- per-shape coverage metrics.
- Proposals land in
teaching/proposals/proposals.jsonlas usual (ADR-0057), visible in the HITL queue (ADR-0161 §1). wrong = 0invariant: each proposal's replay-equivalence gate runs against the GSM8K train sample. If accepting the proposal would liftwrongabove 0 even on a single case, the proposal is auto-rejected at the gate.
Phase D — Operator ratifies through HITL queue
Goal: the operator reviews each recognizer proposal through the existing surfaces (CLI / workflow_dispatch / GitHub PR review) per ADR-0161 §2.
Deliverables:
- No new operator surface. The proposals appear in the queue with their shape category, exemplar coverage, replay evidence, and the ratification CLI command.
- Operator accepts, rejects, or withdraws.
- Engineering wiring (landed alongside the operator surface, ADR-0163.D PR):
generate/recognizer_registry.py— pure projection of acceptedexemplar_corpusproposals from the proposal log into a sorted-tuple of :class:RatifiedRecognizerrecords. In-process cache keyed on the log's (mtime, sha256).generate/recognizer_match.py— per-category rules-only matchers (no LLM, no embedding) honoring the Phase C synthesizer's narrowness rule: out-of-corpus surface forms return None.parsed_anchorscarry extracted tokens from the statement.generate/math_candidate_graph.py— narrowest-edit guard at the per-statement choice loop: before the existing "no admissible candidate for statement" refusal, consult the ratified registry. Recognized statements are skipped fromper_sentence_choices(contribute zero math state), preserving wrong=0 by construction. Empty registry is a no-op.- Downstream consumption of
parsed_anchors(turning recognized rate/temporal surfaces into solver state) is Phase E follow-up.
Phase E — Re-baseline GSM8K train sample
Goal: after each ratification round, re-run the train-sample eval and update the counts.
Deliverables:
- Automated re-baseline triggered by any merge that adds a recognizer.
- Pass criteria for this ADR:
- Round 1 exit:
correct ≥ 10,wrong = 0on the 50-case sample (matches the existing exit criterion in the report'sexit_criterionblock). - Round 2 exit:
correct ≥ 25,wrong = 0. - Round 3 exit:
correct ≥ 35,wrong = 0.
- Round 1 exit:
- Each round runs Phases A → B → C → D → E in sequence.
Phase F — Scale to public, holdout, full GSM8K
Once the train sample clears Round 3, scope expands:
| Split | Cases | Target |
|---|---|---|
public/v1 |
200 | correct ≥ 0.5 × cases, wrong = 0 |
holdout/v1 |
200 | first run is measurement-only; do not tune against |
| Full GSM8K (8,500) | 8,500 | post-Phase F follow-up ADR; out of scope here |
The holdout run is never used to drive recognizer additions. Per ADR-0114a doctrine, holdout is the OOD ratio check; tuning against it would invalidate the eval.
Constraints (non-negotiable)
-
wrong = 0at every phase, every round, every split. Refusals are honest; wrong answers are architectural regressions. Any recognizer that would liftwrongabove 0 is auto-rejected by the replay gate, never by operator judgment alone. -
No hand-rolled recognizers in
generate/. Every recognizer added to the runtime comes from the contemplation → proposal → review corridor. Phase B's exemplar corpus is input to that corridor, not output of it. A PR that adds a regex-style recognizer directly tomath_candidate_parser.pyviolates this ADR and must be rejected. -
Replay-equivalence is a precondition, never permission. Per ADR-0057, replay-equivalence makes a proposal eligible for review, not automatically accepted. This ADR does not weaken that.
-
Active corpus mutation only via
accept_proposal. Per ADR-0152 and ADR-0156/0158, the only path that mutates the active teaching corpus is the reviewed accept path. Recognizer additions land via that path or not at all. -
No tuning against holdout. Phase F's holdout split is measurement-only. Tuning against it makes the eval lie.
-
Determinism preserved. Each round's recognizer addition is a reviewed, append-only mutation. GSM8K runs at any historical SHA replay byte-identically given the corpus at that SHA.
Out of scope
This ADR does not commit to:
- a frontier-model comparison harness beyond what ADR-0119 already scoped;
- a benchmark publication strategy;
- patent prep work;
- Rust backend parity for the math path (waiting on Python semantics to lock, per CLAUDE.md work-sequencing);
- the full GSM8K split (8,500 problems) — that lives in a follow-up
ADR after Phase F clears
public; - non-GSM8K math benchmarks (MATH, AQuA, ASDiv) — scoped by separate ADRs once the corridor proves itself on GSM8K;
- multimodal math (charts, geometry images);
- a math-specific workbench surface — the existing Workbench (ADR-0160
/ 0162) is sufficient; lane-level inspection of refusal histograms
becomes a
RefusalHistogramPanelin the Eval Center (W-030) once that lands.
Implementation plan — first three PRs
PR 1 — Phase A scaffolding (refusal taxonomy)
evals/refusal_taxonomy/lane: contract.md, runner.py, v1/cases.jsonl (mirrors the 50 refused statements fromtrain_sample/v1/report.json).evals/refusal_taxonomy/v1/shape_categories.py— the enum.core teaching refusal-taxonomy --input <path>CLI command for re-running over an arbitrary refused set.- Tests pin the enum coverage and the shape-categorizer's deterministic output.
- Produces an initial histogram of the 50-case sample.
PR 2 — Phase B round 1 exemplar corpora
For the top three shape categories from PR 1's histogram, hand-author
≤ 20 exemplar statements each with expected MathProblemGraph shape.
No runtime change.
PR 3 — Phase C contemplation extension
Extend the contemplation runner to ingest exemplar paths as candidate sources. Surface the per-shape coverage metric in the proposal log. No new ratification path; existing HITL queue (ADR-0161) handles it.
After PR 3 lands, the contemplation runner produces recognizer
proposals; the operator ratifies; Phase E re-baseline confirms correct ≥ 10, wrong = 0. Round 1 closes.
Acceptance criteria
This ADR is ratifiable when:
- The audit findings above are independently verifiable by running
each capability axis lane on
mainand observingwrong = 0. - The GSM8K train-sample
correct: 0, refused: 50, wrong: 0baseline is reproducible at the current commit SHA. - The phase ordering (A → B → C → D → E → F) does not allow Phase B to start before Phase A produces a histogram, nor Phase C before Phase B writes exemplars.
- The
wrong = 0invariant is enforced as an auto-reject in the replay-equivalence gate, not as a post-hoc operator check.
This ADR is delivered when:
- GSM8K
public/v1(200 cases) reachescorrect ≥ 100, wrong = 0. - GSM8K
holdout/v1measurement-only run is recorded once at the end and never used to drive recognizer additions.
Consequences
Positive
- The math roadmap is reduced from "build operators, build axes, build decomposer, build aggregator" to one problem: expand the candidate-graph's admissibility surface through the contemplation corridor. Every other math component is mastered.
- Recognizer additions become a capability of the engine, not an editing task on the operator. The thesis ("decodes, not generates") manifests in the math lane directly.
- The exit criterion (
wrong = 0at every round) is enforceable by the replay gate, not by operator vigilance. - The HITL queue (ADR-0161) absorbs the curriculum-expansion pressure the master plan flagged as a future risk. Math is the first lane that scales through it.
Negative
- Phase A (refusal taxonomy) is upfront measurement work that ships no capability. Two-three days of audit before any GSM8K case starts passing. Worth it; the alternative is operators chasing whichever problem shape caught their eye first.
- The exemplar corpus (Phase B) is hand-authored. Sixty statements per round, hand-checked for shape correctness, is real work. The alternative — auto-mining exemplars from GSM8K itself — would violate the holdout discipline and tune against the benchmark we're trying to honestly measure.
- The
wrong = 0auto-reject gate may auto-reject proposals that are almost right. This is intentional. An almost-right recognizer that produces one wrong answer is worse than a refusal.
Risks
- The taxonomy could fragment. Mitigation: cap initial shape categories at ~9 and require every new category to cite ≥ 3 refused statements. Phase A acceptance test enforces this.
- The HITL queue could backlog. ADR-0161 §4's pending cap (256) applies here. If math proposals saturate the queue, the operator raises the cap via repo variable or pauses contemplation runs.
- Recognizer generalization could overfit to exemplars. Mitigation: every recognizer is replayed against the entire GSM8K train sample and the public capability axes; regression on any axis auto-rejects.
Cross-references
- ADR-0114a — Capability Obligations — perturbation, OOD ratio, depth curve obligations the math lane must keep honoring
- ADR-0119 — GSM8K eval lane — eval lane definition
- ADR-0131.G.* — capability axis lanes — G1..G5, S1 mastered
- ADR-0132/0133/0134/0135 — binding graph — landed substrate
- ADR-0139/0140 — versor arithmetic — landed operator algebra
- ADR-0149/0154 — recognizer pipeline — substrate this ADR builds on
- ADR-0150/0152 — autonomous contemplation + learning-arc corridor — proposal source
- ADR-0155 — CI contemplation runner — async producer
- ADR-0057 — proposal review + replay-equivalence — gating discipline
- ADR-0161 — HITL async queue — review surface
- CLAUDE.md —
wrong = 0discipline, no hidden normalization, exact recall, proposal-only learning
Memory cross-references
- thesis-decoding-not-generating — the load-bearing thesis this ADR applies to math. Every recognizer comes from the engine learning a shape, not from the operator stuffing a regex.
- feedback-address-critiques-dont-waive — the audit critique ("the gap is admissibility, not operators") is acted on here, not noted.
- feedback-adr-cross-reference-discipline — every substrate this ADR builds on is cited; no parallel mechanism is introduced.
- feedback-cleanup-as-you-find — the stale
feat/adr-0131-*andfeat/binding-graph-phase*branches on disk should be deleted as a hygiene PR after this ADR ratifies; the work is already on main. - feedback-scope-time-is-cheap — Phase A is the "pause and scope" move applied to math. Two-three days of taxonomy before any recognizer work prevents weeks of chasing the wrong gap.