# L2 brief — Opus#2 — Semantic-Symbolic Binding Graph, Phase 1 (data model only)

**Worktree setup (do this first, non-negotiable):**

```bash
git worktree add ../core-binding-graph-p1 -b feat/binding-graph-phase1 origin/main
cd ../core-binding-graph-p1
```

**Scope.** Ship Phase 1 *only* of the binding graph layer proposed in `docs/implementation/semantic-symbolic-binding-graph-proposal.md` (PR #170, now on main). Phase 1 is **data-model-only**: frozen dataclasses + invariants + tests. **No runtime wiring, no adapter, no equation binding, no parser change.** Phases 2–5 are deferred to follow-up PRs.

**Reference docs (read these, only these):**
1. `docs/implementation/semantic-symbolic-binding-graph-proposal.md` — your spec. The dataclass list under "Adds" is authoritative.
2. `generate/math_symbolic_normalizer.py` (post-#167) — only to see the existing `Polynomial` dataclass style (frozen, terms-dict, variables-tuple). Match its idiom; do **not** import from it.

**What to ship:**
- `generate/binding_graph/__init__.py` — public API surface.
- `generate/binding_graph/model.py` — frozen dataclasses per the proposal:
  - `SemanticSymbolicBindingGraph`
  - `SymbolBinding`
  - `BoundFact`
  - `BoundEquation`
  - `BoundUnknown`
  - `BoundConstraint`
  - `SourceSpanLink`
  Use `@dataclass(frozen=True, slots=True)`. All collections immutable (`tuple`, `frozenset`, `MappingProxyType` if needed). No mutation methods.
- `generate/binding_graph/allocation.py` — deterministic symbol allocator. Given a sorted iterable of NL noun-phrases, returns a stable `tuple[SymbolBinding, ...]`. Pure function. Deterministic across runs.
- `tests/test_binding_graph_model.py` — 30–50 tests covering: frozen invariants, equality, hashability where applicable, allocation determinism, refusal on bad input (empty span, duplicate binding id, etc.), `SourceSpanLink` round-trip.
- `docs/adr/ADR-0132-binding-graph-data-model.md` — short ADR ratifying Phase 1; cite #170's proposal doc; explicit "Phase 2+ deferred" section.

**Hard constraints:**
- **Pure data layer.** No I/O, no parser calls, no algebra calls, no `numpy`, no runtime field touch.
- **Immutability.** Every field is immutable; every collection is `tuple`/`frozenset`. The "create new objects, never mutate" rule from coding-style.md is load-bearing here.
- **Deterministic allocation.** Same input order → same `SymbolBinding` ids → byte-equal serialization. Lane test asserts this.
- **Refusal-first.** Invalid construction raises a typed error (`BindingGraphError`, sibling of `SymbolicError`); never silently coerces.
- **No coupling to the symbolic normalizer.** The binding graph references symbolic expressions by *string canonical form*, not by importing `Polynomial`. Decoupling is the whole point of the layer.

**Out of scope (do not touch — these are Phase 2+):**
- Adapter from existing `MathProblemGraph` (Phase 2).
- Unit-aware equation binding (Phase 3).
- Question-target binding (Phase 4).
- Bounded grammar integration (Phase 5 / B3).
- Any change to `chat/`, `generate/intent.py`, `generate/realizer.py`.

**Target branch.** PR against `main`. Title: `feat(binding-graph): Phase 1 data model (ADR-0132)`. Body must reference #170 as parent and list Phase 2–5 as deferred follow-ups.

**Exit criterion.** PR opens with CI green, all new tests pass, `pyright` clean on new files, ADR-0132 included. Runtime behavior byte-identical to main (no integration yet, by design).

**Do not stack on another agent's branch.** Target main directly.
