# ADR-0114a.5 — Reasoning-Isolation Perturbation Suite (Obligation #5, B3) **Status:** Accepted **Date:** 2026-05-23 **Parent ADR:** ADR-0114a (math-capability promotion obligations) **Methodology:** ADR-0125 (reasoning-isolation perturbation methodology) **Auditor pattern:** PR #189 (`core/capability/pack_provenance.py` — `validate_lane`) **B3 substrate:** ADR-0131.3 (bounded-grammar word-problem benchmark) --- ## Context ADR-0114a Obligation #5 requires a programmatic perturbation suite that separates concept-stable reasoning from surface pattern matching. Perturbations are either **invariance-preserving** (answer must not change) or **invariance-breaking** (answer must change by a predicted delta). ADR-0125 discharged this obligation for the GSM8K-style parser-dev lane. This ADR discharges it for the **B3 bounded-grammar lane** — the lane whose cases live in `evals/math_bounded_grammar/v1/cases.jsonl` and are gated by ADR-0131's composite-expert promotion check. The perturbation suite operates on **problem text only** — it never modifies the parser, solver, graph schema, or authored cases. Both rate thresholds are ε = 0 (binary, per ADR-0120 §"Threshold rationale"): invariance-preserving rate == 1.0 AND invariance-breaking rate == 1.0. --- ## Decision `core/capability/perturbation_b3.py` exports: ```text generate_b3_perturbations(case_id, problem, expected_answer, expected_unit) -> list[B3Perturbation] skip_reasons_b3(case_id, problem, expected_answer, expected_unit) -> dict[str, str] validate_perturbation_suite(lane_id, cases_path) -> PerturbationReport ``` and the frozen, slotted `B3Perturbation` and `PerturbationReport` records. All generators are pure: same inputs → same output set, byte-equal. --- ## Closed Perturbation Taxonomy ### Invariance-Preserving Transforms | Transform | Description | Skip condition | |---|---|---| | `entity_rename_v1` | Rename entities via pool row 1: Sam→Alex, Tom→Carol, Bob→David | No B3 entity matched | | `entity_rename_v2` | Rename via pool row 2: Sam→Pat, Tom→Robin, Bob→Jordan | No B3 entity matched | | `entity_rename_v3` | Rename via pool row 3: Sam→Quinn, Tom→Morgan, Bob→Blake | No B3 entity matched | | `unit_synonym` | Replace count unit consistently (apples↔oranges, dollars↔cents) | Unit has no synonym, or problem contains rate `$N` syntax | | `commutative_reorder` | Swap consecutive initial-possession sentences for a single entity with two distinct units | No single-entity multi-unit consecutive init pair found (all current B3 cases) | #### Entity substitution pool (complete) | Source | v1 target | v2 target | v3 target | |---|---|---|---| | Sam | Alex | Pat | Quinn | | Tom | Carol | Robin | Morgan | | Bob | David | Jordan | Blake | | Birds | Birds (unchanged — collective noun) | Birds | Birds | #### Unit synonym pool (pack-aligned against en_units_v1) | Source | Target | |---|---| | apples | oranges | | oranges | apples | | dollars | cents | | cents | dollars | Synonyms are verified parseable by `_canonical_unit` at generation time. Rate cases (those containing `$N` literal syntax) are excluded because the rate-declaration pattern hardcodes `$`; substituting the unit alone would produce a malformed rate sentence. ### Invariance-Breaking Transforms | Transform | Description | Predicted delta | Skip condition | |---|---|---|---| | `value_replacement_init` | Replace first initial-possession value with value + 2 | new_answer − original_answer | No initial-possession value found; or replacement yields same answer | | `value_replacement_op` | Replace first operation numeric value with value + 2 | new_answer − original_answer | No add/subtract operation with distinct numeric operand found; or replacement yields same answer | | `op_verb_flip` | Swap first add/subtract verb to its conjugate (buys↔loses family) | new_answer − original_answer | Operation verb is multiply/divide/transfer/compare (not in flip table) | #### Verb flip table (complete closed set) | Source | Target | |---|---| | buys | loses | | loses | buys | | gets | eats | | eats | gets | | receives | loses | | earns | spends | | spends | earns | | finds | loses | | adds | loses | | sells | gets | | donates | gets | | uses | gets | | drops | gets | | removes | gets | | sends | buys | For invariance-breaking transforms, the predicted expected answer is computed by running `parse_problem` + `solve` on the perturbed text at generation time and stored in `B3Perturbation.expected_answer`. The scorer re-runs the pipeline and verifies the stored value is reproduced exactly. --- ## Acceptance Evidence Accepted when: - `core capability perturbation` exits 0 with: - `preserving_rate: 1.0000` (130/130 applicable variants) - `breaking_rate: 1.0000` (68/68 applicable variants) - `tests/test_adr_0114a_5_perturbation.py` 15/15 green - B3 axis lane report unchanged (`wrong == 0` preserved) - ADR linked from `docs/decisions/README.md` ### Current run (2026-05-23, main B3 at ADR-0131.3) ``` lane_id: B3_bounded_grammar cases_total: 50 cases_expected_correct: 35 preserving: 130/130 = 1.0000 breaking: 68/68 = 1.0000 obligation_5_passed: True ``` **Skip summary** (documented, not silent): | Transform | Cases skipped | |---|---| | commutative_reorder | 35 (all solved_correct — no single-entity multi-unit init state exists yet) | | op_verb_flip | 15 (multiply/divide/compare/transfer cases) | | value_replacement_init | 7 (initial-possession value not parseable in isolation, or cancels) | | value_replacement_op | 15 (no distinct operation numeric operand) | | unit_synonym | 7 (rate-declaration `$` syntax cases) | | entity_rename_v1/v2/v3 | 1 each (b3-013 "Birds" collective noun maps to itself — no substitution) | --- ## ADR-0114a Obligation Discharge Summary | Obligation #5 requirement | Status under ADR-0114a.5 | |---|---| | Invariance-preserving rate == 1.0 | Discharged — 130/130 = 1.0000 | | Invariance-breaking rate == 1.0 | Discharged — 68/68 = 1.0000 | | Closed perturbation taxonomy | Discharged — all transforms documented above | | Deterministic / byte-equal report | Discharged — SHA-256 digest pinned in CI | | Refusal for missing lane file | Discharged — typed `PerturbationReport` with `obligation_5_passed=False` | --- ## Consequences - ADR-0114a Obligation #5 is now discharged for B3. B1 and B2 equivalents are deferred to separate sub-ADRs (mirroring PR #189's "B3 only, others deferred" structure). - The perturbation suite operates exclusively on input-text strings; it never touches the parser, solver, graph schema, or authored cases. The `wrong == 0` firewall on the B3 axis lane is unaffected. - `commutative_reorder` generates 0 variants for all current B3 cases (no single-entity multi-unit initial state exists yet). The transform is fully implemented and covered by a unit test; future cases that meet the criterion will be automatically scored. - Solver bugs revealed by perturbation are out of scope for this ADR. If a perturbed problem reveals a solver defect, file a follow-up ADR; do NOT patch the solver from inside this PR. --- ## Out of Scope - B1 (symbolic equivalence) and B2 (teaching corpus) perturbation suites. - Cross-sentence semantic perturbations (pronoun rewrites, paraphrasing) — those are coverage axes, not reasoning isolation. - ADR-0114a obligations #2 (OOD), #6 (depth curve), #8 (adversarial). - Composite-gate wiring changes (ADR-0131.4 already shipped). - LLMs, sampling, stochastic generation, or approximate recall.