Commit graph

5 commits

Author SHA1 Message Date
Shay
4b277d4e84 feat(adr-0174-phase3b): compound-clause held hypotheses
ADR-0174 Phase 3b — emit N anchors for compound-clause discrete-count
sentences sharing one subject + one verb. Architectural substrate;
score on train_sample preserved at 3/47/0 (compound cases like 0027
admit past the recognizer-injection refusal but the rest of the
problem still has downstream complexity — fractions, percent — that
needs Phase 4 + solver work).

generate/comprehension/state.py:
  HYPOTHESIS_CAP raised 4 → 8. Case 0040 emits 5 anchors; cap=8
  gives headroom (7-item lists) without becoming permissive.

generate/recognizer_match.py:
  _try_extract_compound_discrete_count_anchors() — new extractor
  emitting tuple of anchors for compound sentences. Refusal-
  preferring on:
    - no conjunctive separator (single-anchor path)
    - multiplicative/percent/fraction markers
    - head verb not in whitelist
    - any tail clause without grounded (count, observed_noun) pair
    - exceeding HYPOTHESIS_CAP
    - unaccounted digit in tail (wrong=0 hazard defense surfaced by
      2026-05-28 implementation review: bogusnoun would silently fail
      to produce anchor while leaving the digit unaccounted, admitting
      partial state)
  Wired into _match_discrete_count_statement dispatch as fallback when
  single-anchor extraction fails.

tests/test_adr_0174_phase3b_compound_clause.py:
  11 acceptance tests passing — pure conjunctive lists (proper-noun
  + pronoun-subject + single-actor antecedent), refusal-preferring
  discipline (mixed-verb, multiplicative-tail, non-whitelisted-head,
  partial-grounding all-or-nothing), HYPOTHESIS_CAP enforcement,
  multi-actor pronoun defense preserved on compound, wrong=0 +
  case-0050 canary.

tests/test_adr_0174_phase1_held_hypothesis_state.py:
  Updated test_hypothesis_cap_is_four → test_hypothesis_cap_is_eight
  with rationale for the raise.

Phase 3b implementation lookback review (per CLAUDE.md doctrine):
  - Surfaced silent-partial-admission hazard in tail extraction;
    fixed with digit-accounting check before commit
  - Surfaced LATENT regex-path multi-actor pronoun hazard (not
    introduced by Phase 3b; documented in test docstring with
    cross-reference to project-adr-0174-multi-actor-pronoun-hazard
    memory for follow-up)
  - case 0040 ('He now has...') remains refused — 'now' adverb between
    subject and verb defeats the existing canonical regex. Adverb-
    stripping is separate scope (not Phase 3b).

Acceptance:
- 258/258 ADR-0174 + math_problem_graph tests pass
- Smoke 67/67, packs 141/141
- train_sample 3/47/0 preserved (wrong=0 held)
- Case 0027 'Malcolm has 240 followers on Instagram and 500 followers
  on Facebook' now admits via the compound extractor — verified by
  refusal moving to the next sentence (which has 'half' fraction)
2026-05-28 11:49:57 -07:00
Shay
a713d2db33 feat(adr-0174-phase1): held-hypothesis state primitive in comprehension reader
ADR-0174 Phase 1 — substrate only, no admission behavior change.

Adds to generate/comprehension/state.py:
- HYPOTHESIS_CAP (=4, structural assertion per ADR-0174 §Constraints)
- VALID_HYPOTHESIS_CONFIDENCE_RANKS (closed set, no probabilistic ranking)
- Hypothesis dataclass (frozen, slots) — candidate, category_assignments,
  constraint_state, confidence_rank, unresolved. The 'candidate' field is
  typed as object to avoid circular import on math_roundtrip /
  math_candidate_graph candidate types; Phase 2 will pin canonical_bytes
  contract over real candidates.
- UnknownHeld dataclass — token, position, narrowed_categories (frozenset).
  Substrate for Phase 3 'hold instead of refuse' on unknown words; Phase 1
  introduces only the type.
- ProblemReadingState.open_hypotheses + unknown_held fields, both default
  to () (empty tuple). Defaults preserve today's single-committed behavior
  exactly. Confidence-rank uniqueness + density-from-0 enforced at
  __post_init__ as structural invariants.
- Canonical-bytes serializer extended to handle frozenset (sorted list).

Phase 1 acceptance verified:
- 29/29 ADR-0174 Phase 1 tests pass (construction, validation, cap
  enforcement, canonical-bytes determinism, frozenset stability).
- 42/42 existing reader tests pass (test_brief_11_audit +
  test_reader_phase2) — default-empty fields preserve byte-identity.
- Smoke 67/67, packs 141/141.
- train_sample/v1 byte-identical across two runs with use_reader=True.
- wrong=0 invariant held: 3/47/0 unchanged.

No apply_word body changes. The 'thread the hypothesis set' requirement
at Phase 1 is satisfied by field defaults that propagate through every
ProblemReadingState construction site in lifecycle.py without code edits.

Phase 2 (continuous constraint propagation) and Phase 3 (lookback
re-evaluation) will populate these fields with real hypothesis data and
wire the EMIT / ELIMINATE / HOLD operators.
2026-05-28 08:09:00 -07:00
Shay
60043973b0
feat(comprehension/10): Phase 2 statement-frame reader (ADR-0164.4) (#335)
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.
2026-05-27 05:03:56 -07:00
Shay
957e7c6642
feat(comprehension): split ComprehensionState into ProblemReadingState + SentenceReadingState (ADR-0164.3) (#323)
Reconciles the #321 skeleton with ADR-0164.3's two-level state model.

Changes:
  - Renames ComprehensionState → SentenceReadingState (backward-compat alias
    kept; existing callers need not change)
  - Adds 7 new fields to SentenceReadingState (all defaulted so existing
    construction still compiles):
      frame, pending_quantities, pending_entity_ref, pending_verb,
      token_index, lookback (≤8 entries, validated), partial_frame_payload
  - Introduces SentenceFrame (Literal), VerbReference, AppliedCategory,
    FramePayload (stub, frame_kind validated)
  - Adds ProblemReadingState (outer, problem-scoped) with all 7 fields
    per ADR-0164.3 table order, no defaults (explicit construction required)
  - Introduces PartialInitialPossession and PartialOperation (nullable
    precursors to ADR-0115 types), PronounResolution
  - Adds READER_REFUSAL_REASONS (11-member frozenset, closed/ADR-tracked)
    and ReaderRefusal dataclass with reason validation
  - Adds to_canonical_bytes() standalone function implementing
    ADR-0164.3 §Canonical-bytes rules: sort keys, omit None, Decimal→str;
    handles ProblemReadingState, SentenceReadingState, ReaderRefusal
  - SentenceReadingState.canonical_bytes() kept backward-compatible
    (original 5 fields, null for None) — existing pinned-bytes tests pass
  - 47 tests: all original tests pass; new tests cover ProblemReadingState
    construction, determinism gate, sensitivity gate, ReaderRefusal
    construction and every READER_REFUSAL_REASONS entry

Refs: #320 (ADR-0164.3), #321 (comprehension-state-skeleton)
2026-05-26 19:54:17 -07:00
Shay
6a4fcc8b36
feat(comprehension): add ComprehensionState skeleton (#321) 2026-05-26 19:32:22 -07:00