commit 735984720c1a253ad8f8a262b05ffcd2ab11e7cb
parent 657789f4f24952d148eebdea4bb0df934cd7e1c2
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 21 Sep 2026 01:42:20 -0400
deliver: a fixture that measures the cut, and the docs for the panel
deliver-fixture is its own project on its own source, because every test here
WRITES: a cut lands in clips/, a batch in share-*/, and the project's own
scripts leave marker files. Six clips over TWO sections, which is the point --
the panel folds on the id's letter prefix, and one number over the whole cut
hides the section where a clip was thrown out.
The window is generated with keyframes every three seconds, the shape a fetch
with --force-keyframes-at-cuts leaves. a01 starts on one and a02 does not, and
MEASURED, neither copy survives: the aac frames do not end where the keyframe
does, so the copy comes out 0.14 s long. That is the whole argument for the
cut probing its own output instead of reading the window's keyframes, and both
clips still come out to the frame -- which is what the spec asserts, with
ffprobe, rather than asserting a file exists.
The fixture's apply-manifest.py and build.py are two-line stand-ins, and the
docs say why that is the honest limit: the real pair are the REPORT's own,
hand-written, in an unversioned reports directory, and differ per report. What
is under test is that the panel finds them, runs them in order, refuses the
first over a half-walked cut, and shows their output verbatim.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
3 files changed, 446 insertions(+), 0 deletions(-)
diff --git a/umtool/docs/clip-bench.md b/umtool/docs/clip-bench.md
@@ -351,6 +351,70 @@ the CLI's formatting, tmp+rename under a lock, an mtime token). A stale token is
**409 with both values**, never a silent overwrite: the other writer is usually
somebody's judgement.
+## Deliver — the half that starts when the walk ends
+
+The bench answers one question per clip. What a report owes its readers after
+that is a different list, and it lives on the **project page** as `Deliver`
+(`components/projects/DeliverSection.tsx`, `/api/report/deliver`):
+
+| | |
+|---|---|
+| **counts by section** | folded on the id's letter prefix, because `69 of 163 confirmed` says nothing about the section where eleven of nineteen clips were thrown out — and that is the section whose argument has to change. Section HEADINGS are read out of the report's own `content.py`, nth heading to nth letter. |
+| **cut N confirmed clips from cache** | one job, **one step per clip**, so `k of n` and Stop are the job runner's own (`stepIndex`, and a cancel that kills the step's process group). |
+| **not fetched** | a confirmed clip with nothing cached that holds it. Listed, never downloaded here — the managed fetch is the editor's, on the clip page. |
+| **build share batch** | `share-<name>/{orig,std,small}/<Section>/<id>_<date>_<title>.mp4` + `LIST.md`. |
+| **apply rulings** | runs the project's own `apply-manifest.py`, then `umtool corrections`. |
+| **rebuild reports** | runs the project's own `build.py`, once per `content*.py`. |
+
+**A confirmed clip's frames are already on this disk.** Nobody can judge a clip
+until its window is cached, so `clips/<id>.mp4` — what the written report's
+players read — is a cut, not a download. `lib/report/cut.mjs` asks the project
+cache for the tightest window containing the clip and cuts at
+`clip.start − window.from`, through build-video's own `cutArgs()`: one spelling
+of the arithmetic, shared with the render's segment pass.
+
+**The cut is measured, not predicted.** It stream-copies first and then probes
+the result; more than 50 ms off the length asked for and it re-encodes. A copy
+can only begin on a keyframe, and even a keyframe-aligned copy comes out long
+when the aac frames do not end there (0.14 s, measured on the fixture) —
+predicting that from the window's keyframes would have been wrong in exactly
+the cases that matter.
+
+**The two share profiles are `lib/report/encode.mjs`, and only there.** They
+came off `~/reports/elfpire-eva/share-report-clips/reencode.py`, which is where
+the first batch was actually encoded; ported rather than spawned, because a
+python file beside one project's deliverables is not a profile the next project
+can reach.
+
+**Exclusions are the folders, not a list.** The next batch skips every id an
+existing `share-*/LIST.md` already shipped (read back from the file NAMES it
+lists, plus the `<!-- shared-ids: … -->` marker this writes) and every clip
+ruled `incorrect`. A `shared.json` would have been a second record to drift.
+
+**Nothing rewrites prose.** `apply-manifest.py` prints the lines of
+`content*.py` that cite each clip the walk threw out, and the panel shows the
+same grep itself. What a wrong clip does to an argument is a judgement about
+the argument.
+
+**Apply is refused while the walk is unfinished** unless `apply partial` is
+ticked: the script syncs `clips.json` from the manifest and deletes the mp4 of
+every clip whose window moved, so running it over a half-walked cut bakes
+"nobody has looked at this yet" into the deliverable as though it were a
+verdict.
+
+**One job at a time, machine-wide in this process** (`lib/jobs.ts`): a bench
+fetch and a Deliver cut cannot run together, and the second one gets a 409
+naming the first. That is not a limitation to route around — both write the
+same `clips/` directory.
+
+**What the fixture cannot cover.** `apply-manifest.py` and `build.py` are the
+REPORT's own, hand-written, living in an unversioned `~/reports/<project>/`
+directory and differing per report. `e2e/deliver.spec.ts` runs two-line
+stand-ins: what is under test is that the panel finds them, runs them in order,
+refuses the first over a half-walked cut and shows their output verbatim —
+never what they do. A project whose scripts are absent gets the buttons hidden
+or a 400, not a guess.
+
## Discovered by getting it wrong once
**The page and the inbox must answer the same question the same way.** `umtool
diff --git a/umtool/e2e/deliver.spec.ts b/umtool/e2e/deliver.spec.ts
@@ -0,0 +1,211 @@
+import { test, expect } from "@playwright/test";
+import { execFileSync } from "node:child_process";
+import { existsSync, readFileSync } from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+// ---------------------------------------------------------------------------
+// DELIVER: what happens after the last clip is judged.
+//
+// The bench's own specs cover judging. This covers the other half — cutting the
+// confirmed clips out of the cache, packaging a batch, and running the report's
+// own fold-back scripts — and its strongest assertions are on DISK, because
+// every one of these produces a file somebody else will open.
+//
+// The clips are MEASURED, not merely present. A cut that lands on the wrong
+// keyframe still writes an mp4; what makes it wrong is that it is not the
+// seconds that were asked for, and only ffprobe can say so.
+//
+// Nothing here reaches the network: the fixture's window was generated by
+// ffmpeg, the cut reads it, and the panel has no fetch of its own by design.
+// ---------------------------------------------------------------------------
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const FIXTURE = path.join(HERE, "..", ".e2e-song");
+const PROJECT = "reports/deliver-fixture";
+const DIR = path.join(FIXTURE, "reports", "deliver-fixture");
+
+const seconds = (file: string): number =>
+ Number(
+ execFileSync("ffprobe", [
+ "-v", "error", "-show_entries", "format=duration",
+ "-of", "default=nw=1:nk=1", file,
+ ]).toString().trim(),
+ );
+
+/** The job the panel started, polled to a terminal state. */
+async function waitForJob(page: import("@playwright/test").Page, timeout = 120_000) {
+ await expect(page.locator("[data-deliver-job]")).toHaveAttribute(
+ "data-deliver-state",
+ /done|failed/,
+ { timeout },
+ );
+ const state = await page.locator("[data-deliver-job]").getAttribute("data-deliver-state");
+ const log = (await page.locator("[data-deliver-log]").textContent()) ?? "";
+ return { state, log };
+}
+
+test("the deliver panel counts verdicts and folds them by id prefix", async ({ page }) => {
+ await page.goto(`/browse/${PROJECT}`);
+ const panel = page.locator("[data-deliver]");
+ await expect(panel).toBeVisible();
+
+ // 4 confirmed (a01 a02 a03 b01), 1 incorrect (b02), 1 unjudged (b03).
+ await expect(panel).toHaveAttribute("data-deliver-confirmed", "4");
+ await expect(panel).toHaveAttribute("data-deliver-incorrect", "1");
+ await expect(panel).toHaveAttribute("data-deliver-unreviewed", "1");
+
+ // TWO SECTIONS, folded on the letter. The whole reason this is not one
+ // number: section B is where a clip was thrown out.
+ const a = panel.locator('[data-section="A"]');
+ const b = panel.locator('[data-section="B"]');
+ await expect(a).toHaveAttribute("data-section-confirmed", "3");
+ await expect(a).toHaveAttribute("data-section-incorrect", "0");
+ await expect(b).toHaveAttribute("data-section-confirmed", "1");
+ await expect(b).toHaveAttribute("data-section-incorrect", "1");
+ await expect(b).toHaveAttribute("data-section-unreviewed", "1");
+ // And the heading came out of the report's own prose, not out of the id.
+ await expect(a).toContainText("The first fixture section");
+
+ // b01 is confirmed AND already has a file, so it is not waiting on a cut;
+ // a03 has nothing cached, so it is not a cut either. Two left.
+ await expect(panel).toHaveAttribute("data-deliver-need-cut", "2");
+});
+
+test("a confirmed clip with no cached window is listed as not fetched, not cut", async ({
+ page,
+}) => {
+ await page.goto(`/browse/${PROJECT}`);
+ const notFetched = page.locator("[data-not-fetched]");
+ await expect(notFetched).toBeVisible();
+ // a03's window is 30–33 s and the cache holds 0–24. Nothing here downloads
+ // it: the link goes to the bench, where the editor fetch lives.
+ await expect(notFetched.locator('[data-not-fetched-id="a03"]')).toBeVisible();
+ await expect(notFetched.locator('[data-not-fetched-id="a03"]')).toHaveAttribute(
+ "href",
+ `/browse/${PROJECT}/clip/a03`,
+ );
+ // And it is NOT in the cut list, which is the distinction the two headings
+ // exist to make.
+ await expect(page.locator('[data-need-cut] [data-need-cut-id="a03"]')).toHaveCount(0);
+ expect(existsSync(path.join(DIR, "clips", "a03.mp4"))).toBe(false);
+});
+
+test("cutting from cache writes clips/<id>.mp4 of exactly the clip's length", async ({
+ page,
+}) => {
+ test.setTimeout(180_000);
+ await page.goto(`/browse/${PROJECT}`);
+ await page.locator('[data-action="deliver-cut"]').click();
+
+ // `k of n` is the JOB's count -- one step per clip -- so the readout and the
+ // work cannot disagree.
+ // Either "1 of 2" while it runs or "done · 2 steps" if it beat the first
+ // poll: what is asserted is that the readout is the job's step count.
+ await expect(page.locator("[data-deliver-progress]")).toContainText(/of 2|2 steps/);
+ const { state, log } = await waitForJob(page);
+ expect(state, log).toBe("done");
+ expect(log).toContain("CUT-OK a01");
+ expect(log).toContain("CUT-OK a02");
+
+ // THE MEASUREMENT. a01 starts on one of the window's keyframes and a02 does
+ // not; both must come out to the length the manifest asked for.
+ expect(seconds(path.join(DIR, "clips", "a01.mp4"))).toBeCloseTo(3.0, 1);
+ expect(seconds(path.join(DIR, "clips", "a02.mp4"))).toBeCloseTo(3.0, 1);
+
+ // And the panel has nothing left to cut.
+ await page.reload();
+ await expect(page.locator("[data-deliver]")).toHaveAttribute("data-deliver-need-cut", "0");
+});
+
+test("the share batch skips every id an existing LIST.md already shipped", async ({ page }) => {
+ test.setTimeout(180_000);
+ await page.goto(`/browse/${PROJECT}`);
+ await page.locator("[data-batch-name]").fill("second");
+ await page.locator('[data-action="deliver-share"]').click();
+ const { state, log } = await waitForJob(page);
+ expect(state, log).toBe("done");
+
+ const root = path.join(DIR, "share-second");
+ const folder = "A-The-first-fixture-section";
+ for (const variant of ["orig", "std", "small"]) {
+ expect(
+ existsSync(path.join(root, variant, folder, "a01_2025-01-01_A-Fixture-Stream.mp4")),
+ `${variant}/a01`,
+ ).toBe(true);
+ expect(
+ existsSync(path.join(root, variant, folder, "a02_2025-01-02_A-Fixture-Stream.mp4")),
+ `${variant}/a02`,
+ ).toBe(true);
+ }
+
+ // THE EXCLUSIONS. b01 went out in share-first, and the folders are the
+ // record: nothing was written down anywhere else. b02 was ruled incorrect.
+ const list = readFileSync(path.join(root, "LIST.md"), "utf8");
+ expect(list).toContain("a01_2025-01-01");
+ expect(list).not.toContain("b01_");
+ expect(list).not.toContain("b02_");
+ expect(list).toContain("1 already shared (b01)");
+ expect(list).toContain("1 ruled incorrect (b02)");
+ expect(existsSync(path.join(root, "orig", "B-The-second-fixture-section"))).toBe(false);
+
+ // The std encode is the one people download, and it is EXACTLY 1280x720.
+ const size = execFileSync("ffprobe", [
+ "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height",
+ "-of", "csv=p=0", path.join(root, "std", folder, "a01_2025-01-01_A-Fixture-Stream.mp4"),
+ ]).toString().trim();
+ expect(size).toBe("1280,720");
+});
+
+test("apply rulings is refused while the walk is unfinished, and runs the report's own scripts when it is meant", async ({
+ page,
+}) => {
+ test.setTimeout(120_000);
+ await page.goto(`/browse/${PROJECT}`);
+
+ // b03 has never been judged. apply-manifest.py deletes the mp4 of every clip
+ // whose window moved, so running it over a half-walked cut bakes "nobody has
+ // looked at this" into the deliverable as though it were a verdict.
+ await page.locator('[data-action="deliver-apply"]').click();
+ await expect(page.locator("[data-deliver-error]")).toContainText("have not been judged");
+ expect(existsSync(path.join(DIR, "applied.marker"))).toBe(false);
+
+ // Ticked, it proceeds -- and the second step is `umtool corrections`, which
+ // is the same list the page shows, in the form the next sweep's prompt wants.
+ await page.locator('[data-action="apply-partial"]').check();
+ await page.locator('[data-action="deliver-apply"]').click();
+ const { state, log } = await waitForJob(page);
+ expect(state, log).toBe("done");
+ expect(existsSync(path.join(DIR, "applied.marker"))).toBe(true);
+ // VERBATIM. The lines of prose that cite the clip the walk threw out are the
+ // whole point of running it, and the panel must not paraphrase them.
+ expect(log).toContain("content.py:6:");
+ expect(log).toContain("the speaker is the guest, not the host");
+});
+
+test("the panel shows the prose lines citing a clip the walk ruled incorrect", async ({
+ page,
+}) => {
+ await page.goto(`/browse/${PROJECT}`);
+ const cited = page.locator('[data-incorrect="b02"]');
+ await expect(cited).toBeVisible();
+ await expect(cited).toContainText("content.py:6");
+ await expect(cited).toContainText("The claim rests on [clip:b02]");
+});
+
+test("rebuild runs build.py once per content variant present", async ({ page }) => {
+ test.setTimeout(120_000);
+ await page.goto(`/browse/${PROJECT}`);
+ await page.locator('[data-action="deliver-rebuild"]').click();
+ const { state, log } = await waitForJob(page);
+ expect(state, log).toBe("done");
+
+ // content.py -> the default stem; content_lawyer.py -> report-lawyer. Two
+ // steps, two files, and neither overwrote the other's.
+ expect(log).toContain("BUILD-OK content -> report.html");
+ expect(log).toContain("BUILD-OK content_lawyer -> report-lawyer.html");
+ expect(readFileSync(path.join(DIR, "report.html"), "utf8")).toContain("built from content");
+ expect(readFileSync(path.join(DIR, "report-lawyer.html"), "utf8")).toContain(
+ "built from content_lawyer",
+ );
+});
diff --git a/umtool/e2e/fixtures/make-fixture.mjs b/umtool/e2e/fixtures/make-fixture.mjs
@@ -682,6 +682,19 @@ const CUES = {
[3, 6, "It has a second clip to fetch."],
[6, 9, "And nothing else cites it."],
],
+ // The DELIVER fixture's source. Its own, for the same reason vid3/vid4 are
+ // their own: a cut writes clips/<id>.mp4 and packages them, and a spec that
+ // shared a source with the fetch specs would depend on which ran first.
+ vid6: [
+ [0, 3, "The deliver fixture opens."],
+ [3, 6, "The first clip is confirmed."],
+ [6, 9, "And so is the second."],
+ [9, 12, "Which does not start on a keyframe."],
+ [12, 15, "The third is nowhere on this disk."],
+ [15, 18, "The fourth was ruled incorrect."],
+ [18, 21, "The fifth nobody has judged."],
+ [21, 24, "And that is the whole cut."],
+ ],
// Long enough that one clip's PADDED window can contain another's. See
// editor-fetch-reuse-fixture.
vid5: [
@@ -953,6 +966,163 @@ writeProject(
]),
);
+// -- THE DELIVER FIXTURE ------------------------------------------------------
+//
+// What a walked report owes its readers, in one project: the cut clips, the
+// batch, the fold-back. Its own source (vid6) and its own directory, because
+// every test here WRITES -- a cut lands in clips/, a batch in share-*/, and the
+// project's own scripts leave marker files.
+//
+// Six clips over TWO SECTIONS, which is the point: the panel folds on the id's
+// letter prefix, and "4 of 6 confirmed" says nothing about the section where a
+// clip was thrown out.
+//
+// a01 3.00- 6.00 confirmed, cached, no file -> cut; starts ON one of the
+// window's keyframes
+// a02 9.50-12.50 confirmed, cached, no file -> cut; the nearest keyframe
+// is at 9.00, so a copy would
+// be half a second long
+// a03 30.00-33.00 confirmed, NOTHING cached -> "not fetched": a download,
+// which this panel never does
+// b01 0.00- 3.00 confirmed, cached, HAS a file AND is listed in an existing
+// share-first/LIST.md -> the
+// batch must skip it
+// b02 15.00-18.00 INCORRECT, with a correction and a line of prose citing it
+// b03 18.00-21.00 nobody has judged it -> Apply is refused without "partial"
+const DELIVER = writeProject(
+ "deliver-fixture",
+ manifest("deliver-fixture", "The Deliver Fixture", { siteOrigin: "https://archive.example" }, [
+ { type: "clip", id: "a01", video: "vid6", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, verdict: "confirmed", date: "2025-01-01", title: "A Fixture Stream", quote: "The first clip is confirmed." },
+ { type: "clip", id: "a02", video: "vid6", start: 9.5, end: 12.5, cite: 9, section: 0, lock: true, verdict: "confirmed", date: "2025-01-02", title: "A Fixture Stream", quote: "Which does not start on a keyframe." },
+ { type: "clip", id: "a03", video: "vid6", start: 30.0, end: 33.0, cite: 30, section: 0, lock: true, verdict: "confirmed", date: "2025-01-03", title: "A Fixture Stream", quote: "Nothing on this disk holds it." },
+ { type: "clip", id: "b01", video: "vid6", start: 0.0, end: 3.0, cite: 0, section: 0, lock: true, verdict: "confirmed", date: "2025-01-04", title: "A Fixture Stream", quote: "The deliver fixture opens." },
+ { type: "clip", id: "b02", video: "vid6", start: 15.0, end: 18.0, cite: 15, section: 0, lock: true, verdict: "incorrect", correction: "the speaker is the guest, not the host", date: "2025-01-05", title: "A Fixture Stream", quote: "The fourth was ruled incorrect." },
+ { type: "clip", id: "b03", video: "vid6", start: 18.0, end: 21.0, cite: 18, section: 0, lock: true, date: "2025-01-06", title: "A Fixture Stream", quote: "The fifth nobody has judged." },
+ ]),
+);
+
+// The report's OWN scripts, which the panel runs rather than reimplements.
+//
+// Two-line stand-ins for ~/reports/elfpire-eva's apply-manifest.py and
+// build.py: they write a marker and print what the real ones print. That is
+// the whole of what can be tested about them here -- the real pair live beside
+// the prose they rewrite, in an unversioned reports directory, and differ per
+// report. What IS under test is that the panel finds them, runs them in order,
+// refuses to run the first one over a half-walked cut, and shows their output
+// verbatim.
+writeFileSync(
+ path.join(DELIVER, "apply-manifest.py"),
+ `#!/usr/bin/env python3
+# Fixture stand-in: prints what the real one prints, and leaves a marker.
+import json, os, re
+HERE = os.path.dirname(os.path.abspath(__file__))
+m = json.load(open(os.path.join(HERE, "video.manifest.json")))
+clips = [e for e in m["timeline"] if e.get("type") == "clip"]
+wrong = [e for e in clips if e.get("verdict") == "incorrect"]
+content = open(os.path.join(HERE, "content.py"), encoding="utf-8").read().splitlines()
+print("clips.json: 0 field change(s) across 0 clip(s)")
+print("review: %d clips: %d incorrect, %d confirmed, %d not yet reviewed" % (
+ len(clips), len(wrong),
+ sum(1 for e in clips if e.get("verdict") == "confirmed"),
+ sum(1 for e in clips if not e.get("verdict"))))
+for e in wrong:
+ print("== %s" % e["id"])
+ print(" correction: %s" % e.get("correction"))
+ pat = re.compile(r"\\[clip:%s\\]|[\\"']%s[\\"']" % (e["id"], e["id"]))
+ for i, line in enumerate(content):
+ if pat.search(line):
+ print(" content.py:%d: %s" % (i + 1, line.strip()[:220]))
+open(os.path.join(HERE, "applied.marker"), "w").write("applied\\n")
+print("APPLY-DONE")
+`,
+ { mode: 0o755 },
+);
+writeFileSync(
+ path.join(DELIVER, "build.py"),
+ `#!/usr/bin/env python3
+# Fixture stand-in for the report renderer: one file per variant, so "once per
+# content module, with build.py's own stem convention" is checkable on disk.
+import argparse, os
+ap = argparse.ArgumentParser()
+ap.add_argument("--content", default="content")
+ap.add_argument("--out", default="report")
+a = ap.parse_args()
+HERE = os.path.dirname(os.path.abspath(__file__))
+open(os.path.join(HERE, a.out + ".html"), "w").write("<!-- built from %s -->\\n" % a.content)
+print("BUILD-OK %s -> %s.html" % (a.content, a.out))
+`,
+ { mode: 0o755 },
+);
+// The prose. Its SECTIONS' headings are what name a batch's folders (nth
+// heading to nth letter), and one paragraph cites the clip the walk threw out
+// -- which is the line the panel has to surface and never rewrite.
+writeFileSync(
+ path.join(DELIVER, "content.py"),
+ `TITLE = "The Deliver Fixture"
+SECTIONS = [
+ {"id": "one", "heading": "1. The first fixture section",
+ "blocks": [("p", "Two clips carry this one."), ("clips", ["a01", "a02", "a03"])]},
+ {"id": "two", "heading": "2. The second fixture section",
+ "blocks": [("p", "The claim rests on [clip:b02], which the walk threw out."),
+ ("clips", ["b01", "b02"])]},
+]
+`,
+);
+writeFileSync(
+ path.join(DELIVER, "content_lawyer.py"),
+ `TITLE = "The Deliver Fixture, for lawyers"
+SECTIONS = [
+ {"id": "one", "heading": "1. The narrow cut", "blocks": [("clips", ["a01"])]},
+]
+`,
+);
+// A batch that already went out. The folders ARE the record, so the exclusion
+// list is read back from this rather than from a second file that would drift
+// from it.
+mkdirSync(path.join(DELIVER, "share-first"), { recursive: true });
+writeFileSync(
+ path.join(DELIVER, "share-first", "LIST.md"),
+ [
+ "# deliver-fixture — share batch `first`",
+ "",
+ "1 clip, sent before this fixture was born.",
+ "",
+ "## 2. The second fixture section (`B-The-second-fixture-section/`)",
+ "",
+ "- **b01_2025-01-04_A-Fixture-Stream.mp4** — 2025-01-04 · 3s",
+ "",
+ ].join("\n"),
+);
+
+// The cache the cut reads: ONE window holding every clip but a03.
+//
+// KEYFRAMES EVERY THREE SECONDS, which is what a window fetched with
+// --force-keyframes-at-cuts has. a01 starts on one (3.00) and a02 does not
+// (9.50) -- and measured, NEITHER copy survives, because the aac frames do not
+// end where the keyframe does and a copy of this window comes out 0.14 s long.
+// That is exactly why the cut measures its result instead of predicting it
+// from the keyframes, and why both clips still come out to the frame.
+mkdirSync(path.join(DELIVER, "out", "clips-raw"), { recursive: true });
+ff([
+ "-f", "lavfi", "-i", "testsrc=size=320x180:rate=30:duration=24",
+ "-f", "lavfi", "-i", "sine=frequency=440:duration=24",
+ "-t", "24",
+ "-c:v", "libx264", "-pix_fmt", "yuv420p",
+ "-force_key_frames", "expr:gte(t,n_forced*3)",
+ "-c:a", "aac", "-ar", "48000", "-ac", "2",
+ path.join(DELIVER, "out", "clips-raw", "vid6_0.00-24.00.mp4"),
+]);
+// b01 already has its file: it is confirmed AND shared, so it is the clip the
+// batch must leave out rather than one the cut has to make.
+mkdirSync(path.join(DELIVER, "clips"), { recursive: true });
+ff([
+ "-f", "lavfi", "-i", "testsrc=size=320x180:rate=30:duration=3",
+ "-f", "lavfi", "-i", "sine=frequency=330:duration=3",
+ "-t", "3", "-c:v", "libx264", "-pix_fmt", "yuv420p",
+ "-c:a", "aac", "-ar", "48000", "-ac", "2",
+ path.join(DELIVER, "clips", "b01.mp4"),
+]);
+
// -- STUB BINARIES, so a build is offline and deterministic --------------------
//
// The pipeline shells out to yt-dlp for the availability preflight and for every
@@ -1235,4 +1405,5 @@ console.log(` deep/nested/solo-fixture (collapse case), bench-fixture
console.log(` walk-fixture (read-only: w01/w04 walkable, w02 unfetched, w03 judged),`);
console.log(` editor-fetch-{,many-,reuse-}fixture (nothing cached — the editor fetch's subjects),`);
console.log(` longform-fixture (cue gap, legacy .bak, ffmeta), longform-edit-fixture, dash-fixture`);
+console.log(` deliver-fixture (writable: a01/a02 to cut, a03 unfetched, b01 shared, b02 incorrect, b03 unjudged)`);
console.log(` ${taken} candidate files copied, 2 mix tracks synthesised`);