// The timeline (report.json `entries`): the schema and its refusals, the one // newest-first order and what reads it — the citation numbering, cited-in, // the page view and the index, the slides, the exports, the change summary. import { test } from "node:test"; import assert from "node:assert/strict"; import { buildCitedIn } from "./citedIn"; import { effectiveUpdated, entriesNewestFirst, entryOrder, reportDateMs } from "./entries"; import { reportExportHtml } from "./exportHtml"; import { reportExportMarkdown } from "./exportMarkdown"; import { reportChangeSummary } from "./revisions"; import type { Report } from "./schema"; import { buildReportSlides, reportPageAnchorIds, slideForAnchor } from "./slides"; import { reportCitationUses } from "./uses"; import { validateReport } from "./validate"; import { buildReportPageView, citedInHref, citedInViews, reportIndexEntry, type ReportPageView } from "./views"; function report(): Report { return { format: "archilyzer-report", version: 1, id: "living", kind: "sweep", title: "A living report", summary: "It starts [here](cite:v1).", published: "2026-10-01", citations: { v1: { kind: "video", channel: "demo-channel", id: "abc123", start: 10, end: 20, quote: "one" }, w1: { kind: "page", url: "https://example.org/p", title: "A page", quote: "two" }, w2: { kind: "page", url: "https://example.org/q", title: "Another", quote: "three" }, }, // In the file oldest first, as an author appends; one undated-by-time // pair on one day keeps its file order. entries: [ { id: "e1", date: "2026-10-02", title: "The first update", body: "It [began](cite:w2)." }, { id: "e2", date: "2026-10-05T09:30:00Z", title: "The third update", body: "Then [this](cite:w1)." }, { id: "e3", date: "2026-10-03", title: "The second update, a", body: "More." }, { id: "e4", date: "2026-10-03", title: "The second update, b", body: "And more.", updated: "2026-10-04" }, ], sections: [{ id: "s1", title: "Section", body: "A body citing [the page](cite:w1) and [one](cite:v1)." }], }; } const view = (r: Report = report()): ReportPageView => buildReportPageView(r, { record: (c) => ({ channel: c.channel, id: c.id, title: `Record ${c.id}` }), feeds: { rss: "https://site.example/reports/living/feed.xml", json: "https://site.example/reports/living/feed.json" }, }); test("a report with a timeline is sound", () => { assert.deepEqual(validateReport(report()), []); }); test("the order: newest first by date (a day is midnight UTC), the same instant in file order", () => { assert.deepEqual(entryOrder(report().entries!), [1, 2, 3, 0]); assert.deepEqual(entriesNewestFirst(report().entries!).map((e) => e.id), ["e2", "e3", "e4", "e1"]); assert.equal(reportDateMs("2026-10-03"), Date.parse("2026-10-03T00:00:00Z")); assert.ok(Number.isNaN(reportDateMs("yesterday"))); // A date that does not parse sorts last (the validator refuses it anyway). assert.deepEqual(entryOrder([{ date: "nope" }, { date: "2026-01-01" }]), [1, 0]); }); test("refused: a duplicate id, an id shared with a section or claim, a page anchor, a bad id", () => { const msgs = (mutate: (r: Report) => void) => { const r = report(); mutate(r); return validateReport(r).map((p) => `${p.path}: ${p.message}`); }; assert.deepEqual(msgs((r) => (r.entries![1].id = "e1")), ['entries[1].id: "e1" is already the id of entries[0]']); assert.deepEqual(msgs((r) => (r.entries![0].id = "s1")), ['entries[0].id: "s1" is already the id of sections[0]']); assert.match(msgs((r) => (r.entries![0].id = "found"))[0], /^entries\[0\]\.id: "found" is the report page's own anchor/); assert.match(msgs((r) => (r.entries![0].id = "has space"))[0], /^entries\[0\]\.id: is not a reference id/); }); test("refused: a blank or two-line title, a blank body, a bad date, an updated before its date", () => { const msgs = (mutate: (r: Report) => void) => { const r = report(); mutate(r); return validateReport(r).map((p) => `${p.path}: ${p.message}`); }; assert.deepEqual(msgs((r) => (r.entries![0].title = " ")), ["entries[0].title: must not be blank"]); assert.deepEqual(msgs((r) => (r.entries![0].title = "a\nb")), ["entries[0].title: must be one line"]); assert.deepEqual(msgs((r) => (r.entries![0].body = "\n")), ["entries[0].body: must not be blank"]); assert.deepEqual(msgs((r) => (r.entries![0].date = "2026-13-40")), [ "entries[0].date: must be YYYY-MM-DD or an ISO 8601 date-time with a zone", ]); assert.deepEqual(msgs((r) => (r.entries![0].date = "2026-10-02T10:00:00")), [ "entries[0].date: must be YYYY-MM-DD or an ISO 8601 date-time with a zone", ]); assert.deepEqual(msgs((r) => (r.entries![3].updated = "soon")), [ "entries[3].updated: must be YYYY-MM-DD or an ISO 8601 date-time with a zone", ]); assert.deepEqual(msgs((r) => (r.entries![3].updated = "2026-10-02")), ["entries[3].updated: is before the entry's date"]); }); test("refused: an entry citing what the report does not define; an unknown key", () => { const r = report(); r.entries![0].body = "See [that](cite:nope) and [this](cite:)."; assert.deepEqual( validateReport(r).map((p) => `${p.path}: ${p.message}`), [ 'entries[0].body: the link [that](cite:nope) names no citation ("nope" is not in citations)', "entries[0].body: the link [this](cite:) names no citation", ], ); const raw = { ...report(), entries: [{ id: "x", date: "2026-10-02", title: "t", body: "b", extra: 1 }] }; assert.ok(validateReport(raw).some((p) => p.path.startsWith("entries[0]"))); }); test("uses and numbers: the summary, then the entries newest first, then the sections", () => { const uses = reportCitationUses(report()).map((u) => `${u.field}:${u.entryId ?? u.sectionId ?? "-"}:${u.citationId}`); assert.deepEqual(uses, ["summary:-:v1", "entry:e2:w1", "entry:e1:w2", "body:s1:w1", "body:s1:v1"]); const v = view(); assert.deepEqual(Object.fromEntries(Object.values(v.citations).map((c) => [c.id, c.number])), { v1: 1, w1: 2, w2: 3 }); // A citation only an entry cites is in the view (and so in its references). assert.ok(v.citations.w2); // The path names the entry's place in the file, not on the page. assert.deepEqual(reportCitationUses(report()).find((u) => u.entryId === "e1")?.path, ["entries", 0, "body"]); }); test("cited-in: a citation in an entry links to the entry's anchor", () => { const r: Report = { ...report(), entries: [{ id: "e9", date: "2026-10-02", title: "Clip", body: "Watch [it](cite:v1)." }], }; const index = buildCitedIn([r]); const entries = index["demo-channel/abc123/10.00-20.00"]; assert.deepEqual(entries[1], { reportId: "living", sectionId: null, claimId: null, entryId: "e9", citationId: "v1" }); assert.equal(citedInHref(entries[1]), "/reports/living/#e9"); assert.equal(citedInHref(entries[0]), "/reports/living/"); const views = citedInViews(entries, [view(r)]); assert.equal(views[1].entryTitle, "Clip"); }); test("the page view: entries newest first with their dates; updated counts the newest entry; feeds only with entries", () => { const v = view(); assert.deepEqual( v.entries!.map((e) => e.id), ["e2", "e3", "e4", "e1"], ); assert.deepEqual(v.entries![2], { id: "e4", date: "2026-10-03", updated: "2026-10-04", title: "The second update, b", body: "And more.", }); assert.equal(v.updated, "2026-10-05T09:30:00Z"); assert.equal(reportIndexEntry(v).updated, "2026-10-05T09:30:00Z"); assert.equal(v.feeds?.rss, "https://site.example/reports/living/feed.xml"); const none = view({ ...report(), entries: undefined }); assert.equal(none.entries, undefined); assert.equal(none.feeds, undefined); assert.equal(none.updated, undefined); assert.equal(view({ ...report(), entries: [] }).entries, undefined); }); test("effective updated: the latest of updated and the entries' dates, never before published", () => { assert.equal(effectiveUpdated({ published: "2026-10-01" }), undefined); assert.equal(effectiveUpdated({ published: "2026-10-01", updated: "2026-10-09" , entries: [{ date: "2026-10-03" }] }), "2026-10-09"); assert.equal(effectiveUpdated({ published: "2026-10-01", entries: [{ date: "2026-10-03", updated: "2026-10-07" }] }), "2026-10-07"); // An entry dated before publication does not make the report "updated". assert.equal(effectiveUpdated({ published: "2026-10-01", entries: [{ date: "2026-09-01" }] }), undefined); assert.equal(effectiveUpdated({ entries: [{ date: "2026-09-01" }] }), "2026-09-01"); }); test("slides: one per entry, newest first, after In brief; each anchored to its entry", () => { const v = view(); const slides = buildReportSlides(v); assert.deepEqual( slides.map((s) => s.key), ["title", "summary", "entry:e2", "entry:e3", "entry:e4", "entry:e1", "section:s1", "sources"], ); const e2 = slides[2]; assert.equal(e2.kind, "entry"); if (e2.kind !== "entry") return; assert.deepEqual( { anchorId: e2.anchorId, title: e2.title, date: e2.date, datetime: e2.datetime, index: e2.index, count: e2.count, text: e2.text }, { anchorId: "e2", title: "The third update", date: "2026-10-05", datetime: "2026-10-05T09:30:00Z", index: 1, count: 4, text: "Then [this](cite:w1)." }, ); // The page's anchors in the page's order, entries after In brief. const order = reportPageAnchorIds(v); assert.deepEqual(order.slice(0, 6), ["report-head", "in-brief", "e2", "e3", "e4", "e1"]); assert.equal(slideForAnchor(slides, "e4", order)?.key, "entry:e4"); // A report with no timeline: no entry slides. assert.equal(buildReportSlides(view({ ...report(), entries: undefined })).filter((s) => s.kind === "entry").length, 0); }); const FOOTER = { reportSha256: "ab".repeat(32) }; test("the HTML export: the Timeline after In brief and before the sections, newest first, each anchored", () => { const html = reportExportHtml(view(), { footer: FOOTER, image: () => undefined, siteUrl: "https://site.example" }); const at = (s: string) => html.indexOf(s); assert.ok(at('aria-label="In brief"') < at("