ADR-0174 Phase 2 — hoist _initial_admissible / roundtrip_admissible into
hypothesis-based constraint checks with structured elimination tracing.
Admission semantics are byte-equivalent to today; the change is structural.
Adds generate/comprehension/constraint_propagation.py:
- VALID_PREDICATE_NAMES: closed set of 17 sub-check names spanning
initial / composed_initial / operation admissibility predicates.
Adding new names requires an ADR amendment (structural contract with
reader_trace consumer).
- ConstraintResult dataclass: admitted bool + predicates_run trace +
elimination_reason. Validates admitted-vs-reason consistency.
- Elimination dataclass: confidence_rank + predicate + reason for one
hypothesis being eliminated. Serialisable as a reader_trace event.
- hypothesis_from_initial / hypothesis_from_operation: adapters wrapping
CandidateInitial / CandidateOperation as Phase-1 Hypothesis objects
with caller-supplied confidence_rank.
- _check_initial / _check_composed_initial / _check_operation:
decomposed sub-check implementations of the existing admissibility
predicates with first-failure short-circuit (matches current
semantics). Each sub-check populates predicates_run with (name, ok|
fail|skip) so the consumer sees exactly which predicate decided.
- check_constraints: dispatches on candidate type.
- eliminate_violating: bulk filter; returns (survivors, eliminations);
survivors are re-densified to satisfy ProblemReadingState's
open_hypotheses post_init invariant (dense-from-0 ranks);
eliminations carry the original confidence_rank for trace fidelity.
Wires into generate/math_candidate_graph.py at the recognizer
injection site (line 825+): replaces inline _initial_admissible /
roundtrip_admissible dispatch with eliminate_violating. Elimination
events become JSON entries in reader_trace with layer=
'constraint_propagation', phase=2, predicate, reason, sentence_index.
Phase 2 acceptance verified:
- 24/24 ADR-0174 Phase 2 tests pass (emission, parity with existing
predicates on 9 admit/reject cases, redensification, dataclass
invariants, integration).
- 71/71 existing reader + Phase 1 tests still pass.
- Smoke 67/67, packs 141/141, lanes 8/8.
- train_sample/v1 byte-identical across two runs with use_reader=True.
- Score preserved: correct=3 refused=47 wrong=0 — semantics identical
because the decomposed sub-checks short-circuit on the same predicates
the inline checks would have caught.
Trace-event behavior: today's injectors are conservative enough that
zero eliminations occur on train_sample/v1 (no false positives, no
mid-pipeline failures). The wiring is exercised by
test_phase2_event_shape_when_synthesized which proves the trace shape
on a synthetic CandidateInitial that fails initial.unit_grounds. When
Phase 3 begins emitting partial hypotheses from apply_word, the
elimination path will fire on real candidates and the trace will
populate.
Stacks on Phase 1 (feat/adr-0174-phase1-held-hypothesis-state, PR
#416). Merges cleanly into main after PR #416 lands.