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)
7.9 KiB
ADR-0133 — Semantic-Symbolic Binding Graph: Phase 2 adapter from MathProblemGraph
Status: Accepted (Phase 2 only; Phases 3–5 deferred)
Date: 2026-05-23
Parent: ADR-0132 (Phase 1 data model, PR #171)
Related: ADR-0115 (MathProblemGraph origin), ADR-0126 (candidate-graph
parser), ADR-0127 (units pack), ADR-0131 (math-expert rebench / proof corridor),
PR #170 (binding-graph proposal)
Context
ADR-0132 (PR #171) ratified Phase 1 of the binding-graph layer: a pure data
model under generate/binding_graph/ with frozen dataclasses, deterministic
symbol allocation, refusal-first construction, and no coupling to the symbolic
substrate (Polynomial) — symbolic expressions are referenced by canonical
string form only.
The proposal in PR #170 recommends shipping the binding graph in phases. With
the data model locked, the next reviewable seam is a pure-function adapter
from the existing ADR-0115 MathProblemGraph (the math-word-problem parser's
output) into a SemanticSymbolicBindingGraph. Adding the adapter without
runtime wiring keeps the abstraction reviewable in isolation before any
runtime behavior depends on it.
Decision
Add generate/binding_graph/adapter.py exposing
bind_math_problem_graph(g) -> SemanticSymbolicBindingGraph and a typed
AdapterError (sibling of BindingGraphError — both ValueError subclasses,
refusal-first by design).
The adapter is pure: no I/O, no parser calls, no solver calls, no algebra,
no numpy. It performs structural translation only. Mapping discipline is
locked as module-level constants:
Source (MathProblemGraph) |
Output (SemanticSymbolicBindingGraph) |
|---|---|
each entity (str) |
one SymbolBinding with semantic_role="entity", symbol_id="entity_<slug>" |
each InitialPossession |
one SymbolBinding (semantic_role="quantity", symbol_id="q_<entity>_<unit>_t0") plus one BoundFact(value=str(value)) |
each Operation (index i) |
one fresh SymbolBinding (symbol_id="op_{i:03d}_result") plus one BoundEquation with operation_kind verbatim from source |
Unknown |
one synthesized SymbolBinding (semantic_role="unknown") plus one BoundUnknown referencing it |
Operation-kind passthrough. The closed vocab
MathProblemGraph.VALID_OPERATION_KINDS is shared by design — the adapter
emits BoundEquation.operation_kind as a verbatim string passthrough. No
translation, no renaming.
Dependency wiring. BoundEquation.dependencies references the t0
quantity symbol(s) of the actor (and, where applicable, of
Operation.target for transfer and of Comparison.reference_actor for
compare_*), keyed on the unit hint derived from the operand. When no
matching t0 symbol exists (e.g. an actor that started at implicit zero),
the dependency set silently omits it — Phase 2 wires only what the source
graph already pins.
Synthetic source spans. MathProblemGraph carries no native source-text
spans, so every span the adapter emits is synthesized with
source_id == "math_problem_graph". The brief calls for "skip span fields
cleanly when absent" — Phase 2 implements this by anchoring every
SourceSpanLink to a deterministic surface text derived from the binding's
own identity. If a future revision of MathProblemGraph carries real spans,
the adapter is the natural site to propagate them.
Determinism is load-bearing. Two MathProblemGraph instances that compare
equal produce two SemanticSymbolicBindingGraph instances that compare equal
and serialize byte-identically via to_canonical_string(). This is asserted
by tests/test_binding_graph_adapter.py via a hash-stability test that calls
bind_math_problem_graph twice and compares both the structure and the
canonical string.
Refusal-first. The adapter accepts object at the signature boundary and
refuses anything that is not a MathProblemGraph with a typed AdapterError.
Cross-collection invariants violated downstream (these can only fire if the
adapter itself has a bug) are re-raised as AdapterError, so callers see a
single refusal type at the adapter boundary.
Phase 3+ deferred (explicit)
The following remain out of scope for this ADR and are deferred to follow-up PRs:
- Phase 3 — Unit-aware equation admissibility (ADR-0134). Every
BoundEquationPhase 2 emits carriesunit_proof == "deferred_to_phase_3"andadmissibility_status == "pending". This is by design — dimensional analysis is the substance of Phase 3, not Phase 2, and the placeholder makes the gap unambiguous in artifact form. Phase 3's first commit should be a test that asserts no equation carries the placeholder anywhere except in this adapter's output, then the loop that closes the gap. - Phase 4 — Question-target binding refinement. Phase 2 binds the
Unknownto a synthesizedSymbolBinding(semantic_role="unknown") rather than to any prior t0 / result symbol. Phase 4 will resolve the unknown against the operation chain when possible. - Phase 5 — Bounded-grammar / B3 integration. No runtime wiring;
chat/,core/,generate/intent.py, andgenerate/realizer.pyare untouched.
Trust boundary
The adapter consumes a frozen, self-validating MathProblemGraph — already a
trust-boundary-checked artifact (ADR-0115 refuses malformed input at
construction time). The adapter introduces no new arbitrary-code execution,
no filesystem access, no dynamic imports. The only widening of the trust
boundary is at the signature: g: object accepts any caller input and the
adapter is responsible for refusing non-MathProblemGraph inputs with a
typed AdapterError. Tests pin this refusal.
Field invariant
Untouched. The adapter never reaches algebra/, field/, or any runtime
hot-path; versor_condition cannot be affected by this change.
Evidence
generate/binding_graph/adapter.py— adapter (≈300 lines, pure functions, module-level mapping constants).generate/binding_graph/__init__.py— public surface addsbind_math_problem_graphandAdapterError.tests/test_binding_graph_adapter.py— 41 tests covering: refusal on non-graph input, all eightVALID_OPERATION_KINDSround-tripping verbatim, dependency wiring (transfer, apply_rate, compare_additive), introduction order preservation, hash-stability across runs, input immutability, frozen-output invariants, Phase-2 placeholder constants (PHASE_2_UNIT_PROOF,PHASE_2_ADMISSIBILITY), and cross-collection reference invariants. Full lanetests/test_binding_graph_model.py + tests/test_binding_graph_adapter.py: 110/0/0.pyrightclean on new files.- Runtime byte-identical to main: no runtime integration introduced.
PR checklist
- Capability added: typed, deterministic translation from
MathProblemGraph(ADR-0115) toSemanticSymbolicBindingGraph(ADR-0132). No runtime wiring yet — Phase 2 is structural translation only. - Invariant proven:
bind(g) == bind(g)byte-for-byte; two equalMathProblemGraphs produce two equal binding graphs (hash-stability test). All cross-collection references in the emitted binding graph resolve against the emitted symbol table. - CLI / suite:
python3 -m pytest tests/test_binding_graph_model.py tests/test_binding_graph_adapter.py— 110 passed. - Avoided: hidden normalization, stochastic fallback, approximate
recall, dimensional-analysis-by-stealth,
Polynomialcoupling. - Trust boundary: widened only at the adapter signature (
g: object→ refusal-firstAdapterError); no new dynamic execution, no new filesystem access.