Discharges ADR-0114a Obligation #5 for the B3 bounded-grammar lane. Closed perturbation taxonomy (5 invariance-preserving, 3 invariance-breaking transforms) operates on problem text only; parser, solver, and cases.jsonl are untouched. Both rates are ε=0 per ADR-0120 §"Threshold rationale". Results on main B3 (35 solved_correct cases): invariance_preserving: 130/130 = 1.0000 invariance_breaking: 68/68 = 1.0000 obligation_5_passed: True Skipped transforms documented explicitly (not silently absent): commutative_reorder: all 35 — no single-entity multi-unit init state op_verb_flip: 15 — multiply/divide/compare/transfer cases value_replacement_op: 15 — no distinct numeric operand unit_synonym: 7 — rate-declaration $ syntax cases value_replacement_init: 7 — value cancels or not found entity_rename_v{1,2,3}: 1 each — b3-013 "Birds" collective is self-mapping Ships: core/capability/perturbation_b3.py — generator + scorer + validate_perturbation_suite() tests/test_adr_0114a_5_perturbation.py — 15 tests (purity, preserving, breaking, determinism, snapshot, refusal, skip coverage) core/cli.py — core capability perturbation [--lane-id] [--json] evals/obligation_5_perturbation/B3_bounded_grammar.json — written by CLI docs/decisions/ADR-0114a.5-perturbation-suite.md — ADR with taxonomy tables
7.5 KiB
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:
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 perturbationexits 0 with:preserving_rate: 1.0000(130/130 applicable variants)breaking_rate: 1.0000(68/68 applicable variants)
tests/test_adr_0114a_5_perturbation.py15/15 green- B3 axis lane report unchanged (
wrong == 0preserved) - 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 == 0firewall on the B3 axis lane is unaffected. commutative_reordergenerates 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.