Implements the 4-phase documentation reorganization master plan. - Consolidation: Merged brief/, handoff/, planning/, and decisions/ into briefs/, handoffs/, plans/, and adr/ respectively (101 ADRs relocated) - Root Cleanup: Relocated HANDOFF-gpt55-*.md and key top-level docs (runtime_contracts.md, etc.) to canonical folders. Added superseded alerts. - Indices & Navigation: Created docs/README.md navigation document, docs/sessions/README.md index, docs/adr/README.md index - Note: Also includes prior commit adding ADR-0200+ corpus hygiene governance (ADR-0225, dependency map, backfilled cross-references)
5.1 KiB
ADR-0135 — Binding Graph Phase 4: question-target binding refinement
Status: Accepted. Parents: ADR-0132, ADR-0133, ADR-0134. Phase 5 (bounded-grammar / B3 integration): deferred.
Context
Phases 1–3 produced a SemanticSymbolicBindingGraph containing a
BoundUnknown that named the symbol the question targets — but only
the symbol identity, not its temporal index nor the shape of the
expected answer.
For "How many apples does Tina have?" with a three-operation
trajectory, the adapter's BoundUnknown did not distinguish between
Tina's apples at t=0, after the second operation, or at the terminal
state. Three different symbols, one ambiguous question.
Likewise, "How much faster did Tina run?" and "How many apples does
Tina have?" both compile down to a BoundUnknown pointing at a symbol,
but the verdict shape a downstream consumer should produce
(difference vs. count) is qualitatively different.
Phase 4 closes that ambiguity deterministically at the binding-graph layer, without invoking a solver or grammar.
Decision
Two new required fields on BoundUnknown
state_index: StateIndex— a closed tagged union:Literal["initial", "terminal"]orOperation(operation_index: int), whereOperationis a frozen dataclass (not a string) so the index is type-checked at construction.question_form: Literal["count", "rate", "total", "difference", "ratio", "identity"].
Both fields are required — no defaults. Phase 1–3 fixtures that
construct BoundUnknown directly have been updated in the same PR.
Pure-function resolvers — generate/binding_graph/question_target.py
-
resolve_state_index(g)—"terminal"wheng.operationsis non-empty,"initial"otherwise. TheOperationvariant is reserved for future intermediate-state queries; this resolver does not yet produce it. -
infer_question_form(g)— closed dispatch over operations that touch the unknown's entity (actor / target /Comparisonreference_actormatches). Precedence rule (deterministic):- No operations touch →
"identity". - Any
compare_multiplicativetouches →"ratio". - Any
compare_additivetouches →"difference". - Any
apply_ratetouches →"total"whenunknown.unit == numerator_unit;"rate"whenunknown.unit == denominator_unit; otherwise refuse (apply_rate_unit_mismatch). - All touching kinds in
{add, subtract, transfer, multiply, divide}→"count". - Anything else → refuse (
unmappable_question_form).
The precedence is closed and ordered, not a heuristic. A graph mixing
compare_additivewithaddreturns"difference"because the comparison establishes the question's shape regardless of any downstream arithmetic. - No operations touch →
-
bound_unknown_from_math_problem_graph(g)— composes the above and yields a fully-populatedBoundUnknown. The adapter (ADR-0133) calls this in place of its old ad-hoc mapping.
Refusal-first via QuestionTargetError
Sibling of AdapterError and AdmissibilityError. Reasons are a
closed set:
not_a_math_problem_graphunknown_entity_not_in_entitiesapply_rate_unit_mismatchunmappable_question_form
The resolver never silently coerces to a default — ambiguity refuses.
Cross-collection guard
SemanticSymbolicBindingGraph.__post_init__ adds: if any
BoundUnknown.state_index is an Operation, its operation_index
must be < len(equations). Standalone BoundUnknown construction
checks only the sign (>= 0).
Invariants preserved
bind_math_problem_graph(g)remains pure and deterministic. Byte-equal across runs on a giveng. The canonical-string emission changes shape (now includesstate=…andform=…tokens), so hashes differ from Phase 3 main by design — not a regression.- No runtime wiring. No solver coupling. No mutation of
MathProblemGraph. Noalgebra/,chat/,core/edits. - Field-invariant
versor_condition(F) < 1e-6untouched.
Phase 5 — deferred
Bounded-grammar / B3 integration remains out of scope. Phase 4 only determines which symbol the question targets and what form the answer takes; producing surface verdicts from those signals belongs to a later phase.
Trust boundary
question_target.py reads only frozen, validated MathProblemGraph
input. No filesystem access, no dynamic import, no user-controlled
formatting. All refusals are typed.
Tests
tests/test_binding_graph_question_target.py— unit-lane (~45 tests): every operation-kind family, precedence rule, refusal paths, typedOperationstate-index guards.tests/test_binding_graph_adapter_question_target.py— integration (~25 tests): adapter round-trip, hash stability, refusal propagation, cross-collection bounds guard.- Phase 1+2+3 fixtures updated where they constructed
BoundUnknowndirectly (~5 sites).
References
- ADR-0132 — data model.
- ADR-0133 — adapter.
- ADR-0134 — unit-aware admissibility.
generate/math_problem_graph.py— input shape (ADR-0115).