Archilyzer · Source

archilyzer

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

commit b1031364f59c1dfeb92a795340c0c7eb1682eea0
parent ab5d4fae7d8cc8c4796443b32cbc1885e5aa529f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 30 Sep 2026 19:42:46 -0400

Merge r16/research-setup (release 16 slice DX) — the research-only setup told in one place: a Ten-minute setup on the homepage's AI and MCP doc (clone or tarball, pnpm install, claude mcp add archilyzer, claude, /ask); the sites' and the hub's Use-with-AI page is removed and its header, footer, Ask AI, corpus.json and llms.txt links point at the doc; the READMEs register archilyzer; reviewed SHIP

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
MREADME.md | 7++++---
Mcommon/bin/compose-site.ts | 2+-
Mcommon/lib/corpus.test.ts | 37++++++++++++++++++++++++++++++++++---
Mcommon/lib/corpus.ts | 22++++++++++++++--------
Mcommon/lib/project.ts | 6++++++
Mexport/CHANGELOG.md | 1+
Mexport/app/(workspace)/ask/page.tsx | 6+++---
Mexport/app/components/Footer.tsx | 4+++-
Mexport/app/components/Header.tsx | 36++++++++++++++++++++++++------------
Mexport/app/components/MobileMenu.tsx | 25++++++++++++++++++-------
Dexport/app/use-with-ai/page.tsx | 139-------------------------------------------------------------------------------
Aexport/e2e-hub/use-with-ai-link.spec.ts | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mexport/e2e/first-search.spec.ts | 26++++++++++++++++++++------
Mexport/e2e/responsive.spec.ts | 9++++++++-
Mexport/e2e/restore-no-refire.spec.ts | 29++++++++++++++++++-----------
Aexport/e2e/use-with-ai-link.spec.ts | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Mhomepage/CHANGELOG.md | 1+
Mhomepage/content/README.md | 2+-
Mhomepage/content/docs/ai-and-mcp.md | 40++++++++++++++++++++++++++++++++++++++--
Mhomepage/e2e/docs.spec.ts | 34++++++++++++++++++++++++++++++++++
Mmcp/README.md | 15++++++++-------
Mplans/FACTS.md | 33+++++++++++++++++++++++++++++++++
Mplans/STATE.md | 5+++++
Mplans/release-16.md | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
24 files changed, 521 insertions(+), 206 deletions(-)

diff --git a/README.md b/README.md @@ -63,14 +63,14 @@ TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ ``` There is nothing to compile — it runs from source through `tsx`. To register it with -Claude Code: +Claude Code, from the repo's root: ```bash claude mcp add archilyzer \ --env TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ --env ARCHILYZER_EDITOR_URL=http://localhost:3001 \ --env WORKER_TOKEN=… \ - -- pnpm -C /ABS/PATH/TO/this/repo --filter yt-dlp-transcript-mcp exec tsx src/index.ts + -- pnpm -C "$PWD" --filter yt-dlp-transcript-mcp exec tsx src/index.ts ``` The two editor lines are optional: they let `fetch_clip` ask a local editor for clip @@ -318,7 +318,8 @@ quietly sampling. ### Quickstart (no corpus required) ```bash -pnpm install +git clone https://archilyzer.pages.dev/source/archilyzer.git archilyzer # or the tarball on archilyzer.pages.dev/downloads/ +cd archilyzer && pnpm install claude mcp add archilyzer \ --env TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ --env ARCHILYZER_EDITOR_URL=http://localhost:3001 \ diff --git a/common/bin/compose-site.ts b/common/bin/compose-site.ts @@ -179,7 +179,7 @@ async function emitAiFiles(paths: ReturnType<typeof getPaths>): Promise<void> { // siteUrl; otherwise clear any stale copy from a previous build. const sitemapPath = path.join(paths.exportPublicDir, "sitemap.xml"); if (descriptor.siteUrl) { - const routes = ["/", "/use-with-ai", "/changelog"]; + const routes = ["/", "/changelog"]; if (hasArchives) routes.push("/downloads"); if (await exists(path.join(paths.exportPublicDir, DUPLICATES_FILENAME))) { routes.push("/duplicates"); diff --git a/common/lib/corpus.test.ts b/common/lib/corpus.test.ts @@ -9,7 +9,7 @@ import { renderSitemapXml, CORPUS_SPEC_VERSION, } from "./corpus"; -import { PROJECT_GENERATOR } from "./project"; +import { AI_DOC_URL, PROJECT_GENERATOR } from "./project"; import type { PublicSiteDescriptor } from "./siteDescriptor"; // Run with: @@ -159,6 +159,37 @@ test("both builders stamp the project generator, and llms.txt trails it", () => } }); +test("Use with AI is the homepage's AI and MCP doc; the chat is the instance's /ask/", () => { + // Release 16 slice DX: no site or hub has a /use-with-ai page. corpus.json + // keeps the key and names the doc; llms.txt's Ask AI section is two lines. + const site = buildSiteCorpus(descriptor({ siteUrl: "https://demo.example" }), { + hasArchives: false, + }); + const hub = buildHubCorpus( + [{ siteId: "a", siteTitle: "A", siteUrl: "https://a.example" }], + { hubTitle: "The Hub", hubUrl: "https://hub.example", generatedAt: "t" }, + ); + assert.equal(site.useWithAi, AI_DOC_URL); + assert.equal(hub.useWithAi, AI_DOC_URL); + assert.equal(AI_DOC_URL, "https://archilyzer.pages.dev/docs/ai-and-mcp/"); + + const siteLines = renderSiteLlmsTxt(site).split("\n"); + const siteAt = siteLines.indexOf("## Ask AI"); + assert.deepEqual(siteLines.slice(siteAt + 1, siteAt + 3), [ + "- [Ask AI](https://demo.example/ask/): in-browser chat (bring your own API key).", + `- [Use with AI](${AI_DOC_URL}): MCP-server setup for Claude Code, Cursor, and other tools.`, + ]); + const hubLines = renderHubLlmsTxt(hub).split("\n"); + const hubAt = hubLines.indexOf("## Ask AI"); + assert.deepEqual(hubLines.slice(hubAt + 1, hubAt + 3), [ + "- [Ask AI](https://hub.example/ask/): ask across the whole federation (bring your own API key).", + `- [Use with AI](${AI_DOC_URL}): wire up the MCP server.`, + ]); + for (const txt of [renderSiteLlmsTxt(site), renderHubLlmsTxt(hub)]) { + assert.doesNotMatch(txt, /use-with-ai/); + } +}); + test("the spec is 4, and `generator` is still not why", () => { // Guard on the reasoning, not just the number: a bump announces a new // FETCHABLE document. spec 4 is /tags.json. The informational credit string @@ -207,8 +238,8 @@ test("renderRobotsTxt: sitemap line only with an absolute siteUrl", () => { test("renderSitemapXml: one loc per route", () => { const xml = renderSitemapXml({ siteUrl: "https://demo.example", - routes: ["/", "/use-with-ai"], + routes: ["/", "/changelog"], }); assert.match(xml, /<loc>https:\/\/demo\.example\/<\/loc>/); - assert.match(xml, /<loc>https:\/\/demo\.example\/use-with-ai<\/loc>/); + assert.match(xml, /<loc>https:\/\/demo\.example\/changelog<\/loc>/); }); diff --git a/common/lib/corpus.ts b/common/lib/corpus.ts @@ -1,5 +1,5 @@ import type { PublicSiteDescriptor } from "./siteDescriptor"; -import { PROJECT_GENERATOR } from "./project"; +import { AI_DOC_URL, PROJECT_GENERATOR } from "./project"; import { TAGS_FILENAME } from "./curatedTags"; import { CONTRACT, @@ -190,7 +190,9 @@ export type SiteCorpus = { tags?: { url: string; videoField: "curatedTags"; description: string }; // Present when this build ships bulk-download archives (whole-channel zips). bulkArchives?: { manifest: string; note: string }; - // Pointer to the human page and BYO-key chat. + // Pointer to the human page on using the archive with AI: the homepage's AI + // and MCP doc (AI_DOC_URL; the site's own /use-with-ai page until release + // 16). The BYO-key chat is the site's /ask/. useWithAi: string; }; @@ -270,7 +272,7 @@ export function buildSiteCorpus( totals: { channels: channels.length, videos }, channels, shardScheme: SHARD_SCHEME, - useWithAi: join(base, "/use-with-ai"), + useWithAi: AI_DOC_URL, }; // Only advertise the post scheme when this site actually ships posts, so a // pure-video site's corpus.json is unchanged apart from the spec bump. @@ -337,7 +339,7 @@ export function buildHubCorpus( "the whole federation, fetch each site's corpus.json and follow its " + "shardScheme; results can be merged client-side.", }, - useWithAi: join(opts.hubUrl, "/use-with-ai"), + useWithAi: AI_DOC_URL, }; } @@ -362,8 +364,11 @@ export function renderSiteLlmsTxt(corpus: SiteCorpus): string { out.push(""); out.push("## Ask AI"); out.push( - `- [Use with AI](${join(base, "/use-with-ai")}): in-browser chat (bring ` + - `your own API key) and MCP-server setup for Claude Code, Cursor, and other tools.`, + `- [Ask AI](${join(base, "/ask/")}): in-browser chat (bring your own API key).`, + ); + out.push( + `- [Use with AI](${AI_DOC_URL}): MCP-server setup for Claude Code, Cursor, ` + + `and other tools.`, ); out.push(""); out.push("## Corpus"); @@ -426,9 +431,10 @@ export function renderHubLlmsTxt(corpus: HubCorpus): string { out.push(""); out.push("## Ask AI"); out.push( - `- [Use with AI](${join(base, "/use-with-ai")}): ask across the whole ` + - `federation (bring your own API key) or wire up the MCP server.`, + `- [Ask AI](${join(base, "/ask/")}): ask across the whole federation ` + + `(bring your own API key).`, ); + out.push(`- [Use with AI](${AI_DOC_URL}): wire up the MCP server.`); out.push(""); out.push("## Federation"); out.push( diff --git a/common/lib/project.ts b/common/lib/project.ts @@ -44,6 +44,12 @@ export const PROJECT_TAGLINE = "Self-hosted, searchable video-transcript archive // homepage/app/source); this is its no-git alternative. export const PROJECT_DOWNLOADS_URL = `${PROJECT_URL}/downloads/`; +// The homepage's AI and MCP doc (homepage/content/docs/ai-and-mcp.md): the +// published contract, the MCP server and its setup, in one place. Every +// archive's "Use with AI" link goes here, in the same tab (release 16 slice +// DX), as do corpus.json's `useWithAi` and llms.txt's line of that name. +export const AI_DOC_URL = `${PROJECT_URL}/docs/ai-and-mcp/`; + // The `generator` string stamped into corpus.json and llms.txt, so anything // that reads an archive machine-side can find the software that built it. // Shape mirrors the HTML <meta name="generator"> convention. diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **Use with AI goes to the Archilyzer site's AI and MCP doc; the page on each site is gone.** The header's, the slide-out menu's, the footer's and Ask AI's **Use with AI** keep their label and open https://archilyzer.pages.dev/docs/ai-and-mcp/ in the same tab, on every site and the hub, where one block says how to run Claude Code against any archive (the source, `pnpm install`, `claude mcp add archilyzer`, `/ask`). `/use-with-ai/` is no longer built. `corpus.json`'s `useWithAi` names the doc; `llms.txt`'s Ask AI section lists the site's `/ask/` chat and the doc; the sitemap drops `/use-with-ai`. Needs a rebuild and deploy of each site and the hub. - **A search with a layer that has nothing to read finishes.** A "Posts" layer under a tag chip, or a "Live chat" layer where no video in the selection has live chat, read "searched N/M…" for ever and never said "No matching videos."; it now finishes at once, having matched nothing. Needs a rebuild and deploy of each site and the hub. - **A search reads what the visitor ticks under "Search in": Transcripts, Posts and Live chat.** The Filters panel has a new row, **Search in**, beside Type. **Transcripts** and **Posts** are ticked by default and **Live chat** is not; Posts is offered only on a site that has posts, and Live chat only on a site with live chat. The row decides what a plain query reads: with Posts ticked, a plain query now finds posts as well as videos (before, a post was found only by a layer whose scope was "Posts"); with Live chat ticked, it finds live-chat messages too, shown in the same video's card beside the transcript hits, each marked "live chat"; with Transcripts unticked it reads no transcripts. A layer whose scope is picked by name in the query builder ("Live chat", "Posts", "Title / channel", …) reads what it names, whatever the row says. An empty query still lists every video the Type row keeps. With nothing ticked, Search and Apply filters are disabled and the row says "Search in: pick at least one". The Posts box moved here from the Type row, and unticking it no longer empties a layer whose scope is "Posts". Under a tag chip a plain query reads no posts, since a post carries no tags. The row is remembered, and saved with a profile; a shared link does not carry it, so it opens with the reader's own row. A live-chat hit now wears its "live chat" badge wherever it is shown, and the hint under the search bar says to tick Live chat under Search in. Posts unticked is now also remembered after a reload and restored with a profile, which it was not. Needs a rebuild and deploy of each site and the hub. diff --git a/export/app/(workspace)/ask/page.tsx b/export/app/(workspace)/ask/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next"; -import Link from "next/link"; +import { AI_DOC_URL } from "yt-dlp-transcript-common/lib/project"; import { currentSite } from "../../lib/site"; import { instanceMode } from "../../lib/mode"; import AskHub from "../../ask/AskHub"; @@ -34,9 +34,9 @@ export default function AskPage() { browser, sends the relevant excerpts to your chosen AI, and answers with citations. Nothing is hosted here — your key and the requests stay between your browser and the provider. See{" "} - <Link href="/use-with-ai" className="text-brand hover:underline"> + <a href={AI_DOC_URL} className="text-brand hover:underline"> Use with AI - </Link>{" "} + </a>{" "} for other ways to use {isHub ? "every archive on this hub" : "this archive"}. </p> </header> diff --git a/export/app/components/Footer.tsx b/export/app/components/Footer.tsx @@ -5,6 +5,7 @@ import { resolveSocialLinks, } from "yt-dlp-transcript-common/lib/site"; import { + AI_DOC_URL, PROJECT_NAME, PROJECT_URL, } from "yt-dlp-transcript-common/lib/project"; @@ -68,8 +69,9 @@ export default function Footer() { · </span> )} + {/* The homepage's AI and MCP doc, in the same tab (release 16). */} <a - href="/use-with-ai" + href={AI_DOC_URL} className="underline underline-offset-2 hover:text-foreground transition-colors" > Use with AI diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { resolveSocialLinks } from "yt-dlp-transcript-common/lib/site"; import { headerSocialLinks } from "yt-dlp-transcript-common/lib/socialLinks"; -import { INSTANCES_URL } from "yt-dlp-transcript-common/lib/project"; +import { AI_DOC_URL, INSTANCES_URL } from "yt-dlp-transcript-common/lib/project"; import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle"; import { SocialLinks } from "yt-dlp-transcript-common/components/SocialLinks"; import { SocialScroll } from "yt-dlp-transcript-common/components/SocialScroll"; @@ -82,12 +82,14 @@ export default function Header() { // The inline nav from lg. Ask AI is new: the chat was only reachable from // the workspace control or from Use with AI, which is not where anyone looks // for it. - const navLinks = [ + // Use with AI is the homepage's AI and MCP doc (release 16 slice DX): off + // this site, so a plain <a> in the same tab, not a client navigation. + const navLinks: { href: string; label: string; external?: boolean }[] = [ { href: "/", label: "Search" }, { href: "/ask/", label: "Ask AI" }, ...(showDuplicates ? [{ href: "/duplicates", label: "Duplicates" }] : []), ...(showDownloads ? [{ href: "/downloads", label: "Downloads" }] : []), - { href: "/use-with-ai", label: "Use with AI" }, + { href: AI_DOC_URL, label: "Use with AI", external: true }, ]; // The sheet also takes Offline on a PWA-shipping site — gated exactly as // Footer.tsx gates it, so the two never disagree about whether this instance @@ -121,15 +123,25 @@ export default function Header() { </Link> <nav className="hidden lg:flex shrink-0 items-center gap-4 text-sm font-medium"> - {navLinks.map((l) => ( - <Link - key={l.href} - href={l.href} - className="text-foreground hover:text-brand transition-colors" - > - {l.label} - </Link> - ))} + {navLinks.map((l) => + l.external ? ( + <a + key={l.href} + href={l.href} + className="text-foreground hover:text-brand transition-colors" + > + {l.label} + </a> + ) : ( + <Link + key={l.href} + href={l.href} + className="text-foreground hover:text-brand transition-colors" + > + {l.label} + </Link> + ), + )} </nav> <div className="flex min-w-0 items-center gap-5 lg:ml-auto"> diff --git a/export/app/components/MobileMenu.tsx b/export/app/components/MobileMenu.tsx @@ -26,7 +26,9 @@ export default function MobileMenu({ links, instancesUrl, }: { - links: { href: string; label: string }[]; + // `external`: off this site (Use with AI, the homepage's AI and MCP doc), so + // a plain <a> in the same tab rather than a client navigation. + links: { href: string; label: string; external?: boolean }[]; // The Archilyzer home's Official Instances (common/lib/project.ts // INSTANCES_URL); absent on the hub, which lists the instances itself. instancesUrl?: string; @@ -60,12 +62,21 @@ export default function MobileMenu({ <nav className="mt-2 flex flex-col px-2"> {links.map((l) => ( <SheetClose asChild key={l.href}> - <Link - href={l.href} - className="rounded-md px-2 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent" - > - {l.label} - </Link> + {l.external ? ( + <a + href={l.href} + className="rounded-md px-2 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent" + > + {l.label} + </a> + ) : ( + <Link + href={l.href} + className="rounded-md px-2 py-3 text-base font-medium text-foreground transition-colors hover:bg-accent" + > + {l.label} + </Link> + )} </SheetClose> ))} {instancesUrl && ( diff --git a/export/app/use-with-ai/page.tsx b/export/app/use-with-ai/page.tsx @@ -1,139 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { currentSite } from "../lib/site"; -import { instanceMode } from "../lib/mode"; -import { hasArchives } from "../lib/archives"; - -export const metadata: Metadata = { title: "Use with AI" }; - -// A human-facing hub for the "bring your own AI" surface: the in-browser chat, -// the machine-readable discovery files (llms.txt / corpus.json), and the MCP -// server. Hub-aware — on a hub build it frames everything as federation-wide. -// Fully static server component; no data fetching. -export default function UseWithAiPage() { - const site = currentSite(); - const isHub = instanceMode() === "hub"; - const base = site.siteUrl?.replace(/\/+$/, "") ?? ""; - const abs = (p: string) => (base ? `${base}${p}` : p); - const scope = isHub ? "the whole federation" : "this archive"; - const envVar = isHub ? "TRANSCRIPT_HUB_URL" : "TRANSCRIPT_SITE_URL"; - const target = base || (isHub ? "https://your-hub.example" : "https://your-site.example"); - - const mcpSnippet = `{ - "mcpServers": { - "${site.siteId}": { - "command": "pnpm", - "args": [ - "-C", "/path/to/yt-dlp-transcript-browser", - "--filter", "yt-dlp-transcript-mcp", - "exec", "tsx", "src/index.ts" - ], - "env": { "${envVar}": "${target}" } - } - } -}`; - - return ( - <div className="mx-auto flex max-w-3xl flex-col gap-10"> - <header className="flex flex-col gap-3 border-b border-border pb-6"> - <p className="font-mono text-xs uppercase tracking-[0.18em] text-brand"> - Use with AI · {site.headerTitle} - </p> - <h1 className="font-display text-3xl font-semibold leading-tight text-foreground"> - Ask an AI about {scope} - </h1> - <p className="max-w-prose text-sm text-muted-foreground"> - Nothing is hosted or paid for here — you bring your own AI. Chat in your - browser with your own API key, or point a tool like Claude Code at the - machine-readable index and let it browse the transcripts itself. - </p> - </header> - - <section className="flex flex-col gap-3"> - <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground"> - Chat in your browser - </h2> - <div className="flex flex-col gap-3 rounded-lg border border-border bg-card/40 p-5"> - <p className="text-sm text-muted-foreground"> - A retrieval-augmented chat that searches the transcripts and answers - with citations. It runs entirely in your browser and calls your own - provider (Anthropic, OpenAI, or Google Gemini) with a key you supply — - the key stays on your device and requests go straight to the provider. - </p> - <div> - <Link - href="/ask" - className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-brand-strong" - > - Open the chat → - </Link> - </div> - </div> - </section> - - <section className="flex flex-col gap-3"> - <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground"> - For coding agents &amp; LLM tools - </h2> - <div className="flex flex-col gap-3 rounded-lg border border-border bg-card/40 p-5"> - <p className="text-sm text-muted-foreground"> - {scope[0].toUpperCase() + scope.slice(1)} publishes a small, fixed set - of discovery files. An agent (e.g. Claude Code via <code className="font-mono">WebFetch</code>) - can read these and navigate every transcript without any per-video - pages — the paginated shard scheme is documented inline. - </p> - <ul className="flex flex-col gap-2 text-sm"> - <li> - <a href={abs("/llms.txt")} className="font-mono text-brand hover:underline"> - /llms.txt - </a> - <span className="text-muted-foreground"> — an LLM-readable overview and link map.</span> - </li> - <li> - <a href={abs("/corpus.json")} className="font-mono text-brand hover:underline"> - /corpus.json - </a> - <span className="text-muted-foreground"> - {" "}— the channel index and exactly how to fetch any transcript - from the JSON shards{isHub ? " across every member site" : ""}. - </span> - </li> - </ul> - </div> - </section> - - <section className="flex flex-col gap-3"> - <h2 className="font-mono text-xs uppercase tracking-[0.14em] text-muted-foreground"> - MCP server - </h2> - <div className="flex flex-col gap-3 rounded-lg border border-border bg-card/40 p-5"> - <p className="text-sm text-muted-foreground"> - The repo ships an MCP server that exposes {scope} to Claude Code, - Claude Desktop, Cursor, and other MCP clients as tools - (<code className="font-mono">search_transcripts</code>, - {" "}<code className="font-mono">get_transcript</code>, …). It reads the - same static shards — over HTTP or from a local build - {isHub ? ", federating every member site" : ""}. Add it to a client: - </p> - <pre className="overflow-x-auto rounded-md border border-border bg-muted/50 p-3 font-mono text-xs text-foreground"> - <code>{mcpSnippet}</code> - </pre> - <p className="text-xs text-muted-foreground/80"> - Setup details and the <code className="font-mono">claude mcp add</code>{" "} - command are in <code className="font-mono">mcp/README.md</code>. - </p> - </div> - </section> - - {hasArchives() && ( - <p className="text-xs text-muted-foreground/70"> - Ingesting in bulk instead? The{" "} - <a href="/downloads" className="text-brand hover:underline"> - Downloads page - </a>{" "} - has whole-channel transcript zips. - </p> - )} - </div> - ); -} diff --git a/export/e2e-hub/use-with-ai-link.spec.ts b/export/e2e-hub/use-with-ai-link.spec.ts @@ -0,0 +1,48 @@ +import { expect, test, type Locator, type Route } from "@playwright/test"; +import { AI_DOC_URL } from "../../common/lib/project"; + +// Release 16 slice DX, on the hub: no /use-with-ai page; the header's, the +// footer's and Ask AI's "Use with AI" go to the homepage's AI and MCP doc +// (AI_DOC_URL), in the same tab, as plain anchors — the same links a site has. + +async function fulfillJson(route: Route, body: unknown) { + await route.fulfill({ + status: 200, + contentType: "application/json", + headers: { "access-control-allow-origin": "*" }, + body: JSON.stringify(body), + }); +} + +async function expectDocLink(link: Locator) { + await expect(link).toHaveAttribute("href", AI_DOC_URL); + await expect(link).not.toHaveAttribute("target", /./); +} + +test.beforeEach(async ({ page }) => { + // No members: the shelf stays empty and nothing reaches a real origin. + await page.route("**/hub-sites.json", (r) => fulfillJson(r, [])); + await page.route("**/hub-summary.json", (r) => r.fulfill({ status: 404, body: "" })); +}); + +test("the hub's header and footer Use with AI go to the homepage doc", async ({ page }) => { + await page.goto("/"); + await expectDocLink( + page.getByRole("banner").getByRole("link", { name: "Use with AI", exact: true }), + ); + await expectDocLink( + page.getByRole("contentinfo").getByRole("link", { name: "Use with AI", exact: true }), + ); +}); + +test("the hub's Ask AI links Use with AI to the homepage doc", async ({ page }) => { + await page.goto("/ask/"); + await expectDocLink( + page.getByRole("main").getByRole("link", { name: "Use with AI", exact: true }), + ); +}); + +test("the hub has no /use-with-ai page", async ({ page }) => { + const res = await page.request.get("/use-with-ai/"); + expect(res.status()).toBe(404); +}); diff --git a/export/e2e/first-search.spec.ts b/export/e2e/first-search.spec.ts @@ -47,6 +47,24 @@ async function expectClearScreen(page: Page) { await expect(page.getByText(HINT)).toBeVisible(); } +// A route outside the workspace, reached as a client navigation in the same +// page life: the search session unmounts with the workspace and mounts again on +// Back. Until release 16 the header's Use with AI link made that navigation; +// it now leaves the site, and the footer's Changelog is a plain <a> (a new page +// life), so the spec asks the app router itself — `window.next.router`, which +// Next sets for debugging in development and production alike. +async function leaveForChangelog(page: Page) { + await page.evaluate(() => { + ( + window as unknown as { next: { router: { push(href: string): void } } } + ).next.router.push("/changelog/"); + }); + // A dev server compiles /changelog on its first visit. + await expect(page).toHaveURL(/\/changelog\/?$/, { timeout: 20_000 }); + await expect(page.getByRole("heading", { level: 1, name: "Changelog" })).toBeVisible(); + await expect(page.getByTestId("query-builder")).toHaveCount(0); +} + async function expectAllVideos(page: Page) { await expect(page.getByTestId("results-summary")).toHaveText("All videos (3)"); await expect(page.getByTestId("browse-hint")).toBeVisible(); @@ -124,13 +142,9 @@ test.describe("a clear screen until the first Search", () => { await showAll(page); await expectAllVideos(page); // A route outside the workspace: the search session unmounts with it. - await page - .getByRole("banner") - .getByRole("link", { name: "Use with AI", exact: true }) - .click(); - await expect(page).toHaveURL(/\/use-with-ai\/?$/); + await leaveForChangelog(page); await page.goBack(); - await expect(page).not.toHaveURL(/use-with-ai/); + await expect(page).not.toHaveURL(/changelog/); await expectAllVideos(page); }); diff --git a/export/e2e/responsive.spec.ts b/export/e2e/responsive.spec.ts @@ -74,9 +74,16 @@ test.describe("phone layout", () => { "/ask/", "/downloads/", "/duplicates/", - "/use-with-ai/", + "/changelog/", ]) { test(`no horizontal overflow on ${route}`, async ({ page }) => { + // KNOWN, and expected to fail until fixed: /changelog/ (checked here + // since release 16, in place of the removed /use-with-ai/) overflows a + // 390 px phone by ~600 px. Long inline `code` in the released entries + // (file lists like `export/app/ask/{MessageBubble,…}.tsx/ts`) has no + // break opportunity. When the changelog wraps them, this flips red: + // delete the line. + test.fail(route === "/changelog/", "the changelog's long inline code overflows a phone"); await page.goto(route); await page.waitForLoadState("networkidle"); await expectNoHorizontalOverflow(page); diff --git a/export/e2e/restore-no-refire.spec.ts b/export/e2e/restore-no-refire.spec.ts @@ -57,13 +57,20 @@ async function seedStoredSearch(page: Page) { } // A route outside the workspace unmounts the search session; coming back -// mounts it again in the same page life. -async function leaveForUseWithAi(page: Page) { - await page - .getByRole("banner") - .getByRole("link", { name: "Use with AI", exact: true }) - .click(); - await expect(page).toHaveURL(/use-with-ai/, { timeout: 20_000 }); +// mounts it again in the same page life. Until release 16 the header's Use +// with AI link made that client navigation; it now leaves the site, and the +// footer's Changelog is a plain <a> (a new page life), so the spec asks the app +// router itself — `window.next.router`, which Next sets for debugging in +// development and production alike. +async function leaveForChangelog(page: Page) { + await page.evaluate(() => { + ( + window as unknown as { next: { router: { push(href: string): void } } } + ).next.router.push("/changelog/"); + }); + await expect(page).toHaveURL(/\/changelog\/?$/, { timeout: 20_000 }); + await expect(page.getByRole("heading", { level: 1, name: "Changelog" })).toBeVisible(); + await expect(page.getByTestId("query-builder")).toHaveCount(0); } async function expectHeld(page: Page) { @@ -160,9 +167,9 @@ test.describe("restored search waits for the visitor", () => { // …and the stored query is in the box, held. await expectHeld(page); - await leaveForUseWithAi(page); + await leaveForChangelog(page); await page.goBack(); - await expect(page).not.toHaveURL(/use-with-ai/); + await expect(page).not.toHaveURL(/changelog/); // A second mount in the same visit: still held, still the listing. await expectHeld(page); await expect(page.getByTestId("results-summary")).toHaveText("All videos (1)"); @@ -182,12 +189,12 @@ test.describe("restored search waits for the visitor", () => { await expect(page.getByTestId("results-summary")).toContainText("All videos"); await expect(leafInput(page)).toHaveValue(""); - await leaveForUseWithAi(page); + await leaveForChangelog(page); await page .getByRole("banner") .getByRole("link", { name: "Search", exact: true }) .click(); - await expect(page).not.toHaveURL(/use-with-ai/, { timeout: 20_000 }); + await expect(page).not.toHaveURL(/changelog/, { timeout: 20_000 }); // The plain search page reads the stored session: the query is held, and // the listing shows because the visitor asked earlier in this visit. await expectHeld(page); diff --git a/export/e2e/use-with-ai-link.spec.ts b/export/e2e/use-with-ai-link.spec.ts @@ -0,0 +1,49 @@ +import { expect, test, type Locator } from "@playwright/test"; +import { AI_DOC_URL } from "../../common/lib/project"; +import { installRoutes } from "./helpers"; + +// Release 16 slice DX: a site has no /use-with-ai page. Its "Use with AI" +// links — the header's nav (inline from `lg`, in the slide-out menu below it), +// the footer's and the one on Ask AI — go to the homepage's AI and MCP doc +// (AI_DOC_URL), in the same tab, as plain anchors: the doc is on another +// origin, so there is no client navigation to make. The label is unchanged. + +async function expectDocLink(link: Locator) { + await expect(link).toHaveAttribute("href", AI_DOC_URL); + await expect(link).not.toHaveAttribute("target", /./); +} + +test.beforeEach(async ({ page }) => { + await installRoutes(page); +}); + +test("the header's and the footer's Use with AI go to the homepage doc", async ({ page }) => { + await page.goto("/"); + await expectDocLink( + page.getByRole("banner").getByRole("link", { name: "Use with AI", exact: true }), + ); + await expectDocLink( + page.getByRole("contentinfo").getByRole("link", { name: "Use with AI", exact: true }), + ); +}); + +test("the slide-out menu's Use with AI goes to the homepage doc", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto("/"); + await page.getByRole("button", { name: "Open menu" }).click(); + await expectDocLink( + page.getByRole("dialog").getByRole("link", { name: "Use with AI", exact: true }), + ); +}); + +test("Ask AI's Use with AI goes to the homepage doc", async ({ page }) => { + await page.goto("/ask/"); + await expectDocLink( + page.getByRole("main").getByRole("link", { name: "Use with AI", exact: true }), + ); +}); + +test("the page is gone", async ({ page }) => { + const res = await page.request.get("/use-with-ai/"); + expect(res.status()).toBe(404); +}); diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md @@ -1,6 +1,7 @@ # Homepage Changelog ## [Unreleased] +- **The AI and MCP doc has a Ten-minute setup.** Right after the MCP server's introduction, one block runs Claude Code against a published archive, the Jeralyzer as the example: clone the source (or unpack the tarball on Downloads), `pnpm install`, `claude mcp add archilyzer`, start `claude` and try `/ask`; then what it needs, why the server must be registered as `archilyzer` (the shipped `/ask` and `/sweep` call `mcp__archilyzer__…`), the two optional editor lines for `fetch_clip`, `TRANSCRIPT_HUB_URL`, where the `mcp.json` form for other clients is, and WSL2 on Windows. "What it can do" is a heading of its own after it. Every archive's **Use with AI** link now lands on this page. - **`/source/` links the source's history.** A History block — how many commits (past 10,000, "the latest 10,000 of N"), the newest one (linking to its page), and links to the Log, the Refs and the Atom feed — shows when the build published the history pages (`/source/git/`, rendered by stagit); without them there is no History block. The history pages open on the homepage's ground (the reader's stored choice, else Dark; without JavaScript, the system's), start with one line back to `/source/`, and their Files page is an index into the raw tree. The e2e shows the page with and without a history from a fixture publish (`E2E_SOURCE_PUBLIC_DIR`, never read by a production build), and walks the real pages when the checkout has published them. - **The growth chart draws its smallest instances together as Other.** Two or more instances that each hold under 5% of the chart's total are one band, **Other**, on top of the stack, in a near-neutral grey of its own (`--chart-other`: 7.36:1 on the Light ground, 3.22:1 on the Dark one, and apart from every instance colour for colour-blind readers); an instance at exactly 5% keeps its band, and a single one under 5% is not grouped. The other instances keep their bands and their colours. The legend lists them and Other; the caption says what Other is and the chart's description names the instances in it; every month's hover title and the Numbers by year table still name every instance. At this release's numbers Hasanalyzer, Rekietalyzer and Jasolyzer are Other. The instance cards and `/stats` are unchanged. The e2e fixture's fifth site transcribes 4 a day rather than 5, so two of its six sites are grouped. - **An unlisted site is not on the homepage.** A site whose settings turn off **List on the Archilyzer homepage and hub** (`listed: false`) has no Official Instances card, chart series, `/stats` entry or recent item, is not in `channel-sites.json` or `stats/`, and the channels only it carries count in none of the numbers, the headline totals included. The summary's version is 6. The e2e fixture has a seventh, unlisted site that no page names. diff --git a/homepage/content/README.md b/homepage/content/README.md @@ -36,7 +36,7 @@ its public counterpart needs the same change. | `docs/operate.md` | `README.md`, `SCHEDULED_SYNC.md` | editor routes, scheduler settings | | `docs/deploy-cloudflare.md` | `PUBLISH.md` (Cloudflare, R2, cost-abuse) | the 25 MB Pages limit, R2 options | | `docs/deploy-docker.md` | `PUBLISH.md` (building every site in containers) | phase structure, settings names | -| `docs/ai-and-mcp.md` | `mcp/README.md` | tool names, `corpus.json` shape | +| `docs/ai-and-mcp.md` | `mcp/README.md`, `README.md` §1 and §4 | tool names, `corpus.json` shape, the Ten-minute setup's commands (README §1/§4 and mcp/README's "Add to Claude Code" are copies of them: change all three together) | | `docs/faq.md` | — (written for this site) | claims about cost and hardware | ## House rules for these files diff --git a/homepage/content/docs/ai-and-mcp.md b/homepage/content/docs/ai-and-mcp.md @@ -29,8 +29,44 @@ client. It is **a local tool you run yourself**. It changes nothing: it only reads already-published static JSON, either from a directory on disk or over HTTP. - -What it can do: +The one exception is `fetch_clip`, which asks a local Archilyzer editor for a +clip's media: the editor writes it, and the server itself never writes. + +### Ten-minute setup + +To run Claude Code against a published archive, with nothing of your own +hosted — the Jeralyzer here, and any archive's URL works the same: + +```sh +git clone https://archilyzer.pages.dev/source/archilyzer.git archilyzer # or the tarball on /downloads/ +cd archilyzer && pnpm install +claude mcp add archilyzer \ + --env TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ + -- pnpm -C "$PWD" --filter yt-dlp-transcript-mcp exec tsx src/index.ts +claude # then: /ask what has he said about … +``` + +- **What you need:** Node.js 20.9 or newer, pnpm 9 or newer, and Claude Code. + No corpus, no yt-dlp, no GPU, nothing hosted. +- **The source** is the project's read-only [git mirror](/source/); without + git, the same tree is a [tarball](/downloads/): unpack it and carry on from + `cd archilyzer`. +- **Register it as `archilyzer`.** The shipped `/ask` and `/sweep` commands + call `mcp__archilyzer__ask_plan` / `mcp__archilyzer__sweep_plan`, and that + tool name embeds the server name as you registered it. +- **Clips:** two optional `--env` lines, `ARCHILYZER_EDITOR_URL` and + `WORKER_TOKEN` (the editor's own), let `fetch_clip` ask a local editor for + clip media; leave them out for research alone. +- **One archive or several:** `TRANSCRIPT_SITE_URL` reads one archive; + `TRANSCRIPT_HUB_URL`, given a hub's URL, federates every archive on the hub. +- **Another client** (Claude Desktop, Cursor): the same server as an + `mcp.json` entry is in + [mcp/README.md](https://archilyzer.pages.dev/source/tree/mcp/README.md). +- **On Windows,** run all of this inside WSL2, Claude Code included: see + “Claude Code on Windows” in the + [README](https://archilyzer.pages.dev/source/tree/README.md). + +### What it can do - **Search transcripts** for a term, a phrase or a regular expression, with timestamped snippets. Every timestamp is a link to that exact second of the diff --git a/homepage/e2e/docs.spec.ts b/homepage/e2e/docs.spec.ts @@ -61,6 +61,40 @@ test("an internal cross-link stays in the tab; an external one doesn't", async ( } }); +// Release 16 slice DX: the research-only setup lives here alone, and every +// archive's "Use with AI" link lands on this page. +test("the AI and MCP doc's Ten-minute setup registers archilyzer and links the source", async ({ + page, +}) => { + await page.goto("/docs/ai-and-mcp/"); + await expect(page.locator(".doc-measure h3#ten-minute-setup")).toBeVisible(); + + const block = page.locator(".doc-measure pre", { hasText: "claude mcp add archilyzer" }); + await expect(block).toHaveCount(1); + const lines = (await block.innerText()).trim().split("\n"); + expect(lines[0]).toMatch(/^git clone https:\/\/archilyzer\.pages\.dev\/source\/archilyzer\.git archilyzer\b/); + expect(lines[1]).toBe("cd archilyzer && pnpm install"); + expect(lines[2]).toBe("claude mcp add archilyzer \\"); + expect(lines[3]).toBe(" --env TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \\"); + expect(lines[4]).toBe( + ' -- pnpm -C "$PWD" --filter yt-dlp-transcript-mcp exec tsx src/index.ts', + ); + expect(lines[5]).toMatch(/^claude\s+# then:\s+\/ask /); + + // The source and the tarball, linked as the other docs link them: in place. + const source = page.locator('.doc-measure a[href="/source/"]', { hasText: "git mirror" }); + await expect(source).toBeVisible(); + await expect(source).not.toHaveAttribute("target", "_blank"); + await expect( + page.locator('.doc-measure a[href="/downloads/"]', { hasText: "tarball" }), + ).toBeVisible(); + + // The name's reason sits beside it. + await expect( + page.locator(".doc-measure li", { hasText: "Register it as archilyzer" }), + ).toContainText("mcp__archilyzer__ask_plan / mcp__archilyzer__sweep_plan"); +}); + test("an unknown doc slug is a 404, not a crash", async ({ page }) => { const res = await page.request.get("/docs/not-a-real-page/"); expect(res.status()).toBe(404); diff --git a/mcp/README.md b/mcp/README.md @@ -520,11 +520,11 @@ Logs go to stderr; stdout is the MCP JSON-RPC channel. ## Add to Claude Code ```sh -claude mcp add rekietalyzer \ +claude mcp add archilyzer \ --env TRANSCRIPT_SITE_URL=https://rekietalyzer.pages.dev \ --env ARCHILYZER_EDITOR_URL=http://localhost:3001 \ --env WORKER_TOKEN=… \ - -- pnpm -C /ABS/PATH/TO/yt-dlp-transcript-browser --filter yt-dlp-transcript-mcp exec tsx src/index.ts + -- pnpm -C /ABS/PATH/TO/archilyzer --filter yt-dlp-transcript-mcp exec tsx src/index.ts ``` The two editor lines are optional: they let `fetch_clip` ask a local editor for @@ -534,9 +534,10 @@ the same; `fetch_clip` then says it has no editor and fetches nothing. **The `/sweep` and `/ask` commands.** `.claude/commands/{sweep,ask}.md` in this repo call `mcp__archilyzer__sweep_plan` / `mcp__archilyzer__ask_plan` — the tool -name embeds the MCP server name **as you registered it**, so if you used another -name (`rekietalyzer` above), change the `mcp__<name>__` prefix in those two -files to match. `foo:bar` namespacing is plugin-only, so what you type stays +name embeds the MCP server name **as you registered it**, which is why every +example registers it as `archilyzer`. If you use another name (a site's own, +say `rekietalyzer`), change the `mcp__<name>__` prefix in those two files to +match. `foo:bar` namespacing is plugin-only, so what you type stays `/sweep`, not `/archilyzer:sweep`. ## Add to any MCP client (mcp.json) @@ -544,10 +545,10 @@ files to match. `foo:bar` namespacing is plugin-only, so what you type stays ```json { "mcpServers": { - "rekietalyzer": { + "archilyzer": { "command": "pnpm", "args": [ - "-C", "/ABS/PATH/TO/yt-dlp-transcript-browser", + "-C", "/ABS/PATH/TO/archilyzer", "--filter", "yt-dlp-transcript-mcp", "exec", "tsx", "src/index.ts" ], diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -7939,3 +7939,36 @@ source mirror (homepage)". Anchors are at the branch. has no row. - **The export e2e fixture's posts have words of their own** (`kappa`, `sigma`, `omega` — `export/e2e/fixtures/data.ts`, `postsPage`), so a plain "alpha" or "gamma" in a spec reads no post. + +## Use with AI is the homepage's AI and MCP doc (verified 2026-09-30, branch `r16/research-setup`) + +- **The research-only setup lives on the homepage doc; `README.md` §1/§4 and `mcp/README.md` carry + copies to change with it.** `homepage/content/docs/ai-and-mcp.md` "Ten-minute setup": clone (or + the tarball) → `cd archilyzer && pnpm install` → `claude mcp add archilyzer --env + TRANSCRIPT_SITE_URL=…` → `claude`, `/ask`. **README §4's quickstart carries that whole sequence, + in the same order** (its command adds the two optional editor lines). **README §1 and + mcp/README's "Add to Claude Code" and `mcp.json` examples share only the registration**, `claude + mcp add archilyzer … -- pnpm -C <checkout> --filter yt-dlp-transcript-mcp exec tsx src/index.ts`: + README §1 with `"$PWD"` from the repo's root, mcp/README with the placeholder + `/ABS/PATH/TO/archilyzer`. Nothing shares text between `homepage/content` and the READMEs + (`homepage/content/README.md`'s drift table names them). +- **A site has no `/use-with-ai` page** (export and hub). `AI_DOC_URL` (`common/lib/project.ts`, + `${PROJECT_URL}/docs/ai-and-mcp/`) is the target of the header's nav entry (a plain `<a>`: the + header's and `MobileMenu`'s links take `external: true`), the footer's link and Ask AI's link, + label "Use with AI", same tab. `corpus.json`'s `useWithAi` keeps its key and names the doc (site + and hub); `llms.txt`'s "## Ask AI" is two lines, `[Ask AI](<base>/ask/)` and `[Use with AI](<doc>)`; + the sitemap's routes are `/`, `/changelog` (+ `/downloads`, `/duplicates` when built). +- **A route outside the workspace, in the same page life, is reached in a spec through + `window.next.router.push(…)`.** Next 16.2.3 sets `window.next.router` "for debugging" + (`next/dist/client/components/app-router-instance.js:388`), in development and production. It is + undocumented (not in `node_modules/next/dist/docs/`): check it first on a Next upgrade; if it is + gone, the specs fail with a TypeError rather than pass. Since + the header's Use with AI left the site, no site link makes a client navigation to a page outside + the workspace (the footer's are plain `<a>`, a new page life; Downloads and Duplicates show only + when built), so `first-search` and `restore-no-refire` push `/changelog/` that way. +- **A multi-line JSX text that holds an HTML entity loses its leading space** under Next 16.2.3's + SWC: `<b>X</b> is the⏎project&apos;s` compiles to `"is the project's"` (without the entity, or on + one line, `" is the …"`). Put `{" "}` after the element. Measured 2026-09-30 by compiling every + `.tsx` with and without its entities (`next/dist/build/swc` `transform`): 22 texts in 19 files differ, + 1 in `homepage/app/downloads/page.tsx` and 21 in `editor/app/**`; none in `export/app` or + `common/components`. diff --git a/plans/STATE.md b/plans/STATE.md @@ -25,6 +25,11 @@ reviewed SHIP). The operator's runbook is `~/reports/release-15/RUNBOOK.html`. `export`'s `WorkspaceView` `splitOn` has a one-paint flash from a localStorage restore; "Load more results" never resumes a leaf that settled at its cap (`runQueryTree`'s `setHitLimit` reaches running leaves only; on `main` too — release 16 CK re-review R-I1; wants a spec). + - `/changelog/` overflows a 390 px phone: long inline code in a released entry + (`export/CHANGELOG.md:103`, the 133-character `export/app/ask/{useAskChat,…}` list) cannot wrap. Wrap `<code>` in the changelog renderer, then drop + the `test.fail` in `export/e2e/responsive.spec.ts` (release 16 DX). + - The JSX entity/whitespace hazard sweep: 22 texts in 19 files run a word into the element before + them (FACTS, "Use with AI is the homepage's AI and MCP doc"; release 16 DX). **Now (2026-09-28, night): the stats cache key fix — built, reviewed (SHIP AFTER FIXES, then SHIP on re-review; every touch-up done), not merged.** The branch is `fix/stats-cache-key`, and [`stats-cache-key.md`](stats-cache-key.md) diff --git a/plans/release-16.md b/plans/release-16.md @@ -22,7 +22,7 @@ slice's prompt carries its ruling, and this record carries what was built. Rules | Slice | Branch | What | Owns | |---|---|---|---| | CK | `r16/search-in` | A "Search in" row — Transcripts, Posts, Live chat — on the export and hub search, Transcripts and Posts on by default | `common/components/{FiltersPanel,SearchSessionContext,SearchResults,SearchBar,exportFilterStorage}.tsx/.ts`, `common/lib/searchQuery.ts` and `common/lib/search/*` as its prompt names, `export/e2e/search-in.spec.ts` (new) and the specs its prompt names, `export/e2e/helpers.ts`; records: `plans/FACTS.md` | -| DX | `r16/research-setup` | The research-only setup (source → `pnpm install` → `claude mcp add archilyzer` → `/ask`) on the homepage's AI doc, every site's Use-with-AI page and the two READMEs | `homepage/content/docs/ai-and-mcp.md`, `export/app/use-with-ai/page.tsx`, `mcp/README.md`, `README.md` §1/§4 (wording only), `homepage/e2e/docs*.spec.ts` and `export/e2e/use-with-ai*.spec.ts` as its prompt names | +| DX | `r16/research-setup` | The research-only setup (source → `pnpm install` → `claude mcp add archilyzer` → `/ask`) in one place, the homepage's AI and MCP doc; the sites' and the hub's Use-with-AI page removed and its links pointed at the doc; `README.md` §1/§4 and `mcp/README.md` their own copies (as amended) | `homepage/content/docs/ai-and-mcp.md`, `export/app/use-with-ai/` (removed), the Use with AI links (`export/app/components/{Header,MobileMenu,Footer}.tsx`, `export/app/(workspace)/ask/page.tsx`), `common/lib/{project,corpus}.ts` + `common/bin/compose-site.ts` (what named the page), `mcp/README.md`, `README.md` §1/§4 (wording only), `homepage/e2e/docs.spec.ts`, `export/e2e{,-hub}/use-with-ai-link.spec.ts` and the specs that visited the page | ## Slice CK — the ruling (2026-09-30) @@ -384,3 +384,152 @@ fix; the drivers' fix alone passes all 15, the `runLeaf` short-circuit alone pas count is unchanged: one test extended); `pnpm --filter export exec next build` exit 0, 29 s; `search-in`, `query-tree` and `posts-search` at `500417f3`: **38 passed**, 0 failed, 1.9 min. The full export suite and the hub suite were not re-run, as the parent directed. + +### Slice DX, as shipped — the research-only setup, told where a visitor reads (2026-09-30) + +Branch `r16/research-setup` off `main` `2f09b065` (first cut at `2b767bc9`, reset on the amendment), worktree `~/Projects/homepage-social-visible` +(block #3: editor 3301, test 3311, export 3310), one Opus implementer. Scratch files `dx-*` in the +job's `tmp`. Built to the ruling as amended the same evening ("Slice DX — the ruling" above): the +branch was first built to the ruling as first written (a shared `common/lib/researchSetup.ts` and the +block on every site's page); on the amendment it was reset to `main` `2f09b065` (slice CK merged) +and rebuilt, and of that first pass only the READMEs commit was kept (cherry-picked as `7921293f`). + +**What it does.** +- **The homepage's AI and MCP doc has a "Ten-minute setup"** (`homepage/content/docs/ai-and-mcp.md`, + an `###` under "## The MCP server", right after its two opening paragraphs): one `sh` block — + `git clone https://archilyzer.pages.dev/source/archilyzer.git archilyzer # or the tarball on + /downloads/`, `cd archilyzer && pnpm install`, `claude mcp add archilyzer --env + TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev -- pnpm -C "$PWD" --filter yt-dlp-transcript-mcp + exec tsx src/index.ts`, `claude # then: /ask what has he said about …` — and seven notes: what you + need (Node.js 20.9 or newer, pnpm 9 or newer, Claude Code; no corpus, no yt-dlp, no GPU, nothing + hosted; the repo has no `engines` field, so the numbers are README's Requirements table), the + source ([git mirror](/source/), [tarball](/downloads/)), **register it as `archilyzer`** with the + one-sentence reason, the two optional editor lines for `fetch_clip`, `TRANSCRIPT_SITE_URL` / + `TRANSCRIPT_HUB_URL`, the `mcp.json` form for another client (in `mcp/README.md` on the raw + tree), and WSL2 on Windows (the README's "Claude Code on Windows", on the raw tree). "What it can + do:" became `### What it can do`, same words, so the list is not under the setup. Everything else + on the page is unchanged. +- **The sites' `/use-with-ai` page is removed** (`export/app/use-with-ai/`, export and hub). Its + links keep the label **Use with AI** and go to `AI_DOC_URL` (`common/lib/project.ts`, + `${PROJECT_URL}/docs/ai-and-mcp/`), same tab, as plain anchors: the header's nav (its entry, and + `MobileMenu`'s, carry `external: true`), the footer's, and the one on Ask AI. +- **What named the page names the doc:** `corpus.json`'s `useWithAi` (site and hub; the key stays), + `llms.txt`'s "## Ask AI" (two lines now: `[Ask AI](<base>/ask/)` for the chat and `[Use with + AI](<doc>)` for the MCP setup, site and hub), and the sitemap (`/use-with-ai` dropped). +- **The READMEs are their own copies:** `mcp/README.md`'s "Add to Claude Code" and `mcp.json` + examples register `archilyzer` (were `rekietalyzer`) from `/ABS/PATH/TO/archilyzer`, the + directory the clone makes; its renaming paragraph stays and says why every example uses the name. `README.md` §1 registers with `pnpm -C "$PWD"`, "from the repo's + root" (was `/ABS/PATH/TO/this/repo`, against §4 and the Windows notes); §4's quickstart starts + from the source (`git clone …` or the tarball, then `cd archilyzer && pnpm install`). The + homepage's drift table (`homepage/content/README.md`) names README §1/§4 and mcp/README as copies + of the setup's commands, to change together; FACTS says the same. +- **Specs.** `first-search` and `restore-no-refire` left the workspace through the header's Use with + AI, a client navigation in the same page life. No site link makes one to a page outside the + workspace now (the footer's Changelog is a plain `<a>`, a new page life), so they push + `/changelog/` through `window.next.router` (FACTS) and assert the Changelog heading and no query + builder before Back or the header's Search; their assertions are unchanged. `responsive` checks + `/changelog/` in place of `/use-with-ai/`, expected to fail (below). + +**Commits** + +| Commit | What | +|---|---| +| `7921293f` | `docs:` mcp/README registers `archilyzer` in both examples; README §1's `"$PWD"` | +| `8bfecf14` | `common:` `AI_DOC_URL`; `corpus.json` `useWithAi`, `llms.txt`'s Ask AI, the sitemap | +| `e5160555` | `export:` the page removed; the header's, the menu's, the footer's and Ask AI's links; `first-search`, `restore-no-refire`, `responsive`; `use-with-ai-link.spec.ts` (site and hub) | +| `ab8d46a5` | `homepage:` the Ten-minute setup; the drift table; `docs.spec.ts` | +| `217d398f` | `docs:` README §4's quickstart starts from the source | +| `ae206b01` | `export(e2e)`, `homepage(e2e)`: `/changelog/`'s overflow marked `test.fail`; `docs.spec`'s locator | +| `6027b0dc` | `docs:` mcp/README's two examples name the checkout `/ABS/PATH/TO/archilyzer` | +| `20605b5f` | `plans:` this section; FACTS; the export and homepage changelogs | +| `1fcb224d` | `common:` review L3 — `corpus.test` pins `useWithAi` (site and hub) and llms.txt's two Ask AI lines | +| `cadc595e` | `homepage:` review L2 — the doc names `fetch_clip` as the one exception to "it only reads" | +| this commit | `plans:` review L1 (FACTS), L4 and the slices table's DX row; the review's findings; STATE's two follow-ups | + +#### Gates (logs `$T/dx-*.log`) + +- **tsc** (all workspaces) clean, 70 s — after deleting the worktree's `export/.next/dev/types`, + left by the first pass's e2e dev server: its `validator.ts` imported the removed page (the build + regenerates `.next/types`; the dev copy waits for a dev server). Export and homepage alone again + after the spec fixes: clean. +- **common:** 2,380/2,380, 129 s (the count unchanged: `corpus.test.ts`'s sitemap case takes + `/changelog` as its sample route). **`test:scripts`:** 194 passed, 2 skipped (the umtool post-build + check, whose worktree build predates its code; `LIVE`). **homepage unit:** 23/23. +- **Builds:** `pnpm --filter export exec next build` exit 0, 33 s; the hub (`INSTANCE_MODE=hub`, the + same command) exit 0, 31 s. In each `out/`: no `use-with-ai/`; every "Use with AI" in the HTML + (the header's, the footer's, Ask AI's) has `href="https://archilyzer.pages.dev/docs/ai-and-mcp/"`; + no `href="/use-with-ai`. The homepage (`next build`, capped at 5 GB) exit 0, 22 s; the doc carries + the block. `eslint` on the touched export files: clean. +- **e2e:** see the table below. +- **Numbers tool** (`plans/tools/compose-fixture-one-youtube-channel`, composed into scratch): this + slice's differences are exactly `corpus.json`'s `useWithAi`, `llms.txt`'s two Ask AI lines and + `sitemap.xml`'s `/use-with-ai` entry. The committed fixture had already drifted from `main` (`spec` + 3 → 4, four `_headers` entries, `index/sites/testsite/tag-counts.json`), so it was not updated. +- **Privacy:** 0 matches of the operator's user name and 0 of the host name in every changed file, + but `plans/FACTS.md`'s user-name count of 3, which is `main`'s (0 in the lines this slice adds). + + | Run | At | Specs | Result | + |---|---|---|---| + | 1 | `217d398f` | homepage `docs.spec.ts` | 4 passed, **1 failed** (`.doc-measure` matched three elements; fixed in `ae206b01`), 19 s | + | 2 | `217d398f` | export `use-with-ai-link`, `first-search`, `restore-no-refire`, `responsive`, `header` | 61 passed, **1 failed** (`/changelog/` overflows; marked in `ae206b01`), 4.9 min | + | 3 | `ae206b01` | homepage `docs.spec.ts` | **5 passed**, 0 failed, 19 s | + | 4 | `ae206b01` | the full export suite | **271 passed**, 0 failed, 15.1 min (`main`'s 267 + 4; `/changelog/`'s overflow case fails as marked, which the list prints as ✘ and counts as passed) | + | 5 | `ae206b01` | the hub suite | **39 passed**, 0 failed, 1.6 min (`main`'s 36 + 3) | + +#### Found and left + +- **`/changelog/` overflows a 390 px phone by about 600 px** (the new `responsive` case; it was not + checked before). Long inline `code` in released entries (`export/app/ask/{MessageBubble,…}.tsx/ts`, + a JSON literal) has no break opportunity. The case is `test.fail`, so a fix turns it red. Fixing it + is a change to the changelog's rendering, outside this ruling. +- **A multi-line JSX text holding an HTML entity loses its leading space** under Next's SWC (FACTS): + 22 texts in 19 files run a word into the element before it — 1 in `homepage/app/downloads/page.tsx`, + 21 across `editor/app/**`; none in `export/app` or `common/components`. Found on the first pass's + page, which is gone. Not fixed. +- **The compose fixture under `plans/tools/` is behind `main`** (above). Refreshing it is its own + commit. +- `PUBLISH.md`'s registration (`-- pnpm -C "$PWD" archilyzer mcp`) and `AGENTS.md`'s (with the + editor lines) register `archilyzer` and were left as they are; `export/CHANGELOG.md`'s released + entry for the page is history. +- A site deployed before its rebuild still serves `/use-with-ai/` and its old `corpus.json`; the + rebuild removes both together. + +#### Decisions the operator could overturn + +| What I did | The alternative | +|---|---| +| `AI_DOC_URL` in `common/lib/project.ts`, beside `INSTANCES_URL` | `${PROJECT_URL}/docs/ai-and-mcp/` written at each of the six places | +| The header's and the menu's entries render a plain `<a>` (`external: true`) | `next/link` with the absolute URL, which also renders an `<a>` and does not client-navigate | +| `corpus.json` keeps `useWithAi`, now the doc's URL | Drop the key (a contract change) | +| `llms.txt`'s Ask AI section: the site's `/ask/` chat and the doc, two lines | One line, to the doc | +| The sitemap drops `/use-with-ai` and adds nothing | Add `/ask/` | +| `first-search` and `restore-no-refire` reach `/changelog/` through `window.next.router.push` | Click the footer's Changelog: a hard navigation, a new page life, which is not what those tests prove | +| `responsive` checks `/changelog/` with `test.fail` | Drop the route; or fix the changelog's wrapping here | +| "What it can do:" is `### What it can do`, same words | The setup at the end of "## The MCP server", after the list | +| The doc's notes include where the `mcp.json` form is (the removed page carried it) | Leave it out | +| README §4's quickstart gains the clone step, with the tarball's host in its comment | Leave §4 starting at `pnpm install` | +| The doc's `/ask what has he said about …` (README §4's words) | "they", for any archive | + +#### Review + +**Verdict: SHIP AFTER FIXES** (`dx-review.md` in the job's scratch): four lows, no High or Medium. + +| Finding | Where | +|---|---| +| L1: FACTS said README §1, §4 and mcp/README "say the same steps" | This commit: README §4 carries the doc's whole sequence in the same order; README §1 and mcp/README share only the registration command (§1 with `"$PWD"`, mcp/README with `/ABS/PATH/TO/archilyzer`) | +| L2: the doc brought in `fetch_clip` beside "it only reads already-published static JSON" | `cadc595e`: "The one exception is `fetch_clip`, which asks a local Archilyzer editor for a clip's media: the editor writes it, and the server itself never writes." | +| L3: nothing pinned the composer changes | `1fcb224d`: `useWithAi === AI_DOC_URL` for site and hub; the two "## Ask AI" lines of each llms.txt; no `use-with-ai` in either | +| L4: the record's base | This commit: off `main` `2f09b065`, first cut at `2b767bc9` | +| I1: `window.next.router` is undocumented | This commit: FACTS says so, and to check it first on a Next upgrade | +| I2: `test.fail` is satisfied by any failure, not only the overflow | Left as ruled; STATE's follow-up drops the line once the changelog wraps | +| I3: the slices table's DX row described the first ruling | This commit: the row states the amended scope | +| I4: the primary's `export/.next/{,dev/}types` still name the removed page | The parent's, before the post-merge tsc | +| I5–I8 | No action here (deploy order: the homepage with or before the sites and hub; README's Windows blocks' `<your-remote>` a follow-up) | + +**Follow-ups in `STATE.md`:** `/changelog/`'s overflow (wrap `<code>` in the changelog renderer, then +drop the `test.fail`); the JSX entity/whitespace sweep (22 texts in 19 files, FACTS). + +**Gates after the review** (logs `$T/dx-regate.log`, `$T/dx-e2e4.log`): tsc (all workspaces) clean, +125 s; common **2,381/2,381** (+1), 166 s; mcp **271/271**, 59 s; homepage `docs.spec.ts` **5 +passed**, 19 s; export `use-with-ai-link.spec.ts` **4 passed**, 12 s; the hub's **3 passed**, 10 s. +The full suites were not re-run, as the parent directed.