Credibility pass: the session seams sanded from the clips-and-camera batch
Two blind reviews of everything since the last pass (afa0e17), every
finding checked against the code, no behavior changed: all thirty
scene goldens match without a re-bless and the engine suite is
untouched.
Reel and renderer: the camera's look-down rule is stated once, beside
LOOK_DOWN, instead of twice in the effect; the empty aim branch that
stood where a cutaway used to be is gone (the guard it implied is now
explicit); the pit events and the punch are handled by their own
types, not through "in" casts; smoothstep is one export used by every
tween instead of eleven inline copies; the two floor rings share one
painter; project() takes a Billboard instead of a third hand-typed
copy of its fields; the strides-left figure and the web rim no longer
shadow the reel's steps and the pane's fx; the die card's verdict is
built from events, not by matching an emoji; the workshop asks for the
hover cue by name instead of passing an empty click handler.
Server and engine: one requestBase() for the origin, one slug pattern
in store.ts gating both the clip page and its files, one 404 for both;
LOOPBACK sits above its only caller; doCounteract names what a counter
is played against once; fearCells sits beside its own docblock rather
than between sightedCellsFor and its.
Deploy: chromiumExe, the private server, ffmpeg, and the reel rewind
live in deploy/lib/harness.mjs, shared by the gate, the recorder, and
the card cutter instead of pasted three times; the recorder drops its
duplicate frame counters and names its poster settle; the card uses the
gallery's exact gold; the one-time Sentry URL bootstrap leaves
deploy.sh; the backup comment states the rule rather than the incident.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ddd2e347bd
commit
e1a740119c
+12
-45
@@ -11,11 +11,12 @@
|
||||
// Run from the repo root AFTER `npx vite build` in packages/web (the
|
||||
// wrapper deploy/verify-scenes.sh does both). Needs the Playwright
|
||||
// chromium cache (`npx playwright-core install chromium` if missing).
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { chromium } from "playwright-core";
|
||||
import { chromiumExe, rewindReel, startServer } from "./lib/harness.mjs";
|
||||
import { SCREENPLAYS } from "../packages/web/src/fpv/screenplays.ts";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@@ -31,20 +32,6 @@ const CHANNEL_TOL = 6;
|
||||
const FAIL_FRAC = 0.002;
|
||||
const EPOCH = new Date("2026-01-01T00:00:00Z").getTime();
|
||||
|
||||
function chromiumExe() {
|
||||
if (process.env.WIZWAR_CHROME) return process.env.WIZWAR_CHROME;
|
||||
const cache = join(process.env.HOME ?? "", "Library", "Caches", "ms-playwright");
|
||||
const builds = existsSync(cache)
|
||||
? readdirSync(cache).filter((d) => /^chromium-\d+$/.test(d)).sort()
|
||||
: [];
|
||||
for (const build of builds.reverse()) {
|
||||
const exe = join(cache, build, "chrome-mac-arm64",
|
||||
"Google Chrome for Testing.app", "Contents", "MacOS", "Google Chrome for Testing");
|
||||
if (existsSync(exe)) return exe;
|
||||
}
|
||||
throw new Error("no Playwright chromium found — run: npx playwright-core install chromium (or set WIZWAR_CHROME)");
|
||||
}
|
||||
|
||||
/** Decode a PNG to raw RGBA via ffmpeg — the harness's only image dep. */
|
||||
function rgba(png) {
|
||||
return execFileSync("ffmpeg", ["-loglevel", "error", "-i", png,
|
||||
@@ -64,26 +51,11 @@ function compare(goldenPng, actualPng) {
|
||||
}
|
||||
|
||||
// --- A private server on a throwaway data dir. ---------------------------
|
||||
const dataDir = mkdtempSync(join(tmpdir(), "wizwar-scenes-"));
|
||||
const server = spawn("npx", ["tsx", "src/index.ts"], {
|
||||
cwd: join(import.meta.dirname, "..", "packages", "server"),
|
||||
env: { ...process.env, PORT: String(PORT), WIZWAR_DATA_DIR: join(dataDir, "rooms") },
|
||||
stdio: "ignore",
|
||||
});
|
||||
const up = async () => {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try {
|
||||
const r = await fetch(`http://127.0.0.1:${PORT}/`);
|
||||
if (r.ok) return;
|
||||
} catch { /* not yet */ }
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error("server never came up");
|
||||
};
|
||||
const server = startServer(PORT, "wizwar-scenes");
|
||||
|
||||
let failures = 0;
|
||||
try {
|
||||
await up();
|
||||
await server.up();
|
||||
mkdirSync(GOLDENS, { recursive: true });
|
||||
rmSync(DIFFS, { recursive: true, force: true });
|
||||
mkdirSync(DIFFS, { recursive: true });
|
||||
@@ -95,10 +67,8 @@ try {
|
||||
if (!scenes.length) throw new Error(`no scenes match: ${only.join(", ")}`);
|
||||
|
||||
for (const sp of scenes) {
|
||||
// One page per view: the reel steps forward beat to beat under the
|
||||
// paused clock, so a scene's later beats cost only their marginal
|
||||
// steps — and every beat's simulated capture time is still exactly
|
||||
// (steps so far) × 2.6s, identical every run.
|
||||
// One page per view: the reel steps forward beat to beat, so a
|
||||
// scene's later beats cost only their marginal steps.
|
||||
const takes = [
|
||||
{ view: "fpv", beats: [...(sp.beats ?? [])].sort((a, b) => a - b) },
|
||||
{ view: "board", beats: sp.boardBeat === undefined ? [] : [sp.boardBeat] },
|
||||
@@ -109,13 +79,11 @@ try {
|
||||
const page = await ctx.newPage();
|
||||
await page.clock.install({ time: EPOCH });
|
||||
await page.goto(`http://127.0.0.1:${PORT}/?fpv&script=${sp.name}`, { waitUntil: "networkidle" });
|
||||
await page.locator("button", { hasText: "❚❚" }).click();
|
||||
for (let i = 0; i < 4; i++) await page.locator("button", { hasText: "◀" }).click();
|
||||
await rewindReel(page);
|
||||
if (view === "fpv") await page.getByText("your eyes").click();
|
||||
// Freeze the world at a fixed instant, then advance it by hand.
|
||||
// Steps on the way to a beat get 0.6 simulated seconds; the beat
|
||||
// itself gets 2.6 to settle its tween, hold, and fx. The schedule
|
||||
// is fixed, so every run renders the same bytes.
|
||||
// Freeze the world at a fixed instant, then advance it by hand:
|
||||
// steps on the way to a beat get 0.6 simulated seconds, the beat
|
||||
// itself 2.6 to settle its tween, hold, and fx.
|
||||
await page.clock.pauseAt(EPOCH + 60_000);
|
||||
let at = 0;
|
||||
for (const beat of beats) {
|
||||
@@ -154,8 +122,7 @@ try {
|
||||
}
|
||||
await browser.close();
|
||||
} finally {
|
||||
server.kill();
|
||||
rmSync(dataDir, { recursive: true, force: true });
|
||||
server.stop();
|
||||
}
|
||||
if (!update) console.log(`--- ${failures ? `${failures} beat(s) drifted` : "every pinned beat matches its golden"}`);
|
||||
process.exit(failures ? 1 : 0);
|
||||
|
||||
Reference in New Issue
Block a user