Archilyzer · Source

archilyzer

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

commit 0cb01fb60f54654150913c8014c3bc52b5403d6c
parent b15d2a0141a3d05a3267ed847e7ebd685e6704e0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 23 Sep 2026 19:49:50 -0400

plans: record one-core phase 3 slice 4a as shipped; changelog entry

What moved, the five deviations (seams in settingsFieldSchemas.ts to keep
zod out of client bundles; archiveStorage required; saveSettings(patch);
example omits workers; only top-level comments became .describe()), the
four behaviour changes, the three dead example keys, the frozen-input
numbers diff (empty over live, example, fixture and both entrypoint seeds),
and the gates: common 1648, scripts 156 + 1 skip, mcp 219, editor unit 63,
both next builds, e2e 157/157. editor/CHANGELOG.md gains one Unreleased
entry: one settings schema + writer, SETTINGS.md, the
savedVideosLocationId fix.

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

Diffstat:
Meditor/CHANGELOG.md | 1+
Mplans/one-core-phase-3.md | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 82 insertions(+), 0 deletions(-)

diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +- **`settings.json` has one schema and one writer, and its key table is generated.** Every key, its default, its clamp and its documentation is now one zod schema (`common/lib/settingsSchema.ts`); `getSettings`/`writeSettings` both parse through it, and every settings form saves through one helper (`editor/app/settings/saveSettings.ts`) that merges only what the form changed. **`SETTINGS.md`** (new, repo root) lists every key with its default and what it does, and `settings.json.example` is now the full default object — both generated by `common/bin/settings-example.ts` and checked by a test, so neither can drift. Nothing an operator has configured reads differently. **Fixed:** adding or editing a storage location on `/storage` no longer erases the record of which location the saved-video store is on (`storage.savedVideosLocationId`). - **A lane's pause is one key on the lane, and the four old pause fields are gone from `settings.json`.** Holding a lane has been `autoQueue.<lane>.held` since the runner work landed; until now the file also still carried the four flags that used to mean it — `transcriptionsPaused`, `downloadsPaused`, `digest.digestsPaused` and the backwards `backfill.enabled` (where *enabled* meant *not held*) — which were read only when a lane had no `held` yet, to carry an older file's pause across. Every lane now carries its own key, so those four are **deleted**: nothing reads them, no form writes them, and the next settings save drops them from the file. A settings.json that still spells one of them holds nothing with it, so a hand-edited file (or a very old backup restored over a newer one) can no longer resurrect a pause you had lifted, or lift one you had set. "Run the backfill lane" on the diarization page and the Hold/Pause buttons write the one key, as they already did. **UPGRADING: boot once on the release that writes `held` before taking this one.** That release is the one that moved the gate onto the lane and carried the old fields across on read; a single boot of it (any settings save, or just starting the editor and pausing/resuming anything) puts `autoQueue.<lane>.held` in your settings.json, after which **nothing you can see changes here** — the same buttons, the same labels, the same pauses. An install that jumps straight from an older release to this one has no `held` keys at all and **loses its pauses**: transcription, downloads and digests come up running, and the backfill lane comes up held. Re-set them from the dashboard, or add the keys by hand before starting. - **A relocate job says how far it has got.** `rsync` has been printing its progress the whole time (`--info=progress2`) and every frame of it went into the job log as a carriage-return redraw of one line — so a 131 GB move and a 3 MB one looked identical from `/jobs`: a spinner. Now each frame is parsed into the **task bar** every other long job on that page already draws, reading `12.3 GB of 45.6 GB · 27 % · 110.50MB/s · ETA 5:32`, and the log gets **one line per 10 %** instead of several thousand frames of one. The percentage is against the tree the job already measured for its space check, not rsync's own — under incremental recursion that one is a percentage of what it has enumerated so far and walks backwards. - **`/channels` is where storage is managed now.** Two new columns: **Location** (which volume this channel's media is on — *Internal* when it has not moved) and **Size** (every byte under its `data/`, from its last report, sortable biggest-first). Free space is *not* a column, because it is a fact about a disk and not about a channel: there is one read-out per **volume** in a new bar above the rack, and each chip is also a **filter** — `?location=platter` lists exactly the channels on that drive, and `/storage` links straight here with the biggest first. Beside them, **Free up N GB**: type a number, press *Select largest*, and the largest channels still on the internal disk are ticked until the target is met, ready for the Move button that was already there. Channels already on another volume are never picked (moving one frees nothing on the disk you are emptying) and channels whose report carries no size are **skipped and counted** rather than ranked as empty — which would have put the biggest thing on the disk at the bottom of the list. diff --git a/plans/one-core-phase-3.md b/plans/one-core-phase-3.md @@ -477,3 +477,84 @@ claimed fixed; the reads are in-memory). > `buildAutoQueueStatusPayload` (`editor/app/operations/status.ts`) reads configs and state > ONCE via `Promise.all` and hands the same pair to all four lanes. `getAutoRunnerStatus` is > still called per lane and is genuinely in-memory. + +## Slice 4a, as shipped — one settings schema (2026-09-23) + +Branch `one-core/phase-3-s4a` off `main` `54cf1b31`, unmerged. Commits (shas after the +trailer rewrite; this record is `plans:` commit 6 on top): + +| sha | what | +|---|---| +| `50215ab5` | zod `^4.3.6` joins `common` (lockfile +3 lines, no new resolution); the auto-queue defaults/clamps/tree normalisation/lane gate move from `jobs/autoQueuePolicy.ts` to `lib/autoQueueSchema.ts` (picker stays, re-exports every moved name); allow-list entry `lib/settings.ts -> jobs/autoQueuePolicy` burned, **10 → 9**; `autoQueuePolicy.test.ts` repointed (imports only), 59/59; `plans/tools/phase3-settings-numbers.ts` added | +| `e120a2a5` | `workersSchema`, `channelPrioritySchema`, `autoQueueSchema` — zod seams over the existing sanitizers, in `lib/settingsFieldSchemas.ts` | +| `f1abe024` | `lib/settingsSchema.ts`: `siteSettingsSchema` (31 fields, `.describe()` on each), `SiteSettings = z.infer`, `defaults()` = `defaultSiteSettings()` = `parse({})`; `lib/settings.ts` reduced to I/O (1,783 → 250 lines) and `export *`s the schema module; `settingsSchema.test.ts` | +| `85478527` | `editor/app/settings/saveSettings.ts` + unit test; 19 call sites in 11 files converted to patches; `writeSettings` is imported by one editor file | +| `0316e988` | `common/bin/settings-example.ts` (+`--check`), `lib/settingsDocs.ts`, generated `settings.json.example` + `SETTINGS.md`, `settingsDocs.test.ts`; SETUP.md points at SETTINGS.md | + +**What moved.** Every type, constant, clamp and block sanitizer that was in `lib/settings.ts` +is in `lib/settingsSchema.ts` and re-exported, so no importer changed. `getSettings` = +`finishRawMigrations(siteSettingsSchema.parse(premigrateRaw(raw)), raw)`: sweeps→lanes and +mediaRoot→locations rewrite the raw input (keyed on the raw file's absence); legacy +`transcribe*`→app registry and worker synthesis run after the parse, keyed on the raw +object's `transcriptionApp` / `workers` absence. `writeSettings` = +`parse({...deriveWorkerShadow(next), socialLinks: validatedSocialLinks(...)})` + tmp/rename; +the two validators still throw. Every field is `z.unknown().catch(undefined).transform(coerce)` +over the pre-existing clamp or sanitizer; no `.passthrough()`, no `.default()`. + +**Deviations from the spec, and why.** +1. *The zod seams are not beside their sanitizers.* `workers.ts`, `channelPriority.ts` and + (through `jobs/autoQueuePolicy`) `autoQueueSchema.ts` are value-imported by `"use client"` + forms (WorkersConfigForm, ChannelTierSelect, LadderRung, …); a zod import there would ship + zod to the browser, contradicting "zod cannot reach a client bundle". The three schemas + live in `lib/settingsFieldSchemas.ts` (server-only importers); the sanitizers keep their + homes, names and signatures. Verified: no `ZodError`/`_zod` in `editor/.next/static` or + `export/.next/static` after both builds. +2. *`archiveStorage` lost its `?`.* zod 4 cannot express an optional key that is always + emitted (`.optional()` omits it when absent; a transform returning `T | undefined` is a + required key). It was always emitted at runtime, so the shape test pins it as required; + tsc across the workspace needed no change. +3. *`saveSettings(patch)` not `saveSettingsBlock(block, patch)`* — as instructed: + `channels/actions.ts` writes `channelPriority` and all four `autoQueue` roots in one write. +4. *The example omits `workers`.* `defaultSiteSettings().workers` is `[]`; a copied template + spelling `workers: []` would mean zero transcription slots, where an absent key + synthesizes one. Stated in SETTINGS.md. +5. *Only the 31 top-level comments moved into `.describe()`.* The nested block types + (`DigestSettings`, `DiarizationSettings`, …) keep their per-field comments on the + hand-written types, because each block is one sanitizer-backed field, not a zod object. + +**Behaviour changes (all intended, all small).** +- A settings.json containing `null` threw in `getSettings` (`parsed[key]` on null); it now + reads as the empty file, like `[]`, `3` and `{`. +- `adminTitle`, `cookiesFromBrowser`, `archiveStorage.*` are trimmed on read as they always + were on write (one schema). The live file has no untrimmed values. +- `storage/actions.ts`: adding/editing a location used to rebuild the storage block from two + keys and so **erased `storage.savedVideosLocationId`**; the one-level merge keeps it. +- `/settings` form: patches only its own 19 fields; it no longer resets the rollback-only + `transcriptionApps` shadow to `{}` (the shadow is still rewritten from workers every save). + +**Dead example keys removed**: `transcribeBin`, `transcribeModel`, `transcribeArgs` (the +pre-multi-app spelling, migrated on read). + +**Numbers** (`plans/tools/phase3-settings-numbers.ts`, `getSettings()` sorted-key JSON). +The live settings.json was re-saved at 19:24 mid-slice — the operator's Gate B change, applied +through the backfill lane form (lane held, `allowRedownload` off; that save also stripped the +retired `backfill.enabled`, as every save does), not a stray write — so the +comparison runs both builds over the SAME frozen inputs: the live file as of 19:24, the +pre-slice example, the e2e fixture, and both `docker/entrypoint.sh` seeds (parakeet, +whisper). Main `54cf1b31` vs branch tip: **empty diff, 3,846 lines**. The expected +`backfill.enabled` line never appeared: `sanitizeBackfill` already dropped it at main, so it +was not in `getSettings()` output before or after. The regenerated example of course parses +differently from the old one (no legacy `whisper-cli`/`firefox` keys) — by design. + +**Entrypoint seed.** Both seeds (`workers[0]` enabled local parakeet / whisper-cpp, +`parallelTranscriptions: 1`) parse to byte-identical settings through main and through the +schema (included in the numbers above). The entrypoint does not read the example. + +**Gates.** tsc (`pnpm -r --workspace-concurrency=1 exec tsc --noEmit`; the parallel `-r` form +was OOM-killed, exit 137) clean after every commit. common **1625 → 1648** (+20 schema, +3 +docs); `test:scripts` 156 pass + 1 skip of 157 (unchanged); mcp 219/219; editor unit **59 → 63**; +`next build` editor and export clean. +**e2e** (from the worktree root, detached, ports 3311/3310): auto-queue, backfill, digest, +diarization, attribution, scheduler, cadence-ui, storage-locations, channel-storage, workers, +worker-remote, parakeet, parakeet-partial, chough, transcription-app-migration, disk-space, +channel-priority, settings — **157/157 passed, exit 0, 9.9 min**, first run, nothing re-run.