The brief pack referenced `teaching/audit_evidence.py` in 3 spots
(A2 schema field, A2 read-required list, B2 algorithm step 3a). The
actual module on main is `teaching/math_evidence.py` (carries
`MathReaderRefusalEvidence` per ADR-0167).
Sonnet (A2 / PR #380) discovered the discrepancy and correctly used
the real module. This patch corrects the brief so Wave B operators
(B1 / B2) do not hit the same gap on dispatch.
No runtime change. Pure docs.
Coordination artifact for ADR-0172 Tier 1 dispatch. Six PRs in the
ADR (W0, W0.1, W1, W2, W3, W4) collapsed into a 4-wave DAG with two
concurrent briefs per wave A and B.
Operators are dispatched in their own UIs by pointing at the
section heading — "read §Brief A1" etc. Bundling options A/B/C
inside the doc let the operator choose CI economy vs wall-clock.
No runtime change. Pure docs.
Second implementation PR of the ADR-0170 wave. Extends the DCS injector
to emit ``CandidateOperation(kind='add')`` for acquisition verbs
alongside the existing ``CandidateInitial`` emission for possession
verbs. Proves the W1 type-widening with real emission of both union
members.
## What changes
### `generate/recognizer_match.py`
- New `_ACQUISITION_VERBS` frozenset (12 verbs: collect/get/receive/buy
inflections). Each member is a subset of `ADD_VERBS` so the downstream
CandidateOperation post-init whitelist accepts the matched_verb token.
- Extractor now accepts either possession OR acquisition verbs and
records `anchor_kind` (`"possession"` | `"acquisition"`) plus
`verb_token` in the parsed anchor schema.
### `generate/recognizer_anchor_inject.py`
- `inject_discrete_count_statement` dispatches on `anchor_kind`:
- `"possession"` → `CandidateInitial` (existing behavior unchanged)
- `"acquisition"` → `CandidateOperation(add)` (new)
- New helper `_build_operation_from_discrete_count_acquisition`
constructs the operation. Operand uses `_resolve_count_value`;
matched_verb uses `_locate_token` for round-trip ground check.
- Return type uses `InjectorEmission` from W1.
### Tests
- `tests/test_adr_0170_w2_dcs_acquisition_verbs.py` (new) — 22 tests:
- Verb-set membership pins
- Acquisition ⊂ ADD_VERBS sanity check
- Possession + Acquisition disjoint
- Extractor records anchor_kind correctly
- Injector emits CandidateOperation for acquisition verbs
- Possession path still emits CandidateInitial unchanged
- Deliberate exclusions (gained / donated / saved) still refuse
- Case 0050 hazard pinned (does/contemplates not in either set)
- Determinism + roundtrip_admissible passes
- Updated `tests/test_adr_0163_d2_discrete_count_injection.py` to
reflect new anchor schema fields (anchor_kind, verb_token).
- Updated `tests/test_adr_0170_w1_injector_type_widening.py` —
the DCS injector now legitimately returns
`tuple[InjectorEmission, ...]` (not narrower).
## Deliberate exclusions
These verbs are NOT in `_ACQUISITION_VERBS` and the extractor refuses
them — preserving wrong=0:
- `gained / gains / gain` — delta-of-attribute (weight, age), not
acquisition. Admitting as add-operation would risk wrong>0 on
questions that ask total state.
- `donated / donates / donate` — SUBTRACT semantics (actor gives away).
- `saved / saves / save` — ambiguous (time vs money vs effort).
Widening this set is operator-reviewable per `feedback-wrong-zero-
hazard-case-0050` discipline.
## ADR-0131.G.1 branch-disagreement discipline preserved
The regex parser already emits `CandidateOperation(add)` for
acquisition verbs via `ADD_VERBS` for single-word units. The new DCS
injector path emits the same kind of operation for multi-word units
(where the regex parser fails). Collapsed-tie when both paths emit
identical operations on overlapping shapes; no disagreement.
## Test plan
- tests/test_adr_0170_w2_dcs_acquisition_verbs.py: 22 passed (new)
- tests/test_adr_0163_d2_discrete_count_injection.py: ~30 passed
(existing tests updated for new schema fields)
- tests/test_adr_0170_w1_injector_type_widening.py: 6 passed
- tests/test_recognizer_skip_wrong_zero.py + brief_11b + brief_11 +
candidate_graph_wiring + candidate_domain_partition: passed
- evals/gsm8k_math/train_sample/v1: counts=correct=3 refused=47 wrong=0
unchanged (case 0023 still has S2/S3 downstream blockers; W2's value
is infrastructure, not direct lift)
## Hard invariants
- `wrong == 0` preserved (case 0050 hazard pin + deliberate verb
exclusions + roundtrip_admissible gate)
- ADR-0166: no new eval lanes
- No teaching-store / pack mutation
- ADR-0131.G.1 branch-disagreement discipline preserved (acquisition →
operation, not initial)
- Five-layer wrong=0 safety net (ADR-0163.D.2) intact and extended
## W3 NOT in this PR — honest skip
Initial plan was to bundle W2 + W3 (A1 currency_amount injector).
Inspection of the 4 actual `currency_amount` GSM8K refusals showed
none match A1's narrow form (`<ProperNoun> earns|charges $<amount>`):
| Case | Statement | Reason narrow form doesn't fit |
|---|---|---|
| 0019 | "this requires 3 vet appointments, which cost $400 each" | anaphoric subject + multi-quantity |
| 0026 | "Aaron and his brother Carson each saved up $40" | multi-subject + "each" |
| 0028 | "It cost $100,000 to open initially" | pronoun subject |
| 0043 | "Her mother gave her an additional $4, and her father twice as much" | multi-clause + comparative + transfer |
Shipping W3 as-designed would have re-introduced the dead-code pattern
#373 just cleaned up. Skipped honestly; ADR-0172 Tier 1's decomposer
(the next wave) will surface category-shape mismatches like this
programmatically.
* docs(ADR-0172): math-domain corpus-decomposition mechanism (Learning Arc analog)
Scoping ADR for the math-domain analog of cognition's
`teaching/contemplation.py` corpus-decomposition loop (Learning Arc
milestone 2026-05-25).
## What this ADR scopes
A mechanism that reads the math audit corpus and emits
`MathReaderRefusalShapeProposal` records — structural commonalities
across N refusal cases, paired with the candidate mechanism change
that would resolve them (matcher extension, injector sub-shape,
vocabulary addition, frame reclassification).
Today the operator does this decomposition by hand (reads
audit_brief_11.md, identifies the commonality across 21 DCS
refusals, scopes the matcher/injector extension, files a focused PR).
ADR-0172 shifts the decomposition to the engine, with HITL
ratification preserved.
## Sequencing — explicit
ADR-0172 ships AFTER ADR-0170 (injector contract widening),
ADR-0168 (FrameClaim handler), and ADR-0169 (CompositionClaim
handler — reserved). Without those substrates, the decomposer can
identify patterns but cannot route them to a ratification handler
that knows how to materialize them. Cognition's learning arc
followed this same sequencing: substrate first, then decomposer.
## Why this matters
ADR-0167 LexicalClaim shipped the math-domain wire from refusal →
evidence → operator-ratification. ADR-0172 closes the gap to the
engine-decomposes loop — the moment cognition's learning arc
qualitatively shifted from "engine refuses + operator authors" to
"engine teaches itself through reviewed correction."
The Learning Arc memory entry (2026-05-25) names that moment as
when measurable progress accelerated. ADR-0172 makes the math-domain
trajectory toward the same loop explicit in the queue.
## Hard invariants preserved
- wrong=0 by construction (proposals are evidence-only)
- ADR-0166: no new eval lanes
- No teaching-store / pack mutation
- No non-deterministic mechanism (rule-based grouping, not learned
classification)
- Cross-domain partition (ADR-0167 W2-C) preserves cognition
contemplation behavior
No code, no test, no eval, no pack change in this PR.
## Cross-references
- ADR-0056/0057 — cognition contemplation/proposal substrate (template)
- ADR-0167 + FOLLOWUPS §1 — parent evidence wire
- ADR-0168 + ADR-0168.1 — FrameClaim (ratification target)
- ADR-0169 (reserved) — CompositionClaim (ratification target)
- ADR-0170 — injector contract widening (substrate prerequisite)
- Memory: Learning Arc Milestone 2026-05-25 — the moment to recreate
- Thesis: decoding, not generating — the principle preserved
* amend(ADR-0172): add Tier 2 — intensional contemplation with test-and-learn loop
Per operator feedback during ADR-0172 review: the corpus-decomposition
mechanism should not only emit explicit rules (extensional) but also
develop inference (intensional) — recognizing structural equivalence
classes across surface variations without enumerating them.
## Tier 2 — intensional contemplation
Engine recognizes that 'Sam has 5 apples' and 'Sam collected 5 apples'
carry the same canonical proposition structure, without an explicit
verb-list extension. Emits MathReaderInferenceProposal records that
name structural equivalence classes rather than enumerable rules.
This is the thesis word the original draft missed: rationalization.
Tier 1 ratifies rules; Tier 2 ratifies inference.
## Test-and-learn loop
Tier 2 proposals carry held-out test evidence:
1. Decomposer surfaces hypothesis
2. Held-out subset of corpus reserved
3. Bridge applied to held-out cases; admissibility gates run
4. Outcome scored (positive / negative / neutral)
5. Negative-evidence proposals auto-rejected before HITL
6. Operator reviews proposal + test result, not bare claim
This makes Tier 2 thesis-coherent: engine decodes a structural
pattern, tests it against unseen corpus cases, surfaces the test
result. Wrong=0 cannot leak through — held-out test failures reject
internally.
## Updated implementation outline
Tier 1 wave: W1-W4 (schema, decomposer, CLI, workbench integration)
Tier 2 wave: W5-W9 (schema, equivalence-class recognizer, test-and-learn
loop, HITL integration, bridge application path)
## Hard invariants preserved at both tiers
- wrong=0 by construction (Tier 1: evidence-only proposals; Tier 2:
held-out test rejects wrong-admitting bridges internally)
- ADR-0166: no new eval lanes
- No non-deterministic mechanism (rule-based grouping + deterministic
test-and-learn, not learned classification)
- Cross-domain partition preserves cognition contemplation behavior
* amend(ADR-0172): split Tier 2 test-and-learn into two-arm confirmation
Per operator feedback during ADR-0172 review: 'confirm against known
facts/prior solutions' is the missing arm. The Tier 2 test-and-learn
loop now has BOTH:
- Arm 1 (negative / wrong=0 on held-out refusals) — already drafted
- Arm 2 (positive / known-good preservation) — NEW
Arm 2 inherits ADR-0057's replay-equivalence contract: any
inferential bridge that would change a currently-correct outcome is
REJECTED INTERNALLY before reaching HITL, even if the new outcome is
defensible. Existing truth survives; new truth is gated.
Both arms must PASS or be neutral. Either arm rejecting → proposal
does not reach the operator. This makes the engine's reasoning
provably conservative: it confirms against truth it already knows AND
truth it hasn't yet decided.
The 5-step proposal lifecycle is updated to reflect both arms +
test-set partition + per-case verdict tables in the emitted proposal.
No code change. No runtime effect.
* amend(ADR-0172): add foundational reasoning-articulation substrate
Per operator feedback: for the engine to infer/test/learn from
feedback, it must first be able to ARTICULATE its own reasoning in a
structured, persistent, replayable form.
Articulation is the project thesis's 5th anchor ("listen → comprehend
→ recall → think → articulate → learn from reviewed correction →
replay"). Today CORE articulates SURFACE (templated realizer output)
but does not articulate REASONING — the inference chain that took the
engine from refusal corpus to hypothesis to proposal.
Without reasoning-articulation, none of the three loops can work:
- Loop 1 (self-test) has nothing to record about what it tested or why
- Loop 2 (HITL review) sees a black-box conclusion, not inference chain
- Loop 3 (feedback) has no specific step the operator can target with
a rejection rationale
## Substrate: ReasoningTrace schema
Every proposal carries a typed, content-addressable
ReasoningTrace recording each inference step:
ReasoningStep:
step_kind: observation | grouping | abstraction | hypothesis |
test_design | test_application | test_result | conclusion
input_pointers: prior steps + evidence rows
claim: human-readable assertion at this step
justification: why the engine made the claim
output_payload: type-discriminated by step_kind
The trace is byte-identical across replays of the same corpus +
verdict history. Inherits CORE's existing determinism discipline.
## Sequencing
Articulation ships FIRST (new W0 wave) — it is the prerequisite for
Tier 1 and Tier 2 and Loop 3. Each downstream wave emits or consumes
ReasoningTraces.
## Hard invariants preserved
- Deterministic-replay (trace byte-identical under same inputs)
- ADR-0057 replay-equivalence (trace IDs stable across reruns)
- No non-determinism added (rule-based step emission, not learning)
- ADR-0166: no new eval lanes
No code, no test, no eval, no pack change in this PR.
The G.2 test \`_comparative_clause_refusal_count\` reads \`report.json\`
and counts refusals whose reason quotes a statement clause containing
comparative anchors ("more/less than", "twice as many", etc.). After
#359's wrong=0 fix, the candidate-graph emits two refusal-reason
families that both quote a statement:
1. "no admissible candidate for statement: '...'" — parser-path
refusal (the comparative-parse-failure family this metric tracks).
2. "recognizer matched but produced no injection for statement:
'...'" — recognizer-path refusal; the quoted statement may
incidentally contain comparative anchors but the refusal cause is
the missing injector, NOT the comparative parse.
The pre-#359 counter only saw family (1) reasons; post-#359 it
over-counts whenever a recognizer-path refusal quotes a statement
containing comparative anchors. This was the test failure A2's PR
(#369) and the cleanup PR (#373) both surfaced.
## Fix
Filter the counter to exclude family (2) explicitly. Recognizer-path
refusals are tracked separately by the recognizer-wiring test suite;
they don't belong in the G.2 metric.
Result on current main:
- total statements with comparative anchors in refusal reasons: 2
- parser-path: 1 (case 0009, the legitimate G.2-tracked refusal)
- recognizer-path: 1 (filtered out — incidental anchor in #359-format reason)
- G.2 metric correctly reports 1 < baseline 2 → assertion passes
## Also: refresh report.json
The checked-in \`report.json\` was generated pre-#359 with the legacy
refusal-reason format. The runner now emits the new format on every
run; checking in the current output makes the baseline reproducible
and clears the CI friction that A2 originally flagged.
## Test plan
- tests/test_adr_0131_G2_comparatives.py: 25 passed (was 24 pass / 1 fail)
- tests/test_adr_0131_G4_multi_clause.py + G5_aggregate + S1_rate_events: 105 passed
- tests/test_brief_11b_audit_artifact + step2_lexicon + recognizer_skip + brief_11_audit + wiring + partition + adr_0163_d2: 89 passed
- Total: 219 passed
## Hard invariants
- No runtime change
- wrong=0 invariant preserved
- ADR-0166: no new eval lanes
- No teaching-store / pack mutation
First implementation PR of the ADR-0170 wave. Type-level widening only:
the recognizer-injector dispatch now returns
``tuple[InjectorEmission, ...]`` where
``InjectorEmission = CandidateInitial | CandidateOperation``.
The existing ``inject_discrete_count_statement`` continues to emit only
``CandidateInitial`` — the widening unlocks but does not exercise
operation emission. Subsequent W2-W5 PRs ship the per-injector emission
shapes:
- W2 — DCS-S1 acquisition verbs (CandidateOperation(add))
- W3 — A1 currency_amount (CandidateInitial reimplementation)
- W4 — A3 multiplicative_aggregation (CandidateInitial(product))
- W5 — A4 temporal_aggregation (deferred until apply_rate primitive)
## Changes
### `generate/recognizer_anchor_inject.py`
- New `InjectorEmission = Union[CandidateInitial, CandidateOperation]`
- `inject_from_match` return type widened to
`tuple[InjectorEmission, ...]`
- `__all__` exports `InjectorEmission`
- Documentation comment names ADR-0170 §"Implementation outline"
### `generate/math_candidate_graph.py` (admissibility dispatch)
The per-statement admission loop now dispatches admissibility on the
concrete candidate type:
if isinstance(c, CandidateInitial):
if _initial_admissible(c): admitted.append(c)
elif isinstance(c, CandidateOperation):
if roundtrip_admissible(c): admitted.append(c)
No new admission semantics — each type is gated by the predicate it was
already gated by elsewhere in the codebase. The dispatch unifies the
injector path with the parser path.
### `tests/test_adr_0170_w1_injector_type_widening.py` (new)
- Pin: `InjectorEmission` union members are exactly the two candidate types
- Pin: `inject_from_match` return type is widened
- Pin: `inject_discrete_count_statement` still emits CandidateInitial (W1
is type-level only)
- Hazard pin: case 0050 remains refused
- Hazard pin: unparseable-verb refusal path (#359) unchanged
- Anti-regression: canonical DCS narrow-form extraction still works
## Test plan
- tests/test_adr_0170_w1_injector_type_widening.py: 6 passed (new)
- tests/test_adr_0163_d2_discrete_count_injection.py: 21 passed
(existing D.2 v1 injector regression)
- tests/test_brief_11b_audit_artifact.py + step2_lexicon +
recognizer_skip_wrong_zero + brief_11_audit: 55 passed
- tests/test_candidate_graph_recognizer_wiring.py: 7 passed
- tests/test_candidate_domain_partition.py: 5 passed
- tests/test_adr_0131_G2_comparatives + G4 + G5 + S1_rate_events:
130 passed
- Total: 225 passed
- evals/gsm8k_math/train_sample/v1: counts=correct=3 refused=47 wrong=0
(unchanged; verified no behavioral regression)
## Hard invariants
- `wrong == 0` preserved (admissibility dispatch is type-aware but
semantically identical to the parser path's gating)
- ADR-0166: no new eval lanes
- No teaching-store / pack mutation
- Five-layer wrong=0 safety net (ADR-0163.D.2) intact
- Reader path unchanged
Three concrete cleanup items from the day's work, per the
cleanup-as-you-find memory principle.
## 1. Remove inject_rate_with_currency stub
PR #369 (A2 rate_with_currency) shipped a function that always returns
() with an extensive docstring documenting the Rate-not-in-SentenceChoice
schema gap. The function is dead at runtime — `_INJECTORS.get(category)`
returning None has the same downstream behavior as the function
returning (). The 16 tests pinned the empty-tuple return; the case-0050
hazard pin is duplicated in test_recognizer_skip_wrong_zero.py and
test_brief_11b_step2_lexicon.py.
The schema gap is now properly documented in ADR-0170 (PR #372). A
dispatch-table comment at the removal site retains the at-code pointer
to that ADR for anyone wiring a new injector.
Removed:
- `inject_rate_with_currency` function in generate/recognizer_anchor_inject.py
- Its `_INJECTORS` dispatch table entry
- Its `__all__` export
- tests/test_injector_rate_with_currency.py (371 lines, 16 tests)
## 2. Remove docs/handoff/GPT55-MOBILE-DISPATCH.md
Single-session travel-time scaffolding. The 5 tasks it named are
complete or superseded by ADR-0170's findings. Pure historical artifact.
## 3. Remove docs/handoff/WAVE-NEXT-INJECTORS.md
Superseded by docs/handoff/WAVE-NEXT-REVISED.md, which captures
everything load-bearing from the original brief in its A1–A4 findings
table. The "kept for history" justification didn't survive scrutiny:
the document was misframed (over-promised lift; misframed schema work
as injector work). Lessons captured in REVISED + ADR-0170.
Updated cross-references:
- WAVE-NEXT-REVISED.md: removed the "supersedes ... kept for history"
pointer; tightened cross-reference list
- ADR-0167-FOLLOWUPS.md §7: rewrote pointer to name ADR-0170 + REVISED
as the live plan rather than "the original is retained"
## Test plan
- 219 tests passed across G.2/G.4/G.5/S1/Brief 11/B1/B11A/wiring/partition/DCS-D.2
- evals/gsm8k_math/train_sample/v1/report.json untouched (regen
surfaces a separate stale-baseline test issue — out of cleanup scope)
- No runtime behavior change
## Net impact
- 5 files removed (~1200 lines)
- 1 file modified for explanatory comment (~30 lines)
- 2 doc files updated to remove dangling cross-references
- 0 behavioral change
DCS-S1 (proper-noun possession sub-shape expansion) investigation
revealed that the recognizer-injector path's `CandidateInitial`-only
return type is a substrate-level constraint blocking four Wave-Next
sub-shape categories — not just one.
## Two artifacts
1. **`docs/handoff/DCS-S1-FINDING.md`** — investigation result. Of
the 21 DCS-refused GSM8K cases, zero are pure S1-only blockers.
Acquisition-verb expansion (`collected`, etc.) conflicts with
ADR-0131.G.1's branch-disagreement discipline. The right fix is
the DCS injector emitting `CandidateOperation(add)`, but the
`inject_from_match` return type doesn't allow that.
2. **`docs/decisions/ADR-0170-injector-contract-widening.md`** —
scoping ADR. Names the contract change, the four categories it
unblocks (DCS-S1 acquisition, A1 currency, A3 multiplicative,
A4 temporal), the three load-bearing rules it must preserve
(ADR-0131.G.1, SentenceChoice union, admissibility gates), and
a 5-step implementation outline.
## Pattern recognised
Wave-Next surfaced four schema gaps. All four trace to the same
constraint: per-category injectors can only emit `CandidateInitial`.
The right next-capability work is ADR-0170 ratification, then a
small no-behavior-change PR widening the contract, then per-injector
follow-up PRs against the widened contract.
That is the actual lift-per-risk path for GSM8K Round-1 closure.
## Test plan
Docs-only. No code, no test, no eval, no pack change.
## Cross-references
- ADR-0163.D.2 — original parsed_anchors → solver-state ADR
- ADR-0131.G.1 — branch-disagreement discipline ADR-0170 preserves
- ADR-0167 — parallel teaching-corridor mechanism (independent)
- ADR-0167-FOLLOWUPS §7 — Wave-Next findings backlog
- WAVE-NEXT-REVISED — parent plan; ADR-0170 is the upstream blocker
- PR #369 — A2's schema-refusal artifact (first observation of gap)
The Wave-Next injector dispatch (A1-A4) surfaced findings that
invalidate three of the four briefs' lift assumptions:
- A1 currency_amount — sandbox-blocked; real lift potential intact
- A2 rate_with_currency — schema gap (Rate not in SentenceChoice
union); PR #369 documents the gap with concrete 4-step plan
- A3 multiplicative_aggregation — Operation(multiply) spec wrong;
correct emission is CandidateInitial(outer×inner); zero GSM8K cases
match canonical narrow form anyway
- A4 temporal_aggregation — needs apply_rate primitive not in algebra
Three of four are schema-extension ADRs masquerading as injector work.
Only A1 is a true injector + sandbox-fix scenario.
Deliverables:
1. `docs/handoff/WAVE-NEXT-REVISED.md` (new) — supersedes
WAVE-NEXT-INJECTORS.md. Pivots to DCS sub-shape expansion as the
actually-tractable next wave (21-case bucket, existing v1 injector,
#366 spec on main). Captures the three schema gaps + A1's
preserved lift potential for separate ADR work.
2. `docs/handoff/ADR-0167-FOLLOWUPS.md` §7 (new) — points to the
revised plan and summarises the four findings inline.
WAVE-NEXT-INJECTORS.md retained for history.
No code change. No runtime effect. Docs-only.
The wrong=0 fix in #359 changed the candidate-graph's refusal-reason
format when a ratified recognizer matches but its v1 injector returns
():
- Pre-#359: silently drop the recognized statement and admit a partial
graph from the rest — a wrong>0 hazard analogous to case 0050.
- Post-#359: refuse explicitly with reason "recognizer matched but
produced no injection" naming the statement and recognizer category.
Three tests in `test_candidate_graph_recognizer_wiring.py` were written
against the pre-#359 silent-drop behavior:
1. `test_empty_registry_preserves_existing_refusal_reason` — asserted
the old "no admissible candidate" was the only valid format. Updated
to accept either the legacy format OR the new explicit-refusal
format.
2. `test_recognized_rate_statement_no_longer_triggers_per_statement_refusal`
— asserted that recognized statements should NOT cause a per-statement
refusal (encoding the silent-drop premise). Inverted to assert the
correct post-#359 behavior: recognized-but-uninjectable statements
refuse EXPLICITLY, and the statement IS named in the diagnostic.
Renamed to `_refuses_explicitly_post_wrong_zero_fix`.
3. `test_recognized_descriptive_statement_no_longer_triggers_per_statement_refusal`
— same inversion + rename.
Renames preserve the original sites for git-blame continuity while
making the post-#359 contract the documented behavior.
No runtime change. wrong=0 invariant preserved.
Test plan:
- tests/test_candidate_graph_recognizer_wiring.py: 7 passed (was 3 fail / 4 pass)
- tests/test_candidate_domain_partition.py: 5 passed (no cognition regression)
- tests/test_brief_11b_audit_artifact.py + step2_lexicon + recognizer_skip_wrong_zero + brief_11_audit: 55 passed
- Total: 62 passed
Wave-Next A2 brief outcome: the Rate type (ADR-0122) DOES structurally
model a per-unit rate, but it is not a member of the per-sentence
injector contract's SentenceChoice union (CandidateInitial |
CandidateOperation). The injector therefore returns () and documents
the schema gap inline plus in audit_brief_11.md.
Lift count: 0 (expected — the brief explicitly anticipates this
outcome when the schema decision is "no"). Documenting the gap is
the deliverable.
- generate/recognizer_anchor_inject.py: new inject_rate_with_currency
+ dispatch-table entry routing ShapeCategory.RATE_WITH_CURRENCY.
- tests/test_injector_rate_with_currency.py: 16 tests pinning schema
evidence, schema refusal, dispatch wiring, case 0050 hazard,
determinism, and wrong=0 invariant.
- evals/gsm8k_math/train_sample/v1/audit_brief_11.md: appended
Wave-Next A2 section documenting the schema decision, eval delta
(3/0/47 unchanged), case 0050 hazard verification, and the
CandidateRate follow-up sequencing.
Case 0050 hazard pin: sentence 0 ("Mark does a gig every other day
for 2 weeks.") carries no currency symbol — rate_with_currency
never matches it; case stays refused at sentence_index=0.
Adds 3 drain_token lemmas to en_core_math_v1 closing 2 of 3 remaining
lexicon_entry refusals from the prior wave:
- path (case 0049, new lemma)
- journey (case 0049 follow-on after path resolved)
- sees → alias of existing "see" lemma (case 0040)
The third remaining lexicon_entry refusal (case 0001, '+') is
deliberately NOT closed: '+' is an arithmetic operator literal, not a
lexical token. Adding it as drain_token would silently drop arithmetic
content from problems like "5 + 3 apples", a wrong=0 hazard. Documented
in the PR body and audit artifact.
Refusal taxonomy shift:
- unknown_word: 5 → 3 (-2)
- unresolved_pronoun: 3 → 4 (+1) — case 0049's pronoun barrier surfaced
- incomplete_operation: 20 → 21 (+1) — case 0049's quantity gap surfaced
Hard invariants:
- wrong == 0 (admitted=0, verified)
- case 0050 hazard pinned (refused at sentence_index=0)
- manifest checksum unchanged (per-category source file edit)
- no teaching-store mutation; no reader runtime change
Five docs-only tasks GPT-5.5 can pick up via the GitHub connector while
the operator travels. Every task is read-only execution: spec / audit /
ADR drafting, never code or test changes. Risk profile: zero.
Tasks (each opens one PR):
1. ADR-0168 FrameClaim scoping draft (highest priority — next gate
after the LexicalClaim slice)
2. discrete_count_statement injector specification audit (highest-
leverage: 21/47 GSM8K refusals are this category)
3. Ratified-recognizer registry audit (informs Task 2 and future
injector work)
4. FOLLOWUPS §6 holonomy ablation test specification
5. Cognition contemplation partition fix specification (FOLLOWUPS §5a)
Each task carries:
- Files to read first (with paths)
- Deliverable shape (what the output doc must contain)
- PR body requirements
- Explicit out-of-scope list
Hard constraints enforced for the mobile-connector mode:
- One PR per task; explicit file staging; never git add -A
- Markdown-only (CLAUDE.md §Documentation Discipline)
- No code edits — flag in PR body if a task starts needing one
- No engine_state/ commits
- Cite filenames + line numbers; verify before committing
If all five complete, GPT-5.5 opens a meta-PR linking them into
ADR-0167-FOLLOWUPS.
No code change. No runtime effect. Docs-only.
Three small surgical anchors capturing the verified architectural
insight surfaced during the ADR-0167 wave (no new ADR — the gap claim
that prompted this resolved on verification; what remains is a sharper
residual question worth memorialising).
1. CLAUDE.md — new "Schema-Defined Proof Obligations" section between
Documentation Discipline and Validation Through CLI. Generalises
the wrong=0 invariant pattern: schema types that name structural
properties are real only when an executing test can meaningfully
fail under the violations it is written to catch. Three-step rule
for treating a schema as load-bearing.
2. language_packs/compiler.py — ARCHITECTURAL INVARIANT comment on
_apply_mounted_primary_domain_resonance naming it as the single
convergence-decision site for DEPTH_ROOT/DEPTH_RELATION packs.
Anchors the doctrine at the code site so any future modification
trips on the reference to the holonomy proof's coverage gap.
3. docs/handoff/ADR-0167-FOLLOWUPS.md §6 — captures the structural-vs-
blend convergence isolation question. HolonomyAlignmentCase IS
executed today (we verified), but the existing test doesn't
distinguish structurally-derived convergence from blend-induced
convergence. Ablation test or reframed claim — both acceptable
resolutions.
Verified before commit:
- All 13 architectural references in the Gemini analysis resolve
exactly: triliteral 0.30, root 0.40, prefix 0.03/(idx+1), stem 0.24,
_INFLECTION_PRIORITY case-near-last, _apply_mounted_primary_domain_resonance
with 40% English-prototype blend, HolonomyAlignmentCase defined
AND executed
- tests/test_alignment_graph.py: 8 passed (no behavioural change)
- Documentation discipline (#355) honoured: pure Markdown, no HTML
No code behaviour changes. No runtime effect. Drops the larger
ADR-0168-PROPOSAL idea — the gap claim that prompted it dissolved
under verification.
## Summary
Two test failures on origin/main both trace to PR #315 (ADR-0163.D.2 —
discrete_count_statement recognizer + admissibility-intent chain). Earlier
runs treated them as "pre-existing unrelated" — they are not unrelated.
The first is a real wrong>0 hazard.
## Failure 1: silent admission via recognized-but-uninjectable statement
The ratified `discrete_count_statement` recognizer over-matches: ANY
sentence containing a number + noun resolves it, irrespective of the verb.
When `inject_from_match` returns `()` (the round-2 default for v1
categories without an injector), the old code path used `continue` to
silently drop the statement — and the solver then answered from whatever
initial state remained.
Reproduction:
parse_and_solve("Sam has 5 apples. Sam contemplates 3 apples. "
"How many apples does Sam have?")
→ is_admitted=True, answer=5.0 (silent admission of partial graph)
This is exactly the case-0050-class hazard wearing a different hat
(silently admitting an incomplete graph at the problem level).
ADR-0167 / Brief 11 §"correct-count greed" established the principle on
the reader path; this commit extends it to the recognizer path.
Fix: when a recognizer matches but produces no injection, REFUSE.
generate/math_candidate_graph.py:
- Replaced the skip-only `continue` with a CandidateGraphResult
refusal carrying the recognizer category in the reason.
tests/test_math_candidate_graph.py:
- test_unparseable_statement now accepts either the legacy
"no admissible candidate" reason or the new
"recognizer matched but produced no injection" reason.
Both legitimately refuse; what matters is is_admitted=False.
tests/test_recognizer_skip_wrong_zero.py (NEW):
- 5 regression tests pinning the wrong=0 invariant:
* 3 parametrized verbs unknown to both regex parser and reader
(contemplates / ponders / memorises) — must all refuse
* Nonsense token — must refuse
* Anti-regression: known initial + known operation still admits
## Failure 2: cognition audit drop-reason taxonomy
The audit test hardcoded `dropped.reason.startswith("superseded_by:")`
as the only valid drop-reason prefix. Commit da70919 (ADR-0163.D.2)
ratified an admissibility-intent chain that the audit categorizes with
reason `unsupported_intent:admissibility`, which fails this assertion.
Fix: tests/test_teaching_audit.py — expand the allowed-prefix set to
include `unsupported_intent:` with a written rationale. Future drop
classes extend the allowlist deliberately rather than silently
broadening the assertion to any non-empty reason.
## Surfaced regression: partition-test allowlist (ADR-0167 FOLLOWUPS §2)
This PR modifies three test files that the
test_existing_cognition_tests_untouched assertion would reject under
its named-allowlist scheme. Added the three test paths to the allowlist
as the tactical fix; the architectural fix (retire / move to CI / move
to CODEOWNERS) is queued in docs/handoff/ADR-0167-FOLLOWUPS.md §2.
## Test plan
uv run pytest tests/test_recognizer_skip_wrong_zero.py \
tests/test_math_candidate_graph.py \
tests/test_teaching_audit.py \
tests/test_candidate_domain_partition.py \
tests/test_math_evidence_e2e.py \
tests/test_math_evidence_schema.py \
tests/test_math_contemplation_adapter.py \
tests/test_math_claim_signature.py \
tests/test_math_lexical_ratification.py \
tests/test_brief_11b_audit_artifact.py \
tests/test_brief_11b_step2_lexicon.py \
tests/test_brief_11_audit.py
→ 152 passed
## Hard invariants
- wrong == 0 — restored on the recognizer path (was silently violated on main)
- ADR-0166 — no new eval lanes
- No teaching-store mutation, no pack mutation
- The reader path was already correct (it refused these cases); this fix
brings the regex/recognizer path back in line
Captures the named follow-ups that surfaced during the ADR-0167 wave so
they don't drift. Five items, each with scope / why-deferred /
breadcrumbs / acceptance criterion:
1. Frame-opener sub-types (FrameClaim / CompositionClaim / ReferenceClaim
/ SlotClaim) — four additional handlers, each its own ADR
2. Partition test architectural fix — current git-status-at-test-runtime
assertion is structurally brittle (3 options outlined)
3. Two pre-existing main failures (test_unparseable_statement,
test_audit_real_corpus_runs_clean) — fix or quarantine, don't ignore
4. Workbench v1 math-candidate rendering — ADR-0167 §Q4
5. Cross-domain partition risks Gemini flagged (contemplation pack
indexing, replay gate default)
Includes leverage-based sequencing recommendation (no timelines per
project convention).
Docs-only. No code, no test, no eval, no pack change.
Wave 3, closes the LexicalClaim slice of ADR-0167. After this PR the
math reader's refusal taxonomy is evidence, not terminus: lexical
refusals flow through audit row → typed evidence → dedup signature →
HITL ratification (W2-D) → pack write → next-audit-pass-resolves.
Deliverables
------------
- tests/test_math_evidence_e2e.py (new, 7 tests):
* test_full_pipeline_from_audit_to_evidence
* test_e2e_replay_equivalence
* test_lexical_ratification_advances_unknown_word_row (case 0040 'sees')
* test_e2e_determinism_across_processes
* test_cognition_teaching_corridor_unaffected
* test_evidence_dedup_via_claim_signature
* test_audit_artifact_round_trip_with_signatures
- evals/gsm8k_math/train_sample/v1/audit_brief_11.md: Post-W2 baseline
table + cognition regression line + case 0050 hazard status + pointer
to the new e2e regression module.
- tests/test_candidate_domain_partition.py: minimal allowlist patch to
test_existing_cognition_tests_untouched so that future ADR-0167 PRs
can add their own evidence test files without tripping a structurally
brittle hard-coded whitelist (W2-C partition risk; recorded in PR body).
Hard constraints held
---------------------
- wrong == 0: case 0050 hazard still refuses at sentence_index 0
after the tmpdir-pack 'sees' ratification; no admission introduced.
- Cognition regression: zero modifications to cognition test bodies;
only the W2-C whitelist assertion was loosened.
- Determinism: in-process and cross-process evidence_hash byte-identical.
- No real-pack mutation: a per-test digest fixture asserts
language_packs/data/en_core_math_v1/ is byte-identical before and
after each test.
Out of scope
------------
- Frame/Composition/Reference/Slot ratification handlers (follow-up ADRs).
- Workbench v1 wiring of math candidates (ADR-0167 §Q4).
- Auto-ratification — HITL only, forever.
- The two partition risks Gemini flagged in W2-C (cognition pack indexing,
replay-gate default) remain follow-up.
With this PR merged the engine can ratify math-domain lexical claims
from its own refusal evidence through the existing HITL teaching
corridor — the thesis claim of ADR-0167 becomes a concrete green test.
Two docs-only updates capturing the day's work:
1. Appended a "Status update — 2026-05-27 EOD" footer to the Brief 11
handoff doc with the completion table (11A/11B-step-1/11B-step-2
docs+lexicon/11D merged; 11C absorbed into W3-A; 11D candidate E ADR
merged) and the current post-#348 baseline taxonomy.
2. New session doc SESSION-2026-05-27-adr-0167-parallel-dispatch.md
alongside the existing SESSION-2026-05-26-comprehension-reader.md.
Captures the architectural pivot (audit-as-teaching-evidence vs the
rejected refusal-class dispatch table), the parallel-dispatch
experiment (5 operators / 3 waves / 6 PRs), what worked, what
surfaced as load-bearing (case 0050 hazard), and what's deferred.
No code changes. No runtime effect.
Adds an explicit Documentation Discipline section between Semantic Pack
Discipline and Validation Through CLI. Encodes the stance that
text-diffable Markdown is the substrate for ADRs, session docs, audit
artifacts, and handoff briefs.
Sanctions two zero-cost adoptions inside the existing format:
- Mermaid fenced blocks for state machines / sequences / dependency
graphs that genuinely benefit from a picture
- <details> / <summary> collapsibles for long proofs, large tables,
generated logs
Out of scope (explicitly):
- Standalone HTML artifacts with embedded CSS / inline SVG / sidebars
- Dashboards / status pages / visualizers as substitute for a pinned
data artifact
Rationale: the "open in browser" model breaks git diff, determinism
(CSS / SVG element ordering), and cross-agent legibility — all
load-bearing properties for a deterministic cognitive engine.
No code changes. No runtime effect.
Adds `teaching/math_claim_signature.py` with `lexical_claim_signature()`:
sha256 hex of a normalised lexical token, collapsing two refusal cases on
the same surface token into one teaching-corpus candidate.
Normalisation pipeline (documented in module, breaking-change surface):
1. Lowercase surface
2. Strip string.punctuation from both ends (!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)
3. Extract token from refusal_detail via r"no primitive or lexicon match for '([^']+)'"
4. Fallback: use stripped-lowercase surface if regex doesn't match
5. Canonical: "lexical:" + extracted_token
6. sha256 hex of UTF-8 bytes → 64-char lowercase hex
Also adds `teaching/math_contemplation.py` (W2-A adapter included as
union-merge; W2-A worktree was not yet dispatched):
- `audit_to_evidence()`: AuditRow iterable → MathReaderRefusalEvidence tuple
- `audit_problem_to_evidence()`: convenience wrapper for tests and W3-A
- Lexical evidence: claim_signature filled; evidence_hash recomputed to include it
- Non-lexical sub_types: claim_signature stays "" (deferred per ADR-0167 §Q1)
Real-data result on audit_brief_11.json:
- 14 distinct lexical tokens → 14 distinct signatures (no false collisions)
- No duplicate tokens in the 50-case sample; dedup logic verified deterministic
Wave 2, parallel with W2-C/D; depends on W1-A branch.
wrong=0 verified by passing regression suite.
Wave 2, parallel with W2-B/C/D. Implements the type-A→type-B converter
from AuditRow to MathReaderRefusalEvidence per ADR-0167 W2-A brief.
Deliverables:
- teaching/math_contemplation.py:
- audit_to_evidence(audit_rows): pure deterministic adapter, uses
SUB_TYPE_FOR_OPERATOR for subtype assignment, skips rows where
missing_operator is None, leaves claim_signature="" (W2-B will fill)
- audit_problem_to_evidence(problem_text, case_id): convenience wrapper
that runs the reader and adapts the output
- tests/test_math_contemplation_adapter.py: 8 tests covering
determinism, input-order preservation, sub-type mapping
exhaustiveness, distinct hashes across cases, empty input handling,
None-operator skip, and round-trip from problem text
Invariants:
- Deterministic across reruns (verified by determinism rerun)
- No I/O in adapter path
- Input order preserved (no internal sort)
- claim_signature == "" for all W2-A records (W2-B coordination)
Validation:
- tests/test_math_contemplation_adapter.py: 8 passed
- tests/test_math_evidence_schema.py: 11 passed (W1-A regression)
- tests/test_brief_11b_audit_artifact.py + step2_lexicon + brief_11_audit:
45 passed (regression)
- Determinism rerun: identical results
* feat(ADR-0167/W1-A): MathReaderRefusalEvidence schema + canonical-bytes
Foundation type for routing comprehension-reader refusals into the
teaching corridor. Frozen dataclass with sha256 evidence_hash computed
from deterministic canonical bytes (mirrors state.to_canonical_bytes
pattern). Includes SUB_TYPE_FOR_OPERATOR mapping table covering all 13
missing_operator values in the current audit artifact.
Wave 1 only — no runtime mutation, no teaching-store integration, no
admission path. Downstream W2-A/B/C/D type-import from this module.
* feat(ADR-0167/W2-C): domain discriminator + cross-domain audit
- Links to the audit doc: docs/handoff/ADR-0167-W2C-cross-domain-audit.md
- Inventory details: 5 construction sites, 8 consumption sites
- Verification: 0 cognition test files were modified; all tests are green
- Downstream partition work flagged: contemplation indexing (in teaching/contemplation.py) and replay gate (in teaching/proposals.py)
Foundation type for routing comprehension-reader refusals into the
teaching corridor. Frozen dataclass with sha256 evidence_hash computed
from deterministic canonical bytes (mirrors state.to_canonical_bytes
pattern). Includes SUB_TYPE_FOR_OPERATOR mapping table covering all 13
missing_operator values in the current audit artifact.
Wave 1 only — no runtime mutation, no teaching-store integration, no
admission path. Downstream W2-A/B/C/D type-import from this module.
* docs(ADR-0167): audit-as-teaching-evidence (math reader → contemplation wire)
Scoping ADR for Brief 11D Candidate E. Routes math-reader refusal audit
rows into the existing contemplation/HITL teaching corridor as a new
candidate source (`MathReaderRefusalEvidence`).
Key decisions:
- Evidence-only — never directly admits a math fact; only ratification
through HITL queue can change runtime behaviour
- Five sub-types proposed (Lexical / Frame / Composition / Reference /
Slot claims) mapping to the audit taxonomy
- Scope first to LexicalClaim — lowest-risk, highest-count
- Six open questions called out for the implementation ADR
ADR-0166 three-question test passes; implementation passes only when
the six open questions are answered with LexicalClaim-first scope.
No code in this PR.
* docs(ADR-0167): parallel work plan — 6-PR/3-wave dispatch across 5 model operators
## Summary
Lexicon-entry closure track per Brief 11D recommendation (Candidate A,
sub-PR 1). Adds 12 drain_token lemmas + 1 alias to `en_core_math_v1`.
`unknown_word` row strictly decreases: **11 → 5** (-6 cases moved past
the first-pass vocabulary gap). `wrong == 0` preserved. `correct` does
not move because admitted=0 (the unblocked cases now refuse at
downstream frames — real new work becoming visible, not regression, per
Brief 11 §Gate 1).
## Additions (all category=drain_token)
| Lemma | Surfaced from |
|-----------|----------------------------|
| along | case 0049 (3rd-wave) |
| animals | case 0040 (3rd-wave) |
| decrease | case 0005 |
| jacks | case 0024 (jumping jacks) |
| length | case 0006 (3rd-wave) |
| previous | case 0006 |
| reach | case 0015 |
| stray | case 0040 |
| too | case 0039 |
| uphill | case 0049 |
| which | case 0001 |
| your | case 0001 (3rd-wave) |
| weight → weights (alias) | case 0021 |
All classified as `drain_token` (the only category that cannot open a
frame and therefore cannot create wrong admissions per Brief 11
§"correct-count greed" doctrine). Reclassifying any as
accumulation/depletion/transfer verbs would risk wrong>0 by opening a
malformed operation_frame.
## wrong=0 verification
- `assert audit_problem(case_0050)` returns `ReaderRefusal` at
sentence_index 0 (pinned by `test_hazard_case_0050_remains_refused_pre_frame`)
- 50-case audit: `admitted=0, refused=50` (pinned by
`test_no_case_admits_after_lexicon_closure`)
- No reader runtime changes; pack-only mutation in a single
per-category source file
- Manifest checksum unchanged: source-file edit doesn't regenerate the
compiled `lexicon.jsonl`; loader reads per-category sources for
alias-aware entries (see `generate/comprehension/lexicon.py:127`)
## Test plan
- 11 new tests in `tests/test_brief_11b_step2_lexicon.py`:
- 4 pack-additions pinning (categories, provenance, aliases, sort order)
- 4 reader-effect / hazard tests (admitted=0, case 0050 refused,
unknown_word row strictly decreased, manifest checksum unchanged)
- 2 loader-integrity tests (new lemmas + aliases resolve through
`load_lexicon` → `lookup`)
- 12 existing tests in `tests/test_brief_11b_audit_artifact.py` pass
(taxonomy counts updated to post-step-2 values)
- 23 existing tests in `tests/test_brief_11_audit.py` pass
## Hard invariants preserved
- `wrong == 0` — no admissions, no frame-opener miscategorisation
- ADR-0166 — no new canonical eval lanes; existing
`gsm8k_math/train_sample/v1/` artifact updated in-place
- No teaching-store mutation; pack mutation is explicit, single-file,
reviewed
- Manifest checksum unchanged (compiled lexicon.jsonl byte-identical)
## Follow-up
- 3 lexicon_entry refusals remain (case 0001 '+', case 0040 'sees',
case 0049 'path'). Not addressed in this PR: '+' is an arithmetic
literal (would change semantics of drain), 'sees' and 'path' have
many other downstream barriers. Address with next-bottleneck PR.
- The 6 cases now refusing at later frames feed directly into Brief
11D Candidate A sub-PR 2 (which bottleneck class to attack next).
Per Brief 11B-step-2 §Hard constraints: no safe runtime/pack change lifts
any of the 8 pre_frame_filler_sentence cases without violating wrong=0.
This PR publishes the verb-classification analysis as documentation and
leaves the reader runtime and en_core_math_v1 pack unchanged.
Per-case classification:
- 0002 (splits): drain_token; honest blocker is compound_numeric_literal
- 0016 (traveled): drain_token; honest blocker is multi_quantity_composition
- 0025 (go/picking): drain_token; no quantity in sentence (true filler)
- 0028 (opens): drain_token; no quantity (true filler)
- 0030 (decides/go): drain_token; no quantity (true filler)
- 0035 (decided/split): drain_token; no quantity (true filler)
- 0036 (studying): drain_token; no quantity (true filler)
- 0050 (does): modal_aux; HAZARD — naive drain produces wrong>0
because next sentence admits Operation(mark, add, 3, songs)
while the answer requires frequency-by-duration aggregation
(every other day for 2 weeks); blocker is out of scope.
Post-skip simulation: even with the offending sentence elided, every
case still refuses on a downstream bottleneck (lexicon_entry,
pronoun_resolution, unit_binding, fraction_percentage_literal). Zero
lifts are available in Brief 11B-step-2 scope.
wrong=0 verification: no change to lifecycle.py / lexicon.py / audit.py /
en_core_math_v1/**; parent invariants from test_brief_11b_audit_artifact
continue to hold (admitted=0, refused=50, wrong_count=0).
Tests: 11 new tests in tests/test_brief_11b_step2_verb_classification.py
pinning the 8-case enumeration, post-skip refusal taxonomy per case,
hazard case 0050 remaining refused pre-frame, and the 50-case
admitted=0/refused=50/wrong=0 invariant.
Closes the Brief 11 sequence with a decision artifact (not a roadmap)
selecting the next capability after GSM8K Phase 2 reader closure.
Four candidates compared against ADR-0166's three-question test:
- A. Continued GSM8K operator closure
- B. Cross-domain reader generalization
- C. Tool-use trace integration
- D. Workbench demo hardening
Recommendation: continued GSM8K operator closure, starting with the
`lexicon_entry` row of the Brief 11B audit. The only candidate that
passes Q1/Q2/Q3 cleanly today and has an explicit Round-3 finish line.
Docs-only. No code, no test, no eval, no pack change.
## Summary
PR 11B in the Brief 11 sequence. Closes the missing-operator inference gap
left by 11A (#343) and ships the per-case audit artifact that Brief 11 §Gate 2
identifies as "the main Brief 11 artifact."
## Why this PR does NOT touch the reader runtime
The naive closure fix for `pre_frame_filler_sentence` (drain
`statement_terminator` at pre-frame) lifts 2 cases from refused → admitted
but creates a `wrong > 0` hazard on `gsm8k-train-sample-v1-0050`:
```
Mark does a gig every other day for 2 weeks. For each gig, he plays 3 songs.
... How many minutes did he play?
```
With the drain enabled, the reader admits `Operation(mark, add, 3, songs)`
with unknown unit `minute` and would project to a wrong answer. The stricter
variant (`pending_entity_ref is None` + no quantities) fires on 0 of the 11
candidate cases. Per Brief 11 §"Failure modes to avoid §1 — Correct-count
greed," this PR rejects both variants and routes the closure fix to a
follow-up that adds the required verb vocabulary or sentence-intent
classifier.
## Deliverables
- `generate/comprehension/audit.py` — three new missing-operator labels:
- `pre_frame_filler_sentence` (8 cases)
- `descriptive_frame_question` (2 cases)
- `question_frame_slot` (1 case)
Closes the 11-case `None`-operator gap left by 11A.
- `evals/gsm8k_math/train_sample/v1/audit_brief_11.json` — per-case audit
artifact pinned by tests.
- `evals/gsm8k_math/train_sample/v1/audit_brief_11.md` — narrative summary
including the rejected-fix design tension and ranked Brief 11B-step-2
backlog.
- `tests/test_brief_11b_audit_artifact.py` — 12 tests pinning the new labels,
the per-case artifact, the wrong=0 invariant, and the refusal taxonomy.
## Bottleneck taxonomy (after Brief 11B labelling)
| missing_operator | count | category |
|-------------------------------|------:|------------------------|
| quantity_extraction | 9 | incomplete_operation |
| lexicon_entry | 9 | unknown_word |
| multi_quantity_composition | 8 | incomplete_operation |
| pre_frame_filler_sentence | 8 | unexpected_category |
| pronoun_resolution | 3 | unresolved_pronoun |
| fraction_percentage_literal | 3 | unexpected_category |
| unit_binding | 3 | unattached_quantity |
| descriptive_frame_question | 2 | unexpected_category |
| (others, 1 each) | 5 | various |
## Test plan
- 12 new tests in `tests/test_brief_11b_audit_artifact.py` pass
- 23 existing 11A tests in `tests/test_brief_11_audit.py` pass
- No runtime changes; reader byte-identical to main
## Hard invariants preserved
- `wrong == 0` — no runtime change, no new admissions
- ADR-0166 — no new canonical eval lanes added; existing
`evals/gsm8k_math/train_sample/v1/` artifact set extended
- No teaching store / pack mutation
## Follow-up
- **11B-step-2** — verb-vocabulary expansion or sentence-intent classifier
for `pre_frame_filler_sentence` (8 cases). See audit_brief_11.md §"design
tension" for the rejected one-line variants and why they fail wrong=0.
- **11C** — existing-lane capability snapshot (still gated on 11B-step-2 or
another closure pass).
Extend the comprehension reader from question-only scope to whole-
problem scope. Phase 1 (Brief 8 / #326) implemented question_frame;
this brief implements initial_state_frame, operation_frame, and
descriptive_frame, plus finalize() projection into a strict
ADR-0115 MathProblemGraph.
Architecturally correct under ADR-0164.3; not yet productive on
GSM8K train_sample. Below-floor measurement documented; specific
bottlenecks tabled for Phase 2.1 follow-up.
What landed
- Frame-opener dispatch in lifecycle.py for the three new statement
frames, plus rule handlers (_rule_op_*, _rule_preframe_*,
_rule_descriptive_*).
- finalize(state) -> MathProblemGraph | ReaderRefusal: pure
projection with closure checks (entity registry non-empty,
unknown target bound, every op/initial references a known entity,
Decimal precision projects losslessly).
- _classify extended to 3-tuple (category, surface, decimal_value)
with possessive strip retry. Brief 8.2's sentence-initial
lookup-first + gender-skip preserved AND extended to mid-sentence
(gender is enrichment everywhere, never admission).
- Whole-problem coexistence dispatch in math_candidate_graph.py
(config.comprehension_reader_questions=True): reader attempts the
whole problem; on any ReaderRefusal falls through to existing
regex parser. All-or-nothing per the brief.
- Lexicon expansion (carried into renamed proper_noun_gender_*
files): +2 accumulation_verb (adopt, invest), +2 currency_unit_noun
(dollar, cent), +6 capacity_verb (fill, lift, play, work, finish,
drive), +5 female names (allison, brooke, jan, marion, sidney),
+14 male names (bart, fernando, georgie, jake, jed, jeremie, jose,
orlando, rex, rudolph, steve, troy, xavier, yun), +numerous
count_unit_noun, drain_token, time_unit_noun.
- ADR-0164.4-phase2-statement-frame-reader.md — the architectural
rationale and acceptance contract.
Measurement (reader_phase2_delta.json):
flag-OFF: correct=3 refused=47 wrong=0
flag-ON: correct=3 refused=47 wrong=0
delta: 0/0/0
Below the brief's floor of correct >= 4. Architecture is sound — the
reader admits cases as graphs when the structure resolves, refuses
cleanly otherwise, preserves wrong=0 across both flag states.
Bottleneck table (from per-case attribution):
count refusal_class dominant cause
----- ---------------------- ------------------------------------
18 incomplete_operation multi-quantity ops; no-quantity op
11 unknown_word "hundred", "presently", "one-hour",
non-math verbs (compound numerics,
lexicon gaps)
6 unexpected_category fraction / percentage literals;
multi-subject sentences
6 unresolved_pronoun "them", "their", "his" with no
compatible entity
5 unattached_quantity quantity never bound to a unit
1 no_question_target question parsed but slot never set
Closing the gate to mixed-bounded [4, 24] is Phase 2.1 scope: extend
composition rules for multi-quantity ops, add fraction/percentage
primitives (per ADR-0164.1 amendment), expand lexicon for the
remaining unknown_word cases, extend pronoun resolution.
Invariants preserved
- wrong = 0 in both flag states ✓
- flag-OFF byte-identical to today ✓
- determinism (50/50 identical runs) ✓
- Capability axes G1-G5, S1 unchanged ✓
- Reader tests: 19 (Phase 2) + 18 (Phase 1, post-update) + 53 (pack)
+ 76 (lexicon + primitives) = 166 specific to this change; all pass
- core test --suite smoke -q: 67 passed
Rebase note
This PR was authored against an older base; rebased onto current
main to incorporate #333 (Brief 8.2 universal proper_noun_token
primitive) and #334 (ADR-0166 measurement discipline). The rebase
required:
- Lexicon files renamed proper_noun_entity_* -> proper_noun_gender_*
(with the Phase 2 additions merged into the gender_* files)
- Compiled lexicon.jsonl unchanged from #333's 207-entry state
(Phase 2's per-category additions are runtime-visible via the
source loader, not via the compiled file)
- _classify reconciled with Brief 8.2's sentence-initial dispatch +
Phase 2's 3-tuple decimal-value return
- All dispatch tables and category checks updated to reference
proper_noun_token (singular) instead of proper_noun_entity_{f,m}
- Three Phase 1 test expectations updated to reflect Phase 2
behavior (proper noun at position 0 now opens statement pre-frame
instead of refusing; pronoun resolution applies per ADR-0164.2)
Per ADR-0166's three-question test, this PR is honest measurement:
capability exists, at least one case admits, lane distinguishes
presence from absence — which the bottleneck table demonstrates.
Refs ADR-0164.3 §Phasing Phase 2, ADR-0164.1 amendment (Brief 8.2),
ADR-0166 §"Mixed (notable but not blocking)" — except here, below
floor.
Add the fourth governing principle to the family of structural-
invariant ADRs (alongside ADR-0114a anti-overfitting, ADR-0165 regex
scope rule, CLAUDE.md versor invariant). The rule, stated negatively:
do not author eval lanes ahead of the operators those lanes test, and
do not expand the eval surface ahead of the capability that produces
signal on it.
Three-question test for new eval lanes:
1. Does the capability this lane probes exist on main today?
2. Has at least one case admitted end-to-end through that capability?
3. Will running this lane distinguish capability-presence from
capability-absence?
A "no" on any defers the lane until the capability lands. Tier 3 TBD
rows are data debt; running existing lanes to populate them is
permitted (snapshot of current capability) but is NOT a substitute for
capability work.
Why now: a strategic-analysis exchange this session proposed authoring
spatial_geometry_ood, historical_sequence_ood, and other new lanes
while GSM8K-math sits at 3/47/0 and the comprehension reader (ADR-0164)
is mid-build. The proposal's "most impactful next commit is to run all
Tier 3 lanes" framing would have generated noise (lanes refusing
uniformly because their underlying operators don't exist) rather than
the diagnostic signal that justifies prioritization. ADR-0166 mechanizes
the constraint that prevents that pattern.
Session log SESSION-2026-05-27-tier3-sequencing.md captures the
narrative: what the analysis got right (geometry-first as strategic
bet, sequencing instincts), what it missed (GSM8K-math treated as
solved; comprehension reader pivot not in context), and the honest
re-sequence (Brief 10 first; Tier 3 snapshot in parallel; cross-domain
transfer after verifying whether the reader IS the requested
structural-pattern recognizer under a different name).
The session also surfaced a mid-flight diagnostic from PR #332: the
actual GSM8K bottleneck is the ADR-0163 recognizer injector emitting
incomplete graphs, which the reader correctly refuses to admit
(wrong=0 by construction via the new guard). Brief 10 (Phase 2 reader)
dominates here because it replaces the inadequate injector surface
entirely.
No code changes. ADRs only.
Refs ADR-0114a, ADR-0165, CLAUDE.md §"Non-Negotiable Field Invariant".
ADR-0164.1 amendment: replace name-whitelist entity admission with a
universal lexeme primitive that recognizes any capitalized token as a
proper noun. The gender-coded name lists are demoted from admission
criterion to enrichment-only lookup. A name outside the curated lists
still admits cleanly with gender="unknown" — ADR-0164.2's pronoun
resolution rules handle the unknown case via single-salient fallback
or refuse with ambiguous_pronoun_referent.
Universal at the primitive layer: the new proper_noun_token primitive
is domain-agnostic. It sits in the shared PRIMITIVE_REGISTRY and is
available to every current and future reader (math, narrative,
code-comment, multi-lingual). The math reader is its first consumer.
Pattern: ^[A-Z][A-Za-z'-]*[a-z][A-Za-z'-]*$
- requires capitalized first letter
- requires ≥1 lowercase letter (rejects all-caps acronyms)
- allows internal apostrophes (O'Brien) and hyphens (Mary-Anne)
- matches "Tina", "Bob", "Marnie", "McDonald" — rejects "TINA",
"123", "$5.00" (those go to their own primitives)
Sentence-initial lookup-first dispatch (lifecycle._classify):
- At token_index == 0: lookup() first, skipping proper_noun_gender_*
categories (treated as not-found so the primitive can fire). If
lookup misses, primitive scan picks up novel names. Inverts the
question from "is this a name?" to "is this a known common word?"
- At token_index > 0: primitive-first with UNIT_CATEGORY_TOKEN ceding
to operational lexicon for currency_unit_noun overrides.
Lexicon rename (per-category source files):
- proper_noun_entity_female.jsonl -> proper_noun_gender_female.jsonl
- proper_noun_entity_male.jsonl -> proper_noun_gender_male.jsonl
Compiled lexicon.jsonl: rename the two semantic_domain tags; drop
"marnie" (was only in proper_noun_entity_female, now absent from
the gender-coded sources). Net: 208 -> 207 entries. New manifest
checksum: 1fb9b0d790258736267d528e8e8a2436ce88b9ce690805fe2813ba077861ba2a
New helper gender_of_proper_noun(surface, lexicon) returns
Literal["female","male","neuter","unknown"] — pure enrichment lookup,
never gates admission.
Measurement (reader_phase1_plus_proper_noun_delta.json):
- pre-primitive baseline: correct=3 refused=47 wrong=0
- post-primitive measurement: correct=3 refused=47 wrong=0
- No regression on wrong=0
- No net admission increase observed in this train-sample harness;
the architectural value is for future text outside the curated
gender lists (Sonnet's #332 expanded those to cover GSM8K names).
Tests:
- test_lexeme_primitives.py: registry count 8 -> 9, proper_noun_token
fires + variants (Bob, Marnie, McDonald, O'Brien, Mary-Anne),
numeric/all-caps refusals, numeric-literal still wins overlap on "123"
- test_reader_question_frame.py: 5 new tests for sentence-initial
dispatch + unknown-gender pronoun resolution + novel-name admission
via primitive (Zelda)
- test_en_core_math_v1_pack.py: category counts updated; mutual-exclusion
between gender_female and gender_male preserved; total 208 -> 207
- test_lexicon.py: category list + lookup assertion updated to renamed
proper_noun_gender_female
- test_proper_noun_primitive_universality.py: new test module asserting
domain-agnostic property of the primitive
Validation:
- pack + lexicon + primitive tests: 147 passed
- reader + universality tests: 22 passed
- smoke lane: 67 passed
Closes the engine_state question by leaving those files untracked
(repo discipline: runtime artifacts never enter PRs).
Refs ADR-0164.1 amendment, ADR-0164.2 §EntityRegistry, ADR-0165
§Legitimate uses (the new primitive passes the three-question test).
Phase A — RuntimeConfig flag:
core/config.py: adds `comprehension_reader_questions: bool = False`
Default OFF preserves byte-identical behaviour with today.
Phase B — Hybrid wiring in candidate-graph path:
generate/math_candidate_graph.py:
- _try_reader_for_question() dispatches to the comprehension reader
BEFORE the regex question parser; refusal falls through to regex
- reader_trace: tuple[str, ...] field on CandidateGraphResult captures
JSON-encoded admit/fallthrough events for audit
generate/comprehension/lifecycle_runtime_adapter.py (new):
- build_problem_state_from_candidates(): converts regex-parser output
to ProblemReadingState for the reader's pronoun-resolution step
- invoke_reader_for_question(): tokenises sentence, drives lifecycle
- project_to_candidate_unknown(): QuestionTargetSlot → CandidateUnknown
- trace-event constructors for admit and fallthrough
Phase C — Capability-axis regression:
All existing tests pass with flag OFF and ON; zero new regressions.
Two pre-existing failures on main are unrelated to this PR.
Phase D — GSM8K train_sample measurement:
evals/gsm8k_math/train_sample/v1/runner.py: --use-reader flag triggers
baseline-off + reader-on runs and writes reader_phase1_delta.json
evals/gsm8k_math/train_sample/v1/reader_phase1_delta.json (new):
baseline-off: correct=3 refused=47 wrong=0
reader-on: correct=3 refused=47 wrong=0
delta: all zeros — Mixed result expected (Phase 2 scope)
wrong=0 invariant preserved in both modes.
Phase E — Coexistence tests:
tests/test_reader_coexistence.py (new): 13 tests covering
flag-OFF byte-identity, flag-ON determinism, wrong=0 invariant,
trace shape validation, Brief-8 target admission, and fallthrough
preservation for unknown-unit words.
Admission gate result: Mixed (correct=3, below the ≥10 bar).
All statement-side barriers remain in place; Phase 2 (reader for
statement sentences) is required to drive correct≥10. Documented in
reader_phase1_delta.json and train_sample/v1/runner.py docstring.
Adds the three lifecycle functions for the incremental compositional
reader per ADR-0164.3 §Lifecycle API:
- begin_sentence(problem_state, source_text_offset) -> SentenceReadingState
- apply_word(sentence_state, problem_state, word) -> SentenceReadingState | ReaderRefusal
- end_sentence(sentence_state, problem_state) -> ProblemReadingState | ReaderRefusal
Phase 1 scope is question sentences only. The update rules for the
question_frame live in a single readable table (_QUESTION_FRAME_RULES);
statement-side frames (initial_state_frame, operation_frame,
descriptive_frame) refuse with a Phase-2 diagnostic.
The five Brief-8 GSM8K target question sentences (0007, 0017, 0027,
0036, 0043) produce valid QuestionTargetSlot outputs end-to-end.
_interface_stubs.py provides a thin, functional surface for the
lexeme-primitive scanner (Brief 6) and lexicon loader (Brief 7) so
this PR does not block on them. The stub honours the en_core_math_v1
pack entries and adds a closed Phase-1 supplemental vocabulary marked
for fold-in to the pack once Briefs 6/7 land.
Tests cover determinism (byte-equal canonical bytes), the five GSM8K
target sentences with expected (entity, unit_class, kind) triples,
all token-level and sentence-level refusal modes, and lifecycle
invariants (registry preservation, sentence_index advance).
Stacked on feat/state-two-level-split (PR #323) per ADR-0164.3
§Naming — state types live in state.py.
Adds generate/comprehension/lexeme_primitives.py with the eight seed
primitives specified by ADR-0164.1:
decimal-currency-literal (priority 10)
currency-literal (priority 20)
percentage-literal (priority 30)
fraction-literal (priority 40)
time-amount-literal (priority 50)
ordinal-literal (priority 60)
mass-noun-token (priority 70)
numeric-literal (priority 100)
LexemePrimitive and LexemeMatch are frozen/slots dataclasses. scan()
runs primitives in priority order and returns the first hit wrapped in
a MappingProxyType over sorted-key extracted_values for canonical-bytes
stability. All patterns use explicit space characters ([ ]?, [- ]?) not
\s so the ADR-0165 compliance invariant holds.
55 tests cover: construction invariants, canonical fires (each
primitive on its own example), overlap precedence ($18.00, 1/2, 50%),
refusal on Tina/empty/verbs, determinism, sorted-key stability, and
the ADR-0165 compliance smoke test.
Ports the closed-set vocabulary from generate/math_candidate_parser.py and
generate/math_roundtrip.py into a new language pack en_core_math_v1, following
the manifest-checksum discipline of en_core_cognition_v1 and en_core_relations_v1.
208 lemmas across 11 semantic categories:
- accumulation_verb (17) — from ADD_VERBS + _COND_ADD_VERBS + _EARNINGS_VERBS
- depletion_verb (15) — from SUBTRACT_VERBS + _COND_SUBTRACT_VERBS
- transfer_verb (7) — from TRANSFER_VERBS; give/send/return removed from depletion
- currency_unit_noun (8) — from _MASS_NOUNS
- entity_pronoun (4) — from _Q_SUBJECT_PRONOUN
- proper_noun_entity_female (62) — from _FEMALE_NAMES
- proper_noun_entity_male (76) — from _MALE_NAMES
- possession_verb (1) — have/has/had collapsed to bare lemma
- capacity_verb (13) — from _CAPACITY_VERBS (pick/pack/make exclusive here)
- question_open (2) — how, what
- residual_modifier (3) — left, remaining, after (attested in _COND_OP_Q_RE)
Pack is NOT wired into any runtime path (ADR-0164 Phase 3).
Source constants in math_candidate_parser.py are unchanged.
Deferred categories documented in manifest.json `deferred` field.
53 contract tests cover: checksum, per-category counts, provenance,
mutual-exclusivity invariants (acc ∩ dep = ∅, acc ∩ cap = ∅, dep ∩ xfer = ∅),
and ≥2 semantic domains per compiled entry.