import { test } from "node:test"; import assert from "node:assert/strict"; import { isRefId } from "../citations/schema"; import { parseMomentKey } from "../citations/moments"; import { buildCitedIn } from "./citedIn"; import { isReportId, type Report } from "./schema"; import { validateReport } from "./validate"; import { MOMENT_PLACEHOLDER_KEY, REPORT_PLACEHOLDER_ID, CLAIM_ARCHIVES_MAX, CLAIM_ARCHIVE_MIN_OVERLAP, archiveOverlap, buildReportPageView, claimArchives, citedInViews, evidenceClipPath, foundGroups, momentViewPath, orderedCitations, reportAssetPath, reportDateParts, reportIndexEntry, reportFullTitle, reportRevisionLabel, sourceAnchor, subjectMetaParts, sectionCitations, spanLabel, verdictTally, type RecordView, type ReportPageView, } from "./views"; const report: Report = { format: "archilyzer-report", version: 1, id: "demo", kind: "factcheck", title: "A demo fact-check", summary: "It starts [here](cite:v1).", subject: { source: "s0" }, verdicts: { PARTLY: { label: "Half" } }, sources: { s0: { kind: "article", title: "An article", url: "https://example.org/a", note: "The first edition.", archives: [{ label: "archive", url: "https://archive.example.org/a" }], saved: "sources/s0/page.html", }, }, citations: { v1: { kind: "video", channel: "demo-channel", id: "abc123", start: 3126, end: 3151.004, quote: "one" }, a1: { kind: "audio", channel: "demo-podcast", id: "ep-1", start: 10.5, end: 20, quote: "two", speaker: "Guest" }, p1: { kind: "post", channel: "demo-social", id: "123", quote: "three" }, s1: { kind: "source", source: "s0", quote: "four", image: "stills/s1.png" }, w1: { kind: "page", url: "https://example.org/p", title: "A page", quote: "five" }, never: { kind: "page", url: "https://example.org/never", quote: "never cited" }, }, sections: [ { id: "one", title: "One", claims: [ { id: "c1", title: "One", text: "Claim one.", verdict: "CONTRADICTED", sourceQuote: { citation: "s1" }, findings: "See [this](cite:w1) and `[code](cite:never)`.", citations: ["v1", "a1"], }, ], }, { id: "two", title: "Two", body: "A body citing [“a [quoted] post”](cite:p1).", claims: [{ id: "c2", text: "Claim two.", verdict: "PARTLY", citations: ["a1"] }], }, ], }; const record = (channel: string, id: string): RecordView => ({ channel, id, title: `${channel} ${id}` }); const view = buildReportPageView(report, { record: (c) => record(c.channel, c.id), poster: (c) => (c.kind === "video" ? "/poster.png" : undefined), post: () => ({ author: "@someone", text: "three, and more" }), downloads: { json: "/reports/demo/citations.json" }, }); test("the fixture report here is sound", () => { assert.deepEqual(validateReport(report), []); }); test("numbers by first appearance in reading order; an uncited citation is left out", () => { const numbers = Object.fromEntries(Object.values(view.citations).map((c) => [c.id, c.number])); // summary v1; claim c1: source s1, findings w1, list v1 a1; section two body p1. assert.deepEqual(numbers, { v1: 1, s1: 2, w1: 3, a1: 4, p1: 5 }); assert.equal(view.citations.never, undefined); assert.deepEqual( orderedCitations(view).map((c) => c.id), ["v1", "s1", "w1", "a1", "p1"], ); }); test("span and post citations link to their moment page, keyed at two decimals", () => { const v1 = view.citations.v1; assert.equal(v1.kind, "video"); if (v1.kind !== "video") return; assert.equal(v1.moment, "demo-channel/abc123/3126.00-3151.00"); assert.equal(v1.href, "/m/demo-channel/abc123/3126.00-3151.00/"); assert.equal(v1.end, 3151); assert.equal(v1.poster, "/poster.png"); assert.deepEqual(v1.record, record("demo-channel", "abc123")); const p1 = view.citations.p1; assert.equal(p1.kind, "post"); if (p1.kind !== "post") return; assert.equal(p1.href, "/m/demo-social/123/"); assert.equal(p1.author, "@someone"); assert.equal(p1.text, "three, and more"); const a1 = view.citations.a1; assert.equal(a1.kind === "audio" && a1.poster, undefined); }); test("a source citation names its document by id; the document (never its saved copy) is in sources once", () => { const s1 = view.citations.s1; assert.equal(s1.kind, "source"); if (s1.kind !== "source") return; assert.equal(s1.image, "/reports/demo/stills/s1.png"); assert.equal(s1.href, "https://example.org/a"); assert.equal(s1.sourceId, "s0"); assert.equal(s1.sourceTitle, "An article"); assert.equal("source" in s1, false); assert.equal(view.subject, "s0"); assert.deepEqual(Object.keys(view.sources), ["s0"]); assert.equal(view.sources.s0.title, "An article"); assert.equal(view.sources.s0.note, "The first edition."); assert.equal("saved" in view.sources.s0, false); assert.ok(!JSON.stringify(view).includes("sources/s0/page.html")); // the archive list is in the view once assert.equal(JSON.stringify(view).split("https://archive.example.org/a").length - 1, 1); }); test("a claim carries the document's archive links whose context overlaps its sentence, at most five", () => { const sentence = "The council approved the harbor bridge budget in March after a long debate."; const archives = [ { label: "a", url: "https://x/a", context: "the council approved the harbor bridge budget" }, { label: "b", url: "https://x/b", context: "an unrelated paragraph about weather and gardens" }, { label: "c", url: "https://x/c", context: "harbor bridge" }, // too few words in common { label: "d", url: "https://x/d" }, // no context ...Array.from({ length: 7 }, (_, i) => ({ label: `m${i}`, url: `https://x/m${i}`, context: `council approved budget after debate ${i}` })), ]; assert.ok(archiveOverlap(sentence, archives[0].context) >= CLAIM_ARCHIVE_MIN_OVERLAP); assert.equal(archiveOverlap(sentence, archives[1].context), 0); assert.equal(archiveOverlap(sentence, archives[2].context), 0); assert.equal(archiveOverlap(sentence, undefined), 0); const found = claimArchives(sentence, archives); assert.equal(found.length, CLAIM_ARCHIVES_MAX); assert.equal(found[0].label, "a", "kept in the document's order"); assert.ok(!found.some((a) => a.label === "b" || a.label === "c" || a.label === "d")); assert.deepEqual(claimArchives(sentence, []), []); }); test("the builder matches a claim's archives against its source sentence, else its text", () => { const withArchives: Report = { ...report, sources: { s0: { ...report.sources!.s0, archives: [ { label: "in four", url: "https://x/4", context: "four words about something" }, { label: "elsewhere", url: "https://x/e", context: "nothing like either claim at all" }, ], }, }, citations: { ...report.citations, s1: { kind: "source", source: "s0", quote: "Four words about something said here." } }, sections: [ { id: "one", title: "One", claims: [{ id: "c1", text: "Claim one.", sourceQuote: { citation: "s1" } }] }, { id: "two", title: "Two", claims: [{ id: "c2", text: "Something about four words, the claim's own text." }] }, ], }; const v = buildReportPageView(withArchives, { record: (c) => record(c.channel, c.id) }); assert.deepEqual(v.sections[0].claims[0].archives?.map((a) => a.label), ["in four"]); assert.deepEqual(v.sections[1].claims[0].archives?.map((a) => a.label), ["in four"]); assert.equal(view.sections[0].claims[0].archives, undefined, "no context, no match"); }); test("a page citation links out; the verdict overrides are laid over the defaults", () => { const w1 = view.citations.w1; assert.equal(w1.kind === "page" && w1.href, "https://example.org/p"); assert.equal(view.verdicts.PARTLY.label, "Half"); assert.equal(view.verdicts.PARTLY.color, "#e3b23c"); assert.equal(view.verdicts.CONTRADICTED.label, "Contradicted"); }); test("the view keeps the claim structure, with the source sentence as an id", () => { assert.deepEqual(view.sections[0].claims[0], { id: "c1", title: "One", text: "Claim one.", verdict: "CONTRADICTED", sourceQuote: "s1", findings: "See [this](cite:w1) and `[code](cite:never)`.", citations: ["v1", "a1"], }); assert.deepEqual(view.sections[1].claims[0].citations, ["a1"]); assert.deepEqual(view.downloads, { json: "/reports/demo/citations.json" }); }); test("a section's citations, in number order; code is not citing", () => { assert.deepEqual( sectionCitations(view, view.sections[0]).map((c) => c.id), ["v1", "s1", "w1", "a1"], ); assert.deepEqual( sectionCitations(view, view.sections[1]).map((c) => c.id), ["a1", "p1"], ); }); test("the tally counts verdicts in the vocabulary's order; the index entry carries it", () => { assert.deepEqual(verdictTally(view), [ { verdict: "PARTLY", count: 1 }, { verdict: "CONTRADICTED", count: 1 }, ]); const entry = reportIndexEntry(view); assert.equal(entry.href, "/reports/demo/"); assert.equal(entry.citationCount, 5); assert.equal(entry.claimCount, 2); assert.deepEqual(entry.tally, verdictTally(view)); assert.equal(reportIndexEntry({ ...view, kind: "sweep" }).tally, undefined); }); test("cited-in entries resolve to the claim's anchor with titles and the citation's number", () => { const index = buildCitedIn([report]); const entries = citedInViews(index["demo-podcast/ep-1/10.50-20.00"], [view]); assert.deepEqual( entries.map((e) => [e.href, e.claimTitle, e.claimText, e.verdict, e.verdictStyle?.label, e.number]), [ ["/reports/demo/#c1", "One", "Claim one.", "CONTRADICTED", "Contradicted", 4], ["/reports/demo/#c2", undefined, "Claim two.", "PARTLY", "Half", 4], ], ); const summary = citedInViews(index["demo-channel/abc123/3126.00-3151.00"], [view]); assert.equal(summary[0].href, "/reports/demo/"); assert.equal(citedInViews(index["demo-social/123"], []).length, 0); }); test("paths: the clip, the moment view and a report asset", () => { assert.equal( evidenceClipPath({ kind: "span", channel: "demo-channel", id: "-abc", start: 1, end: 2.5 }), "/media/clips/demo-channel/-abc/1.00-2.50.mp4", ); assert.equal(momentViewPath("demo-social/123"), "/m/demo-social/123/moment.json"); assert.equal(reportAssetPath("demo", "./stills/x.png"), "/reports/demo/stills/x.png"); assert.equal(sourceAnchor("s0"), "source-s0"); }); test("the placeholder params can never be a report or a moment", () => { assert.equal(isReportId(REPORT_PLACEHOLDER_ID), false); assert.equal(parseMomentKey(MOMENT_PLACEHOLDER_KEY), null); assert.equal(isRefId("x"), true); }); test("a span reads as clock times", () => { assert.equal(spanLabel(3126, 3151), "52:06–52:31"); assert.equal(spanLabel(3725.9, 3730), "1:02:05–1:02:10"); }); test("a citation with no moment key, or a source the report lacks, fails the build loudly", () => { const bad: Report = { ...report, summary: "[x](cite:bad)", citations: { bad: { kind: "video", channel: "../up", id: "abc", start: 1, end: 2, quote: "q" } }, sections: [], }; assert.throws(() => buildReportPageView(bad, { record: (c) => record(c.channel, c.id) }), /no moment key/); const noSource: Report = { ...report, summary: "[x](cite:s)", sources: {}, subject: undefined, citations: { s: { kind: "source", source: "gone", quote: "q" } }, sections: [], }; assert.throws(() => buildReportPageView(noSource, { record: (c) => record(c.channel, c.id) }), /does not define/); }); test("a report's series rides its views and leads its one-line name", () => { const series = { ...report, series: "On the Record" }; assert.deepEqual(validateReport(series), []); const v = buildReportPageView(series, { record: (c) => record(c.channel, c.id) }); assert.equal(v.series, "On the Record"); assert.equal(reportIndexEntry(v).series, "On the Record"); assert.equal(reportFullTitle(v), `On the Record: ${report.title}`); assert.equal(reportFullTitle(view), report.title, "no series: the title alone"); const index = buildCitedIn([series]); const key = Object.keys(index)[0]; assert.ok(citedInViews(index[key], [v]).every((e) => e.reportTitle === `On the Record: ${report.title}`)); }); test("a claim's flag rides its view; a claim without one carries none", () => { const flagged = structuredClone(report); flagged.sections[0].claims![0].flag = "No source given"; assert.deepEqual(validateReport(flagged), []); const v = buildReportPageView(flagged, { record: (c) => record(c.channel, c.id) }); assert.equal(v.sections[0].claims[0].flag, "No source given"); assert.equal("flag" in view.sections[0].claims[0], false); }); test("the header's date line: the published date, 'updated' when it differs, then the revision", () => { assert.deepEqual(reportDateParts({ published: "2026-10-04", updated: "2026-10-05T09:00:00Z" }), ["2026-10-04", "updated 2026-10-05"]); assert.deepEqual(reportDateParts({ published: "2026-10-04", updated: "2026-10-04" }), ["2026-10-04"], "no 'updated' when the same"); assert.deepEqual(reportDateParts({ updated: "2026-10" }), ["updated 2026-10"]); assert.deepEqual(reportDateParts({}), []); assert.equal(reportRevisionLabel(1), "revision 1"); assert.equal(reportRevisionLabel(2, false), "edited since revision 2"); }); test("the document under review is named by its author, publisher and date, the parts it has", () => { assert.deepEqual(subjectMetaParts({ author: "A. Writer", publisher: "Gazette", date: "2026-09-30T08:00:00Z" }), [ "A. Writer", "Gazette", "2026-09-30", ]); assert.deepEqual(subjectMetaParts({ publisher: " Gazette " }), ["Gazette"]); assert.deepEqual(subjectMetaParts({ author: " " }), []); }); test("a source's accent rides its view and its sentences' views; only #rrggbb is a source accent", () => { const accented = structuredClone(report); accented.sources!.s0.accent = "#8a6fb0"; assert.deepEqual(validateReport(accented), []); const v = buildReportPageView(accented, { record: (c) => record(c.channel, c.id) }); assert.equal(v.sources.s0.accent, "#8a6fb0"); const sentence = Object.values(v.citations).find((c) => c.kind === "source"); assert.ok(sentence && sentence.kind === "source"); assert.equal(sentence.sourceAccent, "#8a6fb0"); assert.equal("accent" in view.sources.s0, false, "absent when the source has none"); for (const bad of ["purple", "#abc", "#8a6fb0; color: red"]) { const b = structuredClone(report); (b.sources!.s0 as Record).accent = bad; assert.ok(validateReport(b).some((p) => p.path === "sources.s0.accent"), bad); } }); test("a citation's origin rides its view; a report with no subject carries none", () => { const withOrigin = structuredClone(report); const ids = Object.keys(withOrigin.citations!); withOrigin.citations![ids[0]].origin = "added"; withOrigin.citations![ids[1]].origin = "subject"; assert.deepEqual(validateReport(withOrigin), []); const v = buildReportPageView(withOrigin, { record: (c) => record(c.channel, c.id) }); assert.equal(v.citations[ids[0]]?.origin, "added"); assert.equal(v.citations[ids[1]]?.origin, "subject"); assert.ok(Object.values(view.citations).every((c) => !("origin" in c)), "absent when not given"); const noSubject = structuredClone(withOrigin); delete noSubject.subject; const paths = validateReport(noSubject).map((p) => p.path); assert.ok(paths.includes(`citations.${ids[0]}.origin`), paths.join()); const bad = structuredClone(report); (bad.citations![ids[0]] as Record).origin = "found"; assert.ok(validateReport(bad).length > 0); }); test("what the check found: ruled claims by verdict, changed first, confirmed last; gist and method ride the view", () => { const r = structuredClone(report); r.method = "Searched the transcripts."; r.sections[0].claims![0].gist = "One line."; const v = buildReportPageView(r, { record: (c) => record(c.channel, c.id) }); assert.equal(v.method, "Searched the transcripts."); assert.equal(v.sections[0].claims[0].gist, "One line."); assert.equal("method" in view, false); const order = ["CONTRADICTED", "NOT_FOUND", "PARTLY", "UNTESTABLE", "CORROBORATED"]; const groups = foundGroups({ sections: [ { id: "s", title: "S", claims: [ { id: "a", text: "a", verdict: "CORROBORATED", citations: [] }, { id: "b", text: "b", verdict: "CONTRADICTED", citations: [] }, { id: "c", text: "c", citations: [] }, { id: "d", text: "d", verdict: "PARTLY", citations: [] }, { id: "e", text: "e", verdict: "CONTRADICTED", citations: [] }, ], }, ], }); assert.deepEqual(groups.map((g) => [g.verdict, g.claims.map((c) => c.id)]), [ ["CONTRADICTED", ["b", "e"]], ["PARTLY", ["d"]], ["CORROBORATED", ["a"]], ]); assert.ok(groups.every((g, i) => i === 0 || order.indexOf(g.verdict) > order.indexOf(groups[i - 1].verdict))); // gist: one line of at most 240; method cites nothing const bad = structuredClone(r); bad.sections[0].claims![0].gist = "two\nlines"; bad.method = "See [this](cite:v1)."; assert.deepEqual(validateReport(bad).map((p) => p.path).sort(), ["method", "sections[0].claims[0].gist"]); const long = structuredClone(r); long.sections[0].claims![0].gist = "x".repeat(241); assert.ok(validateReport(long).some((p) => p.path === "sections[0].claims[0].gist")); }); test("a report's video rides its page view as site-root paths; a report without one carries none", () => { const withVideo = { ...report, video: { src: "video.mp4", poster: "media/poster.jpg", caption: "The cut" } }; assert.deepEqual(validateReport(withVideo), []); const v = buildReportPageView(withVideo, { record: (c) => record(c.channel, c.id) }); assert.deepEqual(v.video, { src: `/reports/${report.id}/video.mp4`, poster: `/reports/${report.id}/media/poster.jpg`, caption: "The cut" }); assert.equal("video" in view, false); });