# ADR-0217 — R2: Finite-Integer Linear-Constraint Setup Compiler (off-serving) > **Renumbered 2026-06-08:** originally landed as ADR-0211, which collided with > ADR-0211 (Conformal Falsification Bench, 2026-06-06). The conformal ADR is the > incumbent (earlier, test-pinned, depended on by ADR-0216); this R2 ADR moves to > the next free number 0217. The ratified decision content is unchanged. **Status:** Accepted (ratified 2026-06-07) **Date:** 2026-06-07 **Author:** Shay **Anchor:** [[thesis-decoding-not-generating]] **Current execution state:** IR (C1) + setup oracle / gold (C2) landed with this ADR; the integer solver (C3), answer-choice verifier (C4), and reader (C5–C9) execute against the gates below. R1 stays frozen at 7/0/3 (ADR-0207 surface untouched). **Builds on (does not replace):** ADR-0207 (GSM8K substrate), ADR-0175 (calibrated learning), ADR-0083 (transitive chain — the one executing R1 reasoning primitive). --- ## 1. The shift this ratifies R1 grew one reader template per problem shape (`more_than`, `times_as_many`, `half`, partition, aggregate-query, inverse). That ladder is now **closed** (R1 = 7/0/3; see `docs/analysis/r1-inventory-ledger-2026-06-07.md`) and the per-shape path does not generalize to harder problems. R2 changes the unit of work from ```text one problem shape -> one reader patch ``` to a **small algebra of reusable setup primitives**: ```text problem text -> entities -> quantities -> relations -> CONSTRAINTS -> goal -> solver -> verifier ``` A new problem family is admitted only when it can be expressed as **known primitive composition + setup oracle + answer verifier + refusal tests** — never as a bespoke matcher. This is the same discipline ADR-0207 ratified for the comprehension substrate, extended one level up to constraint *systems*. > If anyone proposes a "new GSM8K reader" or a per-shape matcher for two-category constraint > problems after this ADR, it is redundant with this document. Redirect here. ## 2. Scope — R2 v1 **In:** a single **two-category, finite-integer linear system** of exactly two equations: ```text x + y = N (total count) a·x + b·y = T (weighted total) x, y ∈ nonnegative integers ``` covering buses/seats, chickens/legs, tickets/prices, coins/values, boxes/items, vehicles/wheels — anything that reduces to this shape. The deliverable is a **verified setup first**, then an exact integer solve, then answer-choice verification. **Deferred to R3 (explicitly NOT in this batch):** ≥3 categories, inequalities, multi-step / mixed constraints, distractor exclusion, rates / unit conversion, and the **typed contemplation loop + reviewed-failure learning** (the plan's Phases 6–7). When the failure-learning half is built it routes through the existing `teaching/*` proposal-only flywheel (ADR-0055/0056/0057) — it MUST NOT become a parallel correction path (CLAUDE.md). ## 3. The IR — `generate/constraint_comprehension` Strings are serialization only; meaning lives in typed, frozen, slotted dataclasses (the R2 twin of `generate.quantitative_expr`): - `expr.py` — `LinearExpr(terms: ((symbol, coeff), …), constant)`, `LinearConstraint(lhs, relation="eq", rhs, source_span?)`. Integers only; no floats representable. - `model.py` — `Unknown(symbol, entity, unit, domain)` with `domain ∈ {nonnegative_integer, integer}`; `AttributeFact(category, measured_unit, value)` (per-category coefficient provenance); `ConstraintQuery(symbol, unit)`; `ConstraintProblem(unknowns, facts, constraints, query)`. The query is a dedicated `ConstraintQuery`, **not** R1's `BoundUnknown` — R2 has no state-index / question-form axis, and forcing R1's type would be a degenerate fit. ## 4. The ruler — `evals/constraint_oracle` (independent) The setup oracle grades a comprehended `ConstraintProblem` against independent gold by a **span-free canonical signature** (`signature.py`): unknowns `(symbol, unit, domain)`, attribute coefficients, the canonical linear system (terms merged + sorted, the lhs constant folded into the rhs, source spans stripped), and the query. Two setups are equal iff every component matches; a mismatch localizes the diverging axis. This is the R2 twin of `evals.setup_oracle.signature`, and — like it — imports **no** `generate.derivation` / `core.reliability_gate` (§6). The gold (`r2_gold.jsonl`, 13 fixtures) carries a **closed `expect` taxonomy**: | `expect` | meaning | gold | graded by | |---|---|---|---| | `solved` | well-formed setup; integer answer; `options[answer] == gold` | int | reader (C5–C9) + solver (C3) + answer-choice (C4) | | `solver_refuses` | well-formed setup, but no nonnegative-integer answer | none | solver (C3) | | `reader_refuses` | incomplete/ambiguous prose the reader must not assemble | none | reader (C5–C9) | Closed refusal sets (grow only by ratified extension, each with its fixture): - `solver_reason ∈ {indistinguishable_weights, non_integer_solution, negative_solution, verification_failed}` - `reader_reason ∈ {missing_total_count, missing_weighted_total, too_many_categories}` (C6/C8 may add coefficient-level reasons — equal coefficients, unit mismatch — each ratified with a fixture). C2 ships a **gold-validation lane** only (no reader yet): every fixture deserializes into the IR, canonicalizes deterministically, has a closed taxonomy, and — for `solved` — a coherent answer key. `python -m evals.constraint_oracle` exits 0 iff `invalid == 0` (currently 13/13). ## 5. The wrong=0 contract R2 carries the same invariant as the rest of the engine: **never emit a wrong answer; refuse instead.** It is enforced at four independent gates, each wired to fail loudly: 1. **Setup oracle** — any drift in unknowns/units/constraints/query vs gold ⇒ `setup_wrong` (the reader must refuse an unsupported shape, never misread it as a simpler one). 2. **Reader** — incomplete or ambiguous prose (missing a constraint, >2 categories) ⇒ a typed refusal at assembly time, never a fabricated constraint. 3. **Solver (C3)** — exact integer arithmetic only: `indistinguishable_weights` (equal coefficients), `non_integer_solution` (`numer % denom ≠ 0` — never rounds), `negative_solution`, and a final `verification_failed` re-substitution check. No floats, no nearest-option snapping. 4. **Answer-choice verifier (C4)** — exactly one option may match the proven value; `0`/`>1` matches refuse; a provided key that disagrees with the proven value is **flagged as a contradiction**, not silently accepted (truth discipline: "the math says A; the key says C — the key is wrong"). ### Schema-proof obligations (per CLAUDE.md) Each gate above is real only because a test fails under the violation it catches: the C2 validator has per-branch meaningful-fail tests (incoherent answer key, three categories, constraint referencing an unknown symbol, refusal carrying a gold, unknown reason/expect); C3/C4 ship the same for their refusals. A gate without such a test is decoration, not proof. ## 6. Off-serving disjointness `generate/constraint_comprehension` and `evals/constraint_oracle` import **no** `generate.derivation` and **no** `core.reliability_gate` — the GSM8K serving path. R2 is a parallel organ graded by its own independent oracle, so it **cannot regress** the sealed serving metric (`train_sample`) or any pinned lane SHA. Consequently the per-commit gate is `pytest` on the R2 files + the R1/15-case regression, **not** the pinned-SHA lane; the SHA gate runs once at PR-submission to confirm zero serving drift. ## 7. Build ladder | Commit | Adds | Local gate | |---|---|---| | C1 | constraint IR (`expr`, `model`) | IR tests | | C2 | gold + signature + validation runner + **this ADR** | `constraint_oracle` 13/13 valid | | C3 | two-variable integer solver + refusal tests | buses→4, chickens→11; refusals fire | | C4 | answer-choice parse + verify (contradiction flag) | computed-vs-key contradiction test | | C5–C9 | reader: category-pair → coefficients → total-count → weighted-total → query-target | setup_wrong stays 0; supported fixtures flip | After C9: R2 setup nonzero-correct / **0 wrong** / rest refused; answers likewise; answer-key contradictions flagged; serving unchanged. (Plan Phases 6–7 are a later batch — §2.) ## 8. What this reuses, and does not reinvent - **ADR-0207** — R2 is the constraint-systems layer *above* the comprehension substrate, off the serving path; it does not touch `generate.derivation`. - **ADR-0175** — a new capability family is admitted only with gold + oracle + refusal tests; R2 adds no per-shape matcher and no serving bridge. - **ADR-0083** — the R1 transitive chain is the one executing R1 reasoning primitive; R2 adds *linear-system solving* as a new, independently-verified primitive (the C3 solver). - **`evals.setup_oracle` pattern** — R2 mirrors the R1 ruler (signature + gold + runner) rather than inventing a new scoring mechanism. - **ADR-0055/0056/0057** — the future failure-learning half (R3) routes through the existing proposal-only teaching flywheel; no parallel correction path. ## 9. Decision Build R2 v1 as the off-serving finite-integer constraint setup compiler on the C1–C9 ladder. The contract is pinned by the gold and this ADR; capability grows only where the gold + oracle + refusal tests already license it. No guessed math, no silent correction, no answer without a proven setup.