* docs(ADR-0136): statement-layer corridor + S.0 taxonomy + resolve math-file conflicts - Add ADR-0136-statement-layer-corridor.md: corridor overview, S.0-S.4 phase table, taxonomy summary (23 context-filler, 4 rate-class, 5 compound, 17 long-tail), standing invariants (admitted_wrong==0, context-filler safety rail, honest delta). - Restore generate/math_*.py to HEAD (main) — stash-pop conflict markers were cosmetic comment rewording; upstream version is authoritative. - Stage docs/reviewers.yaml math_expert_claims addition (already authored). * docs(briefs): archive all 2026-05-23 parallel-dispatch briefs (L1–L17 + README)
6.4 KiB
L5 brief — Opus#2 — Binding Graph Phase 3 (unit-aware equation admissibility)
Worktree setup (do this first, non-negotiable):
git worktree add ../core-binding-graph-p3 -b feat/binding-graph-phase3-units origin/main
cd ../core-binding-graph-p3
Scope. Phase 3 of the binding-graph layer. Phase 1 (#171, ADR-0132) shipped the data model with BoundEquation.unit_proof as a placeholder. Phase 2 (#174, ADR-0133) shipped the MathProblemGraph adapter and explicitly left unit_proof = None as a Phase-3 gap. Close it: add deterministic, refusal-first unit-aware admissibility checking that populates unit_proof and stamps admissibility_status on every BoundEquation produced by the adapter (or constructed directly).
Still no runtime wiring outside generate/binding_graph/ and no solver invocation — Phase 3 is structural and dimensional analysis only. Phase 4 (question-target binding refinement) and Phase 5 (B3 / bounded-grammar integration) remain deferred.
Reference docs (read these, only these):
docs/decisions/ADR-0132-binding-graph-data-model.md+docs/decisions/ADR-0133-binding-graph-adapter.md— your prior phases. The Phase-1 dataclass shape (BoundEquation.unit_proof: UnitProof | None,admissibility_status: Literal[...]) is the contract you're filling in.packs/en_units_v1/(loaded into the runtime via ADR-0127) — the canonical unit vocabulary. Your unit algebra must be expressed in those units only; do not invent new ones. Read the pack's lexicon + relation files to identify the closed set of units and their dimensional families (length, time, mass, currency, count, rate-of-X-per-Y, etc.).
What to ship:
generate/binding_graph/units.py— pure unit algebra. Functions:parse_unit(canonical_id: str) -> UnitVector— convert a closed-vocab unit id into a frozenUnitVector(dimension exponents over the base dimensions in the units pack).unit_product(a: UnitVector, b: UnitVector) -> UnitVector,unit_quotient,unit_inverse— algebra primitives.units_equal(a, b)— strict equality on the dimension-exponent vector.- All pure, frozen, deterministic. No I/O at call time.
generate/binding_graph/admissibility.py—check_admissibility(equation: BoundEquation, *, symbols: Mapping[str, SymbolBinding]) -> AdmissibilityResult. Returns aUnitProofon success; a typedAdmissibilityErrorreason on refusal. Dispatch is onoperation_kind— the closed eight-string vocab is your full case table:add/subtract/compare_additive: lhs unit ≡ rhs unit ≡ each operand unit; refuse if any disagree.multiply/divide: lhs unit = product / quotient of operand units; no equality requirement among operands.transfer: actor and target must share the same unit; lhs unit = that unit.apply_rate: lhs unit = rate-unit × duration-unit (or equivalent); the rate must have formX/Ywhere Y is the operand's unit.compare_multiplicative: lhs is dimensionless (ratio); operand units must be equal so they cancel.- Any operand whose unit is
None(unbound) → refuse withunit_unbound. - Any equation whose
symbolslookup is missing → refuse withunknown_symbol.
generate/binding_graph/adapter.py(modify, surgically): wirecheck_admissibilityinto the adapter so everyBoundEquationit produces carries eitheradmissibility_status="admissible"+ populatedunit_proof, oradmissibility_status="refused"+ populatedrefusal_reason. Do not change the adapter's input/output types or signature. Existing 41 Phase-2 tests must still pass.- Public surface: add
check_admissibility,UnitVector,UnitProof,AdmissibilityError,parse_unit(and minimal algebra primitives) togenerate/binding_graph/__init__.py. tests/test_binding_graph_units.py— 30–40 tests covering the algebra primitives (commutativity, inverse-of-inverse, refusal on unknown unit-id).tests/test_binding_graph_admissibility.py— 50–70 tests covering each operation kind's admissibility rules with both positive and negative cases, plus the typed-refusal contract.tests/test_binding_graph_adapter_units.py— 20–30 integration tests covering the adapter's Phase-3 behavior: every Phase-2 case still round-trips, but nowunit_proofis populated (or refusal is typed); a few intentionally unit-mismatchedMathProblemGraphinputs produceadmissibility_status="refused"with the correct reason.docs/decisions/ADR-0134-binding-graph-admissibility.md— short ADR; cite ADR-0132 + ADR-0133 parents and ADR-0127 (units pack). Explicit "Phase 4–5 deferred" section.
Hard constraints:
- Closed unit vocabulary. Every unit id used in algebra must already exist in
en_units_v1. If you find yourself wanting to invent or coerce, stop and refuse withunknown_unit. - Refusal-first. Unit mismatches never silently coerce. The
BoundEquationexits the adapter withadmissibility_status="refused"and a typedrefusal_reason. Never raise from the adapter on a unit mismatch — the data-model has a slot for it; use the slot. - Determinism.
unit_product(a, b) == unit_product(b, a)byte-equal;bind_math_problem_graph(g)byte-equal across runs (Phase-2 invariant must not regress). - Pure, no I/O. Unit-pack data must be loaded at module import (or lazily memoized) and frozen; no per-call filesystem reads.
- No solver coupling. You're not solving equations. You're verifying the equation, if solved, would be dimensionally consistent.
- Field invariant untouched. Stay out of
algebra/,chat/,core/,generate/intent.py,generate/realizer.py, runtime hot path.
Out of scope (do not touch — Phase 4+):
- Question-target binding refinement (Phase 4).
- Bounded-grammar / B3 integration (Phase 5).
MathProblemGraphitself (input type — read-only here).- Symbolic equivalence engine (#167, #169 — separate lane).
Target branch. PR against main. Title: feat(binding-graph): Phase 3 unit-aware admissibility (ADR-0134). Body must reference ADR-0132, ADR-0133, ADR-0127 (units pack); list Phase 4–5 deferred; report test count (expect ~210 total: 110 from Phase 1+2 still passing, +100ish new).
Exit criterion. PR opens with CI green, all new + existing binding-graph tests pass, pyright clean on new files, ADR-0134 included. Runtime behavior byte-identical to main (no integration yet).
Do not stack on another agent's branch. Target main directly.