Archilyzer · Source

archilyzer

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

commit 904dc0df2ee3ba34c884edebb5e88206e8049d32
parent 807a793aa10a01ce4031feb77042b5fcfc0f765b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 21 Sep 2026 03:22:20 -0400

review: the free-space guard was asking the wrong two directories

**The HIGH one, and it broke the only location on this machine.**
`volumeFreeBytes` compared `stat(root).dev` with its parent's. A root is
`join(mountpoint, relPath)` — production's `platter` is
`/run/media/user/<uuid>/archilyzer-media`, a SUBDIRECTORY of the udisks
mountpoint — so root and parent are on the same filesystem BY CONSTRUCTION
whenever `relPath` is non-empty, and /channels reported "free space unknown"
for a correctly mounted drive. A bind mount reads the same way.

The boundary is tested at `volume.mountpoint` against its own parent now. Two
syscalls, because the header's rule (TABLES NEVER PROBE) rules out reusing
`probeLocationMemo`, which is up to three subprocesses per location. `/` is
skipped: it is its own parent, and the process is reading from it. Verified
against the real settings.json: platter reports 1.34 TB free.

The test had to FIND a real mount (/run, /dev, /sys, /proc) and point a root at
a plain subdirectory of it — a unit test cannot mount anything, and a tmpdir
has no boundary in it at all, which is exactly the shape that made the broken
version look correct. Two negative cases beside it: a mountpoint that is only a
directory, and one that is missing.

**The destructive evict is gated.** It fired on one click, and the age list
includes "any age (everything)". The button is disabled until a dry run has
been done in this session — the preview is the same walk, so the number the
operator unlocks it with is the one the real pass would produce — and "any age"
needs a checkbox as well. Both reset when the age changes, so a preview of
"older than 90 days" can never arm a sweep that takes everything.

**A NaN age would have evicted EVERYTHING, not nothing.** `Math.max(0, NaN)` is
NaN, so the cutoff is NaN and `st.mtimeMs > NaN` is false for every file. The
controller is exported, so it refuses rather than clamps.

**`parseClipWindowName` reads `.mkv` and `.webm` too.** `clipWindowFile` writes
only `.mp4`, but a window left by an older build — or by a hand-run yt-dlp that
ignored the format pin — was invisible to `listClipWindows` AND to
`evictClipWindows`: bytes nothing counted and nothing could remove. `.json` is
deliberately not in the list; a sidecar must never parse as a window.

Noted in FACTS rather than built: per-channel eviction is reachable only
through `/api/ops/evict-clips` — the card is corpus-wide, which is the shape
the bytes have.

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

Diffstat:
Mcommon/controller/evictClipWindows.test.ts | 20++++++++++++++++++++
Mcommon/controller/evictClipWindows.ts | 13++++++++++++-
Mcommon/controller/storageLocations.test.ts | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/controller/storageLocations.ts | 42++++++++++++++++++++++++++++++++----------
Mcommon/lib/clipWindow.test.ts | 8++++++++
Mcommon/lib/clipWindow.ts | 21++++++++++++++++++++-
Meditor/app/storage/components/ClipWindowsCard.tsx | 60+++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Meditor/e2e/storage-locations.spec.ts | 53+++++++++++++++++++++++++++++++++++++++++++++++++++--
Mplans/FACTS.md | 20++++++++++++++++++++
9 files changed, 347 insertions(+), 21 deletions(-)

diff --git a/common/controller/evictClipWindows.test.ts b/common/controller/evictClipWindows.test.ts @@ -218,3 +218,23 @@ test("an unmounted drive is a SKIP, never a clean eviction of nothing", async () assert.match(r.skipped[0], /^alpha: media unreachable/); }); }); + +test("a non-finite age is REFUSED, because clamping it would evict everything", async () => { + // `Math.max(0, NaN)` is NaN, `st.mtimeMs > NaN` is false for every file — so + // the silent failure mode of a bad number was not "evict nothing" but "evict + // the whole corpus". The action validates too; this is the guard on the + // exported function. + await withTmp(async (paths) => { + const clipsDir = await seed(paths, "alpha", { + "10.00-40.00.mp4": { ageDays: 0, size: 1000 }, + }); + for (const bad of [Number.NaN, Number.POSITIVE_INFINITY, -1]) { + await assert.rejects( + () => evictClipWindows({ paths, olderThanDays: bad }), + /finite number of days/, + ); + } + // Nothing was touched on the way to any of those refusals. + assert.equal(await exists(path.join(clipsDir, "10.00-40.00.mp4")), true); + }); +}); diff --git a/common/controller/evictClipWindows.ts b/common/controller/evictClipWindows.ts @@ -185,7 +185,18 @@ export async function evictClipWindows( keptBytes: 0, skipped: [], }; - const days = Math.max(0, opts.olderThanDays); + // REFUSED, NOT CLAMPED, and NaN is why. `Math.max(0, NaN)` is NaN, the cutoff + // is then NaN, and `st.mtimeMs > NaN` is false for every file — so a bad + // number would not have evicted nothing, it would have evicted EVERYTHING. + // The action validates too, but this function is exported and the failure + // mode is silent and total. + if (!Number.isFinite(opts.olderThanDays) || opts.olderThanDays < 0) { + throw new Error( + `olderThanDays must be a finite number of days, zero or more ` + + `(got ${String(opts.olderThanDays)})`, + ); + } + const days = opts.olderThanDays; const cutoffMs = Date.now() - days * DAY_MS; let slugs: string[]; diff --git a/common/controller/storageLocations.test.ts b/common/controller/storageLocations.test.ts @@ -15,9 +15,11 @@ import type { Paths } from "../lib/paths"; import type { SiteSettings } from "../lib/settings"; import { defaultStorage, sanitizeStorage } from "../lib/settings"; import type { StorageLocation } from "../lib/storageLocations"; +import { readdir, stat } from "node:fs/promises"; import { channelsOnLocation, preflightRepoint, + volumeFreeBytes, probeLocationMemo, recordProbedIdentity, repointStorageLocation, @@ -551,3 +553,132 @@ test("the probe memo answers twice from one probe, and refresh bypasses it", asy test("defaultStorage is still the empty list (the harness's assumption)", () => { assert.deepEqual(defaultStorage(), { locations: [], defaultLocationId: "" }); }); + +// --------------------------------------------------------------------------- +// volumeFreeBytes: the mount boundary is at the MOUNTPOINT, not at the root +// --------------------------------------------------------------------------- +// +// The first version of this guard compared `stat(root).dev` with its parent's, +// and that is wrong for the only location on the production machine: a root is +// `join(mountpoint, relPath)`, `platter` is +// `/run/media/user/<uuid>/archilyzer-media`, and a subdirectory is on the same +// filesystem as its parent BY CONSTRUCTION. /channels reported "free space +// unknown" for a correctly mounted drive. A bind mount reads the same way. + +function volumeLoc( + root: string, + volume?: { uuid: string; mountpoint: string; relPath: string }, +): StorageLocation { + return { + id: "platter", + label: "Platter", + root, + autoRepoint: false, + ...(volume ? { volume: { ...volume, fstype: "ext4" } } : {}), + }; +} + +// A REAL MOUNT BOUNDARY, found rather than made: a unit test cannot mount +// anything, and a tmpdir has no boundary in it at all — which is exactly the +// shape that made the broken version look correct. /run, /dev and /sys are +// separate filesystems on any systemd machine; this takes the first one that +// actually crosses a boundary and holds a subdirectory to point a root at. +async function realMountWithSubdir(): Promise< + { mount: string; sub: string } | null +> { + for (const mount of ["/run", "/dev", "/sys", "/proc"]) { + const [here, above] = await Promise.all([ + stat(mount).catch(() => null), + stat(path.dirname(mount)).catch(() => null), + ]); + if (!here || !above || here.dev === above.dev) continue; + const names = await readdir(mount).catch(() => [] as string[]); + for (const name of names) { + const full = path.join(mount, name); + const st = await stat(full).catch(() => null); + // Same filesystem as the mountpoint — a subdirectory of it, not another + // mount nested inside. + if (st?.isDirectory() && st.dev === here.dev) return { mount, sub: full }; + } + } + return null; +} + +test("a root INSIDE a mounted volume reports its free space", async (t) => { + const found = await realMountWithSubdir(); + if (!found) { + t.skip("no mounted filesystem with a plain subdirectory on this machine"); + return; + } + const paths = { channelsDir: process.cwd() } as Paths; + const out = await volumeFreeBytes({ + paths, + locations: [ + volumeLoc(found.sub, { + uuid: "11111111-2222-3333-4444-555555555555", + mountpoint: found.mount, + relPath: path.basename(found.sub), + }), + ], + }); + assert.equal( + typeof out.platter, + "number", + `expected free space for ${found.sub} under mountpoint ${found.mount}`, + ); +}); + +test("a mountpoint that is not a mount reports nothing, however real the directory", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ttb-freebytes-")); + try { + // The shape of an unplugged platter: the mountpoint directory exists, the + // root under it exists, and both are on the parent filesystem. + const mount = path.join(dir, "media", "platter"); + const root = path.join(mount, "archilyzer-media"); + await mkdir(root, { recursive: true }); + const paths = { channelsDir: dir } as Paths; + const out = await volumeFreeBytes({ + paths, + locations: [ + volumeLoc(root, { + uuid: "11111111-2222-3333-4444-555555555555", + mountpoint: mount, + relPath: "archilyzer-media", + }), + ], + }); + assert.equal(out.platter, undefined); + + // WITHOUT A LEARNED IDENTITY the same layout is reported, because a + // location on a plain directory shares its parent's device legitimately. + const plain = await volumeFreeBytes({ + paths, + locations: [volumeLoc(root)], + }); + assert.equal(typeof plain.platter, "number"); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); + +test("a missing mountpoint reports nothing even when the root somehow exists", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ttb-freebytes-")); + try { + const mount = path.join(dir, "gone"); + const root = path.join(dir, "elsewhere", "archilyzer-media"); + await mkdir(root, { recursive: true }); + const out = await volumeFreeBytes({ + paths: { channelsDir: dir } as Paths, + locations: [ + volumeLoc(root, { + uuid: "11111111-2222-3333-4444-555555555555", + mountpoint: mount, + relPath: "archilyzer-media", + }), + ], + }); + assert.equal(out.platter, undefined); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/common/controller/storageLocations.ts b/common/controller/storageLocations.ts @@ -259,16 +259,38 @@ export async function volumeFreeBytes(opts: { // the disk the operator is trying to empty. The location would then read // "233 GB free" about a platter that is not plugged in. // - // Comparing `st.dev` with the PARENT's is what a mount is: crossing a - // mount boundary changes the device number. Only asked for a location - // that has learned a `volume.uuid` — that field is the assertion that - // this root is supposed to be its own volume. A location on a plain - // directory (no identity ever probed, a container, a subdirectory of the - // system disk by design) shares its parent's device legitimately, and - // refusing to report its free space would be wrong. - if (loc.volume?.uuid) { - const parent = await stat(path.dirname(loc.root)).catch(() => null); - if (parent && parent.dev === st.dev) { + // THE BOUNDARY IS TESTED AT THE MOUNTPOINT, NEVER AT THE ROOT, and the + // first version of this got that wrong in the one way that matters on + // this machine. A location's root is `join(mountpoint, relPath)` — the + // production `platter` is `/run/media/user/<uuid>/archilyzer-media`, a + // SUBDIRECTORY of the mountpoint — so the root and its parent are on the + // same filesystem BY CONSTRUCTION whenever `relPath` is non-empty, and + // comparing those two devices reported "free space unknown" for a + // correctly mounted drive. A bind mount reads the same way. + // + // Crossing a mount changes the device number, so `stat(mountpoint).dev` + // against `stat(dirname(mountpoint)).dev` is the honest question, and it + // is two syscalls — TABLES NEVER PROBE (see the header) rules out asking + // `probeLocation`, which is up to three subprocesses. + // + // Only asked of a location that has learned a `volume.uuid`: that field + // is the assertion that the root is supposed to be on its own volume. A + // location on a plain directory (never probed, a container, a + // subdirectory of the system disk by design) shares its parent's device + // legitimately, and withholding its free space would be wrong. + const mountpoint = loc.volume?.uuid + ? (loc.volume.mountpoint ?? "").trim() + : ""; + // `/` is its own parent, so a volume mounted at the root has no boundary + // to test and is trivially there — the process is reading from it. + if (mountpoint && mountpoint !== path.dirname(mountpoint)) { + const [atMount, aboveMount] = await Promise.all([ + stat(mountpoint).catch(() => null), + stat(path.dirname(mountpoint)).catch(() => null), + ]); + // Gone entirely, or present as an ordinary directory on the parent + // filesystem: either way nothing is mounted there. + if (!atMount || (aboveMount && aboveMount.dev === atMount.dev)) { out[loc.id] = undefined; return; } diff --git a/common/lib/clipWindow.test.ts b/common/lib/clipWindow.test.ts @@ -20,6 +20,14 @@ test("a window name round-trips through two decimals", () => { // name can hold still addresses ONE file. assert.equal(clipWindowName(12.004, 41.999), "12.00-42.00"); assert.deepEqual(parseClipWindowName("12.00-42.00.mp4"), { from: 12, to: 42 }); + // A window an older build (or a hand-run yt-dlp that ignored the format pin) + // left behind. Unreadable names were bytes nothing counted and nothing could + // evict, which is the worse half of being invisible. + assert.deepEqual(parseClipWindowName("12.00-42.00.mkv"), { from: 12, to: 42 }); + assert.deepEqual(parseClipWindowName("12.00-42.00.webm"), { from: 12, to: 42 }); + // NOT a sidecar: it is removed with the window it describes and must never + // parse as one. + assert.equal(parseClipWindowName("12.00-42.00.json"), null); assert.deepEqual(parseClipWindowName("12.00-42.00"), { from: 12, to: 42 }); assert.deepEqual(parseClipWindowName("0.00-1234.50.mp4"), { from: 0, diff --git a/common/lib/clipWindow.ts b/common/lib/clipWindow.ts @@ -75,10 +75,29 @@ const WINDOW_RE = /^(\d+(?:\.\d+)?)-(\d+(?:\.\d+)?)$/; // Parse `<from>-<to>.mp4` (or the bare `<from>-<to>`) back into its numbers, or // null for anything else in the directory — a sidecar, a `.part`, a stray. +// THE EXTENSIONS A WINDOW MAY WEAR. `clipWindowFile` writes `.mp4` and only +// `.mp4` (the format is pinned to H.264 so both sides address the same file — +// see the header), so the other two are not speculation about a future format: +// they are what a window fetched by an older build, or by a hand-run yt-dlp +// that ignored the pin, is called. A parser that could not read those names +// made those files invisible to `listClipWindows` AND to `evictClipWindows`, +// which is the worse half — bytes nothing counts and nothing can remove. +// +// 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; + +function stripClipExt(name: string): string { + for (const ext of CLIP_EXTS) { + if (name.endsWith(ext)) return name.slice(0, -ext.length); + } + return name; +} + export function parseClipWindowName( name: string, ): { from: number; to: number } | null { - const stem = name.endsWith(".mp4") ? name.slice(0, -4) : name; + const stem = stripClipExt(name); const m = WINDOW_RE.exec(stem); if (!m) return null; const from = Number(m[1]); diff --git a/editor/app/storage/components/ClipWindowsCard.tsx b/editor/app/storage/components/ClipWindowsCard.tsx @@ -27,9 +27,19 @@ import { evictClipWindowsAction } from "../actions"; // "older than N days", and an operator evicting something a report still cites // has spent a fetch, not lost data. They are entitled to know that beforehand. // -// PREVIEW FIRST, and the preview is the same walk. `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. +// 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 +// "older than 30 days" over a corpus nobody has looked at is a number the +// operator has no way to picture. +// +// "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 +// well, every time — the tick resets when the age changes, so it can never be +// left armed from an earlier, narrower choice. // // ⚠️ Both panels are rendered UNCONDITIONALLY and only `disabled` changes — // `StreamActionLog` calls router.refresh() the instant a run ends, and a panel @@ -39,6 +49,13 @@ const AGES = [0, 7, 30, 90, 180] as const; export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { const [days, setDays] = useState<number>(30); + // Reset by any change of age: a preview of "older than 90 days" says nothing + // about what "any age" would take, and an armed checkbox from a narrower + // choice is exactly the thing this gate exists to stop. + const [previewed, setPreviewed] = useState(false); + const [confirmed, setConfirmed] = useState(false); + const takesEverything = days === 0; + const canEvict = previewed && (!takesEverything || confirmed); return ( <article aria-label="clip windows" @@ -78,7 +95,11 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { <select aria-label="clip eviction age" value={days} - onChange={(e) => setDays(Number(e.target.value))} + onChange={(e) => { + setDays(Number(e.target.value)); + setPreviewed(false); + setConfirmed(false); + }} className="rounded border border-border bg-background px-2 py-1 text-sm" > {AGES.map((d) => ( @@ -89,12 +110,29 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { </select> </label> + {takesEverything && ( + <label className="flex items-start gap-2 text-sm"> + <input + type="checkbox" + aria-label="confirm evicting every window" + checked={confirmed} + onChange={(e) => setConfirmed(e.target.checked)} + className="mt-1" + /> + <span> + Yes, evict <strong>every</strong> fetched window on every drive, + however recently it was fetched. + </span> + </label> + )} + <div className="flex flex-wrap items-start gap-4"> <StreamActionLog key="evict-clips-preview-log" - trigger={() => - evictClipWindowsAction({ olderThanDays: days, dryRun: true }) - } + trigger={() => { + setPreviewed(true); + return evictClipWindowsAction({ olderThanDays: days, dryRun: true }); + }} cancelAction={cancelJobAction} buttonLabel="Preview eviction" runningLabel="Walking…" @@ -107,8 +145,16 @@ export function ClipWindowsCard({ clipsBytes }: { clipsBytes: number }) { buttonLabel="Evict fetched windows" runningLabel="Evicting…" label="Evict fetched windows" + disabled={!canEvict} /> </div> + {!canEvict && ( + <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."} + </p> + )} </article> ); } diff --git a/editor/e2e/storage-locations.spec.ts b/editor/e2e/storage-locations.spec.ts @@ -604,8 +604,19 @@ test("Evict fetched windows removes an old one and leaves a recent one", async ( "by age only", ); - // --- preview deletes nothing ------------------------------------------- + // --- the destructive button is GATED until a preview has run ------------ + // One click must not be able to delete every window in the corpus, and + // "older than 30 days" over a corpus nobody has looked at is a number the + // operator cannot picture. await page.getByLabel("clip eviction age").selectOption("30"); + await expect( + page.getByRole("button", { name: "Evict fetched windows" }), + ).toBeDisabled(); + await expect(page.getByLabel("clip eviction gate")).toContainText( + "Preview first", + ); + + // --- preview deletes nothing ------------------------------------------- await page.getByRole("button", { name: "Preview eviction" }).click(); await expect(page.getByLabel("Preview eviction output")).toContainText( /Would evict 1 window/, @@ -614,7 +625,9 @@ test("Evict fetched windows removes an old one and leaves a recent one", async ( expect(await pathExists(old)).toBe(true); // --- and then it does --------------------------------------------------- - await page.getByRole("button", { name: "Evict fetched windows" }).click(); + const evict = page.getByRole("button", { name: "Evict fetched windows" }); + await expect(evict).toBeEnabled(); + await evict.click(); await expect(page.getByLabel("Evict fetched windows output")).toContainText( /Evicted 1 window/, { timeout: 60_000 }, @@ -625,3 +638,39 @@ test("Evict fetched windows removes an old one and leaves a recent one", async ( expect(await pathExists(recent)).toBe(true); expect(await pathExists(recent.replace(/\.mp4$/, ".json"))).toBe(true); }); + +// "ANY AGE" IS EVERY WINDOW ON EVERY DRIVE. A preview is enough of a pause for +// a dated sweep; it is not enough for that, so the tick is required as well — +// and it resets when the age changes, so it can never be left armed from an +// earlier, narrower choice. +test("evicting at any age needs the tick as well as the preview", async ({ + page, +}) => { + test.setTimeout(90_000); + await resetData("one-youtube-channel-with-data"); + await writeSettings({ adminTitle: "Test Admin", minFreeDiskGB: 0 }); + + await page.goto("/storage"); + const evict = page.getByRole("button", { name: "Evict fetched windows" }); + await page.getByLabel("clip eviction age").selectOption("0"); + const confirm = page.getByLabel("confirm evicting every window"); + await expect(confirm).toBeVisible(); + + // Ticked but never previewed: still refused. + await confirm.check(); + await expect(evict).toBeDisabled(); + + await page.getByRole("button", { name: "Preview eviction" }).click(); + await expect(page.getByLabel("Preview eviction output")).toContainText( + /Would evict/, + { timeout: 60_000 }, + ); + await expect(evict).toBeEnabled(); + + // CHANGING THE AGE DISARMS BOTH. A preview of "any age" says nothing about + // what a narrower sweep would take, and vice versa. + await page.getByLabel("clip eviction age").selectOption("90"); + await expect(evict).toBeDisabled(); + await page.getByLabel("clip eviction age").selectOption("0"); + await expect(page.getByLabel("confirm evicting every window")).not.toBeChecked(); +}); diff --git a/plans/FACTS.md b/plans/FACTS.md @@ -4735,10 +4735,30 @@ or an explicit clips total, a count on the storage page, and an eviction rule"). there is no slug for `runManagedFunction` to check. Without it the pass reports a clean eviction of zero bytes about a platter full of windows. `in-transition` is refused outright: the mover's verify pass compares trees. +- **Per-channel eviction has no UI.** `evictClipWindows` takes a `slug` and the + job sets `channelSlug` (which is what makes the snapshot regen land), but the + only caller that passes one is `POST /api/ops/evict-clips`. The /storage card + is corpus-wide, which is the shape the bytes have — a channel page button is + the obvious next surface and nothing blocks it. - The empty `clips/` is left behind on purpose — `rmdir` would race a fetch that just created it, and an empty directory is invisible to every video-dir enumerator anyway. +### `volumeFreeBytes` tests the mount boundary AT THE MOUNTPOINT + +- **Comparing `stat(root).dev` with its parent's is wrong**, and it broke the + only location on this machine. A root is `join(mountpoint, relPath)`; + `platter` is `/run/media/user/<uuid>/archilyzer-media`, a SUBDIRECTORY of the + mountpoint, so root and parent are on the same filesystem by construction and + /channels reported "free space unknown" for a correctly mounted drive. A bind + mount reads the same way. The question is asked of `volume.mountpoint` + against its own parent — two syscalls, because TABLES NEVER PROBE rules out + `probeLocation`. Only for a location that has learned a `volume.uuid`. +- A unit test cannot mount anything and a tmpdir has no boundary in it at all — + which is exactly the shape that made the broken version look right. The test + FINDS a real one (/run, /dev, /sys, /proc) and points a root at a plain + subdirectory of it. + ### `assertRelocationRootPresent` - **Every absolute `mkdir` in both movers is `{recursive: true}`**, so a move