Archilyzer · Source

archilyzer

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

commit 624a06b4e9541a63ee1757de02f88d799b423c9f
parent b26429eb373a8543ee7f57b2c2c557b10c8daa47
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 05:22:55 -0400

export: a stage for the cited fixture site — copies export/app and its config beside a public/ of the fixture's views, media and compose's own contract files, builds it for real outside export/, audits the out/; the e2e:report suite's config and port

compose-site exports emitFederationFiles and emitAiFiles for the stage.

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

Diffstat:
M.gitignore | 3+++
MENVIRONMENT.md | 2++
Mcommon/bin/compose-site.ts | 8++++++--
Mcommon/lib/envVars.ts | 1+
Mcommon/lib/ports.mjs | 1+
Aexport/e2e-report/contract.ts | 46++++++++++++++++++++++++++++++++++++++++++++++
Aexport/e2e-report/fixtures/sites/reportsite/site.json | 9+++++++++
Aexport/e2e-report/stage.ts | 128+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/package.json | 2++
Aexport/playwright.report.config.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 250 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -138,6 +138,9 @@ yarn-error.log* /export/test-results-hub/ /export/test-results-2origin/ /export/.2origin/ +/export/test-results-report/ +# the cited report-site suite's stage project (export/e2e-report/stage.ts) +/.e2e-report-site/ /export/blob-report/ # homepage's own Playwright output (its suite runs from homepage/) /homepage/test-results/ diff --git a/ENVIRONMENT.md b/ENVIRONMENT.md @@ -117,6 +117,7 @@ Every local server's default port, from `common/lib/ports.mjs`. The primary chec | `HOMEPAGE_PORT` | `3031` | Homepage static `serve out` (start:homepage). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `HOMEPAGE_E2E_PORT` | `3040` | Homepage's own Playwright suite. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `HUB_PORT` | `3041` | Export's hub Playwright suite (e2e:hub). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | +| `REPORT_SITE_E2E_PORT` | `3042` | Export's cited report-site Playwright suite (e2e:report). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `UMTOOL_PORT` | `3050` | Umtool real dev/start (`pnpm dev:umtool`). A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `UMTOOL_E2E_PORT` | `3051` | Umtool's own Playwright suite. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | | `EDITOR_STUB_PORT` | `3052` | Stub editor the umtool e2e suite fetches clips from. A worktree adds its offset (`pnpm wt list`). | common/lib/ports.mjs | @@ -189,6 +190,7 @@ Read only by a test harness, a fake binary or a test-mode branch. Never set one | `E2E_OLLAMA_STUB_MODEL` | `qwen2.5:7b` | The model the ollama stub claims to serve. | editor/e2e/fixtures/ollama-stub.mjs | | `E2E_RACK_SHOTS` | off (spec skipped) | Runs the `/channels` rack screenshot audit. | editor/e2e/channels-rack-audit.spec.ts | | `E2E_TWO_ORIGIN_REBUILD` | off | `1` rebuilds the two-origin suite's cached hub bundle. | export/e2e-2origin/globalSetup.ts | +| `E2E_REPORT_SITE_REUSE` | off (build again) | `1` serves the cited report-site suite's existing stage build (`.e2e-report-site/out`) instead of staging and building the fixture site again — for iterating on the specs alone; it does not see a change to the app. | export/playwright.report.config.ts | | `E2E_SHARDS` | min(max(2, cpus/2), 8) | How many containers `pnpm e2e:sharded` splits the editor suite across (`--shards N` wins). | scripts/run-sharded-e2e.mjs | | `E2E_RETRIES` | `0` | Retries per shard (`--retries N` wins); 0 keeps a sharded run comparable to a serial one. | scripts/run-sharded-e2e.mjs | | `E2E_IMAGE` | `yt-dlp-transcript-browser-e2e` | The sharded e2e run's image tag. | scripts/run-sharded-e2e.mjs | diff --git a/common/bin/compose-site.ts b/common/bin/compose-site.ts @@ -89,7 +89,11 @@ import { copyPublicFile, ownDir, writePublicFile } from "./_publicFile"; // copyPublicFile, and every tree it writes into is ownDir'd first, so nothing // is written THROUGH a link: in a git worktree public/'s entries are links into // the primary checkout (_publicFile.ts). -async function emitFederationFiles( +// +// Exported, with emitAiFiles, for the cited fixture site's e2e staging +// (export/e2e-report/stage.ts), which writes a cited site's contract around +// fixture report views exactly as this compose would. +export async function emitFederationFiles( site: Site, paths: ReturnType<typeof getPaths>, ): Promise<void> { @@ -205,7 +209,7 @@ export const ALLOW_MISSING_MEDIA_ENV = "REPORTS_ALLOW_MISSING_MEDIA"; // navigate the already-served paginated shards; they never enumerate per-video // files, so the count is constant regardless of corpus size. Runs after // site.json and the archives are composed (both feed into these files). -async function emitAiFiles( +export async function emitAiFiles( site: Site, paths: ReturnType<typeof getPaths>, composed: ComposedReports, diff --git a/common/lib/envVars.ts b/common/lib/envVars.ts @@ -188,6 +188,7 @@ const DECLARED: EnvVarDecl[] = [ { name: "E2E_OLLAMA_STUB_MODEL", audience: "test", default: "`qwen2.5:7b`", readBy: "editor/e2e/fixtures/ollama-stub.mjs", doc: "The model the ollama stub claims to serve." }, { name: "E2E_RACK_SHOTS", audience: "test", default: "off (spec skipped)", readBy: "editor/e2e/channels-rack-audit.spec.ts", doc: "Runs the `/channels` rack screenshot audit." }, { name: "E2E_TWO_ORIGIN_REBUILD", audience: "test", default: "off", readBy: "export/e2e-2origin/globalSetup.ts", doc: "`1` rebuilds the two-origin suite's cached hub bundle." }, + { name: "E2E_REPORT_SITE_REUSE", audience: "test", default: "off (build again)", readBy: "export/playwright.report.config.ts", doc: "`1` serves the cited report-site suite's existing stage build (`.e2e-report-site/out`) instead of staging and building the fixture site again — for iterating on the specs alone; it does not see a change to the app." }, { name: "E2E_SHARDS", audience: "test", default: "min(max(2, cpus/2), 8)", readBy: "scripts/run-sharded-e2e.mjs", doc: "How many containers `pnpm e2e:sharded` splits the editor suite across (`--shards N` wins)." }, { name: "E2E_RETRIES", audience: "test", default: "`0`", readBy: "scripts/run-sharded-e2e.mjs", doc: "Retries per shard (`--retries N` wins); 0 keeps a sharded run comparable to a serial one." }, { name: "E2E_IMAGE", audience: "test", default: "`yt-dlp-transcript-browser-e2e`", readBy: "scripts/run-sharded-e2e.mjs", doc: "The sharded e2e run's image tag." }, diff --git a/common/lib/ports.mjs b/common/lib/ports.mjs @@ -42,6 +42,7 @@ export const PORTS = Object.freeze({ HOMEPAGE_PORT: { base: 3031, what: "homepage static `serve out` (start:homepage)" }, HOMEPAGE_E2E_PORT: { base: 3040, what: "homepage's own Playwright suite" }, HUB_PORT: { base: 3041, what: "export's hub Playwright suite (e2e:hub)" }, + REPORT_SITE_E2E_PORT: { base: 3042, what: "export's cited report-site Playwright suite (e2e:report)" }, UMTOOL_PORT: { base: 3050, what: "umtool real dev/start (`pnpm dev:umtool`)" }, UMTOOL_E2E_PORT: { base: 3051, what: "umtool's own Playwright suite" }, EDITOR_STUB_PORT: { base: 3052, what: "stub editor the umtool e2e suite fetches clips from" }, diff --git a/export/e2e-report/contract.ts b/export/e2e-report/contract.ts @@ -0,0 +1,46 @@ +// The cited fixture site's compose step (stage.ts runs it, in a child process +// with the stage's environment): what compose's reports stage writes beside +// the views — the citation downloads — and the cited site's contract, by +// compose's own functions (bin/compose-site.ts emitFederationFiles / +// emitAiFiles), so the stage's public/ is what compose would leave for this +// site. The view JSON, stills and media are the fixture's, already in place. +// +// Every import is dynamic: getPaths() reads the environment on first use. + +import fs from "node:fs"; +import path from "node:path"; +import type { MomentIndexView, ReportIndexView, ReportPageView } from "yt-dlp-transcript-common/lib/report/views"; + +const { getPaths } = await import("yt-dlp-transcript-common/lib/paths"); +const { getSite } = await import("yt-dlp-transcript-common/lib/site"); +const { emitAiFiles, emitFederationFiles } = await import("yt-dlp-transcript-common/bin/compose-site"); +const { citationSet, citationsCsv } = await import("yt-dlp-transcript-common/publish/composeReports"); +const { MOMENTS_INDEX_PATH, REPORTS_INDEX_PATH, reportCitationsDownloadPath, reportViewPath } = await import( + "yt-dlp-transcript-common/lib/report/views" +); +const { readFixtureReport } = await import("../fixtures/report-site/fixture"); + +const paths = getPaths(); +const siteId = process.env.SITE_ID; +if (!siteId) throw new Error("contract.ts: SITE_ID is not set (run it through stage.ts)"); +const site = getSite(siteId, paths); + +const pub = (urlPath: string) => path.join(paths.exportPublicDir, ...urlPath.split("/").filter(Boolean)); +const readJson = <T>(urlPath: string): T => JSON.parse(fs.readFileSync(pub(urlPath), "utf8")) as T; + +const index = readJson<ReportIndexView>(REPORTS_INDEX_PATH); +const moments = readJson<MomentIndexView>(MOMENTS_INDEX_PATH).moments; +const report = readFixtureReport(); +for (const entry of index.reports) { + if (entry.id !== report.id) throw new Error(`contract.ts: the fixture has no report "${entry.id}"`); + const view = readJson<ReportPageView>(reportViewPath(entry.id)); + fs.writeFileSync( + pub(reportCitationsDownloadPath(entry.id, "json")), + `${JSON.stringify(citationSet(report, view), null, 2)}\n`, + ); + fs.writeFileSync(pub(reportCitationsDownloadPath(entry.id, "csv")), citationsCsv(view)); +} + +await emitFederationFiles(site, paths); +await emitAiFiles(site, paths, { reports: index.reports, moments, allowed: [] }); +console.log(`[report-site] composed the contract of cited site "${site.siteId}" into ${paths.exportPublicDir}`); diff --git a/export/e2e-report/fixtures/sites/reportsite/site.json b/export/e2e-report/fixtures/sites/reportsite/site.json @@ -0,0 +1,9 @@ +{ + "siteId": "reportsite", + "siteTitle": "Demo Reports", + "siteDescription": "A cited fixture site: one fact-check and the moments it cites.", + "headerTitle": "Demo Reports", + "siteUrl": "https://reports.example.org", + "publish": "cited", + "reports": ["demo-factcheck"] +} diff --git a/export/e2e-report/stage.ts b/export/e2e-report/stage.ts @@ -0,0 +1,128 @@ +// 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 +// 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. +// +// THE STAGE. `next build` copies the public/ BESIDE ITS PROJECT into out/, and +// export/public (and export/out) are the checkout's own — in the primary +// checkout, the operator's real site build. So the build never runs in +// export/: it runs in a stage project at the repo root (`.e2e-report-site/`, +// gitignored) holding a copy of export/app and export's config files, a link to +// export's node_modules, and its own public/. The stage sits at the same depth +// as export/, so every relative path in the copies (globals.css's +// `../../common/…`, tsconfig's `../tsconfig.base.json`, next.config's +// workspace root) resolves exactly as it does there. +// +// THE PUBLIC DIR is what compose's reports stage would write for the site: +// - the fixture report site (export/fixtures/report-site/public): the view +// JSON R4's builder made, the stills, the post's shot, the evidence clips; +// - the citation downloads and the cited site's contract (site.json, +// corpus.json, llms.txt, robots.txt, sitemap.xml, _headers), written by +// compose's own functions in a child process (contract.ts); +// - export/public's checked-in assets (git-tracked files only — the +// checkout's composed corpus is never read). +// +// Every server-side read points into the stage too (TRANSCRIPTS_DIR, +// SITES_DIR, SETTINGS_FILE, EXPORT_*): the build never sees a real corpus. + +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { builtBundleProblem } from "yt-dlp-transcript-common/lib/builtExport"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const EXPORT_DIR = path.resolve(HERE, ".."); +const REPO = path.resolve(EXPORT_DIR, ".."); + +export const REPORT_SITE_ID = "reportsite"; +export const REPORT_SITE_STAGE = path.join(REPO, ".e2e-report-site"); +export const REPORT_SITE_OUT = path.join(REPORT_SITE_STAGE, "out"); +export const REPORT_SITE_SITES_DIR = path.join(HERE, "fixtures", "sites"); +const FIXTURE_PUBLIC = path.join(EXPORT_DIR, "fixtures", "report-site", "public"); + +// export's files the stage project copies, beside app/ less its tests (the +// build type-checks what it holds, and a test's relative imports reach outside +// app/). The changelog page reads CHANGELOG.md from the project's cwd. +const PROJECT_FILES = ["next.config.ts", "tsconfig.json", "postcss.config.mjs", "package.json", "CHANGELOG.md"]; + +// The environment every stage step runs with. +export function reportSiteEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { + ...base, + NODE_ENV: "production", + SITE_ID: REPORT_SITE_ID, + SITES_DIR: REPORT_SITE_SITES_DIR, + SETTINGS_FILE: path.join(REPORT_SITE_STAGE, "settings.json"), + TRANSCRIPTS_DIR: path.join(REPORT_SITE_STAGE, "transcripts"), + EXPORT_PUBLIC_DIR: path.join(REPORT_SITE_STAGE, "public"), + EXPORT_INDEX_DIR: path.join(REPORT_SITE_STAGE, ".export-index"), + EXPORT_BUILDS_DIR: path.join(REPORT_SITE_STAGE, ".export-builds"), + NEXT_TELEMETRY_DISABLED: "1", + }; + delete env.INSTANCE_MODE; + return env; +} + +function copyApp(): void { + fs.cpSync(path.join(EXPORT_DIR, "app"), path.join(REPORT_SITE_STAGE, "app"), { + recursive: true, + filter: (src) => !/\.test\.tsx?$/.test(src), + }); + for (const f of PROJECT_FILES) { + fs.copyFileSync(path.join(EXPORT_DIR, f), path.join(REPORT_SITE_STAGE, f)); + } + fs.symlinkSync(path.join(EXPORT_DIR, "node_modules"), path.join(REPORT_SITE_STAGE, "node_modules"), "dir"); +} + +function stagePublic(): void { + const pub = path.join(REPORT_SITE_STAGE, "public"); + const tracked = execFileSync("git", ["ls-files", "-z", "--", "public"], { cwd: EXPORT_DIR, encoding: "utf8" }) + .split("\0") + .filter(Boolean); + for (const rel of tracked) { + const dest = path.join(REPORT_SITE_STAGE, rel); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(path.join(EXPORT_DIR, rel), dest); + } + fs.cpSync(FIXTURE_PUBLIC, pub, { recursive: true }); +} + +function run(command: string, args: string[], cwd: string): void { + execFileSync(command, args, { cwd, env: reportSiteEnv(), stdio: "inherit" }); +} + +/** + * Stage, compose and build the cited fixture site; returns its out/. + * `reuse` keeps a stage that already holds a build (E2E_REPORT_SITE_REUSE=1, + * for iterating on the specs alone — it does not see a change to the app). + */ +export function stageReportSite(opts: { reuse?: boolean } = {}): string { + if (opts.reuse && fs.existsSync(path.join(REPORT_SITE_OUT, "corpus.json"))) return REPORT_SITE_OUT; + fs.rmSync(REPORT_SITE_STAGE, { recursive: true, force: true }); + fs.mkdirSync(path.join(REPORT_SITE_STAGE, "transcripts"), { recursive: true }); + fs.writeFileSync(path.join(REPORT_SITE_STAGE, "settings.json"), "{}\n"); + copyApp(); + stagePublic(); + const bin = path.join(EXPORT_DIR, "node_modules", ".bin"); + run(path.join(bin, "tsx"), [path.join(HERE, "contract.ts")], EXPORT_DIR); + run(path.join(bin, "next"), ["build"], REPORT_SITE_STAGE); + return REPORT_SITE_OUT; +} + +// The audit every deploy path asks of a bundle (builtBundleProblem: identity, +// then the cited allowlist and Pages' limits), of the staged build. +export function reportSiteAuditProblem(outDir = REPORT_SITE_OUT): string | null { + return builtBundleProblem(outDir, REPORT_SITE_ID); +} + +// `tsx e2e-report/stage.ts`: stage and build the site, then audit it. +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const out = stageReportSite({ reuse: process.argv.includes("--reuse") }); + const problem = reportSiteAuditProblem(out); + console.log(problem ? `[report-site] AUDIT REFUSES: ${problem}` : `[report-site] ${out} passes the cited audit`); + process.exitCode = problem ? 1 : 0; +} diff --git a/export/package.json b/export/package.json @@ -20,6 +20,8 @@ "e2e": "node ../scripts/queue-lock.mjs --ports EXPORT_E2E_PORT:3020 -- playwright test", "e2e:hub": "node ../scripts/queue-lock.mjs --ports HUB_PORT:3041 -- playwright test --config playwright.hub.config.ts", "e2e:2origin": "node ../scripts/queue-lock.mjs --ports ORIGIN_B_PORT:4610,HUB_A_PORT:4611 -- playwright test --config playwright.2origin.config.ts", + "e2e:report": "node ../scripts/queue-lock.mjs --ports REPORT_SITE_E2E_PORT:3042 -- playwright test --config playwright.report.config.ts", + "build:report-fixture": "tsx e2e-report/stage.ts", "e2e:ui": "playwright test --ui", "deploy": "tsx ../common/bin/archilyzer.ts deploy site" }, diff --git a/export/playwright.report.config.ts b/export/playwright.report.config.ts @@ -0,0 +1,52 @@ +import { defineConfig, devices } from "@playwright/test"; +import { portFor } from "yt-dlp-transcript-common/lib/ports.mjs"; +import { REPORT_SITE_OUT, stageReportSite } from "./e2e-report/stage"; + +// Test-only environment (declared in common/lib/envVars.ts): +// E2E_REPORT_SITE_REUSE=1 serve the stage's existing build instead of +// building again (iterating on the specs alone). + +// 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. +// +// 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 +// here at config load, before the webServer starts, and once — in the runner, +// not again in each worker (Playwright sets TEST_WORKER_INDEX there). +const PORT = portFor("REPORT_SITE_E2E_PORT"); +const baseURL = `http://localhost:${PORT}`; + +if (!process.env.TEST_WORKER_INDEX) { + stageReportSite({ reuse: process.env.E2E_REPORT_SITE_REUSE === "1" }); +} + +export default defineConfig({ + testDir: "./e2e-report", + timeout: 30_000, + retries: process.env.CI ? 2 : 0, + reporter: process.env.CI ? "github" : "list", + outputDir: "test-results-report/", + fullyParallel: false, + workers: 1, + webServer: { + command: `pnpm exec serve ${REPORT_SITE_OUT} -l ${PORT} --no-request-logging`, + url: `${baseURL}/site.json`, + timeout: 60_000, + reuseExistingServer: !process.env.CI, + }, + use: { + baseURL, + trace: "on-first-retry", + screenshot: "only-on-failure", + video: "retain-on-failure", + }, + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 1000 } }, + }, + ], +});