// The fixture's SITES_DIR (`/sites`), which the e2e server reads instead // of the real transcripts/sites (playwright.config.ts). The suite WRITES notes // there -- a report's notes.json is the one corpus file umtool writes -- so it // must never be the real one. // // Called by make-fixture.mjs after the projects and the channels exist, so a // report here can cite the fixture's channels and link its projects. // // sites/priv private, cited-only. Reports: // polemic-alpha PUBLISHED: summary, two sections, cites sitechan/sv1 // (a clip window on disk: plays) and sitechan/sv2 (cues, // no media: the fetch_clip line); video.mp4 + poster.jpg // polemic-beta a DRAFT (not in site.json's reports) // sites/pub public. Reports: // gamma PUBLISHED, one section, no citations // /sitews/ the WORKSPACE (a direct child of REPORTS_ROOT, which the // e2e server takes as ): polemics/drafts/{alpha,beta}.json, // polemics/make-site.py naming `priv`, polemics/out/alpha.{md,html}, // NOTES.md // /sitews/polemic-alpha/ // the article's report-video project: a manifest whose slug // is polemic-alpha, two takes, one verdict // channels/sitechan/data/{sv1,sv2} the cited records import { spawnSync } from "node:child_process"; import { mkdirSync, writeFileSync } from "node:fs"; import path from "node:path"; const put = (file, value) => { mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, typeof value === "string" ? value : JSON.stringify(value, null, 2) + "\n"); }; const ff = (args) => { const r = spawnSync("ffmpeg", ["-nostdin", "-v", "error", "-y", ...args], { encoding: "utf8" }); if (r.status !== 0) throw new Error(`ffmpeg failed: ${r.stderr || r.status}`); }; const clip = (out, seconds, hz) => { mkdirSync(path.dirname(out), { recursive: true }); ff([ "-f", "lavfi", "-i", `testsrc=size=320x180:rate=15:duration=${seconds}`, "-f", "lavfi", "-i", `sine=frequency=${hz}:duration=${seconds}`, "-c:v", "libx264", "-pix_fmt", "yuv420p", "-c:a", "aac", "-shortest", "-movflags", "+faststart", out, ]); }; export const SITE_CUES = { sv1: [ [0, 3, "Opening line of the first source."], [3, 6, "The vote was rigged and everybody knew."], [6, 9, "Nobody checked it at the time."], [9, 12, "Later the story changed again."], [12, 15, "A fifth line for context."], [15, 18, "And a sixth to close."], ], sv2: [ [0, 4, "The second source starts here."], [4, 8, "She said it on a different show."], [8, 12, "Then she said the opposite."], ], }; const ALPHA_BODY_1 = "She said [the vote was rigged](cite:c1) in the first stream. Nobody checked the claim at the time, and the clip shows it.\n\nThe second paragraph repeats the claim for the record."; const BETA_BODY = "The draft's only paragraph opens with a sentence that keeps going, clause after clause, long past the point where any slide could hold it, because nobody has trimmed it yet and the draft is still a draft. " + "Its second sentence is just as long, and it too wanders on through the reasons, the caveats and the asides that belong in the article rather than on a slide. A third follows."; const ALPHA_BODY_2 ="Two years later [she said the opposite](cite:c2), on a different show."; const siteJson = (siteId, title, extra) => ({ siteId, siteTitle: title, groups: [{ id: "default", name: "All channels", selectedByDefault: true, order: 0, inline: true }], defaultGroupId: "default", channels: [{ slug: "sitechan", order: 0 }], ...extra, }); /** * @param {{ dest: string, reports: string, channels: string }} at */ export function makeSitesFixture({ dest, channels }) { const sites = path.join(dest, "sites"); mkdirSync(sites, { recursive: true }); // ---- the cited records ---------------------------------------------------- for (const [vid, rows] of Object.entries(SITE_CUES)) { put(path.join(channels, "sitechan", "data", vid, "transcript.cues.json"), { title: `Site source ${vid}`, uploadDate: "20240315", channel: "Site Channel", webpageUrl: `https://www.youtube.com/watch?v=${vid}`, duration: rows[rows.length - 1][1], cues: rows.map(([start, end, text]) => ({ start, end, text })), }); } // sv1 has a clip window the editor fetched: [0, 12] holds the cited [3, 6]. clip(path.join(channels, "sitechan", "data", "sv1", "clips", "0.00-12.00.mp4"), 12, 523); // ---- the sites ------------------------------------------------------------ put(path.join(sites, "priv", "site.json"), siteJson("priv", "Private Fixture", { audience: "private", search: false, reports: ["polemic-alpha"] })); put(path.join(sites, "pub", "site.json"), siteJson("pub", "Public Fixture", { reports: ["gamma"] })); const alphaDir = path.join(sites, "priv", "reports", "polemic-alpha"); put(path.join(alphaDir, "report.json"), { format: "archilyzer-report", version: 1, id: "polemic-alpha", kind: "sweep", series: "Polemics", title: "Alpha: the rigged vote", subtitle: "What she said, and when", summary: "She said one thing in 2020 and the opposite later.", published: "2026-10-01", updated: "2026-10-07", video: { src: "video.mp4", poster: "poster.jpg" }, citations: { c1: { kind: "video", channel: "sitechan", id: "sv1", start: 3, end: 6, quote: "The vote was rigged and everybody knew.", speaker: "Site Channel", date: "2024-03-15" }, c2: { kind: "video", channel: "sitechan", id: "sv2", start: 8, end: 12, quote: "Then she said the opposite.", speaker: "Site Channel", date: "2024-03-15" }, }, sections: [ { id: "first", title: "The first claim", body: ALPHA_BODY_1 }, { id: "later", title: "Later", body: ALPHA_BODY_2 }, ], }); clip(path.join(alphaDir, "video.mp4"), 4, 440); ff(["-f", "lavfi", "-i", "testsrc=size=320x180:rate=1:duration=1", "-frames:v", "1", path.join(alphaDir, "poster.jpg")]); put(path.join(sites, "priv", "reports", "polemic-beta", "report.json"), { format: "archilyzer-report", version: 1, id: "polemic-beta", kind: "sweep", title: "Beta: a draft", summary: "A draft nobody has published.", // Its one section has no slide points, and its first two sentences run // long for a slide: the source tab's slide lint says so. sections: [{ id: "only", title: "Only section", body: BETA_BODY }], }); put(path.join(sites, "pub", "reports", "gamma", "report.json"), { format: "archilyzer-report", version: 1, id: "gamma", kind: "sweep", title: "Gamma on the public site", published: "2026-09-01", sections: [{ id: "g", title: "Gamma", body: "A public article with nothing cited." }], }); // ---- the workspace they were written in ------------------------------------ const ws = path.join(dest, "sitews"); put(path.join(ws, "polemics", "drafts", "alpha.json"), { id: "polemic-alpha", title: "Alpha: the rigged vote", sections: [] }); put(path.join(ws, "polemics", "drafts", "beta.json"), { id: "polemic-beta", title: "Beta: a draft", sections: [] }); put(path.join(ws, "polemics", "make-site.py"), 'SITE = "priv"\nfor f in DRAFTS.glob("drafts/*.json"):\n rid = f"polemic-{f.stem}"\n'); put(path.join(ws, "polemics", "out", "alpha.md"), "# Alpha\n\nThe rendered draft of **alpha**.\n"); put(path.join(ws, "polemics", "out", "alpha.html"), "

Alpha html

\n"); put(path.join(ws, "NOTES.md"), "# Workspace notes\n\n- one\n- two\n"); // ---- the article's video project ------------------------------------------- const proj = path.join(ws, "polemic-alpha"); put(path.join(proj, "video.manifest.json"), { schemaVersion: 1, slug: "polemic-alpha", title: "Alpha: the rigged vote", generatedBy: "polemics/video/make-videos.py", provenance: { channelSlug: "sitechan", siteOrigin: "https://priv.example" }, timeline: [{ type: "clip", id: "a1", video: "sv1", channel: "sitechan", start: 3, end: 6, quote: "The vote was rigged" }], }); for (const [id, order] of [["deck", 1], ["tight", 2]]) { put(path.join(proj, "takes", id, "take.json"), { id, group: "cut", order, label: id, kind: order === 1 ? "reference" : "similar", preview: "preview.mp4" }); } put(path.join(proj, "takes", "verdicts.json"), { deck: { verdict: "like", note: "", at: "2026-10-07T00:00:00Z" } }); return { sites, workspace: ws, project: proj }; }