commit 5036cb4985d34c0381074fb66f000393b7f2d466
parent 192a13e1aff217f861194186ad7f3e3795553bef
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 9 Oct 2026 17:53:36 -0400
mcp: get_report reads a report's timeline, newest first; `section` may name an entry
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 64 insertions(+), 7 deletions(-)
diff --git a/mcp/src/reports.test.ts b/mcp/src/reports.test.ts
@@ -14,7 +14,7 @@ import {
import { CONTRACT } from "yt-dlp-transcript-common/lib/archive/contract";
import { LocalSource, type ShardSource } from "./source";
import { createServer } from "./server";
-import { renderReportIndex, reportsLine } from "./reports";
+import { renderReportIndex, renderReportPage, reportsLine } from "./reports";
// list_reports / get_report and the "cited-only site" line, over a composed
// public dir on disk read by the real LocalSource.
@@ -222,3 +222,33 @@ test("a source with no reports of its own (a hub) points at its members", () =>
assert.match(out, /reports are per site/);
assert.equal(reportsLine({ supported: false, scope: "full", reports: [] }), null);
});
+
+test("get_report's text: the timeline newest first, dated, before the sections; `section` may name an entry", () => {
+ const view = buildReportPageView(
+ {
+ ...REPORT,
+ entries: [
+ { id: "e-old", date: "2026-10-02", title: "The first update", body: "It began [here](cite:v1)." },
+ { id: "e-new", date: "2026-10-05T09:30:00Z", updated: "2026-10-06", title: "The second update", body: "Then more." },
+ ],
+ },
+ { record: (c) => ({ channel: c.channel, id: c.id, title: `Record ${c.id}` }) },
+ );
+ const text = renderReportPage(view, ORIGIN);
+ const at = (s: string) => text.indexOf(s);
+ assert.ok(at("## Timeline (newest first)") > 0);
+ assert.ok(at("### 2026-10-05T09:30:00Z (updated 2026-10-06) — The second update (#e-new)") > at("## Timeline"));
+ assert.ok(at("### 2026-10-02 — The first update (#e-old)") > at("(#e-new)"));
+ assert.ok(at("## Section one (#one)") > at("(#e-old)"));
+ // An entry's citations, as a section body's.
+ assert.match(text, /\(#e-old\)\nIt began \[here\]\(cite:v1\)\.\n\[1\] video/);
+ // The header's update is the newest entry's.
+ assert.match(text, /updated 2026-10-06/);
+ assert.match(text, /\(sections: one, two; timeline entries: e-new, e-old — pass section:"<id>" for one\)$/);
+
+ const one = renderReportPage(view, ORIGIN, "e-old");
+ assert.match(one, /### 2026-10-02 — The first update \(#e-old\)/);
+ assert.doesNotMatch(one, /e-new|Section one/);
+ const section = renderReportPage(view, ORIGIN, "one");
+ assert.doesNotMatch(section, /Timeline/);
+});
diff --git a/mcp/src/reports.ts b/mcp/src/reports.ts
@@ -21,6 +21,7 @@ import {
verdictTally,
type CitationView,
type ClaimView,
+ type EntryView,
type ReportIndexEntry,
type ReportPageView,
type SectionView,
@@ -214,6 +215,20 @@ function renderSection(view: ReportPageView, s: SectionView, origin: string | nu
return out.join("\n");
}
+// A timeline entry: its date, title and anchor, its body, and the citations
+// it cites.
+function renderEntry(view: ReportPageView, e: EntryView, origin: string | null): string {
+ const out = [`### ${e.date}${e.updated ? ` (updated ${e.updated})` : ""} — ${e.title} (#${e.id})`];
+ out.push(e.body.trim());
+ for (const id of [...new Set(extractCiteRefs(e.body).map((r) => r.id))]) {
+ const c = view.citations[id];
+ if (c) out.push(...citationLines(c, origin));
+ }
+ return out.join("\n");
+}
+
+// One report as text. `sectionId` narrows it to one section — or one
+// timeline entry, by its id.
export function renderReportPage(
view: ReportPageView,
origin: string | null,
@@ -241,10 +256,19 @@ export function renderReportPage(
}
if (view.summary && !sectionId) head.push("", view.summary.trim());
+ // The timeline, newest first, before the sections (as the page has it).
+ const allEntries = view.entries ?? [];
+ const entries = sectionId ? allEntries.filter((e) => e.id === sectionId) : allEntries;
+ const body: string[] = [];
+ if (entries.length > 0) {
+ body.push([`## Timeline (newest first)`, ...entries.map((e) => renderEntry(view, e, origin))].join("\n\n"));
+ }
const sections = sectionId ? view.sections.filter((s) => s.id === sectionId) : view.sections;
- const body = sections.map((s) => renderSection(view, s, origin));
+ body.push(...sections.map((s) => renderSection(view, s, origin)));
const outline = sectionId
? ""
- : `\n\n(sections: ${view.sections.map((s) => s.id).join(", ")} — pass section:"<id>" for one)`;
+ : `\n\n(sections: ${view.sections.map((s) => s.id).join(", ")}` +
+ (allEntries.length > 0 ? `; timeline entries: ${allEntries.map((e) => e.id).join(", ")}` : "") +
+ ` — pass section:"<id>" for one)`;
return `${head.join("\n")}\n\n${body.join("\n\n")}${outline}`;
}
diff --git a/mcp/src/server.ts b/mcp/src/server.ts
@@ -387,7 +387,8 @@ export const TOOLS: Tool[] = [
{
name: "get_report",
description:
- "Read one published report: title, kind, verdict tally, then each " +
+ "Read one published report: title, kind, verdict tally, its timeline " +
+ "(dated entries, newest first) when it keeps one, then each " +
"section's claims (verdict, findings) with every citation's verbatim " +
"quote, original URL (the platform at the cited second, the post, the " +
"document) and the site's moment page. Ids from list_reports.",
@@ -398,7 +399,7 @@ export const TOOLS: Tool[] = [
report: { type: "string", description: "The report id." },
section: {
type: "string",
- description: "Optional: one section's id, to read just that section.",
+ description: "Optional: one section's (or timeline entry's) id, to read just that part.",
},
},
required: ["report"],
@@ -1495,9 +1496,11 @@ async function handleGetReport(
);
}
const section = typeof args.section === "string" && args.section.trim() ? args.section.trim() : undefined;
- if (section && !view.sections.some((s) => s.id === section)) {
+ if (section && !view.sections.some((s) => s.id === section) && !(view.entries ?? []).some((e) => e.id === section)) {
+ const entries = (view.entries ?? []).map((e) => e.id);
return errorText(
- `report ${id} has no section ${section} — its sections: ${view.sections.map((s) => s.id).join(", ")}`,
+ `report ${id} has no section ${section} — its sections: ${view.sections.map((s) => s.id).join(", ")}` +
+ (entries.length > 0 ? `; its timeline entries: ${entries.join(", ")}` : ""),
);
}
return text(renderReportPage(view, await reportOrigin(source), section));