Frozen dataclasses + deterministic allocator + invariants for the Semantic-Symbolic Binding Graph proposed in PR #170. Pure data layer: no parser, no solver, no adapter, no runtime wiring. Phases 2-5 deferred to follow-up PRs. - generate/binding_graph/model.py: SourceSpanLink, SymbolBinding, BoundFact, BoundEquation, BoundUnknown, BoundConstraint, and the top-level SemanticSymbolicBindingGraph container. All @dataclass(frozen=True, slots=True). Refusal-first construction via typed BindingGraphError. Cross-collection referential integrity enforced at __post_init__. - generate/binding_graph/allocation.py: pure deterministic allocate_symbols() — same input order yields byte-equal output. - generate/binding_graph/__init__.py: public API surface. - tests/test_binding_graph_model.py: 69 tests covering frozen invariants, slots enforcement, refusal paths, allocation determinism, canonical-string round-trip, cross-collection integrity. - docs/decisions/ADR-0132-binding-graph-data-model.md: ratifies Phase 1 only; explicit Phase 2-5 deferred section citing #170.
5.7 KiB
ADR-0132 — Semantic-Symbolic Binding Graph: Phase 1 data model
Status: Accepted (Phase 1 only; Phases 2–5 deferred)
Date: 2026-05-23
Parent proposal: docs/implementation/semantic-symbolic-binding-graph-proposal.md (PR #170)
Related: ADR-0115..0118 (math parser/solver/verifier/realizer), ADR-0126
(candidate-graph parser), ADR-0127 (units pack), ADR-0131 (math-expert
rebench / proof corridor)
Context
PR #170 proposed a SemanticSymbolicBindingGraph as the typed compiler
boundary between natural-language semantic parsing and symbolic /
equational solving. The proposal explicitly recommends shipping it in
phases, starting with a data-model-only first PR — no parser, solver,
adapter, or wiring — so the abstraction has a reviewable seam before any
runtime behavior depends on it.
This ADR ratifies that Phase 1 (SSBG-1) scope and pins the resulting
data model.
Decision
Add a pure data layer under generate/binding_graph/:
model.py— frozen, slots-bearing dataclasses:SourceSpanLink—(source_id, start, end, text)with strict half-open-interval validation.SymbolBinding— stablesymbol_id(Python identifier), human- readablename, closed-vocabularysemantic_role, optionalentity/unit, mandatorysource_span+introduced_by.BoundFact—symbol_id = value [unit]lifted from language.BoundEquation—lhs_symbol_id := rhs_canonicalwithdependencies: frozenset[str],operation_kind,unit_proof, closed-vocabularyadmissibility_status, and a typedrefusal_reasoninvariant (required iffstatus == "refused").BoundUnknown— question target bound to a known symbol.BoundConstraint— canonical-string predicate over one symbol.SemanticSymbolicBindingGraph— top-level container; enforces cross-collection referential integrity at construction.
allocation.py—allocate_symbols(noun_phrases, *, source_span, introduced_by, semantic_role, prefix). Pure, deterministic, refusal- first. Identical input → identicaltuple[SymbolBinding, ...], byte-for-byte.__init__.py— public API surface.
Closed vocabularies
SEMANTIC_ROLES = {entity, quantity, rate, duration, count, total, difference, ratio, unknown}ADMISSIBILITY_STATUSES = {admitted, pending, refused}
Extending either is a deliberate ADR change.
Discipline (load-bearing)
- Pure data layer. No I/O, no parser calls, no algebra calls, no
numpy, no runtime field touch. The package is importable with zero side effects. - Immutability. Every dataclass is
@dataclass(frozen=True, slots=True). Every collection field istupleorfrozenset.SourceSpanLink/SymbolBinding/etc. are equality- and hash-stable. - Refusal-first. Invalid construction raises typed
BindingGraphError(sibling ofSymbolicError). Empty strings, non-identifier ids, unknown roles, empty/inverted spans, and missing/spuriousrefusal_reasonall refuse. - No coupling to the symbolic substrate.
rhs_canonicalandpredicateare strings. The binding graph does not importPolynomialfromgenerate.math_symbolic_normalizer— decoupling is the entire point of the layer. The string contract aligns with ADR-0131's byte-equality discriminator. - Deterministic allocation. Symbol ids follow
{prefix}_{slug}_{index:03d}; collisions are disambiguated by the numeric suffix, so same input → byte-equal output across runs.
Cross-collection invariants
SemanticSymbolicBindingGraph.__post_init__ enforces:
symbolscarries uniquesymbol_idvalues;- every
BoundFact.symbol_idreferences a known symbol; - every
BoundEquation.lhs_symbol_idand every dependency references a known symbol; - every
BoundUnknown.symbol_idreferences a known symbol; - every
BoundConstraint.symbol_idreferences a known symbol; - every sub-collection is a
tuple(lists are rejected at construction).
Acceptance evidence
- 69 tests in
tests/test_binding_graph_model.py, covering frozen invariants, slots enforcement, refusal paths, allocation determinism, canonical-string round-trip, and cross-collection integrity. pyrightclean on new files.- Runtime behavior byte-identical to
main: nothing imports the new package yet.
Consequences
- A reviewable seam for the binding graph exists without committing to any specific NL parser, unit algebra, or solver behavior.
- Subsequent phases (see below) can land independently behind the same typed boundary.
- The byte-equality discriminator from ADR-0131 is reinforced: the binding graph speaks the symbolic substrate by canonical string, so graph hashes are stable iff substrate canonicalization is stable.
Phase 2+ deferred (explicitly out of scope here)
- Phase SSBG-2 — adapter from existing
MathProblemGraphinto the binding graph; goal is representational parity with current bounded math behavior, no behavior change. - Phase SSBG-3 — unit-aware equation binding using the ratified units pack (ADR-0127); admit/refuse based on dimension algebra.
- Phase SSBG-4 — question-target binding; refuse on ambiguous or unbound questions.
- Phase SSBG-5 — integration with the bounded grammar lane (ADR-0131 Benchmark 3); each case carries expected binding-graph shape.
These phases land in separate PRs against main, each with its own
ADR, lane evidence, and refusal coverage. They will not be stacked on
this PR's branch.
Non-goals (carried forward from PR #170)
This is not a general NL understanding system, not a chain-of-thought generator, not a substitute for symbolic equivalence (ADR-0131.1.B), not a reopening of arbitrary GSM8K parser expansion, and not a promotion gate by itself.