core/docs/adr/ADR-0133-binding-graph-adapter.md
Shay 54e6bfc0d0
docs: reorganize docs landscape
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)
2026-06-30 16:59:36 -07:00

7.9 KiB
Raw Blame History

ADR-0133 — Semantic-Symbolic Binding Graph: Phase 2 adapter from MathProblemGraph

Status: Accepted (Phase 2 only; Phases 35 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 BoundEquation Phase 2 emits carries unit_proof == "deferred_to_phase_3" and admissibility_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 Unknown to a synthesized SymbolBinding (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, and generate/realizer.py are 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 adds bind_math_problem_graph and AdapterError.
  • tests/test_binding_graph_adapter.py — 41 tests covering: refusal on non-graph input, all eight VALID_OPERATION_KINDS round-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 lane tests/test_binding_graph_model.py + tests/test_binding_graph_adapter.py: 110/0/0. pyright clean on new files.
  • Runtime byte-identical to main: no runtime integration introduced.

PR checklist

  • Capability added: typed, deterministic translation from MathProblemGraph (ADR-0115) to SemanticSymbolicBindingGraph (ADR-0132). No runtime wiring yet — Phase 2 is structural translation only.
  • Invariant proven: bind(g) == bind(g) byte-for-byte; two equal MathProblemGraphs 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, Polynomial coupling.
  • Trust boundary: widened only at the adapter signature (g: object → refusal-first AdapterError); no new dynamic execution, no new filesystem access.