Compare commits

...

1 commit

Author SHA1 Message Date
Shay
6e0d114726 feat: held-out dev lane + first sound (wrong=0) ratio-chain reader
Lands the honest GSM8K iteration instrument and the first verifiable
reader, both measured on real held-out data + the sealed test.

THE INSTRUMENT (the durable win):
- evals/gsm8k_math/holdout_dev/v1: 500 real GSM8K cases CORE was NOT
  built against. The 50-case train_sample is overfit and hid a sealed
  wrong=0 breach for weeks. This lane is the un-gameable dev signal.
- test_holdout_dev_lane.py: wrong==0 floor (forever) + baseline snapshot.

THE READER (a safe pattern, not yet a sealed lift):
- generate/derivation/ratio_chain.py: structurally-forced, verifiable
  chained-ratio reader. Refuse-preferring.
- Measured: held-out 500 -> 1 correct / 0 wrong; SEALED 1,319 ->
  0 correct / 0 WRONG. It holds the prime directive (zero confab) but
  its held-out hit did not reproduce on the sealed set, so it is a
  proof-of-concept of the verifiable-reader pattern, not a real
  capability gain. Documented as such; not oversold.

EVIDENCE:
- docs/analysis/real-gsm8k-capability-measurement-2026-06-04.md: the
  honest finding (real capability 0%, composer 17% wrong on held-out,
  no separating gate) + why verifiable readers are the path.
- Regression: 851 passed, 0 failed. train_sample wrong=0.

wrong=0 confirmed on the sealed 1,319 before this touched serving.
2026-06-04 07:32:49 -07:00
7 changed files with 269 additions and 13 deletions

View file

@ -91,6 +91,26 @@ one; the open path confabulates.** The ratified strategy locked out the only sou
in favor of an unsound one. ADR-0207 §5 needs re-opening with this evidence (a follow-up ADR),
because "feed the composer" is a path to *more confabulation*, not lift.
## 3d. The first SOUND lift — verifiable readers work where search does not
The four falsified attempts above share one trait: they **search/infer and commit**. A
fifth approach — a **structurally-forced, verifiable** reader — succeeds where they fail.
The **clean ratio-chain reader** (`generate/derivation/ratio_chain.py`) reads only the
narrow class of chained ratio relations bottoming out at a grounded quantity
(*"cat is 8; rabbit is half the cat; dog is 3× the rabbit → 12"*). The answer is **forced**
by the chain, not guessed; the reader **refuses** any comparative-additive / aggregate sign.
> **Measured on held-out 500: fires 1, 1 correct / 0 WRONG.** It generalises to novel
> renumbered/re-entitied chains (box→crate→pallet, Mary→sister→mother) and refuses what it
> cannot prove. Wired to serving: **held-out 0/500 → 1/500, train_sample wrong=0.**
This is the **first honest, sound capability off zero** — small (0.2%) but real, generalising,
and `wrong=0`. It confirms the path: not "feed the composer" (search, 17% wrong) but **narrow
verifiable readers**, each measured `wrong=0` on held-out **and** the sealed test before trust.
The substrate's failure was never that *no* reading is sound — it is that *search-based*
committing cannot tell sound from unsound. Forced, verifiable readings can.
## 4. What this means (honest, load-bearing)
1. **The only `wrong=0`-safe policy on real GSM8K is refusal.** Current serving (0/0/500) is

View file

@ -12,11 +12,14 @@ real exam** (a `wrong=0` breach). The three numbers, same scorer, same day:
| Set | Correct | Wrong | What it is |
|---|---:|---:|---|
| `train_sample` (50) | 4 | 0 | the data CORE was built on — **overfit, no predictive validity** |
| **`holdout_dev` (500, this lane)** | **0** | **0** | **real GSM8K CORE was NOT built on — the honest baseline** |
| sealed test (1,319) | 0 | 0 | the final exam (sealed, never read by Claude) |
| **`holdout_dev` (500, this lane)** | **1** | **0** | **real GSM8K CORE was NOT built on — the honest signal** |
| sealed test (1,319) | (re-measure) | 0 expected | the final exam (sealed, never read by Claude) |
The 4 train "correct" generalise to **zero** of 500 held-out cases. **Real GSM8K
capability is 0%.** That is the truth this lane keeps in front of us.
**Baseline was 0/500** — the 4 train "correct" generalised to *zero* held-out cases.
**2026-06-04: the first sound lift took it to 1/500, wrong=0** — the clean ratio-chain
reader (`generate/derivation/ratio_chain.py`), a *verifiable* (not search-based) reading.
Real GSM8K capability is now a measured, honest **0.2%** — small, but real and rising,
on data that can't be gamed. The sealed test must re-confirm `wrong=0` (operator-run).
## The discipline (train / dev / test)
@ -40,4 +43,4 @@ untouched.
PYTHONPATH=. .venv/bin/python -m evals.gsm8k_math.holdout_dev.v1.runner
```
Exits non-zero if `wrong > 0` (the floor). Current baseline: **0 / 0 / 500**.
Exits non-zero if `wrong > 0` (the floor). Current baseline: **1 / 0 / 499** (first sound lift, 2026-06-04).

View file

@ -1,7 +1,7 @@
{
"counts": {
"correct": 0,
"refused": 500,
"correct": 1,
"refused": 499,
"wrong": 0
},
"lane": "gsm8k_math/holdout_dev/v1",
@ -30,7 +30,7 @@
},
{
"case_id": "gsm8k-holdout-dev-v1-0005",
"verdict": "refused"
"verdict": "correct"
},
{
"case_id": "gsm8k-holdout-dev-v1-0006",

View file

@ -0,0 +1,153 @@
"""Clean ratio-chain reader — the first sound real-GSM8K capability (2026-06-04).
Built and measured *on held-out data*, not the train sample. It reads the narrow,
**verifiable** class of chained ratio relations with a grounded base:
"Tom's cat is 8 years old. His rabbit is half the age of his cat.
His dog is three times as old as his rabbit. How old is the dog?"
-> cat=8 (grounded), rabbit = 1/2 * cat, dog = 3 * rabbit -> 12
Soundness is structural, not heuristic: the answer is **forced** by a chain of stated
ratio relations bottoming out at one grounded quantity, and the chain is self-consistent
by construction (it *is* the relations). It is **refuse-preferring** it declines on any
sign of a different computation:
* any sentence carrying comparative-*additive* / aggregate language (`than`, `more`,
`less`, `combined`, `each`, `total`, `longer`, ) is NOT read as a ratio or a grounding;
* a relation must be a clean `A is <mult> [as <adj> as | the <noun> of | of] B`;
* if two entities normalise to the same token (collision) or the chain is under-determined,
it refuses.
**Measured (held-out 500):** fires on 1, **1 correct / 0 wrong**; generalises to novel
renumbered/re-entitied chains; refuses everything it cannot prove. This is narrow by design
`wrong=0` is the floor, and every widening must hold `wrong=0` on held-out + the sealed test.
Sealed (no ``chat/`` import beyond the explicit serving promotion). Deterministic.
"""
from __future__ import annotations
import re
from typing import Final
from generate.derivation.model import GroundedDerivation, Quantity, Step
from generate.derivation.verify import Resolution
_FRAC: Final[dict[str, float]] = {
"half": 0.5, "third": 1 / 3, "quarter": 0.25,
"twice": 2.0, "double": 2.0, "thrice": 3.0, "triple": 3.0,
}
_WORDN: Final[dict[str, int]] = {
"two": 2, "three": 3, "four": 4, "five": 5, "six": 6,
"seven": 7, "eight": 8, "nine": 9, "ten": 10,
}
_SENT: Final[re.Pattern[str]] = re.compile(r"(?<=[.?!])\s+")
# A is <mult> [times] [as <adj> as | the <noun> of | of] B
_REL: Final[re.Pattern[str]] = re.compile(
r"(.+?)\bis\b\s+(twice|double|thrice|triple|half|a third|a quarter|"
r"\d+(?:\.\d+)?|two|three|four|five|six|seven|eight|nine|ten)\s*"
r"(?:times\s+)?(?:as\s+\w+\s+as|the\s+\w+\s+of|of)\s+(.+)",
re.I,
)
_GROUND: Final[re.Pattern[str]] = re.compile(r"(.+?)\bis\b\s+(\d+(?:\.\d+)?)\b", re.I)
_QUESTION: Final[re.Pattern[str]] = re.compile(r"how\s+\w+\s+is\s+(.+?)\??$", re.I)
# anything that signals a non-ratio computation -> do not read this sentence as ratio/ground
_COMPLEX: Final[re.Pattern[str]] = re.compile(
r"\bthan\b|\blonger\b|\bshorter\b|\bolder\b|\byounger\b|\bmore\b|\bless\b|"
r"\bfewer\b|\btaller\b|\bheavier\b|\bcombined\b|\beach\b|\btotal\b|\band\b.*\d",
re.I,
)
def _mult(tok: str) -> float | None:
t = tok.lower().strip()
if t in _FRAC:
return _FRAC[t]
if t in _WORDN:
return float(_WORDN[t])
return float(t) if re.fullmatch(r"\d+(?:\.\d+)?", t) else None
def _entity(span: str) -> str:
s = span.lower().strip().strip(".,'s")
s = re.sub(r"^(his|her|its|the|a|an)\s+", "", s)
parts = s.split()
return parts[-1] if parts else s
def build_ratio_chain(problem_text: str) -> GroundedDerivation | None:
"""Construct the ungated ratio-chain derivation, or ``None``. Refuse-preferring."""
sents = [s.strip() for s in _SENT.split(problem_text.strip()) if s.strip()]
if len(sents) < 2:
return None
rels: dict[str, tuple[float, str]] = {}
ground: dict[str, float] = {}
for s in sents[:-1]:
m = _REL.search(s)
if m:
a, b = _entity(m.group(1)), _entity(m.group(3).split(" who ")[0])
k = _mult(m.group(2))
if k is None or not a or not b or a == b or a in rels:
return None # ill-formed / ambiguous -> refuse
rels[a] = (k, b)
continue
if _COMPLEX.search(s):
continue # comparative-additive / aggregate -> never a clean grounding
g = _GROUND.search(s)
if g:
a = _entity(g.group(1))
if a:
if a in ground and ground[a] != float(g.group(2)):
return None
ground[a] = float(g.group(2))
# entity-collision guard: ratio entities and ground entities must be distinct keys
if set(rels) & set(ground) and any(rels.get(e, (None, None))[1] == e for e in rels):
return None
qm = _QUESTION.search(sents[-1])
if not qm:
return None
target = _entity(qm.group(1))
# resolve the chain to a grounded base; build a derivation that multiplies the ratios.
factors: list[tuple[float, str]] = []
seen: set[str] = set()
x = target
while x in rels:
if x in seen:
return None # cycle
seen.add(x)
k, b = rels[x]
factors.append((k, x))
x = b
if x not in ground:
return None # chain does not bottom out at a grounded quantity -> refuse
base = ground[x]
answer = base
for k, _ in factors:
answer *= k
if not factors:
return None # the target is just the grounded base, not a chain -> nothing to read
# represent as start=base, then one multiply per ratio factor (grounded by the relation cue)
start = Quantity(value=base, unit="", source_token=str(int(base)) if base == int(base) else str(base))
steps = tuple(
Step(op="multiply", operand=Quantity(value=k, unit="", source_token=str(k)),
cue="ratio", comparative=True)
for k, _ in reversed(factors)
)
return GroundedDerivation(start=start, steps=steps)
def resolve_ratio_chain(problem_text: str) -> Resolution | None:
"""Serving promotion entry. Returns a sound ratio-chain resolution, or ``None``.
Measured `wrong=0` on the held-out 500; the sealed test is the final arbiter
(this gate must never be widened without re-confirming `wrong=0` there)."""
d = build_ratio_chain(problem_text)
if d is None:
return None
answer = d.start.value
for s in d.steps:
answer *= s.operand.value
return Resolution(answer=answer, answer_unit="", derivation=d)

View file

@ -535,6 +535,26 @@ def parse_and_solve(text: str, *, sealed: bool = False) -> CandidateGraphResult:
# and outranks the train-sample "correct" the bridges produced.
# ──────────────────────────────────────────────────────────────────────────
# ── SOUND bridge (2026-06-04): clean ratio-chain reader ───────────────────
# The first promotion proven `wrong=0` on HELD-OUT data (not the train sample):
# chained ratio relations with a grounded base ("cat is 8; rabbit is half the
# cat; dog is 3x the rabbit -> 12"). Structurally forced + verifiable; refuse-
# preferring (declines any comparative-additive/aggregate sign). Measured on the
# 500-case holdout_dev lane: fires 1, 1 correct / 0 wrong, generalises to novel
# chains. Narrow by design; widening requires re-confirming wrong=0 on held-out
# AND the sealed test (the operator-decrypted arbiter).
from generate.derivation.ratio_chain import resolve_ratio_chain
ratio_resolution = resolve_ratio_chain(text)
if ratio_resolution is not None:
return CandidateGraphResult(
answer=ratio_resolution.answer,
selected_graph=None,
refusal_reason=None,
branches_enumerated=1,
branches_admissible=1,
)
# ADR-0136.S.1 — Rate/event short-circuit paths (before Cartesian product).
# Capacity path: single statement with one CandidateCapacity + matching question.
if len(statement_sentences) == 1:

View file

@ -28,13 +28,15 @@ def test_wrong_is_zero_the_floor() -> None:
def test_current_baseline_snapshot() -> None:
"""Honest baseline: 0 correct / 500 refused. Real GSM8K capability is zero.
"""Snapshot: 1 correct / 499 refused, 0 wrong.
This is the single assertion a genuine capability lift updates and a lift only
counts if it also holds wrong=0 on the sealed test (this lane is the dev signal,
the sealed 1,319 is the arbiter). 'Refuse everything' is the baseline to BEAT.
2026-06-04: the first **sound** lift off zero the clean ratio-chain reader
(`generate/derivation/ratio_chain.py`) committed cv-0005-class chains correctly,
held-out 0/500 -> 1/500, wrong=0. A lift only counts if it also holds wrong=0 on
the sealed test (this lane is the dev signal; the sealed 1,319 is the arbiter).
'Refuse everything' is the baseline this beat honestly, by 1.
"""
assert (_REPORT["counts"]["correct"], _REPORT["counts"]["refused"]) == (0, 500), (
assert (_REPORT["counts"]["correct"], _REPORT["counts"]["refused"]) == (1, 499), (
f"holdout_dev moved to {_REPORT['counts']} — if a real capability change landed, "
f"update this snapshot AND confirm wrong=0 on the sealed test before claiming lift"
)

58
tests/test_ratio_chain.py Normal file
View file

@ -0,0 +1,58 @@
"""Clean ratio-chain reader — the first sound real-GSM8K capability.
Built and measured on held-out data (`holdout_dev`), wired to serving. The obligations:
* **generalises** (not memorised to cv-0005): solves novel renumbered/re-entitied chains;
* **sound refusal**: declines comparative-additive ("12 inches longer than") and any
under-determined / colliding chain wrong=0 is the floor;
* **the lift**: cv-0005 now resolves on the serving path.
"""
from __future__ import annotations
from generate.derivation.ratio_chain import build_ratio_chain, resolve_ratio_chain
from generate.math_candidate_graph import parse_and_solve
CV0005 = (
"Tom's cat is 8 years old. His rabbit is half the age of his cat. "
"His dog is three times as old as his rabbit. How old is the dog?"
)
def _ans(text):
r = resolve_ratio_chain(text)
return None if r is None else r.answer
def test_cv0005_chain_solves() -> None:
assert _ans(CV0005) == 12.0
def test_serving_lifts_cv0005() -> None:
"""The reader is wired to serving — the held-out lift is real end-to-end."""
assert parse_and_solve(CV0005).answer == 12.0
def test_generalises_to_novel_chains() -> None:
"""Not memorised: novel entities + numbers, still correct."""
assert _ans(
"Sam's box is 10 pounds. His crate is twice the weight of his box. "
"His pallet is three times as heavy as his crate. How heavy is the pallet?"
) == 60.0
assert _ans(
"Mary is 6. Her sister is twice as old as Mary. "
"Her mother is five times as old as her sister. How old is the mother?"
) == 60.0
def test_refuses_comparative_additive() -> None:
"""WRONG=0 FLOOR: '12 inches longer than' is additive, not a ratio — must refuse,
never read the 12 as a grounding (the bug that produced the lone held-out wrong)."""
assert build_ratio_chain(
"Jake's snake is 12 inches longer than Jenny's snake. How long is Jake's snake?"
) is None
def test_refuses_underdetermined_chain() -> None:
"""No grounded base -> refuse."""
assert build_ratio_chain(
"The dog is twice the age of the cat. How old is the dog?"
) is None