// A fixture report site: one fact-check report citing all five citation kinds // (video, audio, post, source, page), and the views compose's reports stage // would write for it (common/lib/report/views.ts) — the report index, the // report's page view, the moment index and one moment view per cited moment. // // The views are BUILT from source/demo-factcheck/report.json by the same // builder compose uses, with a fixed resolver standing in for the corpus; // `write.ts` writes them under public/, and fixture.test.ts holds the written // files equal to a fresh build, so the JSON can never drift from the builder. // // THE REVISIONS. The report has two (common/publish/reportHistory.ts): // source/demo-factcheck/report.v1.json, then report.json — FIXTURE_REVISIONS, // committed at fixed dates by e2e-report/contract.ts. The page view names the // second as current. // // Everything here is neutral demo data. import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildCitedIn } from "yt-dlp-transcript-common/lib/report/citedIn"; import { parseReport } from "yt-dlp-transcript-common/lib/report/validate"; import type { Report } from "yt-dlp-transcript-common/lib/report/schema"; import { reportHistoryPagePath } from "yt-dlp-transcript-common/lib/report/revisions"; import { reportFeedUrls } from "yt-dlp-transcript-common/lib/report/feeds"; import { momentKeyOf, parseMomentKey, type SpanMoment } from "yt-dlp-transcript-common/lib/citations/moments"; import { MOMENT_INDEX_FORMAT, MOMENT_PAGE_FORMAT, MOMENTS_INDEX_PATH, REPORT_INDEX_FORMAT, REPORT_VIEWS_VERSION, REPORTS_INDEX_PATH, buildReportPageView, citedInViews, evidenceClipPath, momentViewPath, reportCitationsDownloadPath, reportExportDownloadPath, reportIndexEntry, REPORT_EXPORT_FORMATS, reportViewPath, type MomentPageView, type RecordView, type ReportIndexView, type ReportPageView, } from "yt-dlp-transcript-common/lib/report/views"; export const FIXTURE_DIR = path.dirname(fileURLToPath(import.meta.url)); export const FIXTURE_PUBLIC_DIR = path.join(FIXTURE_DIR, "public"); export const FIXTURE_REPORT_ID = "demo-factcheck"; // The fixture site's public URL (e2e-report/fixtures/sites/reportsite/site.json // `siteUrl`): the report's timeline feeds are named by it, as compose names them. export const FIXTURE_SITE_URL = "https://reports.example.org"; // The report's revisions, oldest first: the file under source// and the // commit's date. export const FIXTURE_REVISIONS: readonly { file: string; date: string }[] = [ { file: "report.v1.json", date: "2026-10-01T09:00:00Z" }, { file: "report.json", date: "2026-10-04T12:00:00Z" }, ]; const RECORDS: Record = { "demo-channel/abc123": { channel: "demo-channel", channelTitle: "Demo Channel", id: "abc123", title: "Demo stream: the bridge, the move, and the mail", date: "2026-01-10", platform: "youtube", }, "demo-podcast/ep-042": { channel: "demo-podcast", channelTitle: "Demo Podcast", id: "ep-042", title: "Episode 42: a guest looks back", date: "2026-02-02", }, "demo-social/1234567890": { channel: "demo-social", channelTitle: "Demo Social", id: "1234567890", date: "2025-03-14", platform: "x", }, }; const POSTER = "/reports/demo-factcheck/stills/poster.png"; // A post's captured screenshot, where compose copies a cited capture // (publish/citedPostCaptures.ts): committed under public/media/posts/. function shotFor(c: { channel: string; id: string }): string { return `/media/posts/${c.channel}/${c.id}/shot.png`; } export function fixtureReportFile(file = "report.json"): string { return path.join(FIXTURE_DIR, "source", FIXTURE_REPORT_ID, file); } export function readFixtureReport(file = "report.json"): Report { const raw = JSON.parse(fs.readFileSync(fixtureReportFile(file), "utf8")); const parsed = parseReport(raw); if (!parsed.ok || parsed.problems.length > 0) { throw new Error(`fixture report is not sound: ${JSON.stringify(parsed.problems)}`); } return parsed.value; } function recordFor(channel: string, id: string, seconds?: number): RecordView { const r = RECORDS[`${channel}/${id}`]; if (!r) throw new Error(`fixture: no record ${channel}/${id}`); const base = `https://media.example.org/${channel}/${id}`; return { ...r, originalUrl: seconds === undefined ? base : `${base}?t=${Math.floor(seconds)}` }; } export function buildFixtureReportView(report = readFixtureReport()): ReportPageView { return buildReportPageView(report, { record: (c) => recordFor(c.channel, c.id, c.kind === "post" ? undefined : c.start), poster: (c) => (c.kind === "video" ? POSTER : undefined), post: (c) => ({ author: "@demo_account", text: `${c.quote}\n\nPosted to settle it.`, shot: shotFor(c), }), // Every download compose lists when the report was exported (the e2e // stage writes the exports themselves: e2e-report/contract.ts). downloads: { ...Object.fromEntries(REPORT_EXPORT_FORMATS.map((f) => [f, reportExportDownloadPath(report.id, f)])), json: reportCitationsDownloadPath(report.id, "json"), csv: reportCitationsDownloadPath(report.id, "csv"), }, // Its timeline's feeds, as compose names them on a site with a public URL // (contract.ts writes them). feeds: reportFeedUrls(FIXTURE_SITE_URL, report.id), // The newest revision, as compose names it (contract.ts commits both). history: { revision: FIXTURE_REVISIONS.length, date: FIXTURE_REVISIONS[FIXTURE_REVISIONS.length - 1].date, href: reportHistoryPagePath(report.id), current: true, }, }); } // The cue lines around each fixture span: two lines either side. const CUES: Record = { "demo-channel/abc123": [ { start: 3110, end: 3118, text: "Okay so somebody asked about the bridge again." }, { start: 3118, end: 3126, text: "Let me be really clear about this one." }, { start: 3126, end: 3138, text: "The bridge opened in the spring of twenty nineteen," }, { start: 3138, end: 3151, text: "I was there for it." }, { start: 3151, end: 3160, text: "Anyway, back to the mail." }, { start: 3160, end: 3170, text: "Next letter is from a long-time viewer." }, ], "demo-podcast/ep-042": [ { start: 600, end: 610.5, text: "So when did you first go back there?" }, { start: 610.5, end: 628, text: "Like I said before, spring twenty nineteen." }, { start: 628, end: 640, text: "And I have been back once since." }, ], }; export function buildFixtureMomentViews( report = readFixtureReport(), view = buildFixtureReportView(report), ): MomentPageView[] { const citedIn = buildCitedIn([report]); const out: MomentPageView[] = []; const seen = new Set(); for (const [id, c] of Object.entries(report.citations ?? {})) { const key = momentKeyOf(c); if (!key || seen.has(key) || !view.citations[id]) continue; seen.add(key); const m = parseMomentKey(key)!; const entries = citedInViews(citedIn[key] ?? [], [view]); if (c.kind === "video" || c.kind === "audio") { const span = m as SpanMoment; const before = c.pad?.before ?? 0; const after = c.pad?.after ?? 0; out.push({ format: MOMENT_PAGE_FORMAT, version: REPORT_VIEWS_VERSION, key, kind: c.kind, record: recordFor(c.channel, c.id, span.start), quote: c.quote, ...(c.speaker ? { speaker: c.speaker } : {}), ...(c.verification ? { verification: c.verification } : {}), start: span.start, end: span.end, clip: { // A clip cut as sound is an .m4a beside the video clips (compose's // clipPath). src: c.kind === "audio" ? evidenceClipPath(span).replace(/\.mp4$/, ".m4a") : evidenceClipPath(span), start: Math.max(0, span.start - before), end: span.end + after, ...(c.kind === "video" ? { poster: POSTER } : {}), }, cues: (CUES[`${c.channel}/${c.id}`] ?? []).map((q) => ({ ...q, inSpan: q.end > span.start && q.start < span.end, })), citedIn: entries, }); } else if (c.kind === "post") { out.push({ format: MOMENT_PAGE_FORMAT, version: REPORT_VIEWS_VERSION, key, kind: "post", record: recordFor(c.channel, c.id), quote: c.quote, ...(c.date ? { date: c.date } : {}), post: { author: "@demo_account", text: `${c.quote}\n\nPosted to settle it.`, shot: shotFor(c) }, citedIn: entries, }); } } return out; } // Every file of the fixture's public dir, by site-root path, as JSON values. export function buildFixtureFiles(): Record { const report = readFixtureReport(); const view = buildFixtureReportView(report); const moments = buildFixtureMomentViews(report, view); const index: ReportIndexView = { format: REPORT_INDEX_FORMAT, version: REPORT_VIEWS_VERSION, reports: [reportIndexEntry(view)], }; const files: Record = { [REPORTS_INDEX_PATH]: index, [reportViewPath(view.id)]: view, [MOMENTS_INDEX_PATH]: { format: MOMENT_INDEX_FORMAT, version: REPORT_VIEWS_VERSION, moments: moments.map((m) => m.key), }, }; for (const m of moments) files[momentViewPath(m.key)] = m; return files; } export function fixtureFilePath(urlPath: string): string { return path.join(FIXTURE_PUBLIC_DIR, ...urlPath.split("/").filter(Boolean)); } export function serializeFixture(value: unknown): string { return `${JSON.stringify(value, null, 2)}\n`; }