* docs(ADR-0136): statement-layer corridor + S.0 taxonomy + resolve math-file conflicts - Add ADR-0136-statement-layer-corridor.md: corridor overview, S.0-S.4 phase table, taxonomy summary (23 context-filler, 4 rate-class, 5 compound, 17 long-tail), standing invariants (admitted_wrong==0, context-filler safety rail, honest delta). - Restore generate/math_*.py to HEAD (main) — stash-pop conflict markers were cosmetic comment rewording; upstream version is authoritative. - Stage docs/reviewers.yaml math_expert_claims addition (already authored). * docs(briefs): archive all 2026-05-23 parallel-dispatch briefs (L1–L17 + README)
7.8 KiB
L7 brief — Opus#2 — Binding Graph Phase 4 (question-target binding refinement)
Worktree setup (do this first, non-negotiable):
git worktree add ../core-binding-graph-p4 -b feat/binding-graph-phase4-question-target origin/main
cd ../core-binding-graph-p4
Scope. Phase 4 of the binding-graph layer. Phase 1 (#171, ADR-0132) introduced BoundUnknown as the question target. Phase 2 (#174, ADR-0133) mapped MathProblemGraph.Unknown to a BoundUnknown referencing the entity/unit pair. Phase 3 (#176, ADR-0134) added unit-aware admissibility on equations. Phase 4 refines BoundUnknown from "the symbol whose value the solver determines" to "the symbol at a specific temporal/state index with a specific question-form."
Concretely: the question "How many apples does Tina have?" doesn't currently bind to a unique symbol — Tina-apples at t=0, after operation 3, and at the terminal state are three different symbols. Phase 4 deterministically resolves which of those the unknown refers to, and stamps the question form (count / rate / total / difference / ratio / identity) so downstream consumers know what verdict shape to produce.
Still no runtime wiring, still no solver invocation. Phase 5 (bounded-grammar / B3 integration) remains deferred.
Reference docs (read these, only these):
docs/decisions/ADR-0132-binding-graph-data-model.md,ADR-0133-binding-graph-adapter.md,ADR-0134-binding-graph-admissibility.md— your prior phases.BoundUnknownand the adapter'sUnknown → BoundUnknownmapping are the contract you're refining.generate/math_problem_graph.py— input shape; the operation sequence drives the state-index resolution.
What to ship:
generate/binding_graph/model.py(modify, surgically): extendBoundUnknownwith two new mandatory fields:state_index: StateIndex— a closed-vocab tagged union:Literal["initial"],Literal["terminal"], orOperation(operation_index=int)(a frozen dataclass — not a string — so the operation index is type-checked).question_form: Literal["count", "rate", "total", "difference", "ratio", "identity"]. Both fields required — noNonedefaults. Update__post_init__to validate (operation_index≥ 0 and < graph operation count when used inside aSemanticSymbolicBindingGraph; cross-collection check belongs to the graph constructor). Phase 1–3 tests that constructBoundUnknowndirectly will need the new fields supplied; expect ~5–10 test fixture updates inside the binding_graph test files (Phase 1+2+3 by you). Mark those edits explicitly in the PR diff so the reconciliation is auditable.
generate/binding_graph/question_target.py(new) — pure-function resolver:resolve_state_index(g: MathProblemGraph) -> StateIndex— walks the operation sequence, returnsterminalwhen operations exist,initialwhen not.infer_question_form(g: MathProblemGraph) -> Literal[...]— closed-vocab dispatch on the operation kinds touching the unknown's entity/unit; e.g. all-add/subtract/transfer→count; presence ofapply_rate→totalorratedepending on which symbol the unknown binds;compare_additive→difference;compare_multiplicative→ratio; no operations →identity.bound_unknown_from_math_problem_graph(g: MathProblemGraph) -> BoundUnknown— the new public entry point that the adapter uses; deterministic, refusal-first via a typedQuestionTargetError(sibling ofAdapterError/AdmissibilityError).
generate/binding_graph/adapter.py(modify, surgically): replace the existingUnknown → BoundUnknownmapping with a call tobound_unknown_from_math_problem_graph(g). Do not changebind_math_problem_graph's signature; do not change the equation/fact/symbol mapping.- Public surface: add
StateIndex,Operation(state-index variant),resolve_state_index,infer_question_form,bound_unknown_from_math_problem_graph,QuestionTargetErrortogenerate/binding_graph/__init__.py. tests/test_binding_graph_question_target.py— 50–70 tests covering: each operation-kind family producing the expectedquestion_form;state_indexresolution at boundary cases (0 operations, 1 operation, many operations, unknown referencing an entity that no operation touches); refusal-first on the typedQuestionTargetErrorpaths (unknown entity not inentities, ambiguous form when multiple operation kinds touch the same symbol with no clear precedence — pick one closed rule and document it).tests/test_binding_graph_adapter_question_target.py— 30–40 integration tests covering: every Phase-2 adapter test case still round-trips with the refinedBoundUnknown; new cases exercisingstate_indexandquestion_formend-to-end; hash-stability across runs (Phase 2 invariant must not regress); a few intentionally-ambiguousMathProblemGraphinputs produceQuestionTargetError.docs/decisions/ADR-0135-binding-graph-question-target.md— short ADR. Cite ADR-0132, ADR-0133, ADR-0134 parents. Explicit "Phase 5 deferred" section. Document the closedquestion_formvocabulary and the precedence rule when multiple operation kinds touch the same symbol.
Hard constraints:
- Closed
question_formvocabulary. Exactly the six values above; do not invent more. If aMathProblemGraphshape doesn't fit any, refuse withQuestionTargetError(reason="unmappable_question_form"). Never silently coerce to "count" as a default. - Closed
state_indexvocabulary.initial,terminal, orOperation(operation_index=N). No "midway", no negative indices, no strings outside that set. - Refusal-first. Ambiguity refuses with a typed reason; never picks arbitrarily. The precedence rule (when multiple op-kinds touch the unknown's symbol) must be deterministic and documented in the ADR.
- Pure, no I/O. No filesystem reads at call time.
- Determinism.
bind_math_problem_graph(g)byte-equal across runs after the Phase 4 wiring (Phase 2 invariant). TheBoundUnknown's new fields are stable functions of the input. - No solver coupling. You're determining which symbol the question targets and what form the answer takes — not solving anything.
- No mutation of
MathProblemGraph. Input is read-only (frozen anyway, but assert in tests). - Field invariant untouched. Stay out of
algebra/,chat/,core/, runtime hot path.
Compatibility / fixture migration:
- The two new required fields on
BoundUnknownwill break ~5–10 Phase 1+2+3 test fixtures that constructBoundUnknowndirectly. Update them in the same PR. Do not add default values to keep them backward-compat — required fields are load-bearing. - The Phase 2 adapter's behavior changes (
Unknown → BoundUnknownnow populates the new fields). The byte-equal hash-stability invariant must continue to hold on a fresh-run basis (hash will differ from Phase 3 main, by design — that's not a regression).
Out of scope (do not touch — Phase 5):
- Bounded-grammar / B3 integration.
- Any change to
chat/,core/,generate/intent.py,generate/realizer.py,generate/math_problem_graph.py. - B1.B and B1.S follow-ups (separate lanes).
- B2 v1.B enrichment (L6, Gemini's lane).
- Promotion-gate wiring (ADR-0131.4).
Target branch. PR against main. Title: feat(binding-graph): Phase 4 question-target binding (ADR-0135). Body must reference ADR-0132 / -0133 / -0134; list Phase 5 deferred; report test count (expect ~300+ total: 226 from Phase 1+2+3 still passing, +80ish new).
Exit criterion. PR opens with CI green, all new + existing binding-graph tests pass, pyright clean on new files, ADR-0135 included. Runtime behavior byte-identical to main (no integration yet).
Do not stack on another agent's branch. Target main directly. If main does not yet contain ADR-0132 + -0133 + -0134, stop and flag — Phase 4 requires all three.