# ADR-0119.3 — gsm8k_math Lane Runner (Phase 5.3)

**Status:** Accepted
**Date:** 2026-05-22
**Author:** CORE agents + reviewers
**Depends on:** ADR-0114, ADR-0114a, ADR-0115, ADR-0116, ADR-0117, ADR-0118, ADR-0119, ADR-0119.1, ADR-0119.2

---

## Context

Phase 5.3 of [ADR-0119](ADR-0119-gsm8k-eval-lane-roadmap.md). Composes
the Phases 1–4 pipeline (parser → solver → verifier → realizer) into a
per-case scoring decision that the GSM8K eval lane consumes.

Per ADR-0114a Obligation #4, every case must land in exactly one of
three outcomes: `correct`, `wrong`, or `refused`. The lane gates on
**`wrong == 0`** — CORE must refuse rather than confabulate. A
nonzero `wrong` count invalidates the lane regardless of `correct`
rate.

---

## Decision

### `evals/gsm8k_math/runner.py`

Exposes `run_lane(cases, *, config=None) → LaneReport` per the
framework runner interface (`evals/framework.py`). Pure function;
same input → byte-equal `LaneReport.canonical_bytes()`.

Per-case pipeline:

```text
parse_problem(text) → graph     # ParseError → refused
solve(graph)        → trace     # SolveError → refused
verify(graph, trace)            # passed=False → wrong
realize(initial_state, trace)   # RealizerError → wrong
compare answer/unit to expected # mismatch → wrong; match → correct
```

`CaseOutcome` dataclass records: `case_id`, `outcome`, `reason`,
`expected_answer/unit`, `actual_answer/unit`, `trace_hash`,
`realized_prose`. The trace hash + prose are present on every
non-refused outcome, so a downstream depth-curve harness (ADR-0119.6)
or frontier-comparison report (ADR-0119.4) has a full per-case
audit trail to consume.

### Aggregate metrics

| Key | Type | Meaning |
|---|---|---|
| `cases_total` | int | input size |
| `correct` | int | answer matches AND verifier passes AND realizer succeeds |
| `wrong` | int | verifier failed OR realizer failed OR answer mismatched |
| `refused` | int | parser or solver raised typed refusal |
| `correct_rate` / `wrong_rate` / `refused_rate` | float | counts ÷ total |
| `wrong_count_is_zero` | bool | the load-bearing gate |
| `overall_pass` | bool | `wrong == 0 AND correct + refused == total` |

`overall_pass` is the single line that consumers (ADR-0120 eventual
expert-tier gate; ADR-0119.8 lane-gate composer) check.

### Initial measurement (Phase 5.2 corpus, dev + public)

Run against the on-main `evals/gsm8k_math/` corpus (200 CORE-original
cases authored under ADR-0119.2):

| Split | Total | Correct | Wrong | Refused | Overall pass |
|---|---|---|---|---|---|
| dev | 50 | 50 | 0 | 0 | True |
| public | 150 | 150 | 0 | 0 | True |

**200/200 correct; zero wrong; zero refused.** Establishes the lane
baseline. ADR-0119.4 (frontier comparison) and ADR-0119.6 (depth
curve) will pair this measurement with the relevant context. ADR-0119.7
(sealed GSM8K test holdout) will eventually exercise the same runner
against the real GSM8K test set.

---

## ADR-0114a obligation discharge update

ADR-0119.3 hardens **#4 (typed refusal + wrong==0)** at the lane
layer. Previously the discipline lived in the solver (ADR-0116); now
the lane runner enforces and reports it.

| # | Obligation | Status |
|---|---|---|
| 1 | Sealed-holdout discipline | ADR-0119.1 (fab_control); 5.7 for GSM8K test |
| 2 | OOD surface variation | ADR-0118a |
| 3 | Replay-equal trace | ADR-0117 |
| 4 | Typed refusal + wrong==0 | **hardened at lane layer (this ADR)** |
| 5 | Reasoning-isolation perturbation suite | ADR-0125 |
| 6 | Compositional-depth curve | ADR-0119.6 (in flight) |
| 7 | Frontier-baseline comparison | ADR-0119.4 (in flight) |
| 8 | Adversarial generation | ADR-0119.5 (pending) |
| 9 | Determinism | discharged at solver + verifier + realizer + lane layers |
| 10 | Operation provenance via pack | ADR-0116 |

---

## Invariants

### `adr_0119_3_determinism`

Two `run_lane(cases)` calls produce byte-equal `LaneReport.canonical_bytes()`.

### `adr_0119_3_outcome_exhaustive`

Every case lands in exactly one of `correct` / `wrong` / `refused`.

### `adr_0119_3_zero_wrong_on_phase_5_2_corpus`

On the gsm8k_math/dev + public splits (200 cases authored under
ADR-0119.2), `wrong == 0`. Today: dev 50/50, public 150/150.

### `adr_0119_3_refusal_is_typed`

A graph that triggers ParseError or SolveError produces
`outcome == "refused"` with a reason that names the failing stage
(`parser: ...` or `solver: ...`).

### `adr_0119_3_correct_outcomes_carry_audit_trail`

Every `correct` case has a `trace_hash` (64-char hex) and a
`realized_prose` field set. The audit trail enables ADR-0119.4
(frontier-comparison report) and ADR-0119.6 (depth curve) to
inspect per-case reasoning without re-running the pipeline.

---

## Acceptance evidence

- `evals/gsm8k_math/runner.py` exports `run_lane`, `LaneReport`,
  `CaseOutcome`
- `tests/test_gsm8k_math_runner.py` (13 cases) green
- 200/200 correct, 0 wrong, 0 refused on the Phase 5.2 corpus
- Smoke suite green
- ADR linked from `docs/decisions/README.md` index + frontier

---

## Consequences

- Phase 5.3 of [ADR-0119](ADR-0119-gsm8k-eval-lane-roadmap.md) lands.
  Phases 5.4, 5.5, 5.6 can now run against this runner's outputs.
- The "every correct answer ships with replay-equal trace + readable
  prose" promise (ADR-0114a #3 + #10) is now mechanically inspectable
  per-case via `report.case_details[i]["trace_hash"]` and
  `["realized_prose"]`.
- The lane runner is the substrate the eventual ADR-0120 expert-tier
  gate consumes. ADR-0120 will require `overall_pass == True` on both
  the public split AND the sealed holdout.

---

## Out of scope

- Adversarial case authoring + scoring (ADR-0119.5)
- Depth-curve bucketing harness (ADR-0119.6)
- Frontier-baseline comparison report (ADR-0119.4)
- Lane registry shape + ADR-0120 numeric thresholds (ADR-0119.8 +
  ADR-0120)
- Parallel execution / case batching (current runner is single-threaded;
  small dev/public corpora don't need it. Future ADR may add a
  parallel mode that produces the same byte-equal report.)
