// EVERY PLACE A REPORT CITES, in reading order — the one walk the validator, // the numbering and the back-link index share, so they cannot disagree about // what a report cites or in what order. // // Reading order: the summary; then the timeline's entries, newest first // (./entries.ts, as the page shows them); then each section's body, and each of its // claims in turn — the claim's source sentence, its findings, then the // citations listed under it. Within a markdown field, `cite:` links in the // order they are written (lib/citations/inline.ts). // // Pure, no imports but types and the inline parser: the export site can use it. import { extractCiteRefs, numberCitations } from "../citations/inline"; import type { PathSegment } from "../citations/validate"; import { entryOrder } from "./entries"; import type { Report } from "./schema"; export type CitationUseField = "summary" | "entry" | "body" | "sourceQuote" | "findings" | "citations"; export type CitationUse = { citationId: string; // The section and claim the use is in; null in the summary or an entry // (both) or a section's body (the claim). sectionId: string | null; claimId: string | null; // The timeline entry the use is in (field `entry`); absent elsewhere. entryId?: string; field: CitationUseField; // The JSON path of the field (with the list index for `citations`). path: PathSegment[]; // An inline link's label; absent for a listed citation. label?: string; }; export function reportCitationUses(report: Report): CitationUse[] { const out: CitationUse[] = []; const inline = ( md: string | undefined, field: CitationUseField, path: PathSegment[], sectionId: string | null, claimId: string | null, ) => { for (const ref of extractCiteRefs(md)) { out.push({ citationId: ref.id, sectionId, claimId, field, path, label: ref.label }); } }; inline(report.summary, "summary", ["summary"], null, null); const entries = report.entries ?? []; for (const i of entryOrder(entries)) { for (const ref of extractCiteRefs(entries[i].body)) { out.push({ citationId: ref.id, sectionId: null, claimId: null, entryId: entries[i].id, field: "entry", path: ["entries", i, "body"], label: ref.label, }); } } report.sections.forEach((section, si) => { const sp: PathSegment[] = ["sections", si]; inline(section.body, "body", [...sp, "body"], section.id, null); (section.claims ?? []).forEach((claim, ci) => { const cp: PathSegment[] = [...sp, "claims", ci]; if (claim.sourceQuote) { out.push({ citationId: claim.sourceQuote.citation, sectionId: section.id, claimId: claim.id, field: "sourceQuote", path: [...cp, "sourceQuote", "citation"], }); } inline(claim.findings, "findings", [...cp, "findings"], section.id, claim.id); (claim.citations ?? []).forEach((id, i) => { out.push({ citationId: id, sectionId: section.id, claimId: claim.id, field: "citations", path: [...cp, "citations", i], }); }); }); }); return out; } // Each citation's number in the report, from 1, by first appearance in reading // order. Only citations the report defines are numbered (a dangling reference // is a validation problem, not a number); a citation the report defines but // never cites has none. export function reportCitationNumbers(report: Report): Map { const defined = report.citations ?? {}; return numberCitations( reportCitationUses(report) .map((u) => u.citationId) .filter((id) => Object.hasOwn(defined, id)), ); }