Archilyzer · Source

archilyzer

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

commit 3b741a45b76fc61f2c24ed57f337e2f2400fe189
parent a9e15e7b204851f9deb7b8794d87d96bce4d68ef
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 28 Aug 2026 15:42:19 -0400

backfill: a channel job can be scoped to video ids

runBackfillBatch already took `ids` and intersected them with disk. The job
around it did not, so there was no way to run the lane over one video: the
only entry points were the stage card's whole-channel run and the sweep's.

`ids` now threads through runBackfillChannelJob to countBackfillWork and the
batch. Counting matters as much as pulling — countBackfillWork walked every
video dir with no filter, so an ids-scoped run would have sized its progress
bar to the whole channel and stalled one short of complete forever.

It is in `spec.params` beside `kindIds`, so a replayed per-video run stays
per-video rather than quietly widening to the channel; jobReplayRegistry parses
it exactly as it parses kindIds. runOperationChannelJob is untouched — the
dispatcher stays channel-scoped.

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

Diffstat:
Mcommon/controller/backfillBatch.ts | 12+++++++++++-
Mcommon/controller/operationJobs.ts | 18++++++++++++------
Mcommon/controller/videoOperations.test.ts | 25+++++++++++++++++++++++++
Meditor/app/channels/[slug]/backfillActions.ts | 4++++
Meditor/app/jobs/jobReplayRegistry.ts | 9+++++++--
5 files changed, 59 insertions(+), 9 deletions(-)

diff --git a/common/controller/backfillBatch.ts b/common/controller/backfillBatch.ts @@ -870,6 +870,11 @@ export async function countBackfillWork( paths: Paths, channelSlug: string, kindIds?: string[], + // Only these videos, intersected with disk — the same scope the batch takes. + // The progress target must count what THIS run will pull, and an ids-scoped + // run pulls only those; without the filter a per-video run would size its bar + // to the whole channel and stall one short of complete forever. + ids?: string[], ): Promise<{ reachable: number; missingInput: number; @@ -881,7 +886,12 @@ export async function countBackfillWork( if (kinds.length === 0) return { reachable: 0, missingInput: 0, deferred: 0, blocked: 0 }; const dataDir = path.join(paths.channelsDir, channelSlug, "data"); - const dirs = await readdir(dataDir).catch(() => [] as string[]); + const allDirs = await readdir(dataDir).catch(() => [] as string[]); + // Intersected with disk exactly as the batch's own pull does, so an id that + // names nothing counts as nothing rather than as one unit of work. + const dirs = ids + ? ids.filter((id) => allDirs.includes(id)) + : allDirs; const targets = new Map<string, unknown>(); for (const kind of kinds) { targets.set( diff --git a/common/controller/operationJobs.ts b/common/controller/operationJobs.ts @@ -64,6 +64,9 @@ export type BackfillChannelJobOptions = { // card's per-row "run only this" passes one; the group button passes none, // because that station IS the lane rather than one operation on it. kindIds?: string[]; + // Only these videos (intersected with disk by the batch). A stage card passes + // none; the video page passes one. In `spec.params` so a replay stays scoped. + ids?: string[]; // An operator's queue override from the card's QueueControl. `undefined` is // "no control wired up" and takes the lane's own key; "" means immediate. queueKey?: string; @@ -77,27 +80,30 @@ export type BackfillChannelJobOptions = { export async function runBackfillChannelJob( opts: BackfillChannelJobOptions, ): Promise<StreamActionResult> { - const { paths, channelSlug, kindIds } = opts; + const { paths, channelSlug, kindIds, ids } = opts; const result = await runManagedFunction({ kind: BACKFILL_CHANNEL_KIND, queueKey: resolveQueueKey(BACKFILL_QUEUE, opts.queueKey), paths, channelSlug, background: opts.background, - // BOTH fields, because jobReplayRegistry.ts reads `p.kindIds` to replay this - // job and the queue override has to survive a replay too. Undefined fields - // drop out of the JSON, so a run with neither stores neither. + // ALL THREE fields, because jobReplayRegistry.ts replays this job from + // them: the kind scope, the queue override and the id scope all have to + // survive a replay — a replayed per-video run that widened to the whole + // channel would be a different job wearing the same record. Undefined + // fields drop out of the JSON, so a run with none stores none. spec: { kind: BACKFILL_CHANNEL_KIND, slug: channelSlug, - params: { kindIds, queueKey: opts.queueKey }, + params: { kindIds, queueKey: opts.queueKey, ids }, }, fn: async (onLog, signal, setProgress, ctx) => { - const before = await countBackfillWork(paths, channelSlug, kindIds); + const before = await countBackfillWork(paths, channelSlug, kindIds, ids); const batch = await runBackfillBatch({ channelSlug, paths, kindIds, + ids, setProgress, // The bar measures THIS run, from zero. Seeding it with what is already // on disk cannot represent a regeneration, where no file count moves. diff --git a/common/controller/videoOperations.test.ts b/common/controller/videoOperations.test.ts @@ -53,6 +53,7 @@ writeSettings(settingsOn()); const { inspectVideoOperations, orderForVideoPage, shownOnVideoPage } = await import("./videoOperations"); +const { countBackfillWork } = await import("./backfillBatch"); const { getPaths } = await import("../lib/paths"); const { getSettings } = await import("../lib/settings"); @@ -231,6 +232,30 @@ test("shownOnVideoPage: off with nothing on disk hides; off with a sidecar shows assert.equal(shownOnVideoPage(withRecord!), true); }); +// Lives here rather than in backfillBatch.test.ts because countBackfillWork +// reads settings from disk and this file already owns the settings seam. +test("countBackfillWork sizes an ids-scoped run to those ids alone", async () => { + writeSettings( + settingsOn({ + backfill: { enabled: true, weight: 1, concurrency: 1 }, + }), + ); + const idA = "vid-count-a"; + const idB = "vid-count-b"; + await seed(idA); + await seed(idB); + const paths = getPaths(); + // Both are reachable for the text lane — its input is the cue stream, which + // every transcribed video has. + const whole = await countBackfillWork(paths, SLUG, ["attribution-text"]); + assert.ok(whole.reachable >= 2); + // Scoped, the progress target must count what THIS run will pull. + const scoped = await countBackfillWork(paths, SLUG, ["attribution-text"], [ + idB, + ]); + assert.equal(scoped.reachable, 1); +}); + test("orderForVideoPage puts the digest group ahead of the speakers group", async () => { writeSettings(settingsOn()); await seed("vid-sort", { diarization: true }); diff --git a/editor/app/channels/[slug]/backfillActions.ts b/editor/app/channels/[slug]/backfillActions.ts @@ -25,11 +25,15 @@ export async function backfillChannelAction( slug: string, queueKey?: string, kindIds?: string[], + // The video page's per-video Run passes exactly one id; a stage card passes + // none and gets the whole channel, as before. + ids?: string[], ): Promise<StreamActionResult> { return runBackfillChannelJob({ paths: getPaths(), channelSlug: slug, kindIds, + ids, queueKey, onDone: () => revalidatePath(`/channels/${slug}`), }); diff --git a/editor/app/jobs/jobReplayRegistry.ts b/editor/app/jobs/jobReplayRegistry.ts @@ -275,13 +275,18 @@ export const JOB_REPLAY_HANDLERS: Record<string, ReplayHandler> = { // Replays correctly with nothing remembered: the batch re-derives its whole // work-list from disk, so a replay does what is missing NOW rather than what // was missing when the record was written. The kind scope is carried through - // so a replayed single-kind run stays single-kind. + // so a replayed single-kind run stays single-kind — and the ID scope for the + // same reason: a replayed per-video run stays per-video rather than widening + // to the whole channel. "backfill-channel": (spec) => { const { p, queueKey } = params(spec); const kindIds = Array.isArray(p.kindIds) ? p.kindIds.filter((k): k is string => typeof k === "string") : undefined; - return backfillChannelAction(spec.slug, queueKey, kindIds); + const ids = Array.isArray(p.ids) + ? p.ids.filter((k): k is string => typeof k === "string") + : undefined; + return backfillChannelAction(spec.slug, queueKey, kindIds, ids); }, "check-kept-deleted": (spec) => { const { queueKey } = params(spec);