core/docs/decisions/ADR-0114a.5-perturbation-suite.md
Shay 29111b7762
feat(ADR-0114a.5): reasoning-isolation perturbation suite — Obligation #5 wired for B3, PASSING 130/130 preserving, 68/68 breaking (#191)
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
2026-05-23 16:07:59 -07:00

192 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.