Archilyzer · Source

archilyzer

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

commit 7dcaea61ed552981295cf907cdab8cc934c98aff
parent 8d46c5578545b91b181d8d919a3e0b99cfc419a2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue,  6 Oct 2026 11:13:44 -0400

jobs: the seven publish-* stage kinds, the auto-publish runner, label-only entries for the kinds release 18 stops creating; publish stages read "run <id> · <target>" on /jobs and are never re-queued at boot

- jobKinds: publish-update-index … publish-deploy-homepage (replayable, not
  drainable), auto-publish (drainable runner), and build-index, build-stats,
  build-export, build-deploy, build-all, build-deploy-all, deploy-export kept
  known with NO label (their raw kind, as /jobs has always shown them)
- isIngestKind: the kinds whose `done` can change what the index reads —
  every drainable per-channel kind plus the single writers and the
  auto-download unit (tested against the drainable set)
- jobSpecDetail: `run <last six of runId> · <target>` for a publish stage
- bootQueuedJobs: a queued publish stage is cancelled as `publish`, "the
  publish lane re-derives stages from on-disk state", never re-queued

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

Diffstat:
Mcommon/jobs/bootQueuedJobs.test.ts | 40++++++++++++++++++++++++++++++++++++++++
Mcommon/jobs/bootQueuedJobs.ts | 12++++++++++--
Acommon/jobs/jobDetail.test.ts | 42++++++++++++++++++++++++++++++++++++++++++
Mcommon/jobs/jobDetail.ts | 13++++++++++++-
Mcommon/jobs/jobKinds.test.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/jobs/jobKinds.ts | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 354 insertions(+), 3 deletions(-)

diff --git a/common/jobs/bootQueuedJobs.test.ts b/common/jobs/bootQueuedJobs.test.ts @@ -8,6 +8,7 @@ import type { JobMeta } from "./jobMeta"; import type { JobSpec } from "./jobSpec"; import { INTERRUPTED_REASON, + PUBLISH_RESTART_REASON, STORAGE_PASS_WAIT_MS, processIsAlive, settleAfterStoragePass, @@ -555,3 +556,42 @@ test("the queued pass leaves a queued meta whose writer is still alive", async ( await rm(f.root, { recursive: true, force: true }); } }); + +// RELEASE 18: a queued publish stage is never re-queued. The lane and Publish +// now re-derive stages from the stamps; a stage re-queued with its run's +// preconditions would wait on a run that is gone. +test("a queued publish stage is cancelled as `publish`, never re-queued, even with a fresh spec", async () => { + const stage: JobSpec = { + kind: "publish-build-site", + slug: "jeralyzer", + params: { kind: "build-site", target: "jeralyzer", runId: "r1", indexAfter: BOOT - HOUR }, + }; + const f = await fixture([ + { id: "P1", kind: "publish-build-site", queueKey: "publish", spec: stage }, + { id: "P2", kind: "publish-update-index", queueKey: "publish", spec: { kind: "publish-update-index", slug: "_index" } }, + { id: "F1", spec: SPEC }, + ]); + try { + const rq = recordingRequeue(); + const lines: string[] = []; + const res = await settleQueuedJobMetas({ + paths: f.paths, + requeue: rq.fn, + bootedAt: BOOT, + log: (l) => lines.push(l), + }); + assert.deepEqual(rq.calls, [SPEC]); + assert.deepEqual( + res.cancelled.map((c) => [c.id, c.category]).sort(), + [ + ["P1", "publish"], + ["P2", "publish"], + ], + ); + assert.equal((await f.read("P1")).cancelReason, PUBLISH_RESTART_REASON); + assert.match(PUBLISH_RESTART_REASON, /the publish lane re-derives stages from on-disk state/); + assert.match(lines.at(-1) ?? "", /cancelled 2 \(publish 2\)/); + } finally { + await rm(f.root, { recursive: true, force: true }); + } +}); diff --git a/common/jobs/bootQueuedJobs.ts b/common/jobs/bootQueuedJobs.ts @@ -4,6 +4,7 @@ import { writeFileAtomic } from "../lib/jsonFile-server"; import type { Paths } from "../lib/paths"; import { metaPath, readJobMeta, type JobMeta } from "./jobMeta"; import type { JobSpec } from "./jobSpec"; +import { isPublishStageJobKind } from "./jobKinds"; // THE BOOT PASS OVER STALE `queued` METAS (release 9, B4b). // @@ -23,7 +24,10 @@ import type { JobSpec } from "./jobSpec"; // 1. an idle boot (`requeue: null`) cancels everything — it must not resume // work; // 2. `sync` is never re-queued: the heartbeat re-derives due syncs itself, -// paced; +// paced; nor is a publish stage (release 18, `publish-*`): the publish +// lane and Publish now re-derive stages from the stamps on disk, and a +// stage re-queued with its run's preconditions would wait on a run that +// is gone; // 3. a meta queued more than REQUEUE_MAX_AGE_MS before this boot is stale; // 4. a meta with no replay spec cannot be re-queued; // 5. of what is left, only the NEWEST meta per kind + channel + params is @@ -48,7 +52,7 @@ export type RequeueFn = ( ) => Promise<{ ok: true; jobId: string } | { ok: false; error: string }>; export type CancelCategory = - "idle" | "sync" | "stale" | "no-spec" | "superseded" | "refused"; + "idle" | "sync" | "publish" | "stale" | "no-spec" | "superseded" | "refused"; export type BootQueuedResult = { requeued: { id: string; newId: string }[]; @@ -56,6 +60,8 @@ export type BootQueuedResult = { }; export const RESTART_REASON = "server restarted before it ran"; +export const PUBLISH_RESTART_REASON = + "server restarted; the publish lane re-derives stages from on-disk state"; export const REQUEUE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // Key a spec by what it would DO: kind + channel + bucket + params, with the @@ -283,6 +289,8 @@ export async function settleQueuedJobMetas( "sync", "server restarted; the scheduler re-derives syncs", ); + } else if (isPublishStageJobKind(meta.kind)) { + await cancel(meta, "publish", PUBLISH_RESTART_REASON); } else if ( typeof meta.queuedAt !== "number" || opts.bootedAt - meta.queuedAt > maxAgeMs diff --git a/common/jobs/jobDetail.test.ts b/common/jobs/jobDetail.test.ts @@ -0,0 +1,42 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { jobSpecDetail } from "./jobDetail"; + +// Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test jobs/jobDetail.test.ts + +test("a publish stage reads `run <last six of the run id> · <target>`", () => { + assert.equal( + jobSpecDetail("publish-build-site", { + kind: "publish-build-site", + slug: "jeralyzer", + params: { runId: "run-0000abc12-9f8e7d6c", kind: "build-site", target: "jeralyzer" }, + }), + "run 8e7d6c · jeralyzer", + ); + assert.equal( + jobSpecDetail("publish-update-index", { + kind: "publish-update-index", + slug: "_index", + params: { runId: "cli-abcdef" }, + }), + "run abcdef · _index", + ); +}); + +test("a publish stage with no run id (or no spec) adds nothing", () => { + assert.equal(jobSpecDetail("publish-deploy-hub", { kind: "publish-deploy-hub", slug: "_hub" }), undefined); + assert.equal(jobSpecDetail("publish-deploy-hub", undefined), undefined); +}); + +test("every other kind is unchanged: fetch-window keeps its phrase, the rest nothing", () => { + assert.equal( + jobSpecDetail("fetch-window", { + kind: "fetch-window", + slug: "c", + params: { requestedBy: "umtool", manifest: "m", clipId: "c1" }, + }), + "umtool · m/c1", + ); + assert.equal(jobSpecDetail("sync", { kind: "sync", slug: "c", params: { runId: "zzzzzzzz" } }), undefined); + assert.equal(jobSpecDetail(undefined, undefined), undefined); +}); diff --git a/common/jobs/jobDetail.ts b/common/jobs/jobDetail.ts @@ -7,16 +7,27 @@ import type { JobSpec } from "./jobSpec"; // for what. The answer is already on the job — the replay spec's params — so // this reads it rather than adding a second place to store it. // +// A PUBLISH STAGE (release 18, `publish-<stage>`) reads `run <last six of the +// run id> · <target>`: every stage one Publish now, one lane pass or one CLI +// command enqueued carries the same run id, so a run reads as a group on +// /jobs. The target is the spec's slug ("_index", a site id, "_hub", …). +// // Pure and total: a kind with nothing to add returns undefined and the row // renders exactly as it did. export function jobSpecDetail( kind: string | undefined, spec: JobSpec | null | undefined, ): string | undefined { - if (kind !== "fetch-window") return undefined; const p = spec?.params ?? {}; const s = (v: unknown): string | null => typeof v === "string" && v.trim() !== "" ? v.trim() : null; + if (typeof kind === "string" && kind.startsWith("publish-")) { + const runId = s(p.runId); + const target = s(spec?.slug); + if (!runId || !target) return undefined; + return `run ${runId.slice(-6)} · ${target}`; + } + if (kind !== "fetch-window") return undefined; const by = s(p.requestedBy); if (!by) return undefined; const what = [s(p.manifest), s(p.clipId)].filter(Boolean).join("/"); diff --git a/common/jobs/jobKinds.test.ts b/common/jobs/jobKinds.test.ts @@ -6,6 +6,9 @@ import { getJobKind, kindNeedsMedia, kindNeedsText, + isIngestKind, + isPublishStageJobKind, + jobKindIds, } from "./jobKinds"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test jobs/jobKinds.test.ts @@ -211,3 +214,72 @@ test("the feed metadata backfill reads and writes the text tier only", () => { assert.equal(jobKindLabel("feed-metadata"), "Backfill feed metadata"); assert.equal(isDrainableKind("feed-metadata"), false); }); + +// RELEASE 18: the seven publish stages, the lane's runner, and the kinds the +// stages replace (label-only: archived metas still read). +const PUBLISH_LABELS: Record<string, string> = { + "publish-update-index": "Update the index", + "publish-build-site": "Build site", + "publish-deploy-site": "Deploy site", + "publish-build-hub": "Build hub", + "publish-deploy-hub": "Deploy hub", + "publish-build-homepage": "Build homepage", + "publish-deploy-homepage": "Deploy homepage", +}; + +test("release 18: the seven publish stages are replayable, undrainable, labelled", () => { + for (const [kind, label] of Object.entries(PUBLISH_LABELS)) { + const meta = getJobKind(kind); + assert.ok(meta, `${kind} is registered`); + assert.equal(meta.label, label); + assert.equal(meta.replayable, true, `${kind} is replayable`); + assert.equal(isDrainableKind(kind), false, `${kind} is a child process: cancel, not drain`); + assert.equal(kindNeedsMedia(kind), false); + assert.equal(isPublishStageJobKind(kind), true); + } + assert.equal(isPublishStageJobKind("publish-nothing"), false); + assert.equal(isPublishStageJobKind("build-hub"), false); + assert.equal(isPublishStageJobKind(undefined), false); + const runner = getJobKind("auto-publish"); + assert.ok(runner); + assert.equal(runner.label, "Auto-publish runner"); + assert.equal(runner.drainable, true); + assert.equal(runner.replayable, false); +}); + +test("release 18: the kinds no longer created keep their labels (seven of them none)", () => { + for (const kind of [ + "build-index", + "build-stats", + "build-export", + "build-deploy", + "build-all", + "build-deploy-all", + "deploy-export", + ]) { + assert.ok(getJobKind(kind), `${kind} is registered`); + // /jobs shows them by their raw kind, as it always has (jobs.spec reads it). + assert.equal(jobKindLabel(kind), kind); + assert.equal(getJobKind(kind)?.replayable, false); + } + assert.equal(jobKindLabel("build-hub"), "Build hub"); + assert.equal(jobKindLabel("build-deploy-homepage"), "Build & deploy homepage"); +}); + +test("release 18: every drainable kind but the lane runners is an ingest kind", () => { + const runners = new Set(["auto-transcribe", "auto-download", "auto-digest", "auto-backfill", "auto-publish"]); + for (const kind of jobKindIds()) { + if (!isDrainableKind(kind) || runners.has(kind)) continue; + assert.equal(isIngestKind(kind), true, `${kind} is drainable and per channel: an ingest kind`); + } + for (const kind of runners) assert.equal(isIngestKind(kind), false, kind); + // The lane's own unit, and the single writers, count too. + for (const kind of ["auto-download-unit", "import-one", "transcribe-one", "normalize-transcripts"]) { + assert.equal(isIngestKind(kind), true, kind); + } + // Nothing that publishes, moves or only reads. + for (const kind of ["publish-build-site", "build-export", "relocate-channel-media", "scan-media", "fetch-window"]) { + assert.equal(isIngestKind(kind), false, kind); + } + assert.equal(isIngestKind("no-such-kind"), false); +}); diff --git a/common/jobs/jobKinds.ts b/common/jobs/jobKinds.ts @@ -674,6 +674,121 @@ const JOB_KINDS: Record<string, JobKindMeta> = { replayable: false, queueKeyStrategy: "custom", }, + // THE PUBLISH STAGES (release 18): one job per stage on the `publish` queue, + // each a child process (`archilyzer stage <kind> <target>`, publish/ + // stageRun.ts) the editor spawns through runManagedCommand — so Cancel kills + // it, and there is nothing to drain. Replayable: the spec's params ARE the + // StageRequest (publish/publishStages.ts), and a replay re-asks the stage's + // preconditions on disk. The labels are the stages' own (publish/stages.ts + // STAGES[kind].label; jobKinds.test.ts holds them equal). No channelSlug and + // no `needsMedia`: a stage reads the index and the bundles, and the update- + // index child guards its own channels (the index build's hold). + "publish-update-index": { + kind: "publish-update-index", + label: "Update the index", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + "publish-build-site": { + kind: "publish-build-site", + label: "Build site", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + "publish-deploy-site": { + kind: "publish-deploy-site", + label: "Deploy site", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + "publish-build-hub": { + kind: "publish-build-hub", + label: "Build hub", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + "publish-deploy-hub": { + kind: "publish-deploy-hub", + label: "Deploy hub", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + "publish-build-homepage": { + kind: "publish-build-homepage", + label: "Build homepage", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + "publish-deploy-homepage": { + kind: "publish-deploy-homepage", + label: "Deploy homepage", + drainable: false, + replayable: true, + queueKeyStrategy: "custom", + }, + // THE PUBLISH LANE'S RUNNER (publish/publishRunner.ts): one long-lived job on + // queueKey "", like the four auto-queue runners. Drainable — a drain lets the + // stage in flight finish and dispatches no more — and never replayable: a + // loop is not a unit of work. + "auto-publish": { + kind: "auto-publish", + label: "Auto-publish runner", + drainable: true, + replayable: false, + queueKeyStrategy: "parallel", + }, + // THE KINDS RELEASE 18 NO LONGER CREATES. Their archived metas still read, + // so they stay known, labels exactly as they were: the six the hub and the + // homepage have carried since release 13 above, and these seven with NONE + // (/jobs has always shown them by their raw kind, and e2e reads that text). + "build-index": { + kind: "build-index", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, + "build-stats": { + kind: "build-stats", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, + "build-export": { + kind: "build-export", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, + "build-deploy": { + kind: "build-deploy", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, + "build-all": { + kind: "build-all", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, + "build-deploy-all": { + kind: "build-deploy-all", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, + "deploy-export": { + kind: "deploy-export", + drainable: false, + replayable: false, + queueKeyStrategy: "custom", + }, // A REPORT SITE'S EVIDENCE MEDIA (publish/reportMedia.ts): every clip its // published reports cite, cut from the media on disk, and every cited post // capture copied, into the site's report-media cache before its build. @@ -808,6 +923,69 @@ export function jobKindLabel(kind: string): string { return JOB_KINDS[kind]?.label ?? kind; } +// THE PUBLISH STAGES' JOB KINDS (release 18): `publish-<stage kind>`. +export function isPublishStageJobKind(kind: string | undefined): boolean { + return typeof kind === "string" && kind.startsWith("publish-") && JOB_KINDS[kind] !== undefined; +} + +// THE KINDS WHOSE `done` CAN CHANGE WHAT THE INDEX READS (release 18): what +// makes the index stale, and a site's channels "changed", to the publish +// status (publish/publishState.ts). The plan's words are "the drainable +// kinds", and every drainable kind that runs per channel is here +// (jobKinds.test.ts holds that) — plus the per-video and one-shot writers of +// the same text and posts that are not drainable: the auto-download lane's +// unit, a single import, transcription or download, the availability checks, +// the forum import, the feed backfill and the cues sweep. The lane RUNNERS are +// not: their meta is `running` for as long as the lane is, and their units +// either have their own job (auto-download-unit) or none at all +// (transcription, digest and backfill units — publishState.ts reads the +// channel's report regeneration for those). +const INGEST_KINDS: ReadonlySet<string> = new Set([ + "whisper-all", + "whisper-bucket-downloaded-no-transcript", + "whisper-bucket-auto-subs", + "purge-superseded-auto-subs", + "digest-channel-local", + "digest-channel-remote", + "digest-share-cluster", + "normalize-transcripts", + "redownload-incomplete-bucket", + "download-from-playlist", + "download-missing", + "download-missing-subs", + "import-one", + "import-archive-org", + "redownload-archive", + "retry-bucket", + "diarize-channel", + "backfill-channel", + "persist-videos", + "persist-kept", + "fetch-posts", + "import-forum-pages", + "capture-posts", + "check-post-availability", + "sync", + "metadata-scan", + "feed-metadata", + "auto-download-unit", + "whisper-video", + "transcribe-one", + "download-one-pipeline", + "check-availability", + "quick-availability-check", + "check-maybe-missing", +]); + +export function isIngestKind(kind: string | undefined): boolean { + return typeof kind === "string" && INGEST_KINDS.has(kind); +} + +// Every registered kind (for the tests that hold one table to another). +export function jobKindIds(): string[] { + return Object.keys(JOB_KINDS); +} + export function isDrainableKind(kind: string): boolean { return JOB_KINDS[kind]?.drainable ?? false; }