#!/usr/bin/env bun /** * CI replay-determinism verifier. * * Drives a handful of canonical event sequences through a live Session * (capturing to an EventLog) and re-plays each log on a fresh Session, * asserting that the state hashes match byte-for-byte. See * packages/rete/SPEC.md §Replay Determinism and P3.3 in docs/PHASES.md. * * Intentionally imports engine internals via relative paths so the * script is runnable without first building the package — Bun executes * TypeScript sources directly. * * Usage: * bun run scripts/replay-determinism.ts * * Exit status: * 0 — all sequences produced matching hashes * 1 — at least one mismatch (replay is non-deterministic) */ import { Session } from "../packages/rete/src/session.js"; import { EventLog } from "../packages/rete/src/eventlog.js"; import { stateHash, replayFromLog } from "../packages/rete/src/replay.js"; import type { EntityId } from "../packages/rete/src/schema.js"; type Op = | { op: "insert"; id: number; attr: string; value: unknown } | { op: "retract"; id: number; attr: string }; interface TestCase { name: string; operations: Op[]; } const TEST_CASES: TestCase[] = [ { name: "basic-inserts", operations: [ { op: "insert", id: 1, attr: "X", value: 42 }, { op: "insert", id: 2, attr: "Y", value: "hello" }, { op: "insert", id: 3, attr: "Z", value: true }, ], }, { name: "with-retracts", operations: [ { op: "insert", id: 1, attr: "X", value: 100 }, { op: "insert", id: 2, attr: "Y", value: 200 }, { op: "retract", id: 1, attr: "X" }, { op: "insert", id: 1, attr: "X", value: 300 }, ], }, { name: "nested-values", operations: [ { op: "insert", id: 1, attr: "pos", value: { x: 1, y: 2 } }, { op: "insert", id: 2, attr: "tags", value: ["a", "b", "c"] }, ], }, { name: "large-set", operations: Array.from({ length: 100 }, (_, i) => ({ op: "insert" as const, id: i + 1, attr: "N", value: i * 7, })), }, ]; function runCase(tc: TestCase): boolean { const log = new EventLog(); const original = new Session({ autoFire: false, eventLog: log }); for (const op of tc.operations) { const id = op.id as unknown as EntityId; if (op.op === "insert") { original.insert(id, op.attr, op.value); } else { original.retract(id, op.attr); } } const originalHash = stateHash(original); const replayed = replayFromLog(log.getAll()); const replayedHash = stateHash(replayed); const match = originalHash === replayedHash; const status = match ? "MATCH " : "MISMATCH"; console.log( `${status} [${tc.name}] original=${originalHash} replayed=${replayedHash} events=${log.length}`, ); return match; } let allMatch = true; for (const tc of TEST_CASES) { if (!runCase(tc)) allMatch = false; } if (allMatch) { console.log("\nAll replay hashes match."); process.exit(0); } else { console.error("\nOne or more replay hashes diverged — replay is non-deterministic."); process.exit(1); }