feat(brief-11/11B-step-2): lexicon closure — unknown_word 11→5, wrong=0 preserved (#348)

## 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).
This commit is contained in:
Shay 2026-05-27 06:06:41 -07:00 committed by GitHub
parent 40ccefeaa8
commit 66ef4ad07c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 360 additions and 90 deletions

View file

@ -1,6 +1,6 @@
{
"schema_version": 1,
"brief": "Brief 11B \u2014 reader closure audit (per-case)",
"brief": "Brief 11B-step-2 \u2014 lexicon closure",
"adr_chain": [
"0164",
"0164.3",
@ -14,32 +14,33 @@
"admitted": 0,
"refused": 50,
"refusal_reasons": {
"unknown_word": 11,
"unexpected_category": 14,
"incomplete_operation": 18,
"unknown_word": 5,
"unexpected_category": 17,
"unattached_quantity": 4,
"incomplete_operation": 20,
"no_question_target": 1,
"unattached_quantity": 3,
"unresolved_pronoun": 3
},
"missing_operators": {
"lexicon_entry": 9,
"pre_frame_filler_sentence": 8,
"lexicon_entry": 3,
"pre_frame_filler_sentence": 9,
"descriptive_frame_question": 2,
"fraction_percentage_literal": 3,
"fraction_percentage_literal": 4,
"unit_binding": 4,
"multi_quantity_composition": 8,
"quantity_extraction": 9,
"quantity_extraction": 11,
"question_target_slot": 1,
"unit_binding": 3,
"pronoun_resolution": 3,
"compound_time_literal": 1,
"multi_subject_sentence": 1,
"multi_subject_sentence": 2,
"question_frame_slot": 1,
"compound_numeric_literal": 1
}
},
"invariants": {
"wrong_count": 0,
"reader_runtime_changes_in_this_pr": false
"reader_runtime_changes_in_this_pr": false,
"pack_changes": "12 drain_token lemmas + 2 aliases added to en_core_math_v1"
},
"per_case": [
{
@ -47,10 +48,10 @@
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'which'",
"refusal_detail": "no primitive or lexicon match for '+'",
"sentence_index": 1,
"token_index": 16,
"token_text": "which",
"token_index": 23,
"token_text": "+",
"recognized_terms": [
"If",
"she",
@ -67,7 +68,14 @@
"eligible",
"for",
"overtime",
","
",",
"which",
"is",
"paid",
"by",
"your",
"hourly",
"wage"
],
"skipped_frame": "operation_frame"
},
@ -148,12 +156,12 @@
{
"case_id": "gsm8k-train-sample-v1-0005",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'decrease'",
"refusal_reason": "unexpected_category",
"missing_operator": "fraction_percentage_literal",
"refusal_detail": "fraction/percentage literal at position 10 is out-of-scope (embedded-quantifier aggregate; deferred to Phase 2.1)",
"sentence_index": 0,
"token_index": 8,
"token_text": "decrease",
"token_index": 10,
"token_text": "3/4",
"recognized_terms": [
"In",
"one",
@ -162,19 +170,21 @@
"Addison",
"mountain's",
"temperature",
"will"
"will",
"decrease",
"to"
],
"skipped_frame": null
},
{
"case_id": "gsm8k-train-sample-v1-0006",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'previous'",
"refusal_reason": "unattached_quantity",
"missing_operator": "unit_binding",
"refusal_detail": "1 quantities never attached to entity+unit at sentence end",
"sentence_index": 2,
"token_index": 10,
"token_text": "previous",
"token_index": 12,
"token_text": "",
"recognized_terms": [
"Presently",
",",
@ -185,7 +195,10 @@
"are",
"4",
"times",
"the"
"the",
"previous",
"length",
"."
],
"skipped_frame": "operation_frame"
},
@ -385,12 +398,12 @@
{
"case_id": "gsm8k-train-sample-v1-0015",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'reach'",
"refusal_reason": "incomplete_operation",
"missing_operator": "quantity_extraction",
"refusal_detail": "operation_frame closed with no quantity",
"sentence_index": 1,
"token_index": 7,
"token_text": "reach",
"token_index": 12,
"token_text": "",
"recognized_terms": [
"What's",
"the",
@ -398,7 +411,13 @@
"time",
"he",
"takes",
"to"
"to",
"reach",
"the",
"Bronx",
"from",
"Manhattan",
"?"
],
"skipped_frame": "operation_frame"
},
@ -542,18 +561,27 @@
{
"case_id": "gsm8k-train-sample-v1-0021",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'weights'",
"sentence_index": 0,
"token_index": 3,
"token_text": "weights",
"refusal_reason": "unexpected_category",
"missing_operator": "pre_frame_filler_sentence",
"refusal_detail": "category 'statement_terminator' (word='.') at pre-frame position 12 not handled; may be Phase-3 scope",
"sentence_index": 1,
"token_index": 12,
"token_text": ".",
"recognized_terms": [
"John",
"is",
"lifting"
"He",
"bench",
"presses",
"15",
"pounds",
"for",
"10",
"reps",
"and",
"does",
"3",
"sets"
],
"skipped_frame": "descriptive_frame"
"skipped_frame": null
},
{
"case_id": "gsm8k-train-sample-v1-0022",
@ -612,17 +640,19 @@
{
"case_id": "gsm8k-train-sample-v1-0024",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'jacks'",
"refusal_reason": "unexpected_category",
"missing_operator": "multi_subject_sentence",
"refusal_detail": "second entity 'Monday' at pre-frame position 6; multi-subject sentences are Phase-2.1 scope",
"sentence_index": 0,
"token_index": 4,
"token_text": "jacks",
"token_index": 6,
"token_text": "Monday",
"recognized_terms": [
"Sidney",
"does",
"20",
"jumping"
"jumping",
"jacks",
"on"
],
"skipped_frame": null
},
@ -946,32 +976,36 @@
{
"case_id": "gsm8k-train-sample-v1-0039",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'too'",
"sentence_index": 0,
"token_index": 7,
"token_text": "too",
"refusal_reason": "incomplete_operation",
"missing_operator": "quantity_extraction",
"refusal_detail": "operation_frame closed with no quantity",
"sentence_index": 2,
"token_index": 10,
"token_text": "",
"recognized_terms": [
"At",
"the",
"family",
"reunion",
",",
"everyone",
"ate"
"Jose",
"gained",
"two",
"pounds",
"more",
"than",
"twice",
"what",
"Orlando",
"gained",
"."
],
"skipped_frame": "descriptive_frame"
"skipped_frame": "operation_frame"
},
{
"case_id": "gsm8k-train-sample-v1-0040",
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'stray'",
"refusal_detail": "no primitive or lexicon match for 'sees'",
"sentence_index": 0,
"token_index": 8,
"token_text": "stray",
"token_index": 11,
"token_text": "sees",
"recognized_terms": [
"Over",
"several",
@ -980,7 +1014,10 @@
"Daniel",
"has",
"adopted",
"any"
"any",
"stray",
"animals",
"he"
],
"skipped_frame": "initial_state_frame"
},
@ -1164,10 +1201,10 @@
"outcome": "refused",
"refusal_reason": "unknown_word",
"missing_operator": "lexicon_entry",
"refusal_detail": "no primitive or lexicon match for 'uphill'",
"refusal_detail": "no primitive or lexicon match for 'path'",
"sentence_index": 1,
"token_index": 10,
"token_text": "uphill",
"token_index": 21,
"token_text": "path",
"recognized_terms": [
"In",
"the",
@ -1178,7 +1215,18 @@
"walks",
"for",
"6",
"minutes"
"minutes",
"uphill",
",",
"walks",
"for",
"twice",
"this",
"amount",
"of",
"time",
"along",
"a"
],
"skipped_frame": null
},

View file

@ -1,9 +1,8 @@
# Brief 11B — Reader Closure Audit (GSM8K train-sample v1)
This document is the human-readable companion to
`audit_brief_11.json`. It captures the per-case Phase 2 reader audit over the
50-case sealed train sample, with the missing-operator inference extended in
this PR (see `generate/comprehension/audit.py`).
`audit_brief_11.json`. Last updated by **Brief 11B-step-2 lexicon closure**:
12 new `drain_token` lemmas + 1 alias added to `en_core_math_v1`.
## Per-case counts
@ -13,19 +12,26 @@ this PR (see `generate/comprehension/audit.py`).
| refused | 50 |
| **wrong** | **0** |
`wrong == 0` is preserved by construction: this PR does **not** modify the
reader runtime; only the audit inference layer and a new artifact are added.
`wrong == 0` is preserved by construction: the additions are all
`drain_token` (non-frame-opening). The hazard canary case
`gsm8k-train-sample-v1-0050` remains refused at sentence_index 0 — pinned by
`tests/test_brief_11b_step2_lexicon.py::test_hazard_case_0050_remains_refused_pre_frame`.
## Refusal taxonomy
| refusal_reason | count |
|------------------------|------:|
| incomplete_operation | 18 |
| unexpected_category | 14 |
| unknown_word | 11 |
| unattached_quantity | 3 |
| unresolved_pronoun | 3 |
| no_question_target | 1 |
| refusal_reason | count | Δ vs 11B-step-1 |
|------------------------|------:|----------------:|
| incomplete_operation | 20 | +2 |
| unexpected_category | 17 | +3 |
| unknown_word | 5 | **6** |
| unattached_quantity | 4 | +1 |
| unresolved_pronoun | 3 | 0 |
| no_question_target | 1 | 0 |
The 6-case drop in `unknown_word` is the load-bearing lift. Increases in
other rows reflect *previously-hidden* downstream bottlenecks becoming
visible (Brief 11 §Gate 1: "refusal taxonomy may shrink while correct
stays flat — real new work becoming visible, not regression").
## Missing-operator taxonomy (load-bearing)

View file

@ -8,12 +8,14 @@
{"lemma": "age", "category": "drain_token", "aliases": ["ages", "aged"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "ago", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "all", "category": "drain_token", "aliases": [], "provenance": "phase_1_reader_supplemental_2026-05-26"}
{"lemma": "along", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "already", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "also", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "altogether", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "among", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "amusement", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "and", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "animals", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "another", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "any", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "aquarium", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -59,6 +61,7 @@
{"lemma": "dance", "category": "drain_token", "aliases": ["dances"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "daughter", "category": "drain_token", "aliases": ["daughters"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "decided", "category": "drain_token", "aliases": ["decide", "decides"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "decrease", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "degree", "category": "drain_token", "aliases": ["degrees"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "delivery", "category": "drain_token", "aliases": ["deliveries"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "different", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -114,6 +117,7 @@
{"lemma": "instagram", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "insurance", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "into", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "jacks", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "jumping", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "june", "category": "drain_token", "aliases": ["july", "august", "september", "october", "november", "december", "january", "february", "march", "april", "may"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "junior", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -124,6 +128,7 @@
{"lemma": "lake", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "last", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "later", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "length", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "live", "category": "drain_token", "aliases": ["lives", "lived", "living"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "local", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "locals", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -171,12 +176,14 @@
{"lemma": "player", "category": "drain_token", "aliases": ["players"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "pokemon", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "press", "category": "drain_token", "aliases": ["presses", "pressed"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "previous", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "problem", "category": "drain_token", "aliases": ["problems"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "purchase", "category": "drain_token", "aliases": [], "provenance": "phase_1_reader_supplemental_2026-05-26"}
{"lemma": "purpose", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "put", "category": "drain_token", "aliases": ["puts"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "raise", "category": "drain_token", "aliases": ["raises", "raised"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "rate", "category": "drain_token", "aliases": ["rates"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "reach", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "reading", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "ready", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "rent", "category": "drain_token", "aliases": ["rents", "rented"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -216,6 +223,7 @@
{"lemma": "started", "category": "drain_token", "aliases": ["start", "starts"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "still", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "storage", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "stray", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "studying", "category": "drain_token", "aliases": [], "provenance": "phase_1_reader_supplemental_2026-05-26"}
{"lemma": "subway", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "summer", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -238,6 +246,7 @@
{"lemma": "to", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "today", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "together", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "too", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "town", "category": "drain_token", "aliases": ["towns"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "train", "category": "drain_token", "aliases": ["trains"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "travel", "category": "drain_token", "aliases": ["travels", "traveled", "travelling", "traveling"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
@ -249,6 +258,7 @@
{"lemma": "two", "category": "drain_token", "aliases": ["three", "four", "five"], "provenance": "phase_1_reader_supplemental_2026-05-26"}
{"lemma": "until", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "up", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "uphill", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "upload", "category": "drain_token", "aliases": ["uploads", "uploaded"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "varsity", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "very", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
@ -259,10 +269,11 @@
{"lemma": "way", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "weekly", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "weighing", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "weight", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "weight", "category": "drain_token", "aliases": ["weights"], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "well", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "when", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "where", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "which", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "while", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "wire", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "with", "category": "drain_token", "aliases": [], "provenance": "phase_1_reader_supplemental_2026-05-26"}
@ -270,5 +281,6 @@
{"lemma": "without", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "women", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "yet", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_2026-05-26"}
{"lemma": "your", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_brief_11b_step2_2026-05-27"}
{"lemma": "youtube", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}
{"lemma": "youtuber", "category": "drain_token", "aliases": [], "provenance": "phase_2_reader_gsm8k_2026-05-26"}

View file

@ -173,11 +173,15 @@ def test_recognized_terms_only_present_on_post_pre_frame_refusals(artifact):
def test_refusal_reason_distribution_is_stable(artifact):
"""Pin the headline refusal counts so any reader runtime change must
explicitly update this test alongside the artifact."""
# Counts updated by Brief 11B-step-2 lexicon closure (12 drain_token
# additions). The unknown_word row strictly decreased; previously-hidden
# bottlenecks at downstream frames became visible (real new work, not
# regression). See `audit_brief_11.md` for the before/after table.
expected = {
"incomplete_operation": 18,
"unexpected_category": 14,
"unknown_word": 11,
"unattached_quantity": 3,
"incomplete_operation": 20,
"unexpected_category": 17,
"unknown_word": 5,
"unattached_quantity": 4,
"unresolved_pronoun": 3,
"no_question_target": 1,
}

View file

@ -0,0 +1,200 @@
"""Brief 11B-step-2 — pin the lexicon additions and verify wrong=0 + hazard.
Hard invariants:
* `wrong == 0` no admissions, no risky frame-opener miscategorisation.
* The hazard canary case ``gsm8k-train-sample-v1-0050`` MUST remain refused
at sentence_index 0 (pre-frame), the same site as before this PR.
* Every new lemma is classified as ``drain_token`` (the only category that
cannot open a frame and therefore cannot create wrong admissions).
* The compiled lexicon manifest checksum is unchanged (additions live in
the per-category source file; the compiled ``lexicon.jsonl`` is not
regenerated by this PR see ``generate/comprehension/lexicon.py`` for
the load contract).
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from generate.comprehension import lexicon as comprehension_lexicon
from generate.comprehension.audit import audit_problem
from generate.comprehension.state import ReaderRefusal
REPO_ROOT = Path(__file__).resolve().parents[1]
PACK_DIR = REPO_ROOT / "language_packs/data/en_core_math_v1"
DRAIN_PATH = PACK_DIR / "lexicon/drain_token.jsonl"
CASES_PATH = REPO_ROOT / "evals/gsm8k_math/train_sample/v1/cases.jsonl"
NEW_LEMMAS: tuple[str, ...] = (
"along",
"animals",
"decrease",
"jacks",
"length",
"previous",
"reach",
"stray",
"too",
"uphill",
"which",
"your",
)
# Aliases added to existing lemmas in this PR (lemma → new alias)
NEW_ALIASES: dict[str, str] = {
"weight": "weights",
}
PROVENANCE = "phase_2_reader_brief_11b_step2_2026-05-27"
@pytest.fixture(scope="module")
def drain_entries() -> list[dict]:
return [
json.loads(line)
for line in DRAIN_PATH.read_text(encoding="utf-8").splitlines()
if line.strip()
]
@pytest.fixture(scope="module")
def cases() -> list[dict]:
with CASES_PATH.open() as f:
return [json.loads(line) for line in f]
@pytest.fixture(scope="module", autouse=True)
def _clear_lex_cache():
comprehension_lexicon._CACHE.clear()
yield
comprehension_lexicon._CACHE.clear()
# ---------------------------------------------------------------------------
# Pack-additions pinning
# ---------------------------------------------------------------------------
def test_every_new_lemma_is_drain_token(drain_entries):
by_lemma = {e["lemma"]: e for e in drain_entries}
for lemma in NEW_LEMMAS:
assert lemma in by_lemma, f"missing new lemma {lemma!r}"
assert by_lemma[lemma]["category"] == "drain_token"
def test_every_new_lemma_carries_brief_provenance(drain_entries):
by_lemma = {e["lemma"]: e for e in drain_entries}
for lemma in NEW_LEMMAS:
assert by_lemma[lemma]["provenance"] == PROVENANCE, (
f"{lemma!r} provenance mismatch (got {by_lemma[lemma]['provenance']!r})"
)
def test_new_aliases_attached_to_existing_lemmas(drain_entries):
by_lemma = {e["lemma"]: e for e in drain_entries}
for lemma, alias in NEW_ALIASES.items():
entry = by_lemma.get(lemma)
if entry is None:
# Some aliases attach to lemmas in OTHER per-category files; skip
# silently if the lemma isn't in drain_token.
continue
assert alias in entry["aliases"], (
f"{alias!r} not aliased on {lemma!r}: {entry['aliases']!r}"
)
def test_drain_entries_alphabetically_sorted(drain_entries):
lemmas = [e["lemma"] for e in drain_entries]
assert lemmas == sorted(lemmas), "drain_token.jsonl not alphabetically sorted"
# ---------------------------------------------------------------------------
# Reader-level effect — wrong=0 + hazard canary
# ---------------------------------------------------------------------------
def test_no_case_admits_after_lexicon_closure(cases):
"""``wrong == 0`` is preserved: lexicon additions must not produce any
admission. Drain_token cannot open a frame, so no PartialOperation /
PartialInitialPossession can be contributed by these additions."""
for c in cases:
result, _ = audit_problem(c["question"], case_id=c["case_id"])
if not (isinstance(result, ReaderRefusal) or result is None):
pytest.fail(
f"unexpected admission on {c['case_id']!r}"
f"lexicon closure must not lift cases without runtime work; "
f"got graph entities={result.entities!r}"
)
def test_hazard_case_0050_remains_refused_pre_frame(cases):
"""The documented wrong>0 hazard from Brief 11B audit_brief_11.md must
remain refused. Any closure work that admits this case is rejected per
Brief 11 §"correct-count greed."
"""
case = next(c for c in cases if c["case_id"] == "gsm8k-train-sample-v1-0050")
result, _ = audit_problem(case["question"], case_id=case["case_id"])
assert isinstance(result, ReaderRefusal), (
f"case 0050 admitted: {result!r} — this would create wrong>0"
)
# Refusal site must remain at sentence 0 (pre-frame filler), not slip
# forward into the partial Operation(mark, add, 3, songs) zone.
assert result.sentence_index == 0, (
f"case 0050 refusal site moved to sentence {result.sentence_index} "
f"(was 0). Verify no partial graph reaches finalize."
)
def test_unknown_word_row_strictly_decreased(cases):
"""Brief 11B-step-2 exit condition: the unknown_word row in the audit
taxonomy must strictly decrease (Brief 11 §Gate 1)."""
from collections import Counter
reasons = Counter()
for c in cases:
result, _ = audit_problem(c["question"], case_id=c["case_id"])
if isinstance(result, ReaderRefusal):
reasons[result.reason] += 1
# Pre-step-2 baseline (pinned by test_brief_11b_audit_artifact): 11
assert reasons["unknown_word"] < 11, (
f"unknown_word row did not decrease: {reasons['unknown_word']} vs 11 baseline"
)
def test_manifest_checksum_unchanged_by_source_edits():
"""The manifest checksum verifies the compiled ``lexicon.jsonl``; this PR
edits only the per-category source files, so the compiled file (and
therefore the checksum) is intentionally not regenerated."""
manifest = json.loads((PACK_DIR / "manifest.json").read_text())
compiled_path = PACK_DIR / "lexicon.jsonl"
import hashlib
actual = hashlib.sha256(compiled_path.read_bytes()).hexdigest()
assert manifest["checksum"] == actual, (
f"manifest checksum mismatch: declared={manifest['checksum']!r}, "
f"actual={actual!r}. If you regenerated lexicon.jsonl, update manifest."
)
# ---------------------------------------------------------------------------
# Loader integrity — additions are reachable through the live lexicon.
# ---------------------------------------------------------------------------
def test_new_lemmas_resolve_through_load_lexicon():
lex = comprehension_lexicon.load_lexicon()
for lemma in NEW_LEMMAS:
entry = comprehension_lexicon.lookup(lex,lemma)
assert entry is not None, f"{lemma!r} not found by lexicon loader"
assert entry.category == "drain_token"
def test_new_aliases_resolve_through_load_lexicon():
lex = comprehension_lexicon.load_lexicon()
for lemma, alias in NEW_ALIASES.items():
entry = comprehension_lexicon.lookup(lex,alias)
if entry is None:
continue # alias may belong to a different category file
assert entry.lemma == lemma, (
f"alias {alias!r} resolved to {entry.lemma!r}, expected {lemma!r}"
)