// 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, its timeline's feeds (lib/report/feeds.ts, // on the site's public URL), the report's exports (HTML, PDF, // Markdown, evidence pack, by publish/reportExports.ts's writer), its two // revisions (fixture.ts FIXTURE_REVISIONS, committed by that writer into a // store in the stage) and their published history (history.json and the // dumb-HTTP clone, publish/reportHistory.ts) — 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 os from "node:os"; import path from "node:path"; import type { MomentIndexView, MomentPageView, 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 { renderReportJsonFeed, renderReportRss, reportFeedUrls } = await import("yt-dlp-transcript-common/lib/report/feeds"); const { parseSiteUrl } = await import("yt-dlp-transcript-common/lib/siteSchema"); const { MOMENTS_INDEX_PATH, REPORTS_INDEX_PATH, reportCitationsDownloadPath, reportFeedPath, reportViewPath } = await import( "yt-dlp-transcript-common/lib/report/views" ); const { FIXTURE_REVISIONS, buildFixtureReportView, fixtureReportFile, readFixtureReport } = await import( "../fixtures/report-site/fixture" ); const { publishReportHistory, readReportHistoryView } = await import("yt-dlp-transcript-common/publish/reportHistory"); const { openPlaywrightPdfPrinter, writeReportExports } = await import("yt-dlp-transcript-common/publish/reportExports"); const { sha256Hex } = await import("yt-dlp-transcript-common/publish/reportExportFiles"); const { REPORT_EXPORT_FILENAMES, REPORT_EXPORT_FORMATS, momentViewPath, reportExportDownloadPath } = await import( "yt-dlp-transcript-common/lib/report/views" ); 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 = (urlPath: string): T => JSON.parse(fs.readFileSync(pub(urlPath), "utf8")) as T; const index = readJson(REPORTS_INDEX_PATH); const moments = readJson(MOMENTS_INDEX_PATH).moments; const report = readFixtureReport(); const histories: string[] = []; 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(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)); // Its timeline's feeds, as compose writes them: on the site's public URL, // which the view must already name (fixture.ts FIXTURE_SITE_URL). const siteUrl = parseSiteUrl(site.siteUrl); if (!siteUrl) throw new Error("contract.ts: the fixture site has no public siteUrl"); if (JSON.stringify(view.feeds) !== JSON.stringify(reportFeedUrls(siteUrl, entry.id))) { throw new Error(`contract.ts: the view's feeds ${JSON.stringify(view.feeds)} are not the site's (${siteUrl})`); } fs.writeFileSync(pub(reportFeedPath(entry.id, "rss")), renderReportRss(view, { siteUrl })); fs.writeFileSync(pub(reportFeedPath(entry.id, "json")), `${JSON.stringify(renderReportJsonFeed(view, { siteUrl }), null, 2)}\n`); // The report's exports, by the host step's own writer, from the files // already in public/ (its stills, the post's shot, the clips), published // where compose publishes them. Each earlier revision is exported first (its // Markdown, which commits it); the last export commits the report as it is. const printer = openPlaywrightPdfPrinter(); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "report-site-exports-")); const dir = path.join(tmp, entry.id); const gitDir = path.join(tmp, "history-git"); const files = { local: (p: string) => (fs.existsSync(pub(p)) ? pub(p) : undefined), clip: (c: { moment: string }) => { const m = readJson(momentViewPath(c.moment)); return m.clip ? { sitePath: m.clip.src, file: pub(m.clip.src), kind: m.kind === "audio" ? ("audio" as const) : ("video" as const) } : undefined; }, }; try { for (const rev of FIXTURE_REVISIONS.slice(0, -1)) { const earlier = readFixtureReport(rev.file); const reportJson = fs.readFileSync(fixtureReportFile(rev.file)); const r = await writeReportExports({ dir: path.join(tmp, `earlier-${rev.file}`), site, report: earlier, view: buildFixtureReportView(earlier), reportSha256: sha256Hex(reportJson), files, ffmpegBin: paths.ffmpegBin, formats: ["md"], printer: () => printer, zipBin: "zip", now: new Date(rev.date), log: console.log, history: { gitDir, reportJson }, }); if (r.problems.length > 0) throw new Error(`contract.ts: revision ${rev.file}: ${JSON.stringify(r.problems)}`); } const reportJson = fs.readFileSync(fixtureReportFile()); const { result, problems } = await writeReportExports({ dir, site, report, view, reportSha256: sha256Hex(reportJson), files, ffmpegBin: paths.ffmpegBin, formats: REPORT_EXPORT_FORMATS, printer: () => printer, zipBin: "zip", now: new Date(FIXTURE_REVISIONS[FIXTURE_REVISIONS.length - 1].date), log: console.log, history: { gitDir, reportJson }, }); if (result.manifest.revision?.revision !== FIXTURE_REVISIONS.length) { throw new Error(`contract.ts: the fixture's last export is not revision ${FIXTURE_REVISIONS.length}`); } if (problems.length > 0 || result.manifest.notes.length > 0) { throw new Error(`contract.ts: the fixture's exports: ${JSON.stringify([...problems, ...result.manifest.notes])}`); } for (const f of REPORT_EXPORT_FORMATS) { fs.copyFileSync(path.join(dir, REPORT_EXPORT_FILENAMES[f]), pub(reportExportDownloadPath(entry.id, f))); } // The history, published as compose publishes it. const history = await readReportHistoryView(gitDir, { reportId: entry.id, siteUrl: site.siteUrl }); if (!history) throw new Error("contract.ts: the fixture has no revision history"); await publishReportHistory({ gitDir, publicDir: paths.exportPublicDir, view: history }); histories.push(entry.id); } finally { const p = await printer; if (!("missing" in p)) await p.close(); fs.rmSync(tmp, { recursive: true, force: true }); } } await emitFederationFiles(site, paths); await emitAiFiles(site, paths, { reports: index.reports, moments, allowed: [], histories }); console.log(`[report-site] composed the contract of cited site "${site.siteId}" into ${paths.exportPublicDir}`);