#!/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")) sys.path.insert(0, str(Path(__file__).parent / "alert_triage")) from alert_triage import taskset as AT # noqa: E402 from alert_triage.generator import is_held_out # noqa: E402 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" TOLERANCE = 1e-9 def replay_wordle(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 replay_alert_triage(fixture: dict) -> dict[str, float | None]: """The world comes from the seed, the score from the reply strings. Nothing else in the fixture is trusted — the same rule the browser's verify uses.""" seed = fixture["seed"] if is_held_out(seed): raise AssertionError(f"seed {seed} is in the held-out bucket and must never be captured") played = AT.replay(seed, [turn["reply"] for turn in fixture["turns"]]) if played["outcome"] != fixture["outcome"]: raise AssertionError(f"replay gives outcome {played['outcome']!r}, fixture claims {fixture['outcome']!r}") return played["rewards"] REPLAY = {"wordle": replay_wordle, "alert-triage": replay_alert_triage} def _delta(recomputed, recorded) -> float: """None is "not scored". It matches only None; against a number it is a mismatch, not a zero.""" if recomputed is None or recorded is None: return 0.0 if recomputed is recorded else float("inf") return abs(recomputed - recorded) def main() -> int: fixtures = [ f for demo in sorted(p for p in TRACES.iterdir() if p.is_dir()) for f in sorted(demo.glob("*.json")) ] if not fixtures: print("no fixtures found — nothing to verify") return 0 failures = 0 for path in fixtures: demo = path.parent.name if demo not in REPLAY: failures += 1 print(f"{path.parent.name}/{path.name:<20} NO REPLAYER for demo '{demo}'") continue data = json.loads(path.read_text()) try: recomputed = REPLAY[demo](data) except AssertionError as exc: failures += 1 print(f"{path.parent.name}/{path.name:<20} MISMATCH {exc}") continue recorded = data["rewards"] deltas = {k: _delta(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.parent.name}/{path.name:<20} {status} worst delta {worst:.3g} on '{culprit}'") else: print(f"{path.parent.name}/{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())