Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 1a344483b455afc222a539a8cae736e0ca26cd0f
parent c726c16e75c8b2981b196bb2e7bdadb6031cd224
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 05:28:49 -0400

export: the cited report-site suite — the report index as home, the report (verdicts, tally, still, numbered cites with hover/focus previews, Escape, reference anchors, downloads), the three moment kinds with media that 200s, the reports-only shell pages; every page held to no corpus request and no missing file; the audit against the real out/

Served by e2e-report/serve.mjs: serve lists a span moment's directory (its
name has decimals) instead of serving its page.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
MPUBLISH.md | 10++++++++++
MWORKTREES.md | 4++--
Aexport/e2e-report/audit.spec.ts | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e-report/helpers.ts | 47+++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e-report/report-site.spec.ts | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e-report/serve.mjs | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/e2e-report/stage.ts | 2+-
Mexport/playwright.report.config.ts | 10++++++----
8 files changed, 464 insertions(+), 7 deletions(-)

diff --git a/PUBLISH.md b/PUBLISH.md @@ -100,6 +100,16 @@ shell's own pages and files, or a file over 25 MiB, or more than 20,000 files, f the build, and every deploy path refuses it. A site switched to cited is refused at deploy until it is built again. +`pnpm --filter export run e2e:report` builds a cited fixture site for real (one +fact-check citing every kind, its moments and their media) in a stage project at the +repo root, `.e2e-report-site/` — never in `export/`, whose `public/` and `out/` are +the checkout's — audits its `out/` as above and reads it in a browser. +`pnpm --filter export run build:report-fixture` builds and audits it without the +browser. The suite serves the build with `export/e2e-report/serve.mjs`, not `serve`: +`serve` (export's `start`, the container's `site` service) takes a video or audio +moment's directory name, `<start>-<end>` in seconds with decimals, for a file with an +extension, and lists the directory instead of serving its page. + Deploy-only ships whatever is in `export/out`, which the basic build composes one site at a time into a single shared directory — so it **refuses, before starting a job, if `export/out` holds a build of another site** (or no build at all), naming the diff --git a/WORKTREES.md b/WORKTREES.md @@ -78,7 +78,7 @@ queue-lock: still waiting (5m00s) **A long wait here is normal, not a hang** — the serial suite is ~24 minutes. Queued: `pnpm e2e`, `pnpm e2e:sharded` (including its `docker build`), export's `e2e`, -`e2e:hub`, `e2e:2origin`, and homepage's and umtool's `e2e`. Wrapping is at the *package* +`e2e:hub`, `e2e:2origin`, `e2e:report`, and homepage's and umtool's `e2e`. Wrapping is at the *package* level, so `pnpm --filter editor run e2e` is covered too, and a nested invocation passes through instead of deadlocking. Not queued on purpose: a raw `pnpm --filter editor exec playwright test`, the escape hatch for debugging a single spec, and the `e2e:ui` @@ -94,7 +94,7 @@ meant. Every other package's suite goes through its own filter: | Suite | Command | |---|---| | editor | `pnpm e2e [spec…]` | -| export | `pnpm --filter export run e2e` (also `e2e:hub`, `e2e:2origin`) | +| export | `pnpm --filter export run e2e` (also `e2e:hub`, `e2e:2origin`, `e2e:report`) | | homepage | `pnpm --filter homepage run e2e` | | umtool | `SONG_DIR=~/reports/quartering-uh-song/data pnpm --filter umtool run e2e [spec…]` | diff --git a/export/e2e-report/audit.spec.ts b/export/e2e-report/audit.spec.ts @@ -0,0 +1,69 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; +import { citedBuildProblem } from "yt-dlp-transcript-common/lib/builtExport"; +import { CORPUS_ROOT_ENTRIES } from "./helpers"; +import { REPORT_SITE_OUT, reportSiteAuditProblem } from "./stage"; + +// The cited out/ allowlist audit (common/lib/builtExport.ts), held to a REAL +// `next build` of a cited site — the one this suite serves. The audit's list +// was written from what compose puts in public/ and what Next was expected to +// emit; this is where the two meet: if Next emits a file the list does not +// name, the build fails here, and every deploy of it would be refused. + +const out = REPORT_SITE_OUT; +const has = (rel: string) => fs.existsSync(path.join(out, ...rel.split("/"))); + +test("the built cited site passes the audit every deploy path asks", () => { + expect(reportSiteAuditProblem()).toBeNull(); +}); + +test("it holds the reports, the moments, the cited media and the contract", () => { + for (const rel of [ + "index.html", + "reports/index.html", + "reports/index.json", + "reports/demo-factcheck/index.html", + "reports/demo-factcheck/page.json", + "reports/demo-factcheck/citations.json", + "reports/demo-factcheck/citations.csv", + "reports/demo-factcheck/stills/a01.png", + "m/index.json", + "m/demo-channel/abc123/3126.00-3151.00/index.html", + "m/demo-channel/abc123/3126.00-3151.00/moment.json", + "m/demo-podcast/ep-042/610.50-628.00/index.html", + "m/demo-social/1234567890/index.html", + "media/clips/demo-channel/abc123/3126.00-3151.00.mp4", + "media/clips/demo-podcast/ep-042/610.50-628.00.m4a", + "media/posts/demo-social/1234567890/shot.png", + "site.json", + "corpus.json", + "llms.txt", + "sitemap.xml", + ]) { + expect(has(rel), rel).toBe(true); + } + const corpus = JSON.parse(fs.readFileSync(path.join(out, "corpus.json"), "utf8")); + expect(corpus.site).toMatchObject({ id: "reportsite", scope: "cited" }); + expect(corpus.reports.count).toBe(1); + const site = JSON.parse(fs.readFileSync(path.join(out, "site.json"), "utf8")); + expect(site).toMatchObject({ siteId: "reportsite", channels: [], pwa: false }); +}); + +test("it holds nothing of the corpus", () => { + expect(CORPUS_ROOT_ENTRIES.filter(has)).toEqual([]); +}); + +test("the audit refuses the same build with a corpus entry beside it", () => { + const copy = fs.mkdtempSync(path.join(os.tmpdir(), "report-site-audit-")); + try { + fs.cpSync(out, copy, { recursive: true }); + expect(citedBuildProblem(copy)).toBeNull(); + fs.mkdirSync(path.join(copy, "summaries")); + fs.writeFileSync(path.join(copy, "summaries", "manifest.json"), "{}"); + expect(citedBuildProblem(copy)).toMatch(/summaries\//); + } finally { + fs.rmSync(copy, { recursive: true, force: true }); + } +}); diff --git a/export/e2e-report/helpers.ts b/export/e2e-report/helpers.ts @@ -0,0 +1,47 @@ +import { test as base, expect } from "@playwright/test"; +import { CORPUS_PUBLIC_ENTRIES } from "yt-dlp-transcript-common/bin/compose-site"; + +// What a cited site must never ask its own origin for: the corpus-shaped +// entries a full compose writes and a cited compose prunes, less the four a +// cited compose writes again. A path is matched by its FIRST segment, so a +// post capture under /media/posts/ is not /posts/. +const REWRITTEN_FOR_CITED = new Set(["corpus.json", "llms.txt", "robots.txt", "sitemap.xml"]); +export const CORPUS_ROOT_ENTRIES: readonly string[] = CORPUS_PUBLIC_ENTRIES.filter((e) => !REWRITTEN_FOR_CITED.has(e)); + +export function isCorpusPath(pathname: string): boolean { + const first = pathname.split("/").filter(Boolean)[0] ?? ""; + return CORPUS_ROOT_ENTRIES.includes(first); +} + +type Traffic = { + // Every same-origin path the page requested, in order. + requested: string[]; + // Same-origin responses with an error status, as "<status> <path>". + failed: string[]; +}; + +// Every test records what its page asks the site for and, once it is done, +// holds that to the cited promise: nothing of the corpus is requested, and +// nothing the pages ask for is missing from the build. +export const test = base.extend<{ traffic: Traffic }>({ + traffic: [ + async ({ page, baseURL }, use) => { + const origin = new URL(baseURL!).origin; + const traffic: Traffic = { requested: [], failed: [] }; + page.on("request", (req) => { + const u = new URL(req.url()); + if (u.origin === origin) traffic.requested.push(u.pathname); + }); + page.on("response", (res) => { + const u = new URL(res.url()); + if (u.origin === origin && res.status() >= 400) traffic.failed.push(`${res.status()} ${u.pathname}`); + }); + await use(traffic); + expect(traffic.requested.filter(isCorpusPath), "a cited page requested the corpus").toEqual([]); + expect(traffic.failed, "a cited page requested something the build does not hold").toEqual([]); + }, + { auto: true }, + ], +}); + +export { expect }; diff --git a/export/e2e-report/report-site.spec.ts b/export/e2e-report/report-site.spec.ts @@ -0,0 +1,226 @@ +import { expect, test } from "./helpers"; +import type { Locator, Page } from "@playwright/test"; + +// A cited site, as a reader meets it: the report index as the home page, the +// report (verdicts, tally, the source's sentence, numbered inline citations +// with previews, the reference list), the moment pages each citation opens, +// and the shell's pages that say this site publishes its reports only. Every +// test also holds the page's own requests to the cited promise (helpers.ts). +// +// The site is export/fixtures/report-site, staged and built by +// e2e-report/stage.ts: one fact-check citing a video span (c01), an audio +// span (au1), a post (p01), the article's own sentences (a01, a02) and a web +// page (w01). + +const REPORT = "/reports/demo-factcheck/"; +const VIDEO_MOMENT = "/m/demo-channel/abc123/3126.00-3151.00/"; +const AUDIO_MOMENT = "/m/demo-podcast/ep-042/610.50-628.00/"; +const POST_MOMENT = "/m/demo-social/1234567890/"; + +const inlineCite = (page: Page, id: string): Locator => page.locator(`[data-inline-cite="${id}"]`).first(); +const preview = (page: Page, id: string): Locator => page.locator(`[data-cite-preview="${id}"]`); + +async function expectImageLoaded(img: Locator): Promise<void> { + await expect(img).toBeVisible(); + await expect.poll(() => img.evaluate((el) => (el as HTMLImageElement).naturalWidth)).toBeGreaterThan(0); +} + +test("the home page is the report index, and the header links Reports alone", async ({ page }) => { + await page.goto("/"); + await expect(page.locator("[data-report-index]")).toBeVisible(); + const entry = page.locator('[data-report-entry="demo-factcheck"]'); + await expect(entry).toContainText("Checking an example article"); + await expect(page.locator("header nav a")).toHaveText(["Reports"]); + await expect(page.locator("header nav a")).toHaveAttribute("href", "/reports/"); + + await entry.getByRole("link", { name: /Checking an example article/ }).first().click(); + await expect(page).toHaveURL(REPORT); + await expect(page.locator("h1")).toHaveText("Checking an example article"); +}); + +test("the report index page lists the report", async ({ page }) => { + await page.goto("/reports/"); + await expect(page.locator('[data-report-entry="demo-factcheck"]')).toContainText("Checking an example article"); + await expect(page.locator("header nav a")).toHaveText(["Reports"]); +}); + +test("the report: verdict chips, the tally, the source's sentence as a still, numbered inline citations", async ({ page }) => { + await page.goto(REPORT); + + // The tally names each verdict the report reaches once; each claim carries its chip. + const tally = page.locator("[data-verdict-tally]"); + await expect(tally).toBeVisible(); + await expect(tally.locator("[data-verdict]")).toHaveCount(4); + const claims = page.locator("article[data-claim]"); + await expect(claims).toHaveCount(5); + for (const [claim, verdict] of [ + ["claim-1", "CONTRADICTED"], + ["claim-2", "PARTLY"], + ["claim-3", "CONTRADICTED"], + ["claim-4", "UNTESTABLE"], + ["claim-5", "CORROBORATED"], + ]) { + await expect(page.locator(`article#${claim} [data-verdict="${verdict}"]`)).toBeVisible(); + } + + // The claim's sentence in the article, as the reviewed page showed it. + await expectImageLoaded(page.locator('[data-source-sentence="a01"] img[src$="/stills/a01.png"]')); + + // Inline citations carry their number in the report, by first appearance. + for (const [id, n] of [ + ["c01", "1"], + ["au1", "2"], + ["w01", "4"], + ["a02", "5"], + ["p01", "6"], + ]) { + await expect(inlineCite(page, id).locator("[data-cite-number]")).toHaveText(`[${n}]`); + } +}); + +test("hovering an inline citation previews it; leaving closes it", async ({ page }) => { + await page.goto(REPORT); + for (const [id, kind, text] of [ + ["c01", "video", "The bridge opened in the spring of twenty nineteen"], + ["p01", "post", "Never said I would move. Not once."], + ["a02", "source", "He has said many times that he would move away."], + ]) { + await inlineCite(page, id).hover(); + const card = preview(page, id); + await expect(card).toBeVisible(); + await expect(card.locator(`[data-citation-kind="${kind}"]`)).toBeVisible(); + await expect(card).toContainText(text); + // Away from the marker and its card, the preview closes. + await page.mouse.move(2, 2); + await expect(card).toHaveCount(0); + } +}); + +test("focusing an inline citation's number previews it; Escape closes it", async ({ page }) => { + await page.goto(REPORT); + for (const [id, kind] of [ + ["c01", "video"], + ["p01", "post"], + ["a02", "source"], + ]) { + const number = inlineCite(page, id).locator("[data-cite-number]"); + await number.focus(); + const card = preview(page, id); + await expect(card).toBeVisible(); + await expect(card.locator(`[data-citation-kind="${kind}"]`)).toBeVisible(); + await expect(number).toHaveAttribute("aria-expanded", "true"); + await page.keyboard.press("Escape"); + await expect(card).toHaveCount(0); + await expect(number).toHaveAttribute("aria-expanded", "false"); + } +}); + +test("a post citation's preview shows the captured shot", async ({ page }) => { + await page.goto(REPORT); + await inlineCite(page, "p01").locator("[data-cite-number]").focus(); + await expectImageLoaded(preview(page, "p01").locator('img[src="/media/posts/demo-social/1234567890/shot.png"]')); +}); + +test("a citation's number jumps to its entry in the reference list", async ({ page }) => { + await page.goto(REPORT); + const refs = page.locator("[data-reference-list] [data-reference]"); + await expect(refs).toHaveCount(6); + await expect + .poll(() => refs.evaluateAll((els) => els.map((el) => el.id))) + .toEqual(["c-c01", "c-au1", "c-a01", "c-w01", "c-a02", "c-p01"]); + + await inlineCite(page, "w01").locator("[data-cite-number]").click(); + await expect(page).toHaveURL(`${REPORT}#c-w01`); + await expect(page.locator("#c-w01")).toBeInViewport(); + await expect(page.locator("#c-w01")).toContainText("Opened to traffic: April 2019."); +}); + +test("the report's citations download as JSON and CSV", async ({ page, request }) => { + await page.goto(REPORT); + const links = page.locator("[data-citation-downloads] a[download]"); + await expect(links).toHaveCount(2); + for (const href of await links.evaluateAll((els) => els.map((el) => el.getAttribute("href")!))) { + const res = await request.get(href); + expect(res.status(), href).toBe(200); + if (href.endsWith(".json")) expect((await res.json()).format).toBe("archilyzer-citations"); + else expect(await res.text()).toContain("c01"); + } +}); + +test("a video citation opens its moment: the clip, the cue lines with the span marked, where it is cited", async ({ + page, + request, +}) => { + await page.goto(REPORT); + await inlineCite(page, "c01").getByRole("link", { name: "on stream" }).click(); + await expect(page).toHaveURL(VIDEO_MOMENT); + await expect(page.locator("[data-moment]")).toHaveAttribute("data-moment-kind", "video"); + + const video = page.locator("video[data-evidence-clip]"); + await expect(video).toBeVisible(); + const src = await video.getAttribute("src"); + expect(src).toBe("/media/clips/demo-channel/abc123/3126.00-3151.00.mp4"); + const clip = await request.get(src!); + expect(clip.status()).toBe(200); + expect(clip.headers()["content-type"]).toMatch(/^video\/mp4/); + expect((await clip.body()).length).toBeGreaterThan(1000); + // The poster is the report's still. + const poster = await video.getAttribute("poster"); + expect((await request.get(poster!)).status()).toBe(200); + + const cues = page.locator("[data-cue-lines] li"); + await expect(cues).toHaveCount(6); + await expect(page.locator("[data-cue-lines] li[data-in-span]")).toHaveText([ + /The bridge opened in the spring of twenty nineteen,/, + /I was there for it\./, + ]); + + const citedIn = page.locator("[data-cited-in] a"); + await expect(citedIn.and(page.locator(`[href="${REPORT}#claim-1"]`))).toHaveCount(1); + await expect(citedIn.and(page.locator(`[href="${REPORT}#claim-5"]`))).toHaveCount(1); + await citedIn.and(page.locator(`[href="${REPORT}#claim-1"]`)).click(); + await expect(page).toHaveURL(`${REPORT}#claim-1`); + await expect(page.locator("#claim-1")).toBeInViewport(); +}); + +test("an audio moment plays its clip as sound", async ({ page, request }) => { + await page.goto(AUDIO_MOMENT); + await expect(page.locator("[data-moment]")).toHaveAttribute("data-moment-kind", "audio"); + await expect(page.locator("video")).toHaveCount(0); + const src = await page.locator("audio[data-evidence-clip]").getAttribute("src"); + expect(src).toBe("/media/clips/demo-podcast/ep-042/610.50-628.00.m4a"); + const clip = await request.get(src!); + expect(clip.status()).toBe(200); + expect(clip.headers()["content-type"]).toMatch(/^audio\//); + await expect(page.locator("[data-cue-lines] li[data-in-span]")).toHaveText([/Like I said before, spring twenty nineteen\./]); + await expect(page.locator(`[data-cited-in] a[href="${REPORT}#claim-2"]`)).toHaveCount(1); +}); + +test("a post moment shows the captured shot and the post", async ({ page }) => { + await page.goto(POST_MOMENT); + await expect(page.locator("[data-moment]")).toHaveAttribute("data-moment-kind", "post"); + await expectImageLoaded(page.locator('img[src="/media/posts/demo-social/1234567890/shot.png"]')); + await expect(page.locator("[data-moment]")).toContainText("Posted to settle it."); + await expect(page.locator("video, audio")).toHaveCount(0); + await expect(page.locator(`[data-cited-in] a[href="${REPORT}#claim-3"]`)).toHaveCount(1); +}); + +for (const route of ["/ask/", "/downloads/", "/duplicates/"]) { + test(`${route} says this site publishes its reports only`, async ({ page }) => { + await page.goto(route); + await expect(page.getByText("This site publishes its reports only.")).toBeVisible(); + await expect(page.getByRole("link", { name: "Read the reports" })).toHaveAttribute("href", "/reports/"); + await expect(page.locator("header nav a")).toHaveText(["Reports"]); + }); +} + +test("reading the whole site never asks for the corpus", async ({ page, traffic }) => { + for (const route of ["/", "/reports/", REPORT, VIDEO_MOMENT, AUDIO_MOMENT, POST_MOMENT, "/ask/", "/downloads/"]) { + await page.goto(route); + await page.waitForLoadState("networkidle"); + } + // The recorder saw the pages' own requests (the corpus check runs after + // every test, this one included). + expect(traffic.requested).toContain(REPORT); + expect(traffic.requested.some((p) => p.startsWith("/_next/"))).toBe(true); +}); diff --git a/export/e2e-report/serve.mjs b/export/e2e-report/serve.mjs @@ -0,0 +1,103 @@ +// A static server for the staged cited build (playwright.report.config.ts), +// serving a directory the way Cloudflare Pages and the container's Caddy do: +// `/dir/` is `dir/index.html`, `/dir` redirects to `/dir/`. +// +// Not `serve` (export's `start`): serve-handler stats a path whose last segment +// looks like it has an extension as a file, and lists the directory instead of +// serving its index.html — and a span moment's directory is named +// `<start>-<end>` in seconds with two decimals (`3126.00-3151.00`), so every +// video and audio moment page came back as a directory listing. +// +// Byte ranges are honoured (a media element asks for them). +// +// node e2e-report/serve.mjs <dir> <port> + +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; + +const [rootArg, portArg] = process.argv.slice(2); +if (!rootArg || !portArg) { + console.error("usage: serve.mjs <dir> <port>"); + process.exit(2); +} +const ROOT = path.resolve(rootArg); +const PORT = Number(portArg); + +const TYPES = { + ".html": "text/html; charset=utf-8", + ".txt": "text/plain; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".csv": "text/csv; charset=utf-8", + ".xml": "application/xml; charset=utf-8", + ".webmanifest": "application/manifest+json", + ".svg": "image/svg+xml", + ".png": "image/png", + ".ico": "image/x-icon", + ".woff2": "font/woff2", + ".mp4": "video/mp4", + ".m4a": "audio/mp4", +}; + +function statOrNull(p) { + try { + return fs.statSync(p); + } catch { + return null; + } +} + +function sendFile(req, res, file, status = 200) { + const size = fs.statSync(file).size; + const headers = { + "Content-Type": TYPES[path.extname(file).toLowerCase()] ?? "application/octet-stream", + "Accept-Ranges": "bytes", + }; + const range = status === 200 ? /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "") : null; + if (range && (range[1] || range[2])) { + let start = range[1] ? Number(range[1]) : size - Number(range[2]); + let end = range[1] && range[2] ? Number(range[2]) : size - 1; + start = Math.max(0, start); + end = Math.min(end, size - 1); + if (start > end) { + res.writeHead(416, { "Content-Range": `bytes */${size}` }).end(); + return; + } + res.writeHead(206, { ...headers, "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": end - start + 1 }); + if (req.method === "HEAD") return void res.end(); + fs.createReadStream(file, { start, end }).pipe(res); + return; + } + res.writeHead(status, { ...headers, "Content-Length": size }); + if (req.method === "HEAD") return void res.end(); + fs.createReadStream(file).pipe(res); +} + +http + .createServer((req, res) => { + const url = new URL(req.url ?? "/", "http://localhost"); + let pathname; + try { + pathname = decodeURIComponent(url.pathname); + } catch { + return void res.writeHead(400).end(); + } + const file = path.join(ROOT, pathname); + if (file !== ROOT && !file.startsWith(ROOT + path.sep)) return void res.writeHead(403).end(); + const st = statOrNull(file); + if (st?.isDirectory()) { + if (!pathname.endsWith("/")) { + return void res.writeHead(308, { Location: `${url.pathname}/${url.search}` }).end(); + } + const index = path.join(file, "index.html"); + if (statOrNull(index)?.isFile()) return sendFile(req, res, index); + } else if (st?.isFile()) { + return sendFile(req, res, file); + } + const notFound = path.join(ROOT, "404.html"); + if (statOrNull(notFound)?.isFile()) return sendFile(req, res, notFound, 404); + res.writeHead(404).end(); + }) + .listen(PORT, () => console.log(`[report-site] serving ${ROOT} on http://localhost:${PORT}`)); diff --git a/export/e2e-report/stage.ts b/export/e2e-report/stage.ts @@ -1,7 +1,7 @@ // THE CITED FIXTURE SITE, BUILT FOR REAL. // // The report-site suite (playwright.report.config.ts) runs against a real -// static export of a cited site, served by `serve` like a deployed one, so +// static export of a cited site, served like a deployed one (serve.mjs), so // what it checks is what ships: the routes Next emits, the files it copies, // the requests the built pages make. The same out/ is held to the cited // out/ allowlist audit (lib/builtExport.ts) by audit.spec.ts. diff --git a/export/playwright.report.config.ts b/export/playwright.report.config.ts @@ -8,9 +8,11 @@ import { REPORT_SITE_OUT, stageReportSite } from "./e2e-report/stage"; // The CITED report-site suite: a real static export of a cited fixture site // (one fact-check citing every kind, its moments, their media), staged and -// built OUTSIDE export/ — e2e-report/stage.ts says why — and served by `serve` -// like a deployed site. What the specs see is what a cited build ships, and -// audit.spec.ts holds the same out/ to the cited allowlist audit. +// built OUTSIDE export/ — e2e-report/stage.ts says why — and served as Pages +// serves a deployed site (e2e-report/serve.mjs; not `serve`, which lists a +// span moment's directory instead of serving its page). What the specs see is +// what a cited build ships, and audit.spec.ts holds the same out/ to the cited +// allowlist audit. // // Its own suite (`pnpm --filter export run e2e:report`), never part of the // default export suite (testDir ./e2e) nor of the editor's `pnpm e2e`. Staged @@ -32,7 +34,7 @@ export default defineConfig({ fullyParallel: false, workers: 1, webServer: { - command: `pnpm exec serve ${REPORT_SITE_OUT} -l ${PORT} --no-request-logging`, + command: `node e2e-report/serve.mjs ${REPORT_SITE_OUT} ${PORT}`, url: `${baseURL}/site.json`, timeout: 60_000, reuseExistingServer: !process.env.CI,