# ADR-0164.3 — Cross-Sentence Reading State **Status:** Proposed **Date:** 2026-05-26 **Author:** Shay **Anchor:** [[thesis-decoding-not-generating]] **Parent:** [ADR-0164 — Incremental Comprehension Reader](./ADR-0164-incremental-comprehension-reader.md) §Open question #4 **Companions:** [ADR-0164.1 — Lexical Primitive Set Scope](./ADR-0164.1-lexical-primitive-scope.md), [ADR-0164.2 — Pronoun/Entity Resolution Policy](./ADR-0164.2-pronoun-entity-resolution.md), [ADR-0165 — Regex Scope Rule](./ADR-0165-regex-scope-rule.md) **Related downstream types:** [ADR-0115 — `MathProblemGraph`](./ADR-0115-math-problem-parser-and-graph.md), [ADR-0135 — `BoundUnknown` resolver](./ADR-0135-binding-graph-question-target.md) --- ## Context — why two levels ADR-0164 §Decision §2 sketches a single `ComprehensionState` that accumulates entities, quantities, operations, the question target, and a current expectation frame. That sketch is correct for one sentence, but GSM8K problems are multi-sentence: pronouns refer back across sentence boundaries, entities introduced in sentence 1 are mutated in sentence 3, and the question typically lives in the final sentence and refers to state built across all prior sentences. Two structural facts force a split: 1. **Lifetime asymmetry.** Some state (entity registry, accumulated initial possessions, accumulated operations, the unknown target) is *problem-scoped* — it persists across sentence boundaries and accumulates monotonically. Other state (the current expectation frame, pending quantities waiting for unit attachment, the partial frame being built) is *sentence-scoped* — it must reset cleanly at sentence boundaries so a stray expectation from a prior sentence doesn't bleed into the next one. 2. **Refusal locality.** When the reader refuses, the refusal points either at a sentence-internal failure (unexpected category, dangling quantity at sentence end, unfinished frame) or at a problem-level failure (unresolved pronoun, conflicting entity reference, no question target by problem end). Conflating both into one state smears the refusal vocabulary and makes the failure modes harder to diagnose. Collapsing both into a single immutable record is possible but the collapsing buys nothing and costs vocabulary clarity. Two-level keeps each field's lifetime explicit and each refusal mode local to its appropriate level. > **Naming note for [Brief 5](./ADR-0164-incremental-comprehension-reader.md#acceptance-criteria-for-this-adr-proposed--accepted).** > ADR-0164 §Decision §2's sketched `ComprehensionState` is structurally > the **inner** level under this ADR — what is named `SentenceReadingState` > here. The Brief 5 PR (ComprehensionState skeleton) will produce both > `ProblemReadingState` and `SentenceReadingState` to honor the > two-level model documented in this ADR. The sketch is preserved as > the inner-level field list; the outer level is new. --- ## Decision — two-level state model ### `ProblemReadingState` (outer, problem-scoped) Immutable record. Persists across sentence boundaries. Mutated only by `end_sentence` returning a new `ProblemReadingState` that absorbs the just-closed sentence's contribution. | Field | Type | Role | |---|---|---| | `entity_registry` | `tuple[EntityRef, ...]` | Ordered by introduction position. Once an entity enters, it stays. Order-of-introduction matches [ADR-0115 `MathProblemGraph.entities`](./ADR-0115-math-problem-parser-and-graph.md) doctrine. | | `accumulated_initial_state` | `tuple[PartialInitialPossession, ...]` | Initial-state declarations closed at sentence end. Tuple order is order-of-introduction. | | `accumulated_operations` | `tuple[PartialOperation, ...]` | Operation declarations closed at sentence end. Tuple order is order-of-introduction (story order — ADR-0115 doctrine). | | `unknown_target_slot` | `QuestionTargetSlot \| None` | Set exactly once, by the sentence containing the question. Locked after setting. `None` until a question sentence completes. | | `pronoun_resolution_history` | `tuple[PronounResolution, ...]` | Replay-deterministic log of every pronoun resolution made during reading. Per [ADR-0164.2](./ADR-0164.2-pronoun-entity-resolution.md). | | `sentence_index` | `int` | 0-based counter of completed sentences. Increments only on `end_sentence`. | | `source_text_offset` | `int` | Character offset into the source problem text at which the next sentence begins. Maintained for span linkage. | `PartialInitialPossession` and `PartialOperation` are precursors to the ADR-0115 types `InitialPossession` and `Operation`. They are "partial" only in the sense that they can hold `None` for fields that are optional during construction (e.g. a transfer with a missing target). Once committed to `accumulated_*`, every field is set. The finalization step (see §Termination) projects them into the strict ADR-0115 types. ### `SentenceReadingState` (inner, sentence-scoped) Immutable record. Lifetime = one sentence. Created by `begin_sentence`, mutated by `apply_word`, consumed by `end_sentence`. | Field | Type | Role | |---|---|---| | `frame` | `SentenceFrame \| None` | The kind of sentence under construction once enough words have been read to decide. Discriminator: `initial_state_frame`, `operation_frame`, `question_frame`, `descriptive_frame` (context-only, no quantitative contribution). `None` while the frame is still ambiguous (very early in the sentence). | | `expectation` | `ExpectationFrame \| None` | Open expectation slot — what categories would legally close or advance the current frame. Replaced as the frame narrows. `None` means "any frame opener is welcome." | | `pending_quantities` | `tuple[QuantityRef, ...]` | Numbers seen so far in this sentence that haven't been attached to an entity + unit. Drains as units land. Sentence ends with non-empty `pending_quantities` → refusal `unattached_quantity`. | | `pending_entity_ref` | `EntityRef \| None` | The entity reference active in the current frame (typically the sentence subject). Set when a proper-noun entity or a resolved pronoun lands in subject position. | | `pending_verb` | `VerbReference \| None` | The verb captured at frame-determining position, waiting for completion (operand, target). Set on verb landing; consumed when the operation closes. | | `token_index` | `int` | 0-based position within the current sentence. Increments on every `apply_word`. | | `lookback` | `tuple[AppliedCategory, ...]` | Bounded history (≤8 entries) of categories applied in this sentence with their positions. Enables recontextualization without unbounded backtracking. | | `partial_frame_payload` | `FramePayload \| None` | The frame-kind-specific in-construction structure. For `initial_state_frame`: a `PartialInitialPossession` being built up. For `operation_frame`: a `PartialOperation`. For `question_frame`: a `QuestionTargetSlot` being built up. | `SentenceReadingState` has read access to `ProblemReadingState` via the lifecycle API (passed in to `apply_word`); it cannot mutate the outer state directly. Only `end_sentence` produces a new `ProblemReadingState`. --- ## Lifecycle API (signatures only — no implementation) The reader exposes three pure functions. All are deterministic: same inputs → byte-equal outputs and byte-equal canonical hashes. ```python def begin_sentence( problem_state: ProblemReadingState, source_text_offset: int, ) -> SentenceReadingState: """Open a fresh sentence-local state. Resets all sentence-scoped fields. Inherits no transient state from prior sentences — the only access to prior context is the immutable ``problem_state`` argument (used read-only by ``apply_word`` for entity resolution). Pure / deterministic. No I/O. """ def apply_word( sentence_state: SentenceReadingState, problem_state: ProblemReadingState, word: str, ) -> SentenceReadingState | ReaderRefusal: """Advance one token. Returns new sentence state or typed refusal. Lookup order per ADR-0164 §Decision §3: 1. Lexical primitive scan (ADR-0164.1, ADR-0165). 2. Lexicon lookup (en_core_math_v1, per ADR-0164 §Decision §1). 3. Expectation check. 4. Update emit. ``problem_state`` is read-only. Pronoun resolution consults ``problem_state.entity_registry`` via the rules in [ADR-0164.2](./ADR-0164.2-pronoun-entity-resolution.md). Resolutions are recorded to a private buffer that ``end_sentence`` later folds into ``problem_state.pronoun_resolution_history``. Pure / deterministic. No I/O. """ def end_sentence( sentence_state: SentenceReadingState, problem_state: ProblemReadingState, ) -> ProblemReadingState | ReaderRefusal: """Close the sentence, fold its contribution into the problem state. Finalization rules: - ``sentence_state.frame`` must be one of the legal frame kinds. ``None`` at end-of-sentence → refusal ``unfinished_frame`` (we read words and never decided what shape the sentence was). - ``sentence_state.pending_quantities`` must be empty. A non-empty pending list → refusal ``unattached_quantity``. - The ``partial_frame_payload`` is projected into a typed ``PartialInitialPossession`` / ``PartialOperation`` / ``QuestionTargetSlot`` and appended to the appropriate ``problem_state`` tuple. - Newly introduced ``EntityRef`` records are appended to ``problem_state.entity_registry``. - Pronoun resolutions recorded in the sentence state are appended to ``problem_state.pronoun_resolution_history``. - ``sentence_index`` increments by 1. - ``source_text_offset`` advances past the closing punctuation. The returned ``ProblemReadingState`` is the input to the next ``begin_sentence`` call (next sentence) or to the finalization predicate (last sentence). Pure / deterministic. No I/O. """ ``` ### `ReaderRefusal` Typed refusal record. Carries one of a closed set of reasons plus diagnostic detail. ```python @dataclass(frozen=True, slots=True) class ReaderRefusal: reason: str # member of READER_REFUSAL_REASONS detail: str # short human annotation sentence_index: int # which sentence the refusal occurred in token_index: int # position within the sentence (0 if end_sentence-level) token_text: str # the token in question, or "" for non-token refusals ``` ``` READER_REFUSAL_REASONS = frozenset({ # apply_word — token-level "unknown_word", # not in lexicon, no primitive matched "unexpected_category", # category does not satisfy current expectation "expectation_collision", # two frame openers would be legal; precedence undecided "unresolved_pronoun", # pronoun has no matching entity in registry "ambiguous_pronoun_referent", # multiple matching entities in registry # end_sentence — sentence-level "unfinished_frame", # frame never decided "unattached_quantity", # quantity never bound to entity+unit "incomplete_operation", # operation missing operand or target # problem-level (raised by the finalization predicate, not apply/end) "no_question_target", # problem ended with unknown_target_slot=None "dangling_entity", # entity in registry has no initial possession "graph_construction_failure", # MathProblemGraph constructor rejected the projection }) ``` The vocabulary is closed and ADR-tracked. New reasons require an ADR amendment. This mirrors the [ADR-0134 admissibility reason discipline](./ADR-0134-binding-graph-admissibility.md). --- ## What persists vs sentence-local — explicit table | Concern | Where it lives | Lifetime | |---|---|---| | Entity registry | `ProblemReadingState` | All sentences | | Initial possessions accumulated | `ProblemReadingState` | All sentences | | Operations accumulated | `ProblemReadingState` | All sentences | | The unknown target | `ProblemReadingState` | Set once; locked | | Pronoun resolution history | `ProblemReadingState` | All sentences | | Sentence index counter | `ProblemReadingState` | Monotonic | | Current frame kind | `SentenceReadingState` | One sentence | | Current expectation | `SentenceReadingState` | One sentence; replaced as frame narrows | | Pending quantities (un-unit'd) | `SentenceReadingState` | One sentence; drains as units land | | Pending entity reference (subject) | `SentenceReadingState` | One sentence | | Pending verb (operation under construction) | `SentenceReadingState` | One sentence | | Token position counter | `SentenceReadingState` | Resets at sentence boundary | | Recent-category lookback window | `SentenceReadingState` | One sentence (bounded ≤8) | | Frame-payload-in-construction | `SentenceReadingState` | One sentence; projected at `end_sentence` | The rule, stated negatively: **no field is in both levels.** If a field seems to want to live in both, it is split — typically into a "pending" sentence-local version and a "committed" problem-level tuple. --- ## Canonical-bytes serialization Both state levels serialize to deterministic JSON via the same discipline used by [`MathProblemGraph.canonical_bytes`](./ADR-0115-math-problem-parser-and-graph.md) and the existing [`trace_hash`](../runtime_contracts.md) production. ```python def to_canonical_bytes(state: ProblemReadingState | SentenceReadingState) -> bytes: """Sorted-keys, compact-separators JSON. Tuples → lists. Decimal values render as strings to preserve precision (the math graph uses int|float per ADR-0115; the reader uses Decimal internally until projection to the graph). Optional fields are omitted from JSON when None (not serialized as 'null') to keep the canonical form minimal and to prevent spurious differences between states that differ only in which optional fields they chose to set explicitly to None. """ def canonical_hash(state: ProblemReadingState | SentenceReadingState) -> str: """sha256 hex digest of to_canonical_bytes(state). Same shape as ADR-0153 turn-event trace-hash. Identical state → identical hash. This is the determinism gate enforced by the Brief 5 test scaffold. """ ``` Serialization rules (matching existing CORE discipline): 1. **Sort keys at every level** (`json.dumps(..., sort_keys=True)`). 2. **Compact separators** (`separators=(",", ":")`). 3. **Tuple → list**. Tuples carry ordering meaning; that ordering is preserved by JSON array ordering. The list-vs-tuple distinction is lost on the wire (intentional — JSON has no tuples). 4. **Decimal → string**. Use `str(value)` not float coercion. This preserves precision through partial states; the projection to `MathProblemGraph` (which uses `int|float`) happens at finalization and must check loss-of-precision explicitly. 5. **Frozen dataclasses → dict** of field-name → field-value pairs, recursing through children. 6. **Enums and Literals → their string value**. 7. **Optional fields with `None` → omitted from dict** (rule 2 caveat; this prevents `{"x": null}` vs `{}` from being different). ### `ReaderRefusal` is serialized too A refusal is not state, but it must be replay-deterministic for trace audit. `to_canonical_bytes(ReaderRefusal(...))` follows the same rules. Two runs that produce the same refusal produce byte-equal refusal records. --- ## Worked example — gsm8k-train-sample-v1-0001 > "Tina makes $18.00 an hour. If she works more than 8 hours per > shift, she is eligible for overtime, which is paid by your hourly > wage + 1/2 your hourly wage. If she works 10 hours every day for 5 > days, how much money does she make?" Expected outcome under Phase 1: the reader will admit sentence 1 (rate statement) and refuse sentences 2 and 3 with `conditional_*` reasons because conditional structure is not in Phase 1 scope. **The example demonstrates the state model — not solver success.** That is the right kind of demonstration: when the engine refuses, the state at the point of refusal is what tells us *why* and at what specific position, and that is what enables principled corpus growth. ### Sentence 1 — "Tina makes $18.00 an hour." `begin_sentence(problem_state=∅, source_text_offset=0)` produces: ``` SentenceReadingState( frame=None, expectation=None, pending_quantities=(), pending_entity_ref=None, pending_verb=None, token_index=0, lookback=(), partial_frame_payload=None, ) ``` Word-by-word (Phase 1 lexicon + primitive set, illustrative): | pos | word | primitive / lexicon hit | state change | |---|---|---|---| | 0 | `Tina` | lexicon: `proper_noun_entity_female` | `pending_entity_ref = EntityRef("Tina", "female", 0)`; entity not yet in registry — staged for commit at sentence end. | | 1 | `makes` | lexicon: `accumulation_verb` / `rate_emit_verb` | Frame narrows. `pending_verb = VerbReference("makes", "rate_emit", 1)`. `frame = operation_frame` (tentative). `expectation = "QUANTITY (currency) followed by 'an X'"`. | | 2 | `$18.00` | primitive: `currency_literal` → `QuantityRef(Decimal("18.00"), "dollars", "currency", attached_to_entity=None, source_position=2)` | `pending_quantities = (Q$18.00,)`. Expectation advances to `"'an' or 'per' followed by time-unit"`. | | 3 | `an` | lexicon: `per_unit_marker` (closed-set: an, per, every, each per time-unit) | Expectation narrows to `"time-unit-noun"`. Lookback records `per_unit_marker`. | | 4 | `hour` | lexicon: `time_unit_noun` | Rate composition closes. `pending_quantities[0]` (the $18.00) becomes the numerator of a rate; `hour` is the denominator. `frame` confirmed = `operation_frame`. `partial_frame_payload = PartialOperation(actor="Tina", kind="apply_rate", operand=Rate(18.00, "dollars", "hour"))`. `pending_quantities` drains to `()`. | | 5 | `.` | sentence terminator | | `end_sentence(...)` projects `partial_frame_payload` into a typed `PartialOperation`, commits Tina to `entity_registry`, appends the operation to `accumulated_operations`, increments `sentence_index`. ``` ProblemReadingState after sentence 1: entity_registry = (EntityRef("Tina", "female", 0),) accumulated_initial_state = () accumulated_operations = ( PartialOperation( actor="Tina", kind="apply_rate", operand=Rate(18.00, "dollars", "hour"), target=None, ), ) unknown_target_slot = None pronoun_resolution_history = () sentence_index = 1 ``` ### Sentence 2 — "If she works more than 8 hours per shift, ..." `begin_sentence` produces a fresh `SentenceReadingState`. | pos | word | result | |---|---|---| | 0 | `If` | lexicon: `conditional_open` | `frame = conditional_frame` (Phase-1 NOT in scope). Reader refuses. | Refusal: ``` ReaderRefusal( reason="unexpected_category", detail="conditional_open at sentence_index=1, position=0; " "conditional_frame is Phase-1 out-of-scope (ADR-0164 §Phasing)", sentence_index=1, token_index=0, token_text="If", ) ``` This is the **correct** Phase 1 behavior. The state at the moment of refusal is enough to file a typed teaching candidate for the conditional-frame category. The refusal does not corrupt the `ProblemReadingState` built from sentence 1 — sentence 2's failure leaves the outer state at its post-sentence-1 value (refusals do not commit; `end_sentence` is the only commit path). ### Sentence 3 — "If she works 10 hours every day for 5 days, how much money does she make?" Same refusal mode as sentence 2 (`unexpected_category` on the leading `If`). Phase 2 / Phase 3 work expands the conditional-frame vocabulary. ### Pronoun resolution annotation If sentence 2 or 3 *were* in scope (after Phase 2), the word "she" at their leading positions would consult `problem_state.entity_registry = (EntityRef("Tina", "female", 0),)` under [ADR-0164.2](./ADR-0164.2-pronoun-entity-resolution.md)'s gender-plus-recency rule. Exactly one matching entity exists → `PronounResolution(pronoun="she", resolved_to="Tina", at_position=..., entity_source=sentence_0)` is recorded. Zero matching → `unresolved_pronoun`; more than one → `ambiguous_pronoun_referent`. The resolution is appended to `pronoun_resolution_history` exactly when the sentence containing it closes successfully. --- ## Termination predicate A `ProblemReadingState` is valid for handoff to `MathProblemGraph` construction when **all** of the following hold. The predicate is a pure function: same input → same verdict. ```python def is_terminable(state: ProblemReadingState) -> bool: return ( state.entity_registry # ≥1 entity and state.unknown_target_slot is not None # question target bound and _every_op_references_known_entity(state) # closure check and _every_initial_references_known_entity(state) # closure check and _question_target_entity_resolvable(state) # bound vs registry and _no_pending_unresolved_pronouns(state) # no dangling refs and _partial_payloads_project_to_strict_types(state) # ADR-0115 typecheck ) ``` Failure of any condition produces a typed problem-level refusal (see `READER_REFUSAL_REASONS` problem-level group). `is_terminable(state)` true → the reader calls a finalizer that constructs the strict ADR-0115 `MathProblemGraph`: ```python def finalize(state: ProblemReadingState) -> MathProblemGraph | ReaderRefusal: """Project the partial state into a strict MathProblemGraph. Tuple field ordering on the graph mirrors order-of-introduction in ``state.entity_registry`` and source-text order in ``state.accumulated_*``. Decimal values must round-trip losslessly through ``int | float`` (the ADR-0115 type); a precision loss refuses with ``graph_construction_failure``. """ ``` The output of `finalize` is the exact input shape the existing binding-graph adapter (ADR-0133) and `BoundUnknown` resolver (ADR-0135) consume today. The reader does not add a new downstream contract; it replaces the old front-end's output emit. --- ## Refusal modes — summary Three lifetime groupings (mirroring the API): **Token-level (raised by `apply_word`):** - `unknown_word` — token not in lexicon and no primitive matched. - `unexpected_category` — category does not satisfy current expectation and is not a legal frame opener at this position. - `expectation_collision` — two frame openers would be legal at this position. Should not occur given a complete precedence table (defer to ADR-0164.1 / phase-1 measurement); a real occurrence is evidence the precedence rule needs an ADR. - `unresolved_pronoun` — pronoun has no matching entity per ADR-0164.2. - `ambiguous_pronoun_referent` — pronoun has multiple matching entities per ADR-0164.2. **Sentence-level (raised by `end_sentence`):** - `unfinished_frame` — frame kind never resolved. - `unattached_quantity` — at least one number was seen but never attached to an entity + unit. - `incomplete_operation` — operation frame closed with missing operand or missing target. **Problem-level (raised by the finalization predicate):** - `no_question_target` — problem ended with `unknown_target_slot=None`. The question sentence either was refused or never identified itself as a question frame. - `dangling_entity` — entity appears in registry but has no initial possession and is not the subject of any operation. Probably a pronoun mis-resolution upstream. - `graph_construction_failure` — projection into strict ADR-0115 types failed (precision loss, schema violation). The detail field carries the ADR-0115 `MathGraphError` message. Each refusal records `sentence_index` and `token_index` to localize. Refusals are themselves canonical-bytes-serializable (see §Canonical-bytes). --- ## Interaction with other ADRs ### ADR-0164 (parent) ADR-0164's `ComprehensionState` sketch (§Decision §2) is the **inner-level** type under this ADR, renamed `SentenceReadingState`. The outer-level `ProblemReadingState` is new. ADR-0164's §Phasing is unchanged: Phase 1 builds the reader for question sentences, which under the two-level model means Phase 1 implements `apply_word` rules for the `question_frame` kind and a minimal subset of frame openers, enough to admit the question-class refusals that block 34/47 train_sample cases. ### ADR-0164.1 (companion) Lexical primitives are consumed inside `apply_word` step 1 (primitive scan). The primitive registry's emit-category is what the expectation-check step compares against. No state-shape dependency here; ADR-0164.1 specifies *what* primitives produce, ADR-0164.3 specifies *how* the producer's output flows into the state machine. ### ADR-0164.2 (companion) Pronoun resolution is the only place `apply_word` reads from `problem_state` non-trivially. ADR-0164.2 specifies the rule; ADR-0164.3 specifies the carrier (`pronoun_resolution_history` and the per-sentence resolution buffer). The resolution buffer flushes to the outer history only on successful `end_sentence`. ### ADR-0115 (downstream) The `finalize` step produces a strict `MathProblemGraph`. `MathProblemGraph`'s order-of-introduction invariant (ADR-0115 docstring) is preserved by reading the outer state's tuples in their stored order (which is order-of-introduction because that's how they were appended). ### ADR-0134 / ADR-0135 (downstream binding graph) Unchanged. The binding-graph adapter reads `MathProblemGraph`; we produce `MathProblemGraph`; nothing else changes. ### ADR-0165 (regex scope rule) `apply_word` step 1 consults the lexical primitive registry. Every primitive in that registry must satisfy ADR-0165 (orthographic-shape recognition only, never grammar). This ADR does not bypass that rule; if anything it makes the boundary cleaner because primitive hits and lexicon hits are explicitly distinct lookup steps. --- ## Open implementation choices (intentionally not pinned here) These belong in Brief 5 (the `ComprehensionState` / state types skeleton PR) or in follow-up sub-ADRs. Pinning them now would over-commit. 1. **`Decimal` precision setting.** `decimal.getcontext().prec` default is fine for GSM8K (currency to 2 decimal places, quantity counts as integers). Pin in Brief 5 with a `localcontext` block if precision needs to be smaller for canonical bytes. 2. **`lookback` window size.** ADR-0164.3 says ≤8 entries. The exact number can be tuned during Phase 1 measurement; 8 covers all GSM8K question sentences observed at session time. 3. **Frame kind enumeration.** The four discriminator values (`initial_state_frame`, `operation_frame`, `question_frame`, `descriptive_frame`) are sufficient for Phase 1 + Phase 2 scope. Phase 3 may add `conditional_frame` and `rate_emit_frame`; each addition is an ADR. 4. **Source-span linkage.** This ADR includes `token_index` and `source_text_offset` but does not specify a full source-span record. The binding graph's `SourceSpanLink` (ADR-0132) is the downstream consumer; whether the reader emits a `SourceSpanLink` per partial-payload field or a single coarse span per sentence-payload is a Brief 5 decision. --- ## Acceptance criteria (Proposed → Accepted) This ADR moves to Accepted when: 1. Brief 5 lands `generate/comprehension/state.py` exporting `ProblemReadingState` and `SentenceReadingState` (renamed from ADR-0164's `ComprehensionState` sketch per §Decision above) with the field tables matching §Decision exactly. 2. The lifecycle API signatures land as Python stubs (no body) in `generate/comprehension/lifecycle.py`. Phase 1 implements the bodies. 3. `to_canonical_bytes` / `canonical_hash` implementation passes the determinism gate in `tests/test_comprehension_state.py` (Brief 5's test scaffold) on both state levels. 4. `READER_REFUSAL_REASONS` is materialized as a frozenset constant matching the closed set above. --- ## Cross-references - **Parent**: [ADR-0164](./ADR-0164-incremental-comprehension-reader.md). - **Companion sub-ADRs**: [ADR-0164.1 lexical primitive scope](./ADR-0164.1-lexical-primitive-scope.md), [ADR-0164.2 pronoun/entity resolution](./ADR-0164.2-pronoun-entity-resolution.md). - **Boundary invariant**: [ADR-0165](./ADR-0165-regex-scope-rule.md). - **Downstream target type**: [ADR-0115](./ADR-0115-math-problem-parser-and-graph.md). - **Downstream binding-graph consumers (unchanged)**: [ADR-0132](./ADR-0132-binding-graph-data-model.md) / [ADR-0133](./ADR-0133-binding-graph-adapter.md) / [ADR-0134](./ADR-0134-binding-graph-admissibility.md) / [ADR-0135](./ADR-0135-binding-graph-question-target.md). - **Brief 5 (acceptance dependency)**: [ADR-0164 §Acceptance criteria #1](./ADR-0164-incremental-comprehension-reader.md#acceptance-criteria-for-this-adr-proposed--accepted). - **Anchor**: `[[thesis-decoding-not-generating]]` — the state model is the form of *decoding in progress*. Each transition narrows the space of possible meanings until the structure that the source text already implied has surfaced fully.