Phase 1 and Phase 2 of the ADR-0164 reader are correctly implemented but contribute zero eval admissions today. The bottleneck is lexicon coverage (unknown verbs/nouns) and explicit Phase 2.1 scope gates (fractions), not the all-or-nothing dispatch policy. Produces COMPREHENSION-READER-AUDIT.md answering the five Brief C questions: call trace, cognition-lane usage (none), bottleneck analysis, ADR promise audit, and three falsifiable options (operationalize/relabel/retire). Recommendation: relabel (status update) now; lexicon expansion next as the highest-leverage first step toward actual eval lift. Also updates ADR-0164 status from "Proposed" to "Partially implemented" with a Current Status table and next-lift-path summary.
12 KiB
Comprehension Reader Audit
Brief: POST-RAT1-PARALLEL-BRIEFS.md §"Brief C"
Date: 2026-05-27
Operator: Sonnet (investigation phase)
Branch: docs/comprehension-reader-audit
Summary finding
The comprehension reader (ADR-0164 Phase 1 + 2) is not an inert component — it is
actively exercised by tests and correctly wired into parse_and_solve. However it
contributes zero eval admissions today because:
- Phase 2 (whole-problem reader) refuses on every train-sample problem due to fraction/percentage tokens, unresolved pronouns, or multi-quantity structures that are explicitly out-of-scope per Phase 2.1.
- Phase 1 (question-sentence hybrid) admits a
CandidateUnknownbut the statement side never completes — the question reader's admission can only produce a result when the statement sentences also yieldper_sentence_choices; for the 47 refused cases, statements already fail at the regex level, so the question reader's partial success is unreachable.
The reader is a math substrate, not a cognition track. It is not used anywhere in the cognition eval lane. It is the designed long-term replacement for the regex front-end (ADR-0164 §Decision) but is not yet at the coverage threshold where it produces observable eval lift.
Q1 — Call trace: where does _try_comprehension_reader actually run?
Single caller. generate/math_candidate_graph.py::parse_and_solve (line 568).
No other production code calls it.
The call tree within parse_and_solve has two paths:
parse_and_solve(text, config)
│
├── config.comprehension_reader_questions == True [flag-gated]
│ │
│ ├── ADR-0164 Phase 2 — whole-problem reader
│ │ _try_comprehension_reader(text) [line 569]
│ │ → begin_sentence / apply_word / end_sentence / finalize
│ │ → CandidateGraphResult on success
│ │ → None on any RefusalError (falls through to regex path)
│ │
│ └── (Phase 2 fell through) continue to regex statement loop
│ → ADR-0164 Phase 1 — question-sentence hybrid
│ _try_reader_for_question(question_sentence, ...) [line 821]
│ → build_problem_state_from_candidates
│ → invoke_reader_for_question
│ → list[CandidateUnknown] | None
│ → on reader admission: use reader's CandidateUnknown
│ → on refusal: regex question parser (Pattern A/B/C)
│
└── config is None or flag False → regex-only path (unchanged)
The flag comprehension_reader_questions is:
- Declared in
core/config.py:288(defaultFalse) - Set to
Trueonly inevals/gsm8k_math/train_sample/v1/runner.py:92when--use-readeris passed. - Not set anywhere in the live
chat/runtime.pypath or any cognition eval runner.
The reader is never active in production chat turns. It is activated only by an explicit CLI flag in the train-sample eval runner.
Q2 — Does the reader admit anything on the cognition lane?
No. comprehension_reader_questions is not set in any cognition eval runner.
The reader is purely a math-domain component. Cognition evals (core eval cognition,
core test --suite cognition) do not call parse_and_solve and have no concept of
comprehension_reader_questions.
There is no reader-on-cognition usage to measure.
Q3 — Is all-or-nothing the bottleneck, or is the reader itself refusing on simple shapes?
Both, at different phases.
Phase 2 (whole-problem reader, _try_comprehension_reader)
The all-or-nothing policy is architecturally correct but the reader itself refuses
early on nearly every train-sample problem. Confirmed refusal sites in lifecycle.py:
| Token type | Handling | Covers cases |
|---|---|---|
fraction_token / percentage_token |
Explicit refusal at line 344–356: "out-of-scope (embedded-quantifier aggregate; deferred to Phase 2.1)" | 0004, 0005, 0010, 0041, etc. |
unknown_word |
Any word absent from the lexicon (many proper nouns, verbs not yet in lexicon) | Majority of 47 refused |
pronoun_resolution failure |
entity_pronoun without a resolvable prior entity |
0012, 0015, etc. |
multi_quantity_composition |
No composition frame in Phase 2 scope | 0006, 0013, 0025, etc. |
For the current 47 refused cases, the Phase 2 reader fails at or before the first
non-trivial token and returns None, deferring to the regex path. The all-or-nothing
rule is not the marginal bottleneck; lexicon coverage is. Even with per-sentence
relaxation, a sentence containing an unknown verb would refuse.
Phase 1 (question-sentence hybrid, _try_reader_for_question)
Phase 1 is more targeted — it only reads the question sentence, informed by
per_sentence_choices already produced by the regex parser. Its bottleneck is the
upstream statement failure: if the regex parser can't build per_sentence_choices
for a statement sentence, _try_reader_for_question is called with an incomplete or
empty flat list, and build_problem_state_from_candidates has insufficient context
to produce meaningful question-slot resolution.
The Phase 1 reader does admit case 0027 (Malcolm/followers) per
test_reader_coexistence.py::test_case_0027_malcolm_admits. That's the only
confirmed admission on train_sample under flag ON. The 3 currently-correct cases
are unchanged between flag OFF and flag ON (coexistence test).
Conclusion: all-or-nothing is not causing the zero-lift result. The reader admits the cases it can handle; it simply cannot yet handle most train-sample problems because their lexicon coverage and structural scope (fractions, multi-quantity, complex pronouns) exceed Phase 1/2 scope.
Q4 — ADR-0164 Phase 1/2 promises vs current state
| Promise | Status |
|---|---|
| Phase 1: question reader with regex fallthrough | ✅ implemented and tested (52 tests across 3 test files) |
| Phase 1: wrong=0 preserved under flag ON | ✅ verified by test_reader_coexistence.py::TestWrongZeroInvariant |
| Phase 1: admit case 0027 (Malcolm/followers) | ✅ test_case_0027_malcolm_admits |
| Phase 2: whole-problem reader, all-or-nothing | ✅ implemented, _try_comprehension_reader |
| Phase 2: fraction/percentage scope declared | ✅ explicit refusal at lifecycle.py:344, labeled "deferred to Phase 2.1" |
| Phase 2: eval delta on train_sample | ✗ zero new admissions under flag ON |
| Phase 3 (per ADR-0164 §Phasing): remove regex question parser | Not started — reader must reach sufficient question coverage first |
| Lexicon: seed corpus ported from regex parser | ✅ math lexicon loaded at generate/comprehension/lexicon.py::load_lexicon |
The only broken promise is Phase 2's intended eval delta. The ADR did not specify a
minimum lift target for Phase 2 at initial ship — it specified "measure pickup rate
against train_sample/v1 per round." The current pickup rate is 0 new cases on Phase
2 and 0 new cases on Phase 1 (case 0027 is already correct via regex). This is an
accurate measurement, not a latent bug.
There are no ADR-0164 Phase 1/2 contract violations. The reader is operating within its declared scope. The scope is narrow.
Q5 — Three options and measurable tests
Option A: Operationalize (expand scope incrementally)
What: Expand Phase 2 scope to handle one new token class per iteration:
- Common proper nouns not in the lexicon — add lexicon entries via the ratification corridor (ADR-0150/0152/0155/0161). No code change.
multiplicative_aggregationstructures (e.g. "6 baskets × 50 strawberries") — add adistributive_modifierframe rule to Phase 2. ADR required.- Fraction/percentage embedded quantifiers — add Phase 2.1 handling. Separate ADR.
This is the intended path per ADR-0164 §Phasing.
Measurable test: After each lexicon expansion, run uv run python -m evals.gsm8k_math.train_sample.v1.runner --use-reader and count new Phase 2
admissions. A single lexicon batch adding the 15 most common unknown verbs should
move the Phase 2 admission count from 0 to ≥ 2.
Ship as: Series of small PRs, each adding lexicon entries or a single new frame rule. Not this brief — this brief is investigation only.
Option B: Relabel (honest documentation update)
What: Add a status section to ADR-0164 acknowledging the current measurement
(Phase 2: 0 new admissions on train_sample) and naming the scope gates that block
lift. Rename the reader's activation flag from the generic
comprehension_reader_questions to something that signals its scope:
e.g., comprehension_reader_phase2_experimental.
Measurable test: No code change → no measurable test required. The honest claim after relabeling: "the reader is implemented and correct-by-construction; it produces no eval delta because its lexicon and structural scope don't yet cover any of the 47 refused cases."
Ship as: This PR (docs-only). Low risk, zero regression surface.
Option C: Retire (remove dead code)
What: Remove _try_comprehension_reader, _try_reader_for_question, and
lifecycle_runtime_adapter.py; revert the comprehension_reader_questions flag;
remove the 52 tests.
Why not: The reader is not dead code. It is architecturally load-bearing:
- It is the designed replacement for the regex front-end (ADR-0164 §Decision).
- It has 52 tests, 3 test files, 1,872 lines in
lifecycle.py, and a working Phase 1 admission on case 0027. - Retiring it would require reverting ADR-0164 and ADR-0165 (regex scope rule), since those two ADRs are paired: 0165 forbids cross-word regex, and 0164 provides the replacement.
Measurable test for disconfirmation: If any of the 52 reader tests currently
fail or test no meaningful property (see CLAUDE.md §Schema-Defined Proof Obligations),
retirement would be justified for those specific tests. Inspection shows the tests
are substantive — Phase 2 tests (test_reader_phase2.py) exercise actual statement
frame parsing with real admission paths.
Verdict: retire is wrong.
Recommendation
Ship Option B (relabel) in this PR. Add a ## Current Status section to
docs/decisions/ADR-0164-incremental-comprehension-reader.md recording:
- Phase 1: implemented, tested, 0 net new admissions (case 0027 already correct via regex; wrong=0 verified)
- Phase 2: implemented, tested, 0 new admissions (fraction/multi-quantity scope gates block all 47 refused cases; explicit "Phase 2.1" label already in code)
- Next lift path: lexicon expansion via ratification corridor (no code change) followed by Phase 2.1 fraction scope (separate ADR)
Option A (operationalize) follows naturally as the next ADR wave whenever the lexicon ratification corridor is the active dispatch target. It does not require a structural change to the reader — just lexicon entries and optionally a Phase 2.1 fraction rule.
Falsifiability of recommendation
The claim "lexicon expansion is the highest-leverage next step" is falsifiable:
- Add the 10 most-common unknown verbs from the 47 refused cases to the math lexicon
(via
apply_lexical_claimor direct ratification). - Run
uv run python -m evals.gsm8k_math.train_sample.v1.runner --use-reader. - If ≥ 1 new case admits: claim confirmed; lexicon-first is the path.
- If 0 new admissions: claim refuted; the bottleneck is structural (frame rules), not vocabulary — escalate to Phase 2.1 ADR first.
Files read during investigation
generate/math_candidate_graph.py— call sites (lines 445–515, 564–571, 809–833)generate/comprehension/lifecycle.py— reader implementation (1,872 lines)generate/comprehension/lifecycle_runtime_adapter.py— Phase 1 bridge (402 lines)generate/comprehension/state.py— reader state types (828 lines)core/config.py:288— flag declarationevals/gsm8k_math/train_sample/v1/runner.py— sole production flag consumerdocs/decisions/ADR-0164-incremental-comprehension-reader.md— doctrinetests/test_reader_phase2.py(19 tests),test_reader_question_frame.py(20 tests),test_reader_coexistence.py(13 tests)