core/docs/handoff/COMPREHENSION-READER-AUDIT.md
Shay be85c66801 docs(brief-C): comprehension reader audit — zero eval delta diagnosis + next steps
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.
2026-05-27 20:45:31 -07:00

12 KiB
Raw Permalink Blame History

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:

  1. 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.
  2. Phase 1 (question-sentence hybrid) admits a CandidateUnknown but the statement side never completes — the question reader's admission can only produce a result when the statement sentences also yield per_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 (default False)
  • Set to True only in evals/gsm8k_math/train_sample/v1/runner.py:92 when --use-reader is passed.
  • Not set anywhere in the live chat/runtime.py path 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 344356: "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:

  1. Common proper nouns not in the lexicon — add lexicon entries via the ratification corridor (ADR-0150/0152/0155/0161). No code change.
  2. multiplicative_aggregation structures (e.g. "6 baskets × 50 strawberries") — add a distributive_modifier frame rule to Phase 2. ADR required.
  3. 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:

  1. Add the 10 most-common unknown verbs from the 47 refused cases to the math lexicon (via apply_lexical_claim or direct ratification).
  2. Run uv run python -m evals.gsm8k_math.train_sample.v1.runner --use-reader.
  3. If ≥ 1 new case admits: claim confirmed; lexicon-first is the path.
  4. 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 445515, 564571, 809833)
  • 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 declaration
  • evals/gsm8k_math/train_sample/v1/runner.py — sole production flag consumer
  • docs/decisions/ADR-0164-incremental-comprehension-reader.md — doctrine
  • tests/test_reader_phase2.py (19 tests), test_reader_question_frame.py (20 tests), test_reader_coexistence.py (13 tests)