Archilyzer · Source

archilyzer

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

commit 64d316d777c8929baaa2148572ab1a1a61e1662a
parent 3ecb4864cdea7bf54b81d534e12d85d5f282b84d
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 30 Sep 2026 23:30:21 -0400

Merge main (b7d31d9a) into deck/integration — the join before the deck lands

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

Diffstat:
MAGENTS.md | 11+++++++++--
MCONTRIBUTING.md | 15+++++++++++++--
MPUBLISH.md | 8+++++---
MREADME.md | 21+++++++++++++--------
Acommon/components/charts/ChartView.test.ts | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/components/charts/ChartView.tsx | 12+++++++++---
Mcommon/lib/envVars.test.ts | 6++++--
Meditor/CHANGELOG.md | 1+
Mexport/CHANGELOG.md | 1+
Mexport/package.json | 1+
Mexport/playwright.2origin.config.ts | 5++++-
Mhomepage/CHANGELOG.md | 1+
Mhomepage/content/README.md | 2+-
Mhomepage/content/docs/ai-and-mcp.md | 8++++++--
Mhomepage/e2e/docs.spec.ts | 22+++++++++++++++++++---
Mhomepage/e2e/no-data.spec.ts | 9+++++++++
Mmcp/README.md | 36++++++++++++++++++++++--------------
Mplans/FACTS.md | 52+++++++++++++++++++++++++++++++++++++++++++++++-----
Mplans/release-13.md | 369+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mplans/tools/implementer-rules.md | 16+++++++++++++---
Mumtool/report-to-video/README.md | 22++++++++++++++++++----
Mumtool/report-to-video/svg-faces.mjs | 23+++++++++++++++++------
22 files changed, 655 insertions(+), 59 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -54,11 +54,18 @@ claude mcp add archilyzer \ --env TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ --env ARCHILYZER_EDITOR_URL=http://localhost:3001 \ --env WORKER_TOKEN=… \ - -- pnpm -C "$PWD" --filter yt-dlp-transcript-mcp exec tsx src/index.ts + -- pnpm --silent -C "$PWD" archilyzer mcp ``` The two editor lines are optional: they let `fetch_clip` ask a local editor for clip -media (`WORKER_TOKEN` is the editor's own, from `editor/.env`). +media (`WORKER_TOKEN` is the editor's own, from `editor/.env`). `archilyzer mcp` +(`common/bin/mcp.ts`) is `pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts` +behind the repo's CLI: the environment and any `--local`/`--remote`/`--hub` pass +through. **Keep `--silent`** (the long spelling — `claude mcp add` has its own `-s`): +`pnpm archilyzer` is a `pnpm run`, and pnpm 9 and 10 print its `> …` banner to STDOUT, +the JSON-RPC channel, before the server starts (pnpm 11 prints `$ …` to stderr). With +it, stdout carries nothing but the protocol on 9, 10 and 11 — on an installed checkout: +pnpm 11 may install first after a pull, and that output goes to stdout either way. `TRANSCRIPT_HUB_URL` federates several sites; `TRANSCRIPT_LOCAL_DIR` reads a local build off disk. The server never writes to an archive — `fetch_clip` asks the editor, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md @@ -51,11 +51,22 @@ scheme and the shared-data caveat. ## Tests -Unit tests are plain `node:test` files run through `tsx`: +Unit tests are plain `node:test` files run through `tsx`. Each package's suite has a +script: + +```bash +pnpm --filter yt-dlp-transcript-common test # common/ (the globs are in its package.json) +pnpm --filter editor test # editor/app/**/*.test.ts +pnpm --filter export test # export/app/**/*.test.ts +pnpm --filter homepage test # homepage/app/**/*.test.ts +pnpm --filter yt-dlp-transcript-mcp test # mcp/src/*.test.ts +pnpm run test:scripts # scripts/*.test.mjs, umtool/report-to-video/*.test.mjs +``` + +One file on its own: ```bash node_modules/.bin/tsx --test common/jobs/autoQueuePolicy.test.ts -node --test scripts/*.test.mjs ``` Playwright covers the editor UI from `editor/e2e/`. Two ways to run it: diff --git a/PUBLISH.md b/PUBLISH.md @@ -726,9 +726,11 @@ the MCP server reads it over HTTP. Register it against your public URL — as ```sh claude mcp add archilyzer \ --env TRANSCRIPT_SITE_URL=https://<your-site>.pages.dev \ - -- pnpm -C "$PWD" archilyzer mcp + -- pnpm --silent -C "$PWD" archilyzer mcp ``` `archilyzer mcp` starts the same server as `pnpm --filter yt-dlp-transcript-mcp exec -tsx src/index.ts` (the form [mcp/README.md](mcp/README.md) uses), with the -environment passed through and nothing on stdout but the protocol. +tsx src/index.ts`, with the environment passed through. `--silent` is what keeps stdout +the protocol's alone: without it pnpm 9 and 10 print their `> …` script banner there +first (pnpm 11 prints `$ …` to stderr). [mcp/README.md](mcp/README.md) has the other +ways to run and register it. diff --git a/README.md b/README.md @@ -57,20 +57,22 @@ It is a local tool you run yourself, it only reads already-published JSON, and i never modifies the archive: ```bash -# point it at a published instance over HTTP -TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ - pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts +# point it at a published instance over HTTP (from the repo root) +TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev pnpm --silent archilyzer mcp ``` -There is nothing to compile — it runs from source through `tsx`. To register it with -Claude Code, from the repo's root: +There is nothing to compile — it runs from source through `tsx`. `archilyzer mcp` is +the repo's CLI starting `mcp/`'s server. Keep `--silent`: stdout is the MCP protocol's +channel, and without it pnpm 9 and 10 print their `> …` script banner there first +(pnpm 11 prints its `$ …` to stderr). To register it with 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 "$PWD" --filter yt-dlp-transcript-mcp exec tsx src/index.ts + -- pnpm --silent -C "$PWD" archilyzer mcp ``` The two editor lines are optional: they let `fetch_clip` ask a local editor for clip @@ -324,13 +326,16 @@ claude mcp add archilyzer \ --env TRANSCRIPT_SITE_URL=https://jeralyzer.pages.dev \ --env ARCHILYZER_EDITOR_URL=http://localhost:3001 \ --env WORKER_TOKEN=… \ - -- pnpm -C "$PWD" --filter yt-dlp-transcript-mcp exec tsx src/index.ts + -- pnpm --silent -C "$PWD" archilyzer mcp claude # then try: /ask what has he said about magic tournaments? ``` The two editor lines are optional: they let `fetch_clip` ask a local editor for clip media (`WORKER_TOKEN` is the editor's own). Leave them out for research alone. -`-- pnpm -C "$PWD" archilyzer mcp` starts the same server through the repo's CLI. +`archilyzer mcp` is `pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts` +through the repo's CLI. `--silent` (spelled out: `claude mcp add` has its own `-s`) +keeps pnpm's script banner off stdout, which is the protocol's channel — pnpm 9 and 10 +print it there without it. > **Register the server as `archilyzer`.** The shipped commands call > `mcp__archilyzer__ask_plan` / `mcp__archilyzer__sweep_plan`, and that tool name diff --git a/common/components/charts/ChartView.test.ts b/common/components/charts/ChartView.test.ts @@ -0,0 +1,73 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import * as React from "react"; +import { renderToStaticMarkup } from "react-dom/server"; +import { ChartView } from "./ChartView"; +import { newChart, type ChartConfig } from "../../lib/chartConfig"; +import type { ChartData } from "../../lib/chartAggregate"; +import { seriesColor } from "../../lib/homepageChart"; + +// ChartView's colours (release 13, slice W2): a series' colour — and a pie +// slice's — is seriesColor(i), so the sixth reaches --chart-6 instead of +// wrapping back to --chart-1 (the old `i % 5`). What a server render can see +// is the ChartContainer's <style>: one `--color-<id>: <colour>;` per series, +// which every Line/Area/Bar then paints with. The marks themselves need a +// measured container, which a static render does not have. + +// common's tsconfig has `jsx: "preserve"` (Next compiles it), so tsx falls +// back to the classic transform — `React.createElement` on a free `React`. +(globalThis as { React?: typeof React }).React = React; + +function data(n: number): ChartData { + const categories = ["2026-01", "2026-02"]; + return { + categories, + series: Array.from({ length: n }, (_, i) => ({ + key: `channel ${i + 1}`, + points: categories.map((x, j) => ({ x, y: i + j + 1 })), + })), + }; +} + +function render(d: ChartData, config: ChartConfig): string { + return renderToStaticMarkup(React.createElement(ChartView, { data: d, config })); +} + +// The declarations of the light-theme block (ChartStyle writes the same list +// once per theme), as [id, colour] pairs. +function colourDecls(html: string): [string, string][] { + const block = html.match(/\[data-chart=[^\]]+\] \{([^}]*)\}/); + assert.ok(block, "the chart's <style> block is rendered"); + return [...block[1].matchAll(/--color-([\w-]+): ([^;]+);/g)].map((m) => [m[1], m[2]]); +} + +test("a grouped chart's sixth series is --chart-6, and the first five are unchanged", () => { + for (const type of ["line", "area", "bar", "stackedBar"] as const) { + const html = render(data(7), newChart({ type, groupBy: "channel" })); + assert.deepEqual(colourDecls(html), [ + ["s0", "var(--chart-1)"], + ["s1", "var(--chart-2)"], + ["s2", "var(--chart-3)"], + ["s3", "var(--chart-4)"], + ["s4", "var(--chart-5)"], + ["s5", "var(--chart-6)"], + // Past the six validated slots: the hub chart's golden-angle hue, not + // a repeat of --chart-2. + ["s6", seriesColor(6)], + ], type); + assert.notEqual(seriesColor(6), "var(--chart-2)"); + } +}); + +test("a pie's sixth slice is --chart-6", () => { + const d: ChartData = { + categories: ["a", "b", "c", "d", "e", "f", "g"], + series: [{ key: "all", points: "abcdefg".split("").map((x, i) => ({ x, y: i + 1 })) }], + }; + const html = render(d, newChart({ type: "pie" })); + assert.deepEqual( + colourDecls(html).map(([id, c]) => `${id}=${c}`), + Array.from({ length: 7 }, (_, i) => `p${i}=${seriesColor(i)}`), + ); + assert.ok(html.includes("--color-p5: var(--chart-6);")); +}); diff --git a/common/components/charts/ChartView.tsx b/common/components/charts/ChartView.tsx @@ -26,6 +26,7 @@ import { import { useMediaQuery } from "../../lib/useMediaQuery"; import type { ChartData } from "../../lib/chartAggregate"; import { xAxisLabel, yAxisLabel, type ChartConfig } from "../../lib/chartConfig"; +import { seriesColor } from "../../lib/homepageChart"; import { BAR_GAP } from "./surfaceGap"; const AXIS_LABEL_STYLE = { fill: "var(--muted-foreground)", fontSize: 11 }; @@ -34,12 +35,17 @@ const AXIS_LABEL_STYLE = { fill: "var(--muted-foreground)", fontSize: 11 }; // can't appear in `--color-<key>`), while keeping the human label for legends. // An ungrouped chart has a single "all" series — label it by its Y metric // (e.g. "Total views") rather than the meaningless internal key. +// +// A series' colour (and a pie slice's) is seriesColor(i), the one the hub's +// cross-site chart uses: the six validated slots --chart-1..6, then hues +// spread by the golden angle. This chart used to cycle its own `i % 5`, so +// the sixth series repeated the first and --chart-6 was never reached. function seriesMeta(data: ChartData, config: ChartConfig) { const single = config.groupBy === "none"; return data.series.map((s, i) => ({ id: `s${i}`, label: single ? yAxisLabel(config) : s.key, - color: `var(--chart-${(i % 5) + 1})`, + color: seriesColor(i), })); } @@ -90,7 +96,7 @@ export function ChartView({ })); const pieConfig: ShadcnChartConfig = {}; data.categories.forEach((label, i) => { - pieConfig[`p${i}`] = { label, color: `var(--chart-${(i % 5) + 1})` }; + pieConfig[`p${i}`] = { label, color: seriesColor(i) }; }); return ( <ChartContainer config={pieConfig} className="h-[240px] sm:h-[320px] w-full"> @@ -98,7 +104,7 @@ export function ChartView({ <ChartTooltip content={<ChartTooltipContent />} /> <Pie data={pieData} dataKey="value" nameKey="name" innerRadius={40}> {pieData.map((_, i) => ( - <Cell key={i} fill={`var(--chart-${(i % 5) + 1})`} /> + <Cell key={i} fill={seriesColor(i)} /> ))} </Pie> </PieChart> diff --git a/common/lib/envVars.test.ts b/common/lib/envVars.test.ts @@ -19,8 +19,10 @@ const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ". const CODE_ROOTS = ["common", "editor", "export", "homepage", "mcp/src", "scripts"]; const SKIP_DIRS = new Set(["node_modules", ".next", "out", "public", "test-results", "blob-report", "test-transcripts"]); -// The platform's own variables: read here, documented by node, Next, a shell. -const PLATFORM = new Set(["CI", "NODE_ENV", "NEXT_RUNTIME", "LD_LIBRARY_PATH", "PATH", "HOME", "NODE_OPTIONS"]); +// The platform's own variables: read here, documented by node, Next, a shell, +// Playwright (TEST_WORKER_INDEX, which it sets in each worker process; the +// two-origin config reads it to stage only in the runner). +const PLATFORM = new Set(["CI", "NODE_ENV", "NEXT_RUNTIME", "LD_LIBRARY_PATH", "PATH", "HOME", "NODE_OPTIONS", "TEST_WORKER_INDEX"]); function codeFiles(): string[] { const out: string[] = []; diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -9,6 +9,7 @@ - **The "built <when>" line in the Homepage section of `/sites` updates after a build.** When a **Build homepage** or **Build & deploy homepage** lane ends, the page re-renders, so the line says what **Deploy homepage** would ship now. It used to keep saying what `homepage/out` held when the page loaded. This happens whether the build succeeds, fails or is cancelled, because a failed build may already have changed `homepage/out`. - **`/jobs` names the hub's and the homepage's jobs.** They show as **Build hub**, **Deploy hub**, **Build & deploy hub**, **Build homepage**, **Deploy homepage** and **Build & deploy homepage**, not as `build-hub`, `build-homepage` and so on. - **A job cancelled before it started now stays cancelled.** Its record on disk kept saying "queued", so a restart could put a job you had just cancelled back in its queue, and a clip fetch cancelled while waiting could be reported as still queued. Jobs still waiting when the editor shuts down are handled as before: the next start settles or re-queues them. +- **A site's Charts tab gives a sixth series its own colour.** The dashboard's charts coloured their series from five colours and started again at the sixth, so a chart broken down by six or more channels drew the sixth in the first one's colour. The sixth now takes the palette's sixth colour, and each from the seventh on a hue of its own; the first five are unchanged. The published sites' charts get the same change with their next build. ## [0.11.0] - 2026-09-30 - **Transcripts that arrived after a video was first seen are counted.** The stats behind the homepage, the hub and every site's charts were cached per video and refreshed only when the video's metadata changed, so a transcript that came later — a Whisper run days after the download, or a video downloaded after the last index build — never reached them, and a video with YouTube captions alone had no transcription date. Counts and charts were low; the homepage could show a site with 0 transcripts, 0 channels and 0 hours while it served its videos. A stat is now also redone whenever the index re-reads the video, every transcript has a date, and a captioned video is dated by when its captions arrived rather than by a later Normalize run, so its place on "Transcribed over time" can move. **After updating, rebuild and restart the editor before anything else:** until then, **Build stats dataset** runs the old code and would undo the new stats, while a site, hub or homepage build already runs the new code — and the first stats build of any kind re-reads every video once (about 10–30 minutes on a large archive; it can be stopped and picks up where it stopped). Then build the index, the stats, the homepage, the hub, and the sites. diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -4,6 +4,7 @@ - **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. +- **A chart's sixth line, bar or slice has a colour of its own.** A chart broken down by channel, or by anything else, took its colours from a set of five and started again at the sixth, so the sixth series wore the first one's colour and the two could not be told apart. The sixth now takes the palette's sixth colour, a rust, chosen to stand apart from the other five on every ground, including for colour-blind readers. From the seventh on, each gets a hue of its own instead of repeating one. The first five are unchanged. Needs a rebuild and deploy of every export site and the hub. ## [0.11.0] - 2026-09-30 - **The charts count every transcript, once the site is rebuilt.** A transcript that arrived after its video was first indexed was missing from the charts' transcript and cue counts and from "Transcribed over time", and a video with YouTube captions alone had no transcription date. Both are counted now, and a captioned video is dated by when its captions arrived. diff --git a/export/package.json b/export/package.json @@ -16,6 +16,7 @@ "build:hub": "tsx ../common/bin/archilyzer.ts build hub", "start": "serve out", "lint": "eslint", + "test": "tsx --test \"app/**/*.test.ts\"", "e2e": "node ../scripts/queue-lock.mjs --ports EXPORT_E2E_PORT:3020 -- playwright test", "e2e:hub": "node ../scripts/queue-lock.mjs --ports HUB_PORT:3041 -- playwright test --config playwright.hub.config.ts", "e2e:2origin": "node ../scripts/queue-lock.mjs --ports ORIGIN_B_PORT:4610,HUB_A_PORT:4611 -- playwright test --config playwright.2origin.config.ts", diff --git a/export/playwright.2origin.config.ts b/export/playwright.2origin.config.ts @@ -20,7 +20,10 @@ const B_PORT = portFor("ORIGIN_B_PORT"); const A_PORT = portFor("HUB_A_PORT"); const baseURL = `http://localhost:${A_PORT}`; -stageTwoOrigins(); +// Stage once, in the runner. Playwright evaluates this config again in every +// worker process (it sets TEST_WORKER_INDEX there before loading it), and a +// re-stage deletes and rewrites originB under the `serve` already serving it. +if (!process.env.TEST_WORKER_INDEX) stageTwoOrigins(); export default defineConfig({ testDir: "./e2e-2origin", diff --git a/homepage/CHANGELOG.md b/homepage/CHANGELOG.md @@ -2,6 +2,7 @@ ## [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. +- **The Ten-minute setup starts the server with the source's own command, `pnpm --silent -C "$PWD" archilyzer mcp`**, as the README and the MCP server's README do. `--silent` keeps pnpm's own lines off the output the client reads the server's replies on, and a note says so. The note on other clients gives the `mcp.json` entry's arguments in the same form. - **`/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`, `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/ai-and-mcp.md` | `mcp/README.md`, `README.md` §1 and §4 | tool names, `corpus.json` shape, the Ten-minute setup's commands, `pnpm --silent -C … archilyzer mcp` (README §1/§4, mcp/README's "Add to Claude Code" and its `mcp.json`, and AGENTS.md's no-corpus block are copies of them: change them 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 @@ -42,7 +42,7 @@ git clone https://archilyzer.pages.dev/source/archilyzer.git archilyzer # or t 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 + -- pnpm --silent -C "$PWD" archilyzer mcp claude # then: /ask what has he said about … ``` @@ -54,13 +54,17 @@ claude # then: /ask what has he said about … - **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. +- **Keep `--silent`.** Claude Code reads the server's replies on its standard + output, and without it some pnpm versions print a line of their own there + first. `archilyzer mcp` is the source's own command for starting the server. - **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.json` entry, `"command": "pnpm"` with + `"args": ["--silent", "-C", "/ABS/PATH/TO/archilyzer", "archilyzer", "mcp"]`, 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 diff --git a/homepage/e2e/docs.spec.ts b/homepage/e2e/docs.spec.ts @@ -76,10 +76,13 @@ test("the AI and MCP doc's Ten-minute setup registers archilyzer and links the s 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', - ); + // Release 13 slice W2: the server starts through the source's own command, + // with --silent so pnpm prints nothing on the protocol's stdout first. + expect(lines[4]).toBe(' -- pnpm --silent -C "$PWD" archilyzer mcp'); expect(lines[5]).toMatch(/^claude\s+# then:\s+\/ask /); + const text = await page.locator("main").innerText(); + expect(text).toContain("archilyzer mcp"); + expect(text).not.toContain("exec tsx src/index.ts"); // 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" }); @@ -95,6 +98,19 @@ test("the AI and MCP doc's Ten-minute setup registers archilyzer and links the s ).toContainText("mcp__archilyzer__ask_plan / mcp__archilyzer__sweep_plan"); }); +// The setup's notes carry inline code (the mcp.json args); none of it may +// widen the page on a phone. The commands block scrolls in its own box. +test("the AI and MCP doc fits a 390 px phone", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 800 }); + await page.goto("/docs/ai-and-mcp/"); + await expect(page.locator(".doc-measure h3#ten-minute-setup")).toBeVisible(); + const [scroll, client] = await page.evaluate(() => [ + document.documentElement.scrollWidth, + document.documentElement.clientWidth, + ]); + expect(scroll).toBeLessThanOrEqual(client); +}); + 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/homepage/e2e/no-data.spec.ts b/homepage/e2e/no-data.spec.ts @@ -15,6 +15,15 @@ import { FIXTURE_SUMMARY_NAME } from "./fixture-summary"; test.describe.configure({ mode: "serial" }); test("with no summary, / and /stats/ render no numbers and say why", async ({ page }) => { + // `mode: "serial"` orders this file's tests, not the files: under + // `--workers=N` (N > 1) the other specs run beside this one and read the + // same summary file while it is moved aside, so they would draw the empty + // state and fail. Only a one-worker run (the config's own) can check it. + const workers = test.info().config.workers; + test.skip( + workers > 1, + `no-data moves the shared e2e summary aside, which races the other specs when workers > 1 (this run: ${workers})`, + ); const file = path.resolve(test.info().project.testDir, FIXTURE_SUMMARY_NAME); const aside = `${file}.aside`; fs.renameSync(file, aside); diff --git a/mcp/README.md b/mcp/README.md @@ -365,9 +365,9 @@ treated as no filter, so planning can never make a whole-corpus scan slower. ### Benchmark -`mcp/bench/` drives the **real server over stdio**, through the same -`pnpm --filter … exec tsx src/index.ts` command line the client is registered -with, and times a fixed query set: +`mcp/bench/` drives the **real server over stdio**, through +`pnpm --filter … exec tsx src/index.ts` — the command `archilyzer mcp`, which a +client is registered with, runs — and times a fixed query set: ```bash pnpm --filter yt-dlp-transcript-mcp bench @@ -501,21 +501,30 @@ of the package entirely. ## Run it -From the monorepo (cwd is set to the package dir by `pnpm --filter … exec`, so -the TypeScript path alias resolves): +From the monorepo root, through the repo's CLI. `archilyzer mcp` +(`common/bin/mcp.ts`) starts the server exactly as `pnpm --filter +yt-dlp-transcript-mcp exec tsx src/index.ts` does — mcp's own `tsx`, cwd this +package's dir (so the TypeScript path alias resolves), the environment and every +argument after `mcp` passed through: ```sh # a deployed site -pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts --remote https://rekietalyzer.pages.dev +pnpm --silent archilyzer mcp --remote https://rekietalyzer.pages.dev # a hub, federating every member site -pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts --hub https://archilyzer-hub.pages.dev +pnpm --silent archilyzer mcp --hub https://archilyzer-hub.pages.dev -# local shards on disk -pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts --local ../export/public +# local shards on disk (a relative path resolves from mcp/, so give an absolute one) +pnpm --silent archilyzer mcp --local "$PWD/export/public" ``` -Logs go to stderr; stdout is the MCP JSON-RPC channel. +Logs go to stderr; stdout is the MCP JSON-RPC channel. **Keep `--silent`** (spelled +out — `claude mcp add` has its own `-s`): `pnpm archilyzer` is a `pnpm run`, and +without it pnpm 9 and 10 print their `> …` script banner to stdout before the server +starts, which a client reads as broken protocol (pnpm 11 prints `$ …` to stderr). +With it, nothing precedes the protocol on 9, 10 or 11 — on an installed checkout: +after a pull that changed the lockfile, pnpm 11 may install first and print that to +stdout whatever the flags, so run `pnpm install` first. ## Add to Claude Code @@ -524,7 +533,7 @@ 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/archilyzer --filter yt-dlp-transcript-mcp exec tsx src/index.ts + -- pnpm --silent -C /ABS/PATH/TO/archilyzer archilyzer mcp ``` The two editor lines are optional: they let `fetch_clip` ask a local editor for @@ -548,9 +557,8 @@ match. `foo:bar` namespacing is plugin-only, so what you type stays "archilyzer": { "command": "pnpm", "args": [ - "-C", "/ABS/PATH/TO/archilyzer", - "--filter", "yt-dlp-transcript-mcp", - "exec", "tsx", "src/index.ts" + "--silent", "-C", "/ABS/PATH/TO/archilyzer", + "archilyzer", "mcp" ], "env": { "TRANSCRIPT_SITE_URL": "https://rekietalyzer.pages.dev", diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -88,6 +88,12 @@ script (`tsx --test src/*.test.ts`). `common/package.json` has **no `scripts` ke neither `editor` nor `export` has a `test` script. ~46 `*.test.ts` files across the repo are unrunnable as a suite today. +*(Amended 2026-09-28, release 13 slice W2: no longer true. Every package with unit tests has a +`test` script — `common` (`pnpm --filter yt-dlp-transcript-common test`), `mcp`, `homepage`, +`export` (added by W2: `tsx --test "app/**/*.test.ts"`, 11 files / 98 tests) and `editor` +(added by W1 in the same release). The list is in `CONTRIBUTING.md`, "Tests", and the gate list +in `plans/tools/implementer-rules.md`. Still no vitest or jest.)* + ### `subsCache.ts` has no IndexedDB - `common/components/subsCache.ts` — memory + react-query only. @@ -6989,6 +6995,18 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a - **`pnpm archilyzer <command>`** is a root script (`package.json`: `pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts`). pnpm prints its `$ …` line on stderr, so `-- pnpm -C "$PWD" archilyzer mcp` is a clean MCP registration (stdout carries only the protocol). + - *Amended 2026-09-28 (release 13, slice W2 fix round, W2 review S1): true on pnpm 11 only.* + `pnpm archilyzer` is a `pnpm run`, and **pnpm 9 and 10 print their script banner (a blank line, + `> yt-dlp-transcript-browser-monorepo@0.1.0 archilyzer <dir>`, `> <command>`, a blank line) to + STDOUT** before the server starts — ahead of the JSON-RPC answer. pnpm 11 prints `$ <command>` + to stderr. `pnpm exec` prints no banner on any of them. **Register it as + `-- pnpm --silent -C "$PWD" archilyzer mcp`** (the long spelling: `claude mcp add` has its own + `-s`); measured with an initialize request against an empty `--local` dir, stdout is exactly one + line, the answer, on 9.15.4, 10.18.0 and 11.26.0 (`--silent` also drops pnpm 11's stderr `$` + line; the server's own banner stays on stderr). 9.15.4 is what the Dockerfiles install, and + `package.json` pins no `packageManager`. Separately, pnpm 11 may install before a `run` or an + `exec` in a checkout whose install is missing or stale, and prints that to stdout whatever the + flags (both forms). Every doc example carries `--silent` since this amendment. - **`archilyzer doctor [--json]`** (`common/bin/doctor.ts`, row `archilyzer.ts:280`) is STRICTLY read-only: it stats the LMDB index and never opens it, never mkdirs, never writes settings, and calls a port "in use" when a TCP connect succeeds (it never binds). The one process-state change @@ -7134,6 +7152,9 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a - `export/app/**/*.test.ts` — 11 files, 98 tests at `e1ef1923` (3 under `ask/`, 8 under `lib/`) — are in no suite script: `export/package.json` has no `test` script. Run them with `export/node_modules/.bin/tsx --test`. All 98 passed at O1c's review (2026-09-28). + - *Amended 2026-09-28 (release 13, slice W2):* `export/package.json` now has + `"test": "tsx --test \"app/**/*.test.ts\""` — `pnpm --filter export test`, 11 files, + 98 tests, all passing — and it is named in the implementer gate list. - **`normalizeSocialSvg` themes a SINGLE-colour icon only** (`common/lib/settingsSchema.ts:1373`). The solid colours of the root and every child outside a `<mask>` or `<clipPath>` are counted together (spellings folded by `paintKey` `:1304`: `#FFF`, `#ffffff`, `white`, `rgb(255,255,255)` @@ -7189,13 +7210,25 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a - **`no-data.spec.ts` renames the fixture aside** (`.e2e-summary.json.aside`, same gitignore pattern) and restores it in `finally`; it runs serially (`:15`). A killed run can leave the `.aside`; the next config start rewrites the fixture anyway. + - *Amended 2026-09-28 (release 13, slice W2):* `mode: "serial"` orders one file's tests, not the + files, so under `--workers=N` the other specs read the summary while it is aside. The test now + skips itself when `test.info().config.workers > 1`, naming the run's worker count in the skip + reason; the config's own `workers: 1` still runs it. +- **`playwright.2origin.config.ts` stages only in the runner** (release 13, slice W2): Playwright + evaluates a config again in every worker process, after setting `TEST_WORKER_INDEX` there + (`playwright/lib/worker/workerMain.js:60`, 1.59.1), and each evaluation re-ran + `stageTwoOrigins()` — which `rmSync`s and rewrites `.2origin/originB` under the running `serve`. + The call is now `if (!process.env.TEST_WORKER_INDEX) stageTwoOrigins();`. (UI mode's + out-of-process loader would still evaluate it without that variable; the CLI loads test files + in-process.) ### report-to-video measures the brand's face per character (O5) - **Under `render.brand: "archilyzer-media"` the rail, ledger, scroll and chart SVG text are IBM Plex Sans, fitted by the face's own advances**, not an average. `svg-faces.mjs`: `FIRA_SANS` - (`{family, em: 0.5}`, `:44`), `IBM_PLEX_SANS` (a measured face, `:60`), `textWidth(text, size, face, - {weight, ls})` (`:73`), `FALLBACK_EM = 1.3` for a character Plex does not map (`:41`). **1.3 em + (`{family, em: 0.5}`, `:55`), `IBM_PLEX_SANS` (a measured face, `:71`), `textWidth(text, size, face, + {weight, ls})` (`:84`), `FALLBACK_EM = 1.3` for a character Plex does not map (`:52`; anchors as + of release 13 W2, which lengthened the header comment by eleven lines). **1.3 em covers emoji, CJK and the common scripts, but not every fallback glyph** (O5 review L2, measured through the branded conf: `⟹` 1.42 em, `﷽` 1.93, `Ⅷ` 1.31) — a label made mostly of long arrows would overrun by ~9 %; no real label has one. The table is `face-metrics.mjs`, GENERATED by `fonts/gen-face-metrics.py` (fontTools @@ -7204,8 +7237,10 @@ out. O3's facts are the section just above ("O3 — runner lows"). Anchors are a - `render-cards.mjs` `fit(text, size, maxPx, face = FIRA_SANS, run)` (`:428`) and `wrapPx(…, face)` (`:1010`): an average face (Fira) runs the old code path verbatim; a measured face keeps the longest prefix whose width — ellipsis, weight and letter-spacing included — fits. **Kerning is - ignored, and the plain sum is NOT conservative in general** (O5 review L1; `svg-faces.mjs:32-35` and - the README still say it is): IBM Plex Sans has POSITIVE kerning pairs (412 at wght 400, 348 at 700, + ignored, and the plain sum is NOT conservative in general** (O5 review L1; *amended 2026-09-28, + release 13 slice W2:* `svg-faces.mjs:32-47`, the `FALLBACK_EM` doc comment and the README's "Why a + table, not an average" now say so, and say 1.3 em is not a bound for every glyph — comments and + prose only, no code changed, so no render was needed): IBM Plex Sans has POSITIVE kerning pairs (412 at wght 400, 348 at 700, up to +55 units: `r”`, `y’`, `f”`, `TT`, `AA`; bold `(j` +70), so `T`×60 at 13 px renders 464 px of ink against a table sum of 446.2. Over the 144 real strings and their capitals the worst net kerning is +0.50 px, so it is sub-pixel today. The fix, if it ever matters, is positive-only kern @@ -7961,10 +7996,17 @@ source mirror (homepage)". Anchors are at the branch. 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`: + mcp add archilyzer … -- pnpm --silent -C <checkout> archilyzer mcp`: 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). + - *Amended 2026-09-30 (release 13 slice W2, brought to main):* every copy — the doc's block and + its `mcp.json` note, README §1/§4, mcp/README's "Add to Claude Code" and `mcp.json` (`"args": + ["--silent", "-C", <checkout>, "archilyzer", "mcp"]`), AGENTS.md's no-corpus block — runs the + server as `pnpm --silent -C <checkout> archilyzer mcp` (was `pnpm -C <checkout> --filter + yt-dlp-transcript-mcp exec tsx src/index.ts`; why `--silent`: "The CLI's last subcommands" above). + `homepage/e2e/docs.spec.ts` pins the doc's line and that `exec tsx src/index.ts` is not on the + page. - **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, diff --git a/plans/release-13.md b/plans/release-13.md @@ -968,4 +968,373 @@ changed or a path W1's specs drive. | `bcba6bdf` | the merge of `main` `4a186547` (W3); the changelog and the Record kept both sides | | _this_ | `plans:` the second join and its re-gates | +### Slice W2, as shipped — the export, homepage and docs lows (2026-09-28) + +Branch `r13/lows-export` off `main` `bf6904e8`, fast-forwarded to `main` `441bdbb2` (this plan) +before any change; worktree `/home/user/Projects/r13-lows-export`, port block #14 (export e2e +4420, homepage e2e 4440, origin B / hub A 6010 / 6011). One Opus implementer; scratch files `w2-*` +in the job's `tmp/overnight`. Six lows left by release 11, each small; one extra file was needed +(`common/lib/envVars.test.ts`, below). + +**1. Charts reach `--chart-6`.** `common/components/charts/ChartView.tsx` coloured its series, its +pie slices' config and its pie `Cell`s with its own `var(--chart-${(i % 5) + 1})`, so the sixth +series wore `--chart-1` and the slot release 11 added was reached only by the hub's cross-site +chart. All three now take `seriesColor(i)` (`common/lib/homepageChart.ts`): `--chart-1..6`, then +the golden-angle hues the hub chart already uses. Series 1–5 are unchanged. The consumers are +`SearchChartPanel` (the export's and the hub's search chart view) and `ChartCard` (the editor's +`/sites/<id>/charts` dashboard); both pass ChartView their data unchanged, so the new test covers +them. The homepage and umtool do not import ChartView. `ChartView.test.ts` (new, 2 tests) renders +ChartView server-side and reads the `ChartContainer`'s `<style>` — one `--color-<id>: <colour>;` +per series, which every Line/Area/Bar paints with — for line, area, bar, stackedBar (7 series) and +pie (7 slices). + +**2. An export unit `test` script.** `export/package.json`: `"test": "tsx --test +\"app/**/*.test.ts\""` — 11 files (3 under `ask/`, 8 under `lib/`), **98 tests, all passing**, as +FACTS predicted. The implementer gate list (`plans/tools/implementer-rules.md`) now names it, the +homepage's (2 tests) and `pnpm --filter editor test` (W1 adds that script; before it, the `tsx +--test` form); `CONTRIBUTING.md`'s "Tests" lists every package's script. FACTS amended in its two +places ("There is no vitest or jest", and O1c's "in no suite script"). + +**3. O5's wording lows (O5 review L1, L2), comments and prose only.** +`umtool/report-to-video/svg-faces.mjs:32-47`, the `FALLBACK_EM` doc comment and the README's "Why +a table, not an average" now say: kerning is ignored and the plain sum is NOT the conservative +side in general (412 / 348 positive pairs, up to +55 units; `T`×60 at 13 px is 464 px of ink +against 446.2), true only empirically (real text's worst net kerning +0.50 px); and 1.3 em covers +emoji, flags, CJK and the common scripts but not every fallback glyph (`⟹` 1.42 em, `﷽` 1.93, +`Ⅷ` 1.31; ~9 % overrun for a label made of them). **No code changed**: the file printed with +comments stripped (the TypeScript printer, `removeComments`) is identical before and after +(1,329 characters), so an unbranded render is byte-identical without a render, and none was run. +FACTS' O5 entry: its anchors (`:52`, `:55`, `:71`, `:84`) and its "still say it is" amended. + +**4. The 2origin stage guard.** `export/playwright.2origin.config.ts`: `if +(!process.env.TEST_WORKER_INDEX) stageTwoOrigins();`. Playwright evaluates a config again in each +worker process after setting `TEST_WORKER_INDEX` there (`playwright/lib/worker/workerMain.js:60`, +1.59.1, in the worker's constructor, before `_loadIfNeeded` loads the config). Each evaluation +`rmSync`ed and rewrote `.2origin/originB` under the `serve` already serving it — and with +`E2E_TWO_ORIGIN_REBUILD=1` inherited, **rebuilt the whole hub inside the worker**, deleting +`hubA` under its `serve` (see "They bite"). The common suite's env-var registry then failed ("every +variable the code reads is declared": `TEST_WORKER_INDEX`). It is Playwright's variable, set by it +and documented by it, so it joins `envVars.test.ts`'s `PLATFORM` set (beside `CI`, `NODE_ENV`) +rather than `envVars.ts`, whose test audience must be `E2E_`-prefixed and declared in a playwright +config (`babd4b75`). + +**5. `no-data.spec` skips on `workers > 1`.** `mode: "serial"` orders one file's tests, not the +files; under `--workers=N` the other specs read the summary while it is moved aside. The test +calls `test.skip(test.info().config.workers > 1, …)` before it renames anything, with the run's +worker count in the reason. The config's own `workers: 1` still runs it. + +**6. The MCP docs use `archilyzer mcp`.** `-- pnpm -C "$PWD" archilyzer mcp` (`/ABS/PATH/…` where +the old example had one) in `AGENTS.md` (the no-corpus block), `README.md` (both `claude mcp add` +blocks; the run-it example is now `TRANSCRIPT_SITE_URL=… pnpm archilyzer mcp`; the "starts the +same server through the repo's CLI" line now names the long form instead) and `mcp/README.md` ("Run +it" — `pnpm archilyzer mcp --remote|--hub|--local "$PWD/export/public"`, since a relative +`--local` resolves from `mcp/`; "Add to Claude Code"; the `mcp.json` form, `"args": ["-C", …, +"archilyzer", "mcp"]`; the Benchmark paragraph). **mcp/README's examples now register the server +as `archilyzer`** (they said `rekietalyzer`), and its `/sweep`-`/ask` paragraph says why. Every env +line is unchanged. `PUBLISH.md`'s "(the form mcp/README.md uses)" was no longer true and goes. +Checked through this worktree: `pnpm -C <worktree> archilyzer mcp --local <empty dir>` fed an +initialize request answers it as the FIRST stdout line; pnpm's `$ …` line and the server's banner +are on stderr. The grep for the old form (`yt-dlp-transcript-mcp exec`, `mcp add`) found nothing +in the homepage's docs content or in scripts; what it found and left is under "Found and left". + +| sha | what | +|---|---| +| `760af290` | `charts:` ChartView colours from `seriesColor(i)` (series, pie config, pie cells); `ChartView.test.ts` (2) | +| `546a6dd3` | `export:` the `test` script; the gate list, CONTRIBUTING's "Tests", two FACTS amendments | +| `271f49fa` | `umtool:` svg-faces' header and `FALLBACK_EM` comments, the README's "Why a table" (O5 L1, L2); FACTS' O5 entry | +| `5a4ec2fd` | `export:` the 2origin config stages only where `TEST_WORKER_INDEX` is unset | +| `0a1b3e6b` | `homepage:` `no-data.spec` skips on `workers > 1`; FACTS (this and the stage guard) | +| `ec0a11dc` | `docs:` AGENTS.md, README.md, mcp/README.md, PUBLISH.md — the `archilyzer mcp` form | +| `7a538650` | `export:` `[Unreleased]` — a chart's sixth series has a colour of its own | +| `babd4b75` | `common:` `envVars.test.ts` — `TEST_WORKER_INDEX` is a platform variable | +| _this_ | `plans:` this record | + +**Gates**, all from the worktree root: +- **tsc** (`pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit`) clean before every + code commit (`w2-tsc-1.log` … `w2-tsc-4.log`). +- **common 2,116/2,116** (`w2-u-common-2.log`, on `babd4b75`): 2,114 before W2 + ChartView's 2. + (The slice prompt's 2,112 is an older count.) The first run, on `7a538650`, was **2,115/2,116** + — the env-var registry on `TEST_WORKER_INDEX`, fixed by `babd4b75`. +- **editor unit 85/85** (`tsx --test "app/**/*.test.ts"` in `editor/`; this branch has no editor + script yet). +- **`test:scripts` 185 + 1 skip of 186.** +- **mcp 269/269.** +- **export unit (the new script) 98/98**, 11 files. **homepage unit 2/2.** +- **`pnpm --filter export exec next build`** ok, 76 s, no dangling `export/public` links before it. +- **The hub build** ok: `e2e:2origin` ran with `E2E_TWO_ORIGIN_REBUILD=1`, i.e. `archilyzer build + hub` on this tree (compiled 9.4 s, 18 pages). +- **`pnpm --filter editor exec next build`** ok, compiled in 84 s, 161 s in all (ChartView reaches + the editor's charts tab). +- **Homepage build: not run.** No homepage app code changed (only an e2e spec), and the homepage + does not import ChartView. +- **e2e**, detached, no queue wait at the time: + - export `charts.spec.ts`: **8 passed, 0 failed, 1.1 min**; + - homepage, the full suite: **31 passed, 0 failed, 1.3 min** (`no-data` among them, run by + `workers: 1`); + - homepage `no-data.spec.ts --workers=2`: **1 skipped**; + - `e2e:2origin` (rebuild): **3 passed, 0 failed**, 10.7 s of tests, 54 s with the hub build. + - The 2origin run's `compose hub` replaced seven `export/public` links with its own files (and + removed `hub-summary.json`'s), as FACTS says it does; they were relinked per path afterwards, + no dangling links, and the primary's targets kept their 00:51 mtimes. +- **Numbers tool: none.** Nothing ran against the live :3001 editor or the real corpus. + +**They bite.** +- **ChartView:** with `ChartView.tsx` as at `441bdbb2` (uncommitted), both new tests fail: `s5` is + `var(--chart-1)` and `s6` `var(--chart-2)` where `var(--chart-6)` and `hsl(105 64% 55%)` are + expected; the pie's `p5` is `var(--chart-1)` (`w2-bite-chartview.log`). +- **The stage guard,** two ways (`w2-bite-2origin.log`): loading the config under `tsx` with + `TEST_WORKER_INDEX=0` leaves `.2origin/originB/site.json`'s mtime unchanged; unset, it restages; + the OLD config with `TEST_WORKER_INDEX=0` restages. And in real runs: release 11's integration + `e2e:2origin` log (`int-e2e-2origin.log`), `o1-e2e-2origin-1.log` and `o6b-rv-2origin.log` each + show `archilyzer.ts build hub` **twice** in one run, the second after "Running 3 tests using 1 + worker" — the worker rebuilt the hub. W2's run shows it once. +- **no-data:** the old spec under `--workers=2` **ran (1 passed, 15.8 s)**; the new one skips. +- **The registry:** `5a4ec2fd` alone fails the common suite; `babd4b75` is what makes it pass. + +**Found and left.** +- **The export's public "Use with AI" page still shows the long form.** + `export/app/use-with-ai/page.tsx:22-33` builds an `mcpServers` snippet with `"--filter", + "yt-dlp-transcript-mcp", "exec", "tsx", "src/index.ts"`, registered under the site's id rather + than `archilyzer`. It is reader-facing code on every site and the hub — outside W2's files, and it + would need its own `[Unreleased]` bullet and a check of the page. For a later slice. +- **Comments that still describe the long form**, all true (the two forms run the same thing): + `common/bin/archilyzer.ts:249` (the `mcp` row's usage), `common/bin/mcp.ts:2`, and + `mcp/bench/{bench,smoke}.ts`, which say the bench spawns "the command line the client is + registered with". Records (`plans/mcp-fetch-clip.md:216`, `release-1x.md`) keep what was run. +- **From the seventh series on,** ChartView's colours are the golden-angle hues the hub chart + already uses, not a per-base validated palette (before: a repeat of 1–5). +- **`homepageChart.ts`' header** names the hub chart and the site cards as `seriesColor`'s users; + ChartView is a third. Not W2's file. +- **The editor's `/sites/<id>/charts` takes the same change.** Only the export changelog has a + bullet, as the slice prompt said; whether `editor/CHANGELOG.md` wants one is the parent's call. +- **UI mode** (`playwright test --ui`) loads test files in an out-of-process loader that evaluates + the config without `TEST_WORKER_INDEX`, so it would stage once more there. The CLI loads them + in-process. +- **`main` moved to `e6c5d2e3` during the slice** (release 12's changelog fix). Not merged: the + rules say merge `main` again only when the parent asks. + +#### Fix round (review: SHIP AFTER FIXES, `w2-review.md`) + +The review found one should-fix and answered the two questions above; the first and fifth +found-and-left bullets are therefore closed. + +- **S1 — `pnpm archilyzer mcp` was not a clean JSON-RPC stream on pnpm 9 and 10.** `pnpm + archilyzer` is a `pnpm run`; pnpm 9 and 10 print its banner (a blank line, `> <pkg>@<ver> + archilyzer <dir>`, `> <command>`, a blank line) to STDOUT before the server starts; only pnpm + 11 prints `$ …` to stderr, which is all this slice had checked. 9.15.4 is what the Dockerfiles + install, and nothing pins the version. Every example is now `pnpm --silent -C … archilyzer mcp` + (the long spelling: `claude mcp add` has its own `-s`) — AGENTS.md, README.md (three), mcp/README.md + ("Run it" three, "Add to Claude Code", the `mcp.json` args), PUBLISH.md — and the five claims that + stdout is clean now say it is `--silent` that makes it so. Measured with an initialize request + and an empty `--local` dir: stdout is exactly one line, the answer, on **9.15.4, 10.18.0 and + 11.26.0** (`w2-fix-mcp-*-stdout.txt`); without `--silent` 9.15.4 gives 5 lines, the banner first + (`w2-fix-mcp-9-nosilent-stdout.txt`). FACTS' release 11 entry ("a clean MCP registration") is + amended. `CONTRIBUTING.md:157`'s `pnpm archilyzer mcp` stays: it lists the CLI's commands for a + person at a shell, not a registration. +- **Q1 — the public "Use with AI" page moved too.** *Superseded by release 16 DX: the page is + gone from `main`, and this change with it (below, "Brought to main").* The snippet is built by + `export/app/use-with-ai/mcpSnippet.ts` (pure): keyed **`archilyzer`** on every site and the hub + (a site-id key breaks `/ask` and `/sweep`, whose tool names embed the key), `"args": + ["--silent", "-C", "/path/to/yt-dlp-transcript-browser", "archilyzer", "mcp"]`, the site's + `TRANSCRIPT_SITE_URL` or the hub's `TRANSCRIPT_HUB_URL` line kept, values JSON-escaped. A line + under it says why the name matters. The built page (`export/out/use-with-ai/index.html`) shows + exactly that. `mcpSnippet.test.ts` (3) parses the snippet; **with the page's old template put + back in the module (uncommitted), all 3 fail** (`w2-fix-bite-snippet.log`). Its own + `[Unreleased]` bullet in `export/CHANGELOG.md`. +- **Q2 — `editor/CHANGELOG.md`** gets a new `## [Unreleased]` above `[0.10.0]` with one bullet: a + site's Charts tab gives a sixth series its own colour. The only `editor/**` file W2 touches; W1 + adds its own `[Unreleased]` block, for the parent to join. + +| sha | what | +|---|---| +| `84992e73` | `docs:` `--silent` on every `archilyzer mcp` example; the stdout claims say why; FACTS | +| `48d2df8e` | `export:` `use-with-ai/mcpSnippet.ts` + test (3); the page uses it, keyed `archilyzer`; `[Unreleased]` bullet — **superseded by release 16 DX** (the page, the module, its test and the bullet are not on `main`) | +| `8995d635` | `editor:` `[Unreleased]` — the Charts tab's sixth series colour | +| _this_ | `plans:` this fix round | + +**Gates:** tsc clean (`w2-tsc-5.log`); export unit **101/101** (98 + 3); common **2,116/2,116**; +`pnpm --filter export exec next build` ok, 97 s, no dangling links before or after; e2e +`responsive.spec.ts` alone (the one spec that visits `/use-with-ai/`, for phone overflow): **10 +passed, 0 failed, 56 s**. + +**Found and left in the fix round** (the review's lows): +- **L1 — pnpm 11 may install before it runs, and prints that to stdout even with `--silent`.** In + a checkout with no install (and, the reviewer infers, one whose lockfile changed since the last + install) pnpm 11.26 installed before both `run` and `exec` and printed `Already up to date` / + `Done in …` to stdout. It hits the old `exec` form equally, so it is not W2's regression; + AGENTS.md and mcp/README.md now say "on an installed checkout" / "run `pnpm install` first". +- **L2 — `README.md:114` says `corepack enable` makes "the pinned pnpm version" used; nothing is + pinned** (`package.json` has no `packageManager`; `Dockerfile:234-236` says so and installs + 9.15.4). The unpinned version is what made S1 reachable. For a later slice: pin, or reword. +- L3 (series 7+ hues) is the bullet above; the two nits (N1: `no-data` also skips when it is the + only file in a `--workers=2` run; N2: `5a4ec2fd` alone is red until `babd4b75`) are left as the + review says. + +#### Brought to main (2026-09-30) + +Reviewed SHIP on 2026-09-28 and never merged: `main` went on through releases 14, 15 and 16, the +0.11.0 cut, and W3's and W1's merges, to `3a6b3f70`, 290 commits past this branch's fork +(`441bdbb2`). The branch merged `main` (a merge, not a rebase, so the reviewed shas stand) at +**`25042803`**. One W2 change yielded to `main`: **the Use-with-AI page and its snippet (fix round +Q1, `48d2df8e`) are gone**, because release 16 DX deleted the page. W2's command form went +where DX moved the setup, the homepage's AI and MCP doc (`8469e790`), as the parent ruled. + +**Conflicts and how each was resolved:** +- **`export/app/use-with-ai/page.tsx`, modify/delete: deleted.** DX removed + `export/app/use-with-ai/` (the setup lives only in `homepage/content/docs/ai-and-mcp.md`, and the + sites' Use with AI links point there). `48d2df8e`'s page change, `mcpSnippet.ts` and + `mcpSnippet.test.ts` (3 tests), which `main` never had, were removed with it and not brought + back. Its `export/CHANGELOG.md` bullet was dropped too. Fix round Q1 is marked superseded above. +- **`README.md` §1, two hunks.** DX had changed the registration to run "from the repo's root" + with `"$PWD"`; W2 had changed the command and added the paragraph on `--silent`. The result + keeps both: W2's paragraph, ending "To register it with Claude Code, from the repo's root:", + and `-- pnpm --silent -C "$PWD" archilyzer mcp`. **§4 merged without a conflict** and reads as + DX built it: `git clone …` (or the tarball), `cd archilyzer && pnpm install`, `claude mcp add + archilyzer`, `claude` and `/ask`, then the editor-lines note, the name's reason and the WSL2 + line, with W2's command and its `--silent` note. +- **`mcp/README.md`, three hunks.** "Add to Claude Code" uses DX's `/ABS/PATH/TO/archilyzer` + with W2's `pnpm --silent -C … archilyzer mcp`. The naming paragraph is DX's ("why every example + registers it as `archilyzer`"), which says what W2's did. The `mcp.json` args are W2's command + laid out as DX laid out its list: `"--silent", "-C", "/ABS/PATH/TO/archilyzer", "archilyzer", + "mcp"`. +- **`common/components/charts/ChartView.tsx`: both imports.** W2's `seriesColor` and release 14's + `BAR_GAP` (`./surfaceGap`, the stacked bars' 2 px gap) are both kept, and the spread on `<Bar>` is + unchanged. `--chart-other` (release 14 CF) is a `tokens.css` token for the homepage's growth + chart; ChartView never names it, so there was nothing to reconcile there. +- **`editor/CHANGELOG.md`:** `[Unreleased]` holds W3's four bullets and W1's four as `main` has + them, then W2's (a site's Charts tab gives a sixth series its own colour). `[0.11.0]` and below + are `main`'s. **The bullet is still true on `main`:** `main`'s ChartView coloured + `var(--chart-${(i % 5) + 1})` in all three places, and the editor's `/sites/<id>/charts` reaches + it through `EditorChartsClient` → `ChartsDashboard` → `ChartCard` → `ChartView`. +- **`export/CHANGELOG.md`:** `[Unreleased]` holds release 16's three bullets, then W2's + sixth-series bullet. Checked by eye in both changelogs: no W2 bullet sits in a released section, + and each appears once. +- **`plans/release-13.md`:** the Record keeps W3's and W1's sections as `main` has them; W2's + follow, before the Rollout. `git diff main` on the file adds W2's lines and removes none. +- **Merged without a conflict, both sides kept** (each re-read on the merged tree): `AGENTS.md` + (DX did not touch its no-corpus block, so W2's `pnpm --silent -C "$PWD" archilyzer mcp` and its + paragraph stand), `PUBLISH.md`, `CONTRIBUTING.md`, `plans/tools/implementer-rules.md`, + `plans/FACTS.md`, `export/package.json` (W2's `test` beside `main`'s scripts), + `export/playwright.2origin.config.ts`, `common/lib/envVars.test.ts`, `homepage/e2e/no-data.spec.ts`. + +**After the merge, the parent's ruling on the one place (`8469e790`).** DX's "Ten-minute setup" +in `homepage/content/docs/ai-and-mcp.md` registered with `-- pnpm -C "$PWD" --filter +yt-dlp-transcript-mcp exec tsx src/index.ts`. It now reads `-- pnpm --silent -C "$PWD" +archilyzer mcp`. A note says why `--silent` stays and that `archilyzer mcp` is the source's own +command. The note on other clients gives the `mcp.json` args in the same form. The drift table +(`homepage/content/README.md`) names the form and every copy (README §1/§4, mcp/README's two +examples, AGENTS.md's block). FACTS' "Use with AI is the homepage's AI and MCP doc" is amended. +The homepage changelog has a bullet under DX's. **`homepage/e2e/docs.spec.ts`** (DX's) now pins the +new line, checks that `<main>` contains `archilyzer mcp` and not `exec tsx src/index.ts`, and adds +one case: the doc fits a 390 px phone, since its notes now carry a long inline code span (the page +has no horizontal scroll). `export/e2e/use-with-ai-link.spec.ts` is DX's, unchanged. A grep of +the docs (`README.md`, `mcp/README.md`, `AGENTS.md`, `PUBLISH.md`, `CONTRIBUTING.md`, the homepage's +content) finds `exec tsx src/index.ts` only where a line explains what `archilyzer mcp` runs. + +| sha | what | +|---|---| +| `25042803` | the merge of `main` `3a6b3f70`; the conflicts resolved as above | +| `8469e790` | `docs:` the homepage doc's setup in W2's form; the drift table, FACTS, `docs.spec.ts`, the homepage `[Unreleased]` bullet | +| `280b957a` | `plans:` this subsection; fix round Q1 marked superseded | +| _this_ | `plans:` the e2e runs | + +**Re-gates on the merged tree** (logs `$T/w2-*.log` in the job's `tmp/`): +- **tsc** (all workspaces) clean before each commit: 98 s at the merge, 42 s at `8469e790`. The + worktree's generated `export/.next/types` and `export/.next/dev/types` named the removed page; + they were deleted first (gitignored; not source files). + + | Suite | Result | + |---|---| + | common | **2,404/2,404**, 134 s: `main`'s 2,402 plus ChartView's 2 | + | export unit | **98/98**, 11 files, both as `pnpm --filter export test` (4 s) and as the rules' raw `pnpm exec tsx --test "app/**/*.test.ts"` in `export/` (5 s): the fix round's 101 less `mcpSnippet.test.ts`'s 3 | + | homepage unit | **23/23** | + | editor unit | **101/101**, as `pnpm --filter editor test` and as the raw `tsx --test` in `editor/` | + | `test:scripts` | **194 passed, 2 skipped (196)**: `LIVE`, and UT's check skipping this worktree's missing umtool build | + | mcp | **271/271**, 28 s | + +- **Builds.** `pnpm --filter export exec next build`: exit 0, 49 s, no dangling `export/public` + links before or after; `out/` has no `use-with-ai/`, and its index's two "Use with AI" links go + to `https://archilyzer.pages.dev/docs/ai-and-mcp/`. **The homepage**, `pnpm --filter homepage run + build:nodata` (`next build`, no data hook) under `systemd-run … MemoryMax=5G MemorySwapMax=0` + with the corpus linked `-T`: exit 0, 29 s wall, max RSS 778,156 KB. The link was removed after. + The built doc carries `archilyzer mcp` 3 times and `exec tsx src/index.ts` 0 times, and none of + its 13 `.nft.json` traces names `transcripts/`. +- **e2e**, detached and queued, after the builds, from the worktree root (no queue wait for any + run). Another session's video renders and the live editor's transcription held the load + average at **33–42 throughout**, so the export runs are slow and their failures are timeouts or + 5 s expects: + + | Run | Specs | Result | + |---|---|---| + | 1 | homepage, the full suite (`docs`, `no-data`, `instance-colours` among them) | **106 passed**, 0 failed, 5.9 min | + | 2 | homepage `no-data.spec.ts --workers=2` | **1 skipped** (W2's guard) | + | 3 | export `use-with-ai-link.spec.ts`, `charts.spec.ts` | **14 passed, 1 failed**, 2.8 min: `charts.spec.ts:234` (stacked bars' gap), a bar's computed `stroke` and `strokeWidth` read as `""`. That is what a detached node gives, most likely because the chart re-rendered for Group by → Media type after the first bars were visible. It passed in runs 4 and 7 | + | 4 | export, the full suite | **238 passed, 11 failed, 22 did not run**, 56.9 min (DX's 15.1). The 22 are `header.spec.ts`'s rest, behind its `:273` timeout (2 min, a `page.goto`). The 11: `brand:177`, `first-search:140`, `header:273`, `modal-close` ×2, `modal-digest` ×2, `responsive` `/downloads/`, `search-duplicates:150`, `search-results-virtualization` ×2 — 8 test timeouts, 3 five-second expects (a search summary still at "(0)", a card not yet mounted) | + | 5 | the hub suite | **39 passed**, 0 failed, 4.9 min (DX's 39) | + | 6 | `e2e:2origin` with `E2E_TWO_ORIGIN_REBUILD=1` | **3 passed**, 0 failed, 28 s of tests, 212 s with the hub build; `archilyzer.ts build hub` **once** in the log (W2's stage guard). No primary `export/public` entry newer than the run's start; the links relinked per path, none dangling | + | 7 | export: run 4's eight failing files + `charts.spec.ts` (`$T/w2-specs-rerun.txt`, 91 tests) | **90 passed, 1 failed**, 19.5 min: `search-duplicates.spec.ts:150` again (card not visible in 5 s). `/changelog/`'s overflow failed as marked (`test.fail`, counted passed) | + | 8 | `search-duplicates.spec.ts --repeat-each=3`: A, this tree; B, `main`'s `ChartView.tsx` swapped in (uncommitted, restored after); A2, this tree again | A **10 passed, 2 failed** (`:126` twice, the same 5 s card wait), 2.4 min; B **12 passed**, 1.4 min; A2 **12 passed**, 1.5 min | + + Every one of the export suite's 271 tests passed at least once on this tree. Its runtime + difference from `main` is `ChartView.tsx`'s colours: `seriesColor` from `homepageChart.ts`, + a pure module whose one other import is a type. Run 8 shows the duplicates spec's failure on + this tree and passes on it too, so it is not this change. It is flaky under load: the card + wait is a 5 s expect. +- **Numbers tool:** none. + +**A stray index build ran against the real corpus (2026-09-30, 21:05–21:08).** The first +homepage build attempt ran `pnpm --filter homepage run build`, the script the prompt names, with +the primary's `transcripts/` linked in per the umtool rule. **`build` has a `prebuild` hook, +`build:data` = `build:index && compose`** (the twin-script note in `homepage/next.config.ts`), so +pnpm ran `archilyzer index` through the link before `next build`. It was seen in the log and +stopped: the wrapper was killed (`rc=137`, 162 s), the link removed, and the orphaned index +process SIGTERMed (`ELIFECYCLE 143`). `compose homepage` and `next build` never started. +- **What it did.** It scanned the corpus and logged `Diff: +0 added, ~2 changed, -0 removed, 81690 + total.` (no schema change, no held channel). It wrote the 2 changed videos' records to the + primary's `transcripts/index.mdb` (last modified 21:08:23) with `main`'s index code. Nothing + under `common/controller/`, nor `build-index.ts` or `paths.ts`, changed between `e9e3e4ec` + (releases 14+15, the live editor's build) and `3a6b3f70`. Then it began the shared page trees. Those + resolve under the CHECKOUT's `export/.export-index/shared`, the worktree's own: a new 2.5 GB + directory, 354 files, 18 channels in sort order up to `destiny` (killed writing + `page-0030.json.tmp`). Subtitles, posts, digests, the per-site loop and + `INDEX_SCANNED_AT_KEY` were never reached. +- **What it left.** Every complete page and manifest it wrote is **byte-identical** to the + primary's (`cmp`; manifests compared without `generatedAt`), so the `pageHashes` it stored for + those 18 channels match the primary's files. For the other channels nothing changed. The 2 + changed videos' `mtimes` now read as indexed, so the editor's next **Build index** will not + count them as changed. Any build with any other mutation rewrites every channel's pages from + LMDB, and a page whose content moved has a new hash, so it is written. The worktree's + `export/.export-index` was deleted afterwards. +- **Owed to the operator / parent:** whether to run one **Build index** on :3001 (it settles the + 2 videos' pages, if their change touched page content, and records the scan time). Not done + here: the session does not drive the live editor. +- **The rule it broke:** "never run an app, an index or a fixture builder through" the link. The + homepage gate is `build:nodata` (or `exec next build`), never `build`. The rules file's umtool + paragraph is umtool-specific and does not say so for the homepage; the record leaves the rules + to the parent. + +**Found and left in the merge.** +- **`charts.spec.ts:234` reads computed styles from bar nodes the next render may replace** + (run 3). Waiting for the Media-type series (the legend's entries) before reading `stroke` would + close it. Release 14's spec, not W2's. +- **Under a load average near 40, the export suite's 5 s expects and 30 s test timeouts fail** + (runs 4, 7, 8), and `header.spec.ts`'s serial block skips 22 tests behind one timeout. Nothing + here is new on this tree. +- The homepage doc's `--silent` note says pnpm prints "a line of their own" first; on pnpm 9 and 10 + it is a four-line banner. The wording is approximate, not wrong. Left as written. + ## Rollout + +Release 13's three slices were reviewed SHIP on 2026-09-28 and merged on 2026-09-30, after releases +14, 15 and 16, each brought to `main` by a merge (never a rebase) that a read-only review checked for +lost hunks: W3 `4a186547`, W1 `3a6b3f70`, W2 `6c6793d3`. The Phase 5 slices (`r13/phase-5`) are not +part of this: P0's plan still awaits the operator. + +1. The build image rebuilt in the primary from the merged `Dockerfile.build` (27 s warm, 1.66 GB — + it was 7.72 GB; `archilyzer doctor` reports it ok). The build-only Build all through it follows + the site rollout, when the machine is free. +2. The editor: a capped build and one restart (W1's editor lows; W3's deploy guard runs inside the + editor's deploy job). umtool is not rebuilt (its only change is two comments). +3. The homepage, the hub and the six sites: owed with release 16's rollout (W2's sixth series colour + and the MCP command form on the doc). One rollout covers 13 and 16. +4. Records: this section, `STATE.md`, memory. diff --git a/plans/tools/implementer-rules.md b/plans/tools/implementer-rules.md @@ -39,6 +39,12 @@ the Next.js reference for this version. or export-related e2e, because the live editor's build-deploy job regenerates the primary's. - **Export build gate is `pnpm --filter export exec next build`** — NOT `run build`, which runs `build:data` into a worktree-local `transcripts/`. +- **Homepage build gate is `pnpm --filter homepage run build:nodata`** (or `exec next build`) — NEVER + `run build`: its `prebuild` runs `archilyzer index` and `compose homepage` against whatever + `transcripts/` is visible. With the primary's corpus linked in for a capped build (the umtool rule + below), `run build` is an index build against the real corpus (release 13 W2's catch-up, 2026-09-30: + three minutes, two records rewritten by identical code, stopped). The corpus link is for `next build` + only; never run a package's `build` script, a `prebuild` hook, an index or a fixture builder with it. - **tsc per commit:** `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit` from the worktree root must be clean before every commit (worktrees have no stale `.next/dev/types`). - **Never boot a second editor against the real corpus.** Your worktree's e2e uses its own @@ -73,9 +79,13 @@ the Next.js reference for this version. - `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit` — clean. - `pnpm --filter yt-dlp-transcript-common test` (1738 at release 4; record the new count); - editor unit `pnpm exec tsx --test "app/**/*.test.ts"` run in `editor/` (72); `pnpm run - test:scripts` (156 + 1 skip); mcp `pnpm --filter yt-dlp-transcript-mcp test` (219) — check - `package.json` for the exact script names before running. + editor unit `pnpm --filter editor test` (the script lands in release 13; before it, `pnpm exec + tsx --test "app/**/*.test.ts"` run in `editor/`; 72 at release 4); `pnpm run test:scripts` + (156 + 1 skip); mcp `pnpm --filter yt-dlp-transcript-mcp test` (219); export unit `pnpm + --filter export test` (11 files, 98 tests at release 13); homepage unit `pnpm --filter + homepage test` (23 at release 16) — check `package.json` for the exact script names before + running. The export and homepage unit suites take seconds: run them whenever a slice touches + `export/`, `homepage/` or the `common/` code they import. - `pnpm --filter editor exec next build` and `pnpm --filter export exec next build`. - **umtool's build runs with the corpus visible, under a memory cap.** A worktree has no `transcripts/`, so a path Turbopack traces as a directory is empty there. It was hundreds of GB in diff --git a/umtool/report-to-video/README.md b/umtool/report-to-video/README.md @@ -1091,10 +1091,24 @@ An average cannot promise a column. Measured the same way, Plex Sans averages and pushing the manifests' longest real text through each call site, even Fira's 0.50 lets the scroll's bold, letter-spaced column head overrun (152 px rendered in 140). The table does not: over the same runs, every Plex one -renders inside its budget (widest 98.9 %). Kerning is ignored: the plain sum of -advances came out at or above the rendered width of every one of those runs, so -it is the conservative side. A character the face does not map (emoji, CJK, -drawn by fontconfig's fallback) counts 1.3 em. +renders inside its budget (widest 98.9 %). + +Two limits, both measured and both sub-visible in the real manifests: + +- **Kerning is ignored, and the plain sum is not the conservative side in + general.** Plex Sans has positive kern pairs (412 at regular, 348 at bold, up + to +55 units: a closing quote after `r`, `y` or `f`, `TT`, `AA`; bold `(j` + +70), so text made of them renders wider than the table says — `T` × 60 at + 13 px is 464 px of ink against a sum of 446.2. Over the manifests' 144 real + strings, and the same in capitals, the worst net kerning is **+0.50 px**, so a + fitted run can end half a pixel past its budget at most. If that ever + matters, `gen-face-metrics.py` would emit the positive pairs and + `textWidth()` add them (the branded path only). +- **A character the face does not map** (emoji, CJK, drawn by fontconfig's + fallback) counts `FALLBACK_EM`, 1.3 em. That covers emoji (~1.23 em), flags, + CJK (at most 1 em) and the common scripts, but not every fallback glyph: a few + wide symbols exceed it (`⟹` 1.42 em, `﷽` 1.93, `Ⅷ` 1.31), so a label made + mostly of them would overrun by about 9 %. No real label has one. `face-metrics.mjs` is generated: `python3 fonts/gen-face-metrics.py` (fontTools) rewrites it from the vendored font, and a test fails when it is stale. diff --git a/umtool/report-to-video/svg-faces.mjs b/umtool/report-to-video/svg-faces.mjs @@ -29,15 +29,26 @@ // 140). A table does not: fit() keeps the longest prefix whose // measured width, ellipsis included, is inside the budget. // -// The measurement sums advances and ignores kerning: over the manifests' -// longest real text fitted at every call site, the rendered ink width of every -// Plex run came out at or under the sum, so the sum is the conservative side. -// A character the face does not map is drawn by fontconfig's fallback (emoji, -// CJK) and counted at FALLBACK_EM, wider than either. +// The measurement sums advances and ignores kerning, and the sum is NOT the +// conservative side in general: IBM Plex Sans has positive kern pairs (412 at +// wght 400, 348 at 700, up to +55 units -- a closing quote after r, y or f, +// TT, AA; bold "(j" +70), so text made of them renders wider than its sum ("T" +// x60 at 13 px: 464 px of ink against a sum of 446.2). It holds empirically: +// over the manifests' real text (144 strings, and the same in capitals) the +// worst net kerning is +0.50 px, so a fitted run can end up to half a pixel +// past its budget. If that ever matters, gen-face-metrics.py would emit the +// positive kern pairs and textWidth() add them (the branded path only). +// +// A character the face does not map is drawn by fontconfig's fallback and +// counted at FALLBACK_EM. 1.3 em covers emoji (~1.23 em), flags, CJK (at most +// 1 em) and the common scripts, but not every fallback glyph: a few wide +// symbols exceed it (U+27F9 1.42 em, U+FDFD 1.93, U+2167 1.31), so a label made +// mostly of them would overrun by about 9 %. No real label has one; raising it +// would only cut text with unmapped characters sooner. import { IBM_PLEX_SANS_METRICS } from "./face-metrics.mjs"; -/** An unmapped character's advance, in em: wider than a CJK ideograph (1 em) or a colour emoji (~1.25). */ +/** An unmapped character's advance, in em: wider than a CJK ideograph (1 em) or a colour emoji (~1.23), not every fallback glyph (above). */ export const FALLBACK_EM = 1.3; /** The unbranded face, and its average advance per character. */