Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 95331b3e5c2f3a761f830384af59b20c3fcf1a34
parent 729f2e96aa7a62d73346dc7728ce8653c20b46e5
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  5 Oct 2026 13:05:47 -0400

export: a report-only site with one report opens on it (no nav links); the multi-report home drops its duplicate title; shell pages link home

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mexport/CHANGELOG.md | 1+
Mexport/app/(workspace)/page.tsx | 12++++++++++--
Mexport/app/components/Header.tsx | 3++-
Mexport/app/components/reports/ReportIndex.tsx | 18+++++++++++-------
Mexport/app/components/reports/parts.tsx | 9++++++---
Mexport/app/lib/nav.ts | 6++++--
Mexport/app/lib/reports.test.ts | 29+++++++++++++++++++++++++----
Mexport/e2e-report/report-site.spec.ts | 26++++++++++++--------------
8 files changed, 71 insertions(+), 33 deletions(-)

diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **A report-only site with one report opens on that report.** Its home page is the report itself and its header links nothing; with more reports the home page is the list, without repeating the site's title under the header. Pages a report-only site does not have link home. - **`pnpm start:export` serves a built site's moment pages.** It used `serve`, which listed a video or audio moment's directory (`3126.00-3151.00`) instead of serving its page; it now runs `export/scripts/serve-out.mjs`, which serves directories as Cloudflare Pages does, on `EXPORT_DEV_PORT` (3000). - **MCP: a site's reports can be read, and a site that publishes only reports says so instead of looking empty.** Two new tools: `list_reports` lists the reports a site publishes (id, title, kind, claim and citation counts, a fact-check's verdict tally, its page), and `get_report` reads one — its tally, then each section's claims with their verdicts and findings and, for every citation, the verbatim quote, the original (the platform at the cited second, the post, the document) and the site's moment page; `section` reads one section. On a site that publishes only its reports, `list_channels`, `list_sources` and `resolve_source` say "cited-only site: N report(s)" where they said "No channels found"; on a site with reports as well, `list_sources` and `resolve_source` say how many. The archive readers (local, remote) read `corpus.json` spec 5: a cited site is an empty corpus without an error, and a local copy of one is never read from a stale `transcripts/` folder. A hub has no reports of its own; `list_reports` says to name a member site. - **The hub leaves out a site that publishes only its reports.** A site with search off (`search: false`) is not a searchable archive, so it is no hub member (not in federated search, the hub's `corpus.json` or `llms.txt`) and no other site's footer links to it, whatever its **List on the Archilyzer homepage and hub** setting says. A site with reports that publishes its full corpus is a member as before. Needs a hub rebuild and deploy once such a site exists. diff --git a/export/app/(workspace)/page.tsx b/export/app/(workspace)/page.tsx @@ -1,10 +1,11 @@ import { isCitedSite } from "yt-dlp-transcript-common/lib/siteSchema"; import { currentSite } from "../lib/site"; import { instanceMode } from "../lib/mode"; -import { readReportIndex } from "../lib/reports"; +import { readReportIndex, readReportView, reportIds } from "../lib/reports"; import NewSinceLastVisit from "../components/NewSinceLastVisit"; import HubHome from "../components/hub/HubHome"; import ReportIndex from "../components/reports/ReportIndex"; +import ReportArticle from "../components/reports/ReportArticle"; export default async function Home() { // Hub mode renders the federated shelf + cross-site search and reads no local @@ -20,8 +21,15 @@ export default async function Home() { // report index (a static export has no redirect to send it there), and it // has no summaries manifest, so countTranscripts() — which throws without // one — is never reached: its module is not even loaded on this path. + // + // With ONE published report the home page IS that report (the header then + // links nothing: there is nowhere else to go). With more, the index -- no + // heading of its own: the header already names the site. if (isCitedSite(settings)) { - return <ReportIndex index={readReportIndex()} heading={settings.siteTitle} tagline={settings.homeTagline} />; + const ids = reportIds(); + const only = ids.length === 1 ? readReportView(ids[0]) : null; + if (only) return <ReportArticle view={only} />; + return <ReportIndex index={readReportIndex()} heading={null} tagline={settings.homeTagline} />; } const { countTranscripts } = await import("yt-dlp-transcript-common/lib/transcripts"); const count = await countTranscripts(); diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx @@ -14,7 +14,7 @@ import { headerMarkPalette } from "../lib/brand"; import { instanceMode } from "../lib/mode"; import { hasArchives } from "../lib/archives"; import { hasDuplicates } from "../lib/duplicates"; -import { hasReports } from "../lib/reports"; +import { hasReports, reportIds } from "../lib/reports"; import { headerNavLinks } from "../lib/nav"; import { isCitedSite } from "yt-dlp-transcript-common/lib/siteSchema"; import MobileMenu from "./MobileMenu"; @@ -83,6 +83,7 @@ export default function Header() { const cited = isSite && isCitedSite(site); const navLinks = headerNavLinks({ cited, + singleReport: cited && reportIds().length === 1, reports: isSite && hasReports(), duplicates: !cited && hasDuplicates(), downloads: !cited && hasArchives(), diff --git a/export/app/components/reports/ReportIndex.tsx b/export/app/components/reports/ReportIndex.tsx @@ -3,8 +3,10 @@ import { VerdictTally } from "yt-dlp-transcript-common/components/report/Verdict import type { ReportIndexView } from "yt-dlp-transcript-common/lib/report/views"; import { EmptyState, Eyebrow, dateLabel } from "./parts"; -// The report index: `/reports/`, and a cited site's home page. One card per -// published report, in the site's order (site.json `reports`). +// The report index: `/reports/`, and a cited site's home page when it has more +// than one report. One card per published report, in the site's order +// (site.json `reports`). `heading: null` draws no heading (the home page: the +// header already names the site). const plural = (n: number, noun: string) => `${n.toLocaleString()} ${noun}${n === 1 ? "" : "s"}`; @@ -14,7 +16,7 @@ export default function ReportIndex({ tagline, }: { index: ReportIndexView | null; - heading?: string; + heading?: string | null; tagline?: string; }) { const reports = index?.reports ?? []; @@ -23,10 +25,12 @@ export default function ReportIndex({ } return ( <div className="mx-auto flex w-full max-w-3xl flex-col gap-6"> - <header className="flex flex-col gap-1"> - <h1 className="font-display text-3xl font-semibold tracking-tight text-foreground">{heading}</h1> - {tagline && <p className="text-sm text-muted-foreground">{tagline}</p>} - </header> + {(heading || tagline) && ( + <header className="flex flex-col gap-1"> + {heading && <h1 className="font-display text-3xl font-semibold tracking-tight text-foreground">{heading}</h1>} + {tagline && <p className="text-sm text-muted-foreground">{tagline}</p>} + </header> + )} <ul data-report-index="" className="flex flex-col gap-4"> {reports.map((r) => { const dates = [ diff --git a/export/app/components/reports/parts.tsx b/export/app/components/reports/parts.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { ExternalLink } from "lucide-react"; import { sourceAnchor, type SourceView } from "yt-dlp-transcript-common/lib/report/views"; import type { SourceArchive } from "yt-dlp-transcript-common/lib/citations/schema"; +import { reportIds } from "../../lib/reports"; // Small pieces the report, index and moment pages share. @@ -88,13 +89,15 @@ export function EmptyState({ title, children }: { title: string; children: React // What a corpus-shaped route (search's chat, downloads, duplicates, offline) // renders on a CITED site, which publishes only its reports and their moments. // The route still exists in the static build (every route does); it shows -// nothing of the corpus and points at the reports. +// nothing of the corpus and points at the home page -- the index, or the one +// report. export function NotOnCitedSite({ title }: { title: string }) { + const one = reportIds().length === 1; return ( <EmptyState title={title}> This site publishes its reports only.{" "} - <Link href="/reports/" className={textLink}> - Read the reports + <Link href="/" className={textLink}> + {one ? "Read the report" : "Read the reports"} </Link> . </EmptyState> diff --git a/export/app/lib/nav.ts b/export/app/lib/nav.ts @@ -11,14 +11,16 @@ export type NavLink = { href: string; label: string; external?: boolean }; // // A CITED site (site.json `search: false`) publishes only its reports and // their moments: no search, no chat, no downloads or duplicates, and nothing -// for an AI to read — its nav is Reports alone. +// for an AI to read — its nav is Reports alone, or nothing when its one +// report IS the home page (`singleReport`). export function headerNavLinks(has: { cited: boolean; reports: boolean; duplicates: boolean; downloads: boolean; + singleReport?: boolean; }): NavLink[] { - if (has.cited) return [{ href: "/reports/", label: "Reports" }]; + if (has.cited) return has.singleReport ? [] : [{ href: "/reports/", label: "Reports" }]; return [ { href: "/", label: "Search" }, { href: "/ask/", label: "Ask AI" }, diff --git a/export/app/lib/reports.test.ts b/export/app/lib/reports.test.ts @@ -155,14 +155,33 @@ test("the index lists the report with its tally", () => { assert.ok(html.includes("5 claims · 6 citations")); }); -test("a cited site's home is the report index and never counts transcripts", async () => { +test("a cited site with one report: its home IS the report, and never counts transcripts", async () => { // There is no summaries manifest here: countTranscripts() would throw. const html = render(await home.default()); - assert.match(html, /<h1[^>]*>Demo Reports<\/h1>/); - assert.match(html, /data-report-entry="demo-factcheck"/); + assert.match(html, /<h1[^>]*>Checking an example article<\/h1>/); + assert.match(html, /data-claim=/); + assert.doesNotMatch(html, /data-report-index/); + assert.doesNotMatch(html, /Demo Reports/, "the site title is the header's, not the page's"); +}); + +test("a cited site with more than one report: its home is the index, with no heading of its own", async () => { + const file = path.join(publicDir, "reports", "index.json"); + const original = fs.readFileSync(file, "utf8"); + try { + const index = JSON.parse(original); + index.reports = [...index.reports, { ...index.reports[0], id: "demo-second", title: "A second report", href: "/reports/demo-second/" }]; + fs.writeFileSync(file, JSON.stringify(index)); + const html = render(await home.default()); + assert.match(html, /data-report-index/); + assert.match(html, /data-report-entry="demo-factcheck"/); + assert.match(html, /data-report-entry="demo-second"/); + assert.doesNotMatch(html, /<h1/, "no heading: the header already names the site"); + } finally { + fs.writeFileSync(file, original); + } }); -test("the header nav: Reports when the build has one; a cited site has Reports alone", () => { +test("the header nav: Reports when the build has one; a cited site has Reports alone, or nothing with one report", () => { const full = headerNavLinks({ cited: false, reports: true, duplicates: false, downloads: true }); assert.deepEqual( full.map((l) => l.label), @@ -172,6 +191,8 @@ test("the header nav: Reports when the build has one; a cited site has Reports a assert.deepEqual(headerNavLinks({ cited: true, reports: true, duplicates: true, downloads: true }), [ { href: "/reports/", label: "Reports" }, ]); + // One report is the home page: nowhere else to go. + assert.deepEqual(headerNavLinks({ cited: true, reports: true, duplicates: false, downloads: false, singleReport: true }), []); }); test.after(() => fs.rmSync(root, { recursive: true, force: true })); diff --git a/export/e2e-report/report-site.spec.ts b/export/e2e-report/report-site.spec.ts @@ -1,7 +1,7 @@ import { expect, test } from "./helpers"; import type { Locator, Page } from "@playwright/test"; -// A cited site, as a reader meets it: the report index as the home page, the +// A cited site, as a reader meets it: its one report as the home page, the // report (verdicts, tally, the source's sentence, numbered inline citations // with previews, the reference list), the moment pages each citation opens, // and the shell's pages that say this site publishes its reports only. Every @@ -25,25 +25,23 @@ async function expectImageLoaded(img: Locator): Promise<void> { await expect.poll(() => img.evaluate((el) => (el as HTMLImageElement).naturalWidth)).toBeGreaterThan(0); } -test("the home page is the report index, and the header links Reports alone", async ({ page }) => { +test("one report: the home page IS the report, and the header links nothing", async ({ page }) => { await page.goto("/"); - await expect(page.locator("[data-report-index]")).toBeVisible(); + await expect(page.locator("h1")).toHaveText("Checking an example article"); + await expect(page.locator("article[data-claim]")).toHaveCount(5); + await expect(page.locator("[data-report-index]")).toHaveCount(0); + await expect(page.locator("header nav a")).toHaveCount(0); +}); + +test("the report index page still lists the report, and links it", async ({ page }) => { + await page.goto("/reports/"); const entry = page.locator('[data-report-entry="demo-factcheck"]'); await expect(entry).toContainText("Checking an example article"); - await expect(page.locator("header nav a")).toHaveText(["Reports"]); - await expect(page.locator("header nav a")).toHaveAttribute("href", "/reports/"); - await entry.getByRole("link", { name: /Checking an example article/ }).first().click(); await expect(page).toHaveURL(REPORT); await expect(page.locator("h1")).toHaveText("Checking an example article"); }); -test("the report index page lists the report", async ({ page }) => { - await page.goto("/reports/"); - await expect(page.locator('[data-report-entry="demo-factcheck"]')).toContainText("Checking an example article"); - await expect(page.locator("header nav a")).toHaveText(["Reports"]); -}); - test("the report: verdict chips, the tally, the source's sentence as a still, numbered inline citations", async ({ page }) => { await page.goto(REPORT); @@ -209,8 +207,8 @@ for (const route of ["/ask/", "/downloads/", "/duplicates/"]) { test(`${route} says this site publishes its reports only`, async ({ page }) => { await page.goto(route); await expect(page.getByText("This site publishes its reports only.")).toBeVisible(); - await expect(page.getByRole("link", { name: "Read the reports" })).toHaveAttribute("href", "/reports/"); - await expect(page.locator("header nav a")).toHaveText(["Reports"]); + await expect(page.getByRole("link", { name: "Read the report" })).toHaveAttribute("href", "/"); + await expect(page.locator("header nav a")).toHaveCount(0); }); }