Files
wizwar6e/deploy/replay-verify.mjs
Eric WagonerandClaude Fable 5 1cf5d0a68d The determinism harness moves into deploy/ for real
deploy/verify-ledgers.sh fetches every production ledger and strictly
replays it against the local engine (any refusal or crash fails; a
lobby that never dealt is trivially clean). Run from the repo root
before deploying engine changes — the check both of tonight's replay
breaks would have caught.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
2026-08-19 22:06:56 -04:00

22 lines
1.1 KiB
JavaScript

// Strict full-ledger replay: ANY refused command is a hard failure.
import { readFileSync } from "fs";
import { createGame, applyCommand } from "../packages/engine/src/game.ts";
const lines = readFileSync(process.argv[2], "utf8").trim().split("\n").map((l) => JSON.parse(l));
const meta = lines.find((l) => l.kind === "meta");
const start = lines.find((l) => l.kind === "start");
if (!start) {
console.log(`OK ${process.argv[2].split("/").pop()}: lobby only, nothing to replay`);
process.exit(0);
}
const joins = lines.filter((l) => l.kind === "join");
const playerIds = [...new Set([meta.hostId, ...joins.map((j) => j.name)])];
let { state } = createGame({ playerIds, seed: meta.seed, sets: start.expansion ? ["basic", "expansion1"] : ["basic"], colors: start.colors, deckRev: start.deckRev });
let n = 0;
for (const l of lines) {
if (l.kind !== "command") continue;
const r = applyCommand(state, l.playerId, l.command);
if (!r.ok) { console.log(`FAIL ${process.argv[2].split("/").pop()} seq ${l.seq}: ${r.error}`); process.exit(1); }
state = r.state; n++;
}
console.log(`OK ${process.argv[2].split("/").pop()}: ${n} commands replay clean`);