Archilyzer · Source

archilyzer

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

commit 882f0c781afc628f463b17a7ad7850f90ab9cb9e
parent 9cf4ec234dc395a3c5014957da9c4d51f4b9692c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  1 Oct 2026 20:46:51 -0400

common, umtool: a scope-less marker aimed at <root>/<slug>/data holds the text; the hook writes nothing under a marker

markerHoldsText: a tier migration, or the old whole-directory mover's marker
(no scope, a retired-shape target), holds the text in the guard, the digest
lane's pick-to-run backstop and the cues twin; a media move does not. The
tier hook leaves a file real while a .relocating.json stands on its channel.

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

Diffstat:
Mcommon/controller/autoRunner.ts | 3++-
Mcommon/lib/channelMedia.test.ts | 17+++++++++++++++++
Mcommon/lib/channelMedia.ts | 15++++++++++++++-
Mcommon/lib/mediaTier-server.test.ts | 13+++++++++++++
Mcommon/lib/mediaTier-server.ts | 15+++++++++++++++
Mumtool/report-to-video/cues.mjs | 13+++++++++----
6 files changed, 70 insertions(+), 6 deletions(-)

diff --git a/common/controller/autoRunner.ts b/common/controller/autoRunner.ts @@ -86,6 +86,7 @@ import { downloadQueueKey } from "../lib/queueKeys"; import { isGateHeld } from "../lib/pauseGates"; import { inspectChannelMedia, + markerHoldsText, readRelocationMarker, type ChannelMediaLocation, type ChannelMediaStatus, @@ -1921,7 +1922,7 @@ async function runLoop( // is, and a digest writes only text. A tier migration rebuilds `data/` // itself, and holds it too. const marker = await readRelocationMarker(paths, channelSlug); - if (marker && (kind !== "digest" || marker.scope === "tier-migration")) { + if (marker && (kind !== "digest" || markerHoldsText(marker))) { onLog( `Auto-${kind}: skipping ${channelSlug}/${pick.videoId}, ` + `${mediaHoldText("in-transition")} — a relocation ` + diff --git a/common/lib/channelMedia.test.ts b/common/lib/channelMedia.test.ts @@ -431,3 +431,20 @@ test("clearRelocationMarker removes the marker and touches nothing else", async await clearRelocationMarker(paths, "alpha"); }); }); + +test("a scope-less marker aimed at the retired <root>/<slug>/data holds the text (the old mover)", async () => { + await withTmp(async (paths, root) => { + const channelDir = await seedChannel(paths, "alpha"); + await mkdir(path.join(channelDir, "data"), { recursive: true }); + await writeFile( + path.join(channelDir, RELOCATION_MARKER_FILENAME), + JSON.stringify({ target: relocatedDataDir(root, "alpha"), direction: "out", phase: "copy" }), + ); + assert.equal((await inspectChannelMedia(paths, "alpha", undefined, { fresh: true })).text.readable, false); + await writeFile( + path.join(channelDir, RELOCATION_MARKER_FILENAME), + JSON.stringify({ target: relocatedMediaDir(root, "alpha"), direction: "out", phase: "copy" }), + ); + assert.equal((await inspectChannelMedia(paths, "alpha", undefined, { fresh: true })).text.readable, true); + }); +}); diff --git a/common/lib/channelMedia.ts b/common/lib/channelMedia.ts @@ -167,6 +167,19 @@ export function relocatedDataDir(root: string, slug: string): string { return path.join(root.trim(), slug, "data"); } +// WHETHER A MARKER HOLDS THE TEXT TOO. A media move (`scope` "media", or none +// with a `<root>/<slug>/media` target) carries `media/` only, so its channel's +// text stays readable. A tier migration rebuilds `data/` itself; and a marker +// with no scope whose target is the retired `<root>/<slug>/data` shape is the +// old whole-directory mover's, copying `data/` — both hold the text. +export function markerHoldsText( + marker: Pick<RelocationMarker, "scope" | "target">, +): boolean { + if (marker.scope === "tier-migration") return true; + if (marker.scope === "media") return false; + return path.basename(marker.target.replace(/\/+$/, "")) === "data"; +} + // The sentence a legacy channel is refused with, naming the way out. export function legacyDetail(slug: string): string { return ( @@ -482,7 +495,7 @@ async function inspectOnDisk( target: mediaDir ?? marker.target, text: { dir: dataDir, - readable: textReadable && marker.scope !== "tier-migration", + readable: textReadable && !markerHoldsText(marker), }, status: "in-transition", detail: diff --git a/common/lib/mediaTier-server.test.ts b/common/lib/mediaTier-server.test.ts @@ -15,7 +15,9 @@ import { import { existsSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import { RELOCATION_MARKER_FILENAME } from "./channelMedia"; import { + TIER_RELOCATION_MARKER, channelMediaLink, relocatedMediaDir, removeMediaFile, @@ -270,3 +272,14 @@ test("the link carries the file's mtime, so an lstat answers freshness without t assert.equal(l.mtimeMs, when.getTime()); await rm(f.root, { recursive: true, force: true }); }); + +test("a move marker on the channel: the hook writes nothing into media/", async () => { + assert.equal(TIER_RELOCATION_MARKER, RELOCATION_MARKER_FILENAME); + const f = await fixture(); + await mkdir(channelMediaLink(f.paths, f.slug)); + await writeFile(path.join(f.channelsDir, f.slug, RELOCATION_MARKER_FILENAME), "{}"); + assert.equal(await tierMediaFile(f.videoDir, "audio.mp3"), "left"); + assert.deepEqual(await readdir(channelMediaLink(f.paths, f.slug)), []); + assert.ok((await lstat(path.join(f.videoDir, "audio.mp3"))).isFile()); + await rm(f.root, { recursive: true, force: true }); +}); diff --git a/common/lib/mediaTier-server.ts b/common/lib/mediaTier-server.ts @@ -47,6 +47,10 @@ import { onDrive, stalledLocationForPath } from "./storageHealth"; // The one name a channel's media tier is reached by: `channels/<slug>/media`. export const MEDIA_LINK_NAME = "media"; +// channelMedia.ts's RELOCATION_MARKER_FILENAME, as a literal because that +// module imports this one (mediaTier-server.test.ts pins that they agree). +export const TIER_RELOCATION_MARKER = ".relocating.json"; + export function channelMediaLink( paths: Pick<Paths, "channelsDir">, slug: string, @@ -107,6 +111,17 @@ export type TierOptions = { // answers within the watchdog's budget. False for a classic channel (no // `media`), a dangling link (an unmounted drive), a stalled drive. async function mediaTierReady(mediaRoot: string): Promise<boolean> { + // A move of this channel's media in flight (or interrupted): its marker + // stands in the channel dir, and nothing is written into `media/` while it + // does — the file stays real and the next sweep tiers it. (The writers that + // call the hook are held by the media guard during a move; this is the + // backstop for one that started before the marker.) + try { + await lstat(path.join(path.dirname(mediaRoot), TIER_RELOCATION_MARKER)); + return false; + } catch { + /* no marker: the normal case */ + } let target = mediaRoot; try { const l = await lstat(mediaRoot); diff --git a/umtool/report-to-video/cues.mjs b/umtool/report-to-video/cues.mjs @@ -293,19 +293,24 @@ export function createCueSource({ // A tier migration in flight (or interrupted) is rebuilding `data/`: half // of two places at once. The editor refuses such a channel; so does this. - // A media move (`scope` "media", or none) leaves the text alone. + // A media move leaves the text alone. let marker = null; try { marker = JSON.parse(await readFile(path.join(channelDir, ".relocating.json"), "utf8")); } catch { /* no marker: the normal case */ } - if ( + // `markerHoldsText` in channelMedia.ts: a tier migration, or a scope-less + // marker aimed at the retired `<root>/<slug>/data` shape (the old mover, + // copying the whole `data/`). + const holdsText = marker && typeof marker.target === "string" && marker.target.trim() && - marker.scope === "tier-migration" - ) { + (marker.scope === "tier-migration" || + (marker.scope !== "media" && + path.basename(marker.target.trim().replace(/\/+$/, "")) === "data")); + if (holdsText) { fail( `its media layout is being migrated (phase "${marker.phase ?? "copy"}") — ` + `wait for archilyzer storage migrate-tier to finish`,