Archilyzer · Source

archilyzer

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

commit 09aba7056f3ee0828fd6706b9f7fc050f0097283
parent 0dc9bd6b8b90838b94f57b9d46f6561ce247823f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 19:17:29 -0400

plans: slice P as shipped — cut a release from the CLI (one writer in common/controller/cutRelease.ts; archilyzer release show/cut locally; POST /api/ops/cut-release for pnpm ops; the form an adapter); the editor [Unreleased] bullet

Gates: tsc clean; common 2,009; editor unit 85; test:scripts 174 + 1 skip;
mcp 269; editor build ok; e2e spec list 53/53 (1.7 min). Manual proof of
release show / release cut editor next (no --commit) in the worktree, restored.

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/release-10.md | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 175 insertions(+), 0 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -9,6 +9,7 @@ - **An agent working through the MCP server asks the editor for a clip instead of running yt-dlp.** The MCP server has a new tool, `fetch_clip`. Given a citation's channel, video id, start and end and a one-line reason, it asks the local editor for that window through `POST /api/media/fetch-window`: the same paced, cookie-aware job umtool uses, which records who asked and why beside the file. It answers with the file's path in the corpus (`channels/<slug>/data/<id>/clips/`). The window is the cited span with 3 seconds either side, at most 15 minutes. `full: true` asks for the whole recording instead, which lands in the saved-video store and needs a video the editor already knows. A Rumble citation's id (the embed id the archive publishes) is mapped to the id the editor names the video's folder by, through the archive record's link. The editor must already archive the channel: pointed at a public site with a fresh editor, every clip gets a 404 `Channel "<slug>" not found`. The tool waits up to 90 seconds by default (at most 300) and otherwise returns the job's id, to wait on with `job`; the fetch carries on in the editor either way. While it waits it sends a progress notification per poll to a client that asks for progress. A client whose requests time out at 60 seconds (the MCP SDK's default) must raise that or pass `wait_seconds` of 50 or less. No request to the editor waits more than 15 seconds. If the editor stops answering mid-fetch, the answer gives the job's id and says not to ask again from scratch. The `/ask` and `/sweep` plans now tell the agent to use it and never to run yt-dlp itself. The MCP needs `ARCHILYZER_EDITOR_URL` and `WORKER_TOKEN` (the editor's own) in its environment, so re-register it with the two `--env` lines in the README; without them the tool says so and fetches nothing. The MCP server itself still writes nothing. The README's `yt-dlp --download-sections` command is now only the fallback for a machine with no editor. - **"Persist source video" and a whole-recording fetch download the video even when it already has a transcript.** On a channel that takes YouTube's subtitles, the button — and a whole-recording request from umtool or the MCP server's `fetch_clip` with `full: true`, which run the same job — fetched only the subtitles again when the video already had a transcript or captions, and finished with no file. It now downloads the source and moves it into the saved-video store. YouTube's subtitles are fetched again first, as on any re-download; a Whisper transcript is not touched, and no audio is extracted beside a transcript. **Persist kept now** on a channel's Cleanup stage does the same for every kept video, so on such a channel it now downloads each kept video's source. The video page's Source video card offers the button on these channels too; it used to say persistence was for transcribe-handling channels only. - **Each video keeps a history of how its metadata changed at the source.** Every download that rewrites a video's `metadata.info.json` and changes anything in it adds one entry to `metadata.history.json` beside it: the old and new value of each field that changed (title, description, duration, availability, chapters and the rest), the view, like and comment counts that moved, and which of the fields that change on every fetch (format URLs, thumbnails, caption URLs) differed, compared by fingerprint only. A caption language appearing or disappearing counts as a change. The newest 200 entries are kept. The video page shows the history under the description: "Metadata rewritten N× · last … by …: <what changed>", with each entry's old → new values when opened. The history starts with the first rewrite after this update. +- **Release notes can be cut from the command line, with or without the editor running.** `archilyzer release show` prints each changelog's latest release, its date, how many bullets wait under `[Unreleased]`, and what `next` and `next-minor` would be. `archilyzer release cut <editor|export|all> <X.Y.Z|next|next-minor> [--commit] [--date YYYY-MM-DD]` turns `## [Unreleased]` into the dated heading and prints one line per changelog, for example `editor: ## [0.10.0] - 2026-09-26 (committed 1a2b3c4d)`. `next` is the patch bump of the latest heading and `next-minor` the minor bump. `all` cuts both changelogs with one version, worked out from the higher of their two latest headings. With `--commit` it makes one `Release <workspace> <version>` commit per changelog, and it stops at the first failure. Run it from the checkout (`pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts release …`); no editor is needed. A running editor takes the same cut over HTTP: `pnpm ops cut-release --json '{"workspace":"all","version":"next","commit":true}'` (`POST /api/ops/cut-release`). The two commands and the **Cut release** form on `/changelog` and `/sites` share one code path, so they refuse the same things in the same words: nothing pending, a malformed version or date, and a commit while any file other than the changelogs is uncommitted. The form itself is unchanged. No `package.json` version is bumped and no tag is made: the changelog heading is the version. ## [0.9.0] - 2026-09-26 - **Every page now has a ground and an accent to choose, and the five theme families are gone.** The theme menu (the palette button beside the quick toggle, in the editor's sidebar and in the header of every published site, the hub and the homepage) has two groups. **Base** is System, Light, Sepia or Dark; Sepia is new, a warm paper ground for long reading. **Accent** is Signal, Brass, Vermilion, Violet, Sakura, Blue or Green, with the site's own tagged *default*; a site with a custom hex offers it first as *Site colour*. The quick toggle cycles System → Light → Sepia → Dark. A published site opens on the reader's system setting, in the accent its site form sets. The hub and the homepage open on Dark, in Signal, even with JavaScript off, and the editor follows the system, in Signal. Each accent has a value for each ground that reads at 4.5:1, and a custom hex is darkened or lightened per ground to match. A reader's accent is remembered only while it differs from the site's: picking the site's own again forgets it, so the reader follows the site if its accent changes later. Base, Archive, Selenized, Swiss and Archilyzer are gone. A choice made before this update carries over once: light stays light (Archive light becomes Sepia), dark stays dark and system stays system; the family itself is dropped. Headings are Archivo, text is IBM Plex Sans and figures are IBM Plex Mono everywhere, with one corner radius. Success, warning and other status text reads at 4.5:1 on its own tinted fill on every ground; on Light, success and warning are a shade deeper than before for it. Chart colours are fixed per ground and never follow the accent; the third is a violet, well clear of the red that marks a recording as gone. The phone's browser bar takes the page's ground, not the accent. Needs a rebuild and deploy of every site, the hub and the homepage. diff --git a/plans/release-10.md b/plans/release-10.md @@ -1529,6 +1529,180 @@ three hub bullets. footer's Ko-fi link on each site. An installed PWA picks up the worker change on its next update check, and no data cache moves. +### Slice P, as shipped — cut a release from the CLI (2026-09-26) + +Branch `cli/cut-release` off `main` `84c502f3`, worktree `/home/user/Projects/cli-cut-release`, +one Opus implementer. The plan is [`cut-release-cli.md`](cut-release-cli.md), Implementation +items 1–5. The operator asked (2026-09-26): "Are you able to cut releases with the CLI ops? add that +ability if not." The answer was no: the only cutter was the "Cut release" form. Now three callers +share one writer: +- the form, which is unchanged; +- `archilyzer release cut|show`, local, with no editor running; +- `pnpm ops cut-release`, through a new `POST /api/ops/cut-release`. + +**The writer** is `common/controller/cutRelease.ts`. +- `cutReleaseForWorkspace({workspace, version, commit, date?, root?})` is the old server action's + body moved to common, in the same order: the dirty-tree guard, the read, `cutRelease`, the + atomic write, then the path-limited `Release <workspace> <version>` commit. Every refusal keeps + its old wording. It returns + `{ok: true, workspace, version, heading, committed, commitSha?} | {ok: false, workspace, error}`. +- `version` is a literal X.Y.Z(-pre), `next` (`suggestNextVersion`, the patch bump the form + pre-fills) or `next-minor` (the new `suggestNextMinorVersion`: `0.9.3` → `0.10.0`). +- `cutReleases({workspace: "all", …})` cuts editor, then export, with the SAME version: + - A keyword resolves against the HIGHER of the two latest headings (`compareVersionCores`), so + neither changelog goes backwards when they have drifted apart. + - Both changelogs may be dirty. The guard's allowed set is both files, so export's uncommitted + bullets do not block editor's commit. + - It makes two commits in today's message form. + - It stops at the first failure and returns `{ok, version, results, notAttempted}`. `results` + lists every workspace attempted, so a refused export still reports the editor it already cut + and committed. +- `describeRelease(workspace)` is the read-only side of `release show`. It uses the new + `getLatestRelease` (version + date), `hasUnreleasedHeading` and `countUnreleasedBullets` + (top-level bullets; nested items are not counted). +- **One behaviour change, deliberate.** A failed write (`writeFileAtomic` throwing) is now + `{ok: false, error: "Could not write <file>: …"}` instead of an exception, so `all` can still + say what it did. The form shows such a failure as its alert instead of an error boundary. +- `root` is for tests only. It means the standard layout under that directory and ignores the + environment, so every test builds its own git repo and passes it. Without `root`, the writer + uses `getPaths()`: the monorepo root, plus the `EDITOR_/EXPORT_CHANGELOG_FILE` overrides. +- `git.ts` gains `headSha`. +- **No `package.json` bump and no git tag** (decision 4). Neither has been a convention here: every + workspace is 0.1.0, and the repo has no tags. The changelog heading is the version. + +**The form** (`editor/app/sites/lib/cutReleaseAction.ts`) is now a FormData adapter over +`cutReleaseForWorkspace`. Its fields, labels, "Invalid workspace." / "Version is required." and +returned state are unchanged, and `cut-release.spec.ts` passes unmodified (3/3). Its three +`revalidatePath`s moved to `revalidateAfterReleaseCut.ts`, which the ops route shares. + +**The local verbs** are two rows in `common/bin/archilyzer.ts` over `common/bin/release.ts`: +- `archilyzer release show [editor|export]`. `all` is accepted as "both", the default. It prints + one line per changelog, plus an `all:` line when both are shown, which says what + `release cut all next` would cut: + `editor: latest 0.9.0 (2026-09-26); 8 bullets pending under [Unreleased]; next 0.9.1, next-minor 0.10.0`. +- `archilyzer release cut <editor|export|all> <X.Y.Z|next|next-minor> [--commit] [--date YYYY-MM-DD]`. + - It prints one line per changelog, for example + `editor: ## [0.10.0] - 2026-09-26 (committed 1a2b3c4d)`, `… (not committed)`, + `export: failed — <sentence>` or `export: not cut — stopped at the failure above`. + - A bad target, version or date is refused before anything is read, with exit 2. A failed cut + exits 1. + - `--date` is used verbatim (decision 5). The default is today, in local time, as the form + stamps it. +- Both verbs work with no editor running, from the checkout they are run in. + +**The remote verb.** `POST /api/ops/cut-release` takes `{workspace, version, commit?, date?}`. It +is synchronous and built on `ops()`. +- A bad workspace (`oneOf`), version, date or `commit`, or an unknown key, is an OpsInputError + 400 before any read. +- `commit` defaults to false. The CLI's `--commit` is opt-in too; only the form defaults it on. +- A full cut answers 200 `{ok: true, version, results}`. +- A refusal answers 400 `{ok: false, error, version, results, notAttempted}`. `error` is the + writer's sentence, prefixed `<workspace>: ` for `all`. +- The changelog pages are revalidated whenever anything was cut. +- `scripts/archilyzer-ops.mjs`: `ACTIONS` gains `cut-release`, and the header gains an example. + The usage block says what the body takes, that `all` cuts both with one version and a commit + each, and that only an editor built from release 10 or later has the route (an older one + answers 404). With no editor running, it points to `archilyzer release cut`. + +**Two fixes the route made necessary.** +- **The e2e server's export changelog.** `EDITOR_CHANGELOG_FILE` already pointed the test server + at a gitignored copy, but nothing redirected the export changelog. The new route would have let + a spec cut the worktree's tracked `export/CHANGELOG.md`, the trap FACTS records for the editor + one. `dev:test`/`start:test` now set + `EXPORT_CHANGELOG_FILE=$(pwd)/test-export-changelog.md`, which is gitignored. The `paths.ts` + comment says why. +- **`/sites` read a different file from the one its form cuts.** It read + `path.join(exportDir, "CHANGELOG.md")`, while the action wrote `exportChangelogFile`. The two + are the same path unless `EXPORT_CHANGELOG_FILE` is set, which the e2e server now does. The page + now reads `exportChangelogFile`, as `/changelog` reads `editorChangelogFile`. Production is + unchanged. No spec reads the export notes' content (`deploy-page.spec.ts` checks only the + heading's position; 4/4). + +**Docs.** Neither `README.md` nor `AGENTS.md` mentions cutting a release (grepped for +"Cut release", "re-cut", "cut a release", "changelog"), so neither changes (item 5). The runbook is +the parent's. + +| sha | what | +|---|---| +| `9224059a` | `common:` the `cutRelease` controller (the one writer; next / next-minor; `all`; `describeRelease`), the changelog helpers, `headSha`; `cutRelease.test.ts` 12 + `changelog.test.ts` 4 | +| `ea390ebc` | `editor:` the form's action is a FormData adapter; `revalidateAfterReleaseCut`; `/sites` reads the export changelog the form cuts | +| `2657aa9b` | `common:` `archilyzer release show` / `release cut` (`bin/release.ts`); `_cli.test.ts` +6 | +| `867aaf15` | `editor:` `POST /api/ops/cut-release`; `pnpm ops` `ACTIONS` + usage; `archilyzer-ops.test.mjs` +1 | +| `7ef9dd6c` | `editor(e2e):` `ops-cut-release.spec.ts` (3); the test server's `EXPORT_CHANGELOG_FILE` + `.gitignore`; the `paths.ts` comment | +| `80212507` | `editor(e2e):` the second-cut expectation corrected (see e2e run 1) | +| _this_ | `plans:` this record; the `editor/CHANGELOG.md` `[Unreleased]` bullet | + +**Gates**, all from the worktree root; the logs are `p-*.log` in the job's scratch dir. +- **tsc** (`pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit`) was clean before every + commit, in 72 s, 36 s, 34 s, 73 s and 57 s (`p-tsc1`–`p-tsc5`). The last run covers the tip's + code. +- **common 2,009/2,009** (1,987 + 22: cutRelease 12, changelog 4, `_cli` 6), 45 s. +- **editor unit 85/85**, 4 s. +- **`test:scripts` 174 + 1 skip** (173 + 1 before, +1 for `cut-release`), 9 s. +- **mcp 269/269**, unchanged, 18 s. +- **`pnpm --filter editor exec next build`: ok**, 38 s. `ƒ /api/ops/cut-release` is in the route + table. +- **The export build was not run.** Nothing under `export/` changed. +- **e2e** (editor suite, queued and detached, spec list in `p-specs.txt`; no queue wait on either + run). The list is `cut-release`, `ops-cut-release` (new), the three specs that grep `api/ops` + (`ops-api`, `chat-only`, `channel-rename`), and the two `/sites` specs (`deploy-page`, + `sites-crud`) for the loader change. + + | run | passed | failed | time | + |---|---|---|---| + | 1 | 52 | 1 | 2.2 min | + | 2 (after `80212507`) | **53** | **0** | **1.7 min** | + + Run 1's failure was the new spec's own mistake, not a flake. A second cut of an + already-cut file finds no `## [Unreleased]` heading at all, so the writer answers "Could not + find a `## [Unreleased]` heading to cut from.", not "Nothing pending". Run 2's per-spec + counts: `cut-release` 3 (unchanged spec), `ops-cut-release` 3, `ops-api` 22, `chat-only` 5, + `channel-rename` 2, `deploy-page` 4, `sites-crud` 14. The worktree's tracked changelogs were + untouched after both runs. +- **The manual proof** (`p-proof.log`) ran in the worktree at `80212507`, with a clean tree. Both + changelogs' md5s were identical before and after, the tree was clean after, and nothing was + committed. + ``` + $ archilyzer release show + editor: latest 0.9.0 (2026-09-26); 8 bullets pending under [Unreleased]; next 0.9.1, next-minor 0.10.0 + export: latest 0.9.0 (2026-09-26); 4 bullets pending under [Unreleased]; next 0.9.1, next-minor 0.10.0 + all: next 0.9.1, next-minor 0.10.0 + $ archilyzer release cut editor next + editor: ## [0.9.1] - 2026-09-26 (not committed) + $ archilyzer release show editor + editor: latest 0.9.1 (2026-09-26); no [Unreleased] heading; next 0.9.2, next-minor 0.10.0 + $ archilyzer release cut editor next # again + editor: failed — Could not find a `## [Unreleased]` heading to cut from. + $ archilyzer release cut site next + release cut: "site" is not editor, export or all + $ archilyzer release cut editor next --date 26/09/2026 + release cut: Date "26/09/2026" is not in YYYY-MM-DD form. + ``` + After the cut, `git diff` was the one heading line; `git checkout -- editor/CHANGELOG.md` + restored it. `--commit` was never run against a real checkout; the commit path is proven by + `cutRelease.test.ts` in temp repos. +- **Numbers tool: none.** + +**Found and left.** +- **Through `pnpm --filter … exec`, every non-zero exit reads as 1.** pnpm's recursive runner maps + it. `archilyzer build site --bogus` does the same, so this is not the slice's doing. Run + directly (`common/node_modules/.bin/tsx bin/archilyzer.ts …`, or `pnpm exec` from `common/`), + a refusal exits 2. +- **`--commit` refuses on any untracked file.** `git status --porcelain` lists `??` entries, and + that was already the form's behaviour: an untracked `settings.json.pre-priority-*` blocked the + 0.9.0 cut. The primary's tree was clean at this writing (0 dirty paths). +- **The `_lib.ts` header says every ops route "call[s] ONE existing server action".** This one + calls the controller that the server action also calls. The route's comment says so; the + header is left as it is. +- **Commit trailers** carry `Claude Opus 5.5 (1M context)`, as slices M–O's do. +- **Rollout** is the plan's. From the primary, after this merges and before step 1's restart: + `pnpm --filter yt-dlp-transcript-common exec tsx bin/archilyzer.ts release show`, then + `… release cut all next --commit`. With both changelogs at 0.9.0, that cuts both as 0.9.1; use + `next-minor` or an explicit version for 0.10.0. After the restart, check `pnpm ops cut-release` + on the live editor with a request that is refused before anything is written, for example + `--json '{"workspace":"site","version":"next"}'` (expect a 400 naming the three workspaces). + The route has no read-only mode: `release show` is the CLI's. + ## Rollout Nothing is rolled out, except that **Jeralyzer is already on the brand, in Signal** (a build-deploy