← snapshot
10833 bytes
/**
* Records a narrated tour of Lovelace with Playwright.
*
* Each scene holds for as long as its narration clip runs (from
* tour/audio/manifest.json), so the finished video needs no manual sync —
* the mux in build.sh simply concatenates the clips in order.
*
* Everything here drives the real UI through real clicks. Nothing is faked
* and no state is injected: the decision Jackie proposes during the tour is
* genuinely created through the API by the running server.
*/
import { mkdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { chromium } from "@playwright/test";
import { loadManifest } from "./narrate.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const OUT = path.join(HERE, "out");
const BASE = process.env.TOUR_BASE ?? "http://localhost:3000";
const API = process.env.TOUR_API ?? "http://localhost:8787";
const VIEWPORT = { width: 1280, height: 800 };
/** Breathing room after the words stop, so a cut never lands on a syllable. */
const TAIL_MS = 550;
async function main() {
const manifest = await loadManifest();
const dwell = new Map(manifest.scenes.map((s) => [s.id, s.ms + TAIL_MS]));
const timeline = [];
await rm(OUT, { recursive: true, force: true });
await mkdir(OUT, { recursive: true });
const browser = await chromium.launch({
args: ["--force-prefers-reduced-motion=false"],
});
const context = await browser.newContext({
viewport: VIEWPORT,
deviceScaleFactor: 2,
recordVideo: { dir: OUT, size: VIEWPORT },
colorScheme: "dark",
});
const page = await context.newPage();
const t0 = Date.now();
/** Hold the current shot for exactly this scene's narration. */
const scene = async (id, fn) => {
const startMs = Date.now() - t0;
if (fn) await fn();
const wait = dwell.get(id) ?? 4000;
// Time already spent acting counts toward the scene's length.
const spent = Date.now() - t0 - startMs;
await page.waitForTimeout(Math.max(400, wait - spent));
timeline.push({ id, startMs, endMs: Date.now() - t0 });
console.log(` ${id.padEnd(12)} @${(startMs / 1000).toFixed(1)}s`);
};
// Click an offer/chip by its visible text, and wait for Jackie to settle.
const goPlace = async (needle) => {
const target = page
.locator(".jac-offer, .jac-seen-chip")
.filter({ hasText: needle })
.first();
await target.waitFor({ state: "visible", timeout: 15000 });
await target.click();
await page.waitForTimeout(700);
};
console.log("recording…");
// ---- 1-4: the front door -------------------------------------------
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForSelector(".jac-orb", { timeout: 20000 });
await scene("open");
await scene("stage");
// Put the richer repo on screen: it has a blocked gate to show.
await scene("alert", async () => {
const meridian = page.locator(".jac-seg button", { hasText: "Meridian" });
if (await meridian.count()) {
await meridian.first().click();
await page.waitForTimeout(900);
}
});
await scene("offers");
// ---- 5-8: wandering the floor --------------------------------------
await scene("gate", async () => {
await goPlace("blocked promotion");
});
await scene("file", async () => {
await goPlace("session.rs");
});
await scene("decision", async () => {
await goPlace("session cache");
});
await scene("cloth", async () => {
await goPlace("cloth so far");
});
// ---- 9-13: the interview -------------------------------------------
await scene("record", async () => {
// Record-why is offered against a concrete place, so stand on the file.
await goPlace("session.rs");
});
await scene("interview", async () => {
await page.locator(".jac-offer--record").first().click();
await page.waitForTimeout(1200);
// No microphone in a recorded browser; take the keyboard path.
const typeInstead = page.locator("button", { hasText: "Type instead" }).first();
if (await typeInstead.count()) await typeInstead.click();
});
const ANSWERS = [
"Multi-tenancy. A size-based evictor lets one busy tenant push another tenant's sessions out, which reads as a random logout.",
"The fixed TTL under sustained load. I have not proven the refusal path stays graceful when sessions arrive faster than they expire.",
"Eviction policy and admission control are one concern wearing two names. Unifying them would have touched the auth module.",
"Another week would have bought a real capacity test. The refusal threshold is currently a guess that happens to be round.",
];
await scene("answer", async () => {
for (const answer of ANSWERS) {
const box = page.locator(".jac-stage-type textarea");
if (!(await box.count())) break;
await box.fill("");
// Typed rather than pasted, so the recording shows it being written.
await box.type(answer, { delay: 8 });
await page.locator("button", { hasText: /^Answer$/ }).first().click();
await page.waitForTimeout(900);
const typeInstead = page.locator("button", { hasText: "Type instead" }).first();
if (await typeInstead.count()) await typeInstead.click();
await page.waitForTimeout(300);
}
await page.waitForTimeout(1200);
});
await scene("draft");
let decisionHref = null;
await scene("propose", async () => {
const propose = page.locator("button", { hasText: "Propose it as Jackie" }).first();
if (await propose.count()) {
await propose.click();
await page.waitForTimeout(1800);
}
const link = page.locator('a[href*="/decisions/"]').first();
if (await link.count()) decisionHref = await link.getAttribute("href");
});
// ---- 14-16: the thesis, and settling it ----------------------------
await scene("thesis", async () => {
if (decisionHref) {
await page.goto(`${BASE}${decisionHref}`, { waitUntil: "networkidle" });
}
await page.waitForTimeout(600);
});
await scene("attest", async () => {
const typeInstead = page.locator("button", { hasText: "Type it instead" }).first();
if (await typeInstead.count()) {
await typeInstead.click();
await page.waitForTimeout(400);
}
const box = page.locator(".jac-panel--human textarea").first();
if (await box.count()) {
await box.type(
"I traced the eviction path by hand. Entries expire on the clock and never on capacity, so one tenant's load cannot end another tenant's session. The refusal threshold is the part I would revisit first.",
{ delay: 6 },
);
await page.waitForTimeout(700);
const sign = page.locator("button", { hasText: "Sign in your own words" }).first();
if (await sign.count()) {
await sign.click();
await page.waitForTimeout(1800);
}
}
});
await scene("admitted", async () => {
await page.goto(
`${BASE}/repos/meridian-systems/promote?from=${encodeURIComponent("feature/session-cache")}&into=main`,
{ waitUntil: "networkidle" },
);
await page.waitForTimeout(1600);
});
// ---- 17-18: the detail view ----------------------------------------
await scene("diff", async () => {
await page.goto(
`${BASE}/repos/meridian-systems/diff?from=${encodeURIComponent("feature/session-cache")}&into=main`,
{ waitUntil: "networkidle" },
);
await page.waitForTimeout(900);
});
// A branch that touches nothing governed — the honest-zero case.
await scene("ungoverned", async () => {
await seedUngoverned();
await page.goto(
`${BASE}/repos/meridian-systems/promote?from=${encodeURIComponent("docs/plain")}&into=main`,
{ waitUntil: "networkidle" },
);
await page.waitForTimeout(1600);
});
// ---- 19-22: the rest of the surface --------------------------------
await scene("log", async () => {
await page.goto(`${BASE}/repos/meridian-systems/log`, { waitUntil: "networkidle" });
await page.waitForTimeout(800);
});
await scene("registry", async () => {
await page.goto(`${BASE}/registry`, { waitUntil: "networkidle" });
await page.waitForTimeout(700);
});
await scene("wizard", async () => {
await page.goto(`${BASE}/new`, { waitUntil: "networkidle" });
await page.waitForTimeout(700);
const name = page.locator("#wiz-name");
if (await name.count()) await name.type("Pemberton Mills", { delay: 26 });
await page.waitForTimeout(700);
const cont = page.locator("button", { hasText: /^Continue$/ }).first();
if (await cont.count()) {
await cont.click();
await page.waitForTimeout(900);
}
});
await scene("close", async () => {
await page.goto(BASE, { waitUntil: "networkidle" });
await page.waitForTimeout(800);
});
console.log("finishing video…");
const video = page.video();
await context.close();
await browser.close();
const raw = video ? await video.path() : null;
await writeFile(
path.join(OUT, "timeline.json"),
`${JSON.stringify({ viewport: VIEWPORT, video: raw ? path.basename(raw) : null, timeline }, null, 2)}\n`,
);
console.log(`video: ${raw}`);
}
/**
* Creates a branch whose blast radius nothing governs, so the tour can show
* the honest-zero verdict. Idempotent: a second run just fails the commit.
*/
async function seedUngoverned() {
const post = (p, body) =>
fetch(`${API}${p}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}).then((r) => r.json().catch(() => null));
const detail = await fetch(`${API}/api/repos/meridian-systems`).then((r) => r.json());
const human = detail.humans?.[0];
if (!human) return;
const main = detail.refs.find((r) => r.name === "main");
if (!main) return;
await post("/api/repos/meridian-systems/refs", {
name: "docs/plain",
from: { ref: "main" },
});
// Re-commit main's tree plus one ungoverned doc.
const head = await fetch(
`${API}/api/repos/meridian-systems/snapshots/${main.head}`,
).then((r) => r.json());
const files = await Promise.all(
(head.files ?? []).map(async (f) => {
const blob = await fetch(
`${API}/api/repos/meridian-systems/blobs/${f.blob}`,
).then((r) => r.json());
return { path: f.path, content: blob.content ?? "" };
}),
);
files.push({
path: "docs/readme.md",
content: "# notes\n\nNothing here is governed by any decision.\n",
});
await post("/api/repos/meridian-systems/commits", {
ref: "docs/plain",
actor: { kind: "human", id: human.id },
provenance: "human",
message: "docs: a note nothing governs",
files,
});
}
main().catch((e) => {
console.error(e);
process.exit(1);
});