Capture harness, fixture verification, CI, and the public README
The site does no live inference. Rollouts are captured once against spark-1 and replayed at their recorded wall-clock — a public demo with no auth cannot hold an API key, and a recorded run can be scrubbed, permalinked, blind-compared and verified in ways a live one cannot. What stops it being a video is that the browser re-derives every number from the recorded moves. verify_fixtures.py is the Python half of that: it replays every committed fixture through the engine and reproduces its own rewards. All 16 land at delta 0.0. A fixture that cannot be regenerated is a claim with no receipt. First real measurement, thinking off, 8 seeds: solved 0/8. The model repeats guesses it has already played, invents words (trape, slith, postt, boomy), and contradicts its own feedback — consistency 0.09 to 0.17. That is the published failure taxonomy showing up in our own data on the first run, and it is why `consistency` is a reward component rather than a footnote. A capture failure is recorded as a turn with a null reply, never dropped. A capture that silently discarded failed turns would be reporting a better model than the one that ran. CI gates both halves and four things that fail silently in production: the word lists must rebuild byte-identically, the prerendered routes must carry their own baked og tags (crawlers do not run JS, so without them every shared link previews as the homepage), no blob: URL may reach the bundle (the site's CSP has no worker-src, so it falls back to default-src 'self' and a blob worker is blocked with no error), and the conformance digest must match across languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Replay every committed fixture and reproduce its recorded rewards.
|
||||
|
||||
A fixture is a claim: "this model, on this seed, scored this." The claim is
|
||||
only worth anything if it can be re-derived from the moves it records. This
|
||||
does that in Python; `verify.ts` does the same thing in the browser, live, in
|
||||
front of the visitor.
|
||||
|
||||
Exits non-zero if any fixture's rewards cannot be reproduced from its own turns.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent / "wordle_five"))
|
||||
|
||||
from wordle_five import solver as S # noqa: E402
|
||||
from wordle_five.engine import Game # noqa: E402
|
||||
from wordle_five.protocol import parse_guess # noqa: E402
|
||||
from wordle_five.reward import Episode, score # noqa: E402
|
||||
|
||||
TRACES = Path(__file__).parent.parent / "public" / "traces" / "wordle"
|
||||
TOLERANCE = 1e-9
|
||||
|
||||
|
||||
def replay(fixture: dict) -> dict[str, float]:
|
||||
game = Game(seed=fixture["seed"])
|
||||
if game.answer != fixture["answer"]:
|
||||
raise AssertionError(
|
||||
f"seed {fixture['seed']} gives {game.answer!r}, fixture claims {fixture['answer']!r}"
|
||||
)
|
||||
for turn in fixture["turns"]:
|
||||
guess = parse_guess(turn["reply"])
|
||||
if guess is None:
|
||||
game.rejected += 1
|
||||
continue
|
||||
game.play(guess)
|
||||
|
||||
episode = Episode(
|
||||
answer=game.answer,
|
||||
guesses=[g for g, _ in game.history],
|
||||
patterns=[p for _, p in game.history],
|
||||
rejected=game.rejected,
|
||||
reference_depth=S.reference_depth(game.answer),
|
||||
)
|
||||
return score(episode)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fixtures = sorted(TRACES.glob("*.json"))
|
||||
fixtures = [f for f in fixtures if f.name != "manifest.json"]
|
||||
if not fixtures:
|
||||
print("no fixtures found — nothing to verify")
|
||||
return 0
|
||||
|
||||
failures = 0
|
||||
for path in fixtures:
|
||||
data = json.loads(path.read_text())
|
||||
recomputed = replay(data)
|
||||
recorded = data["rewards"]
|
||||
deltas = {k: abs(recomputed[k] - recorded[k]) for k in recorded}
|
||||
worst = max(deltas.values())
|
||||
status = "ok" if worst <= TOLERANCE else "MISMATCH"
|
||||
if worst > TOLERANCE:
|
||||
failures += 1
|
||||
culprit = max(deltas, key=deltas.get)
|
||||
print(f"{path.name:>20} {status} worst delta {worst:.3g} on '{culprit}'")
|
||||
else:
|
||||
print(f"{path.name:>20} {status} delta {worst:.1e}")
|
||||
|
||||
print(f"\n{len(fixtures)} fixtures, {failures} mismatched")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user