Archilyzer · Source

archilyzer

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

commit 58dbf5ad2bdc141cb9a7b96ce812c2497f545e98
parent 904dc0df2ee3ba34c884edebb5e88206e8049d32
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 21 Sep 2026 03:31:02 -0400

review: a gate that opens on the click that opens it is not a gate

**`previewed` was set in the trigger.** The action returns as soon as the job
is enqueued, so an operator could press Preview and then Evict before a single
count had appeared. `StreamActionLog` gains `onSettled(started)` — called when
the job's stream has been read to the end, or with `false` when the trigger
refused before one existed — and the card arms on that. The age-keyed reset is
unchanged, so a preview of "older than 90 days" still cannot arm a sweep that
takes everything. The spec now asserts the button is STILL disabled between the
click and the terminal line, which is the window the bug lived in.

**`listClipWindows` still pre-filtered on `.mp4`.** With the parser widened but
the lister not, a `.mkv` window was invisible to the video page and to
`findContainingClipWindow` — the fetch dedup, which would then have paid a
source for those seconds again — while `evictClipWindows` could see and delete
it. A file one half of the code owns and the other cannot see is the worst of
both. One predicate now (`hasClipWindowExt`, beside the extension list the
parser strips), and a `.json` sidecar still has none of those extensions, so it
is read BY a window and never listed AS one.

common/lib/clipWindow-server.test.ts is new: a `.webm` window lists and is found
by `findContainingClipWindow`, all three extensions list and nothing else does,
and a video dir with no `clips/` lists nothing rather than throwing.

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

Diffstat:
Mcommon/components/StreamActionLog.tsx | 13+++++++++++++
Acommon/lib/clipWindow-server.test.ts | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/clipWindow-server.ts | 6+++++-
Mcommon/lib/clipWindow.ts | 14++++++++++++--
Meditor/app/storage/components/ClipWindowsCard.tsx | 21+++++++++++++++------
Meditor/e2e/storage-locations.spec.ts | 4++++
6 files changed, 140 insertions(+), 9 deletions(-)

diff --git a/common/components/StreamActionLog.tsx b/common/components/StreamActionLog.tsx @@ -26,6 +26,17 @@ type Props = { * that need a typed-confirmation gate before allowing the action. */ disabled?: boolean; + /** + * Called when a run SETTLES — the job's stream has been read to the end (or + * the trigger refused before one existed). `started` is false for a refusal. + * + * WHY THIS IS NOT "ON CLICK". A caller gating a destructive button behind a + * dry run has to arm it when the dry run has ANSWERED, not when it was + * asked: the trigger returns as soon as the job is enqueued, so flipping a + * flag there lets an operator press Preview and then Evict before a single + * count has appeared — which is the whole thing the gate exists to stop. + */ + onSettled?: (started: boolean) => void; }; type StatusPoll = { @@ -42,6 +53,7 @@ export function StreamActionLog({ cancelAction, extraControls, disabled = false, + onSettled, }: Props) { const accessibleName = label ?? buttonLabel; const router = useRouter(); @@ -154,6 +166,7 @@ export function StreamActionLog({ setError((e as Error).message); } finally { setRunning(false); + onSettled?.(started); // A run that actually started (success, mid-stream error, or cancel) // may have mutated server-derived data: a saved shard config, channel // counts, bucket lists, failed-transcription lists. Re-fetch the server diff --git a/common/lib/clipWindow-server.test.ts b/common/lib/clipWindow-server.test.ts @@ -0,0 +1,91 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { findContainingClipWindow, listClipWindows } from "./clipWindow-server"; + +// THE LISTER AND THE EVICTOR MUST AGREE ABOUT WHAT A WINDOW IS. +// +// They did not: `listClipWindows` pre-filtered on `.mp4` while +// `parseClipWindowName` had been widened, so a `.mkv` window was invisible to +// the video page and to `findContainingClipWindow` — the fetch dedup, which +// would then have paid a source for those seconds again — while +// `evictClipWindows` could see and delete it. A file one half of the code owns +// and the other cannot see is the worst of both. + +async function withClips( + files: Record<string, string>, + fn: (videoDir: string) => Promise<void>, +): Promise<void> { + const dir = await mkdtemp(path.join(tmpdir(), "ttb-clipwin-")); + try { + const clips = path.join(dir, "clips"); + await mkdir(clips, { recursive: true }); + for (const [name, body] of Object.entries(files)) { + await writeFile(path.join(clips, name), body); + } + await fn(dir); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test("a .webm window lists, and the fetch dedup finds it", async () => { + await withClips( + { + "10.00-40.00.webm": "not really webm", + "10.00-40.00.json": JSON.stringify({ requestedBy: "umtool" }), + }, + async (videoDir) => { + const windows = await listClipWindows(videoDir); + assert.deepEqual( + windows.map((w) => w.file), + ["10.00-40.00.webm"], + ); + assert.equal(windows[0].from, 10); + assert.equal(windows[0].to, 40); + // The sidecar was read BY the window, never listed AS one. + assert.equal(windows[0].provenance?.requestedBy, "umtool"); + + const found = await findContainingClipWindow(videoDir, 12, 30); + assert.equal(found?.file, "10.00-40.00.webm"); + // And a span it does not hold is still a miss. + assert.equal(await findContainingClipWindow(videoDir, 12, 90), null); + }, + ); +}); + +test("all three extensions list, and nothing else does", async () => { + await withClips( + { + "1.00-2.00.mp4": "a", + "3.00-4.00.mkv": "b", + "5.00-6.00.webm": "c", + // A sidecar with no window: it has no media extension, so it is not a + // window — and listing it would put a file with no bytes of media in + // front of the operator as one. + "7.00-8.00.json": "{}", + // Not a `<from>-<to>` name at all. + "notes.txt": "d", + "audio.mp4": "e", + }, + async (videoDir) => { + const windows = await listClipWindows(videoDir); + assert.deepEqual( + windows.map((w) => w.file).sort(), + ["1.00-2.00.mp4", "3.00-4.00.mkv", "5.00-6.00.webm"], + ); + }, + ); +}); + +test("a video dir with no clips/ lists nothing rather than throwing", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ttb-clipwin-")); + try { + assert.deepEqual(await listClipWindows(dir), []); + assert.equal(await findContainingClipWindow(dir, 1, 2), null); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/common/lib/clipWindow-server.ts b/common/lib/clipWindow-server.ts @@ -9,6 +9,7 @@ import { clipWindowFile, clipWindowSidecar, parseClipProvenance, + hasClipWindowExt, parseClipWindowName, tightestClipWindow, type ClipProvenance, @@ -36,7 +37,10 @@ export async function listClipWindows(videoDir: string): Promise<ClipWindow[]> { const entries = await readdir(dir).catch(() => [] as string[]); const out: ClipWindow[] = []; for (const name of entries) { - if (!name.endsWith(".mp4")) continue; + // THE SHARED PREDICATE, not a second copy of the extension list — see + // `hasClipWindowExt`. A `.json` sidecar has none of these extensions, so it + // never lists as a window; it is read BY the window it describes. + if (!hasClipWindowExt(name)) continue; const span = parseClipWindowName(name); if (!span) continue; const abs = path.join(dir, name); diff --git a/common/lib/clipWindow.ts b/common/lib/clipWindow.ts @@ -85,10 +85,20 @@ const WINDOW_RE = /^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/; // // NOT `.json`: a sidecar is removed with the window it describes and must // never parse as one itself. -const CLIP_EXTS = [".mp4", ".mkv", ".webm"] as const; +export const CLIP_WINDOW_EXTS = [".mp4", ".mkv", ".webm"] as const; + +// ONE PREDICATE, so the lister and the evictor cannot disagree about what a +// window is. They did: `listClipWindows` pre-filtered on `.mp4` while the +// parser had been widened, so a `.mkv` window was invisible to the video page +// and to `findContainingClipWindow` (the fetch dedup — it would have paid for +// those seconds again) while `evictClipWindows` could see and delete it. A file +// one half of the code owns and the other cannot see is the worst of both. +export function hasClipWindowExt(name: string): boolean { + return CLIP_WINDOW_EXTS.some((ext) => name.endsWith(ext)); +} function stripClipExt(name: string): string { - for (const ext of CLIP_EXTS) { + for (const ext of CLIP_WINDOW_EXTS) { if (name.endsWith(ext)) return name.slice(0, -ext.length); } return name; diff --git a/editor/app/storage/components/ClipWindowsCard.tsx b/editor/app/storage/components/ClipWindowsCard.tsx @@ -30,11 +30,16 @@ import { evictClipWindowsAction } from "../actions"; // PREVIEW FIRST, AND THE PREVIEW IS THE GATE. `dryRun` runs the identical pass // and deletes nothing, so the number in the log is produced by the code that // would do the work rather than by a second estimate that can disagree — and -// until one has run in this session the destructive button is disabled. One -// click should not be able to delete every clip window in the corpus, and +// until one has ANSWERED in this session the destructive button is disabled. +// One click should not be able to delete every clip window in the corpus, and // "older than 30 days" over a corpus nobody has looked at is a number the // operator has no way to picture. // +// ARMED ON `onSettled`, NEVER IN THE TRIGGER. The trigger returns as soon as +// the job is enqueued, so flipping the flag there would let somebody press +// Preview and then Evict before a single count had appeared — a gate that +// unlocks on the click that opens it is not a gate. +// // "ANY AGE" IS A SECOND GATE. `0` means every window on every drive, which is // a legitimate ask (the operator is emptying a disk) and the one setting where // a preview alone is not enough of a pause. It needs the checkbox ticked as @@ -129,9 +134,13 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { <div className="flex flex-wrap items-start gap-4"> <StreamActionLog key="evict-clips-preview-log" - trigger={() => { - setPreviewed(true); - return evictClipWindowsAction({ olderThanDays: days, dryRun: true }); + trigger={() => + evictClipWindowsAction({ olderThanDays: days, dryRun: true }) + } + // `started` is false when the action refused before a job existed — + // a refusal is not a preview, so it arms nothing. + onSettled={(started) => { + if (started) setPreviewed(true); }} cancelAction={cancelJobAction} buttonLabel="Preview eviction" @@ -152,7 +161,7 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { <p aria-label="clip eviction gate" className="text-xs text-muted-foreground"> {previewed ? "Tick the box above to evict every window." - : "Preview first — the eviction button unlocks once you have seen what it would take."} + : "Preview first — the eviction button unlocks once the dry run has reported what it would take."} </p> )} </article> diff --git a/editor/e2e/storage-locations.spec.ts b/editor/e2e/storage-locations.spec.ts @@ -660,7 +660,11 @@ test("evicting at any age needs the tick as well as the preview", async ({ await confirm.check(); await expect(evict).toBeDisabled(); + // ARMED BY THE ANSWER, NOT BY THE CLICK. The action returns as soon as the + // job is enqueued, so a gate flipped in the trigger would already be open + // here — which is the whole thing it exists to stop. await page.getByRole("button", { name: "Preview eviction" }).click(); + await expect(evict).toBeDisabled(); await expect(page.getByLabel("Preview eviction output")).toContainText( /Would evict/, { timeout: 60_000 },