Archilyzer · Source

archilyzer

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

commit d853116d6040a12650b3d4ad6ef7c711e151925c
parent ad8d17d231c61e80c74aaaaedb7308655b8534d3
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 19 Sep 2026 03:23:29 -0400

e2e: the cut is derived, refused when it does not fit, and what the build renders

Five in the bench spec: a reviewed extent of 0–12 cut to 3–9 because that is
where the quote is (not the extent's edges), the UNMATCHED path returning 200
with its score and writing nothing, the writer refusing a cut outside its
window and a half-written pair, pulling the extent in past the cut clearing it
in the same save, and the CLI pass proposing cuts while writing none.

And the build proves the other half: build-fixture's c01 is reviewed at
3.00–6.00 and cut to 3.50–4.20, so the `cut` event names the cut plus its lead
-in and the encoded segment is that length -- while the `fetch` event beside it
still says `cached: true` against the EXTENT's file, which is what makes a
re-cut free. The 4.20 is chosen deliberately: more than snapWindow from the
next silence, so snapping cannot carry the end back out to the extent and make
the test pass whether or not the cut was honoured. It caught that on the first
run.

verify-build measures a clip against its CUT when it has one. Its "did this
come out at half the length the timeline asks for" floor summed extents, and
would have fired on a build that did exactly what the manifest said.

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

Diffstat:
Mumtool/e2e/build.spec.ts | 20++++++++++++++++++++
Mumtool/e2e/clip-bench.spec.ts | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/e2e/fixtures/make-fixture.mjs | 8+++++++-
Mumtool/report-to-video/verify-build.mjs | 11+++++++++--
4 files changed, 162 insertions(+), 3 deletions(-)

diff --git a/umtool/e2e/build.spec.ts b/umtool/e2e/build.spec.ts @@ -116,6 +116,26 @@ test("a build runs end to end, offline, and the file is verified", async ({ requ // the stub for it -- the same containment rule the bench's wide fetch relies on. const fetches = events.filter((e) => e.ev === "fetch"); expect(fetches.find((e) => e.id === "c01")).toMatchObject({ cached: true }); + + // EXTENT vs CUT. c01 was reviewed at 3.00-6.00 and cut to 3.50-4.20, so the + // build renders the cut (plus the 0.4 s lead-in) and not the extent -- while + // the FETCH above still asked for the extent, which is what makes a re-cut + // free. + const cut = events.find((e) => e.ev === "cut" && e.id === "c01") as + | { seconds: number; cut: [number, number] } + | undefined; + expect(cut, "the build should say it is cutting inside the extent").toBeTruthy(); + expect(cut!.cut[0]).toBeCloseTo(3.1, 2); + expect(cut!.cut[1]).toBeCloseTo(4.2, 2); + + // And the encoded segment is that length, not the extent's three seconds. + // Snapping still moves the start to the silence at 2.90; the end is more + // than snapWindow from the next one and stays where the cut put it. + const snap = events.find((e) => e.ev === "snap" && e.id === "c01") as + | { seconds: number } + | undefined; + expect(snap!.seconds).toBeGreaterThan(0.8); + expect(snap!.seconds).toBeLessThan(2.5); }); test("an existing deliverable is not destroyed to make a new one", async ({ request }) => { diff --git a/umtool/e2e/clip-bench.spec.ts b/umtool/e2e/clip-bench.spec.ts @@ -50,6 +50,8 @@ const readClip = (id: string) => { date?: string; correction?: string; verdict?: string; + cutStart?: number; + cutEnd?: number; }[]; }; return m.timeline.find((e) => e.id === id)!; @@ -918,3 +920,127 @@ test("an after-only fetch leaves the before edge exactly where it was", async ({ expect(after.windows[0].from).toBe(before.windows[0].from); expect(after.windows[0].to).toBeGreaterThan(before.windows[0].to); }); + + +// --------------------------------------------------------------------------- +// The extent you reviewed, and the cut that plays. +// +// The window is a judgement about the RECORDING -- how much of it is worth +// having. The cut is a different question, derived from where the quote +// actually is, which is what lets one reviewed extent be re-cut for another +// report without watching anything again. +// --------------------------------------------------------------------------- + +test("the cut is derived from where the quote is, inside the extent", async ({ + page, + request, +}) => { + // Review c01 outward the way somebody does when they see how useful the clip + // could be: 0.00-12.00, four cues wide. + const { token: t } = await token(request, "c01"); + await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c01", start: 0, end: 12, token: t }, + }); + + const { token: t2 } = await token(request, "c01"); + const r = await request.post("/api/report/cut", { + data: { project: PROJECT, clip: "c01", token: t2 }, + }); + const j = (await r.json()) as { ok: boolean; score: number; matched: string }; + expect(j.ok).toBe(true); + expect(j.score).toBeGreaterThanOrEqual(0.6); + // "and because" lives in the run-on cue at 3-6, and the cut runs outward to + // the sentence that closes at 9 -- not to the extent's edges. + expect(readClip("c01").cutStart).toBe(3); + expect(readClip("c01").cutEnd).toBe(9); + + await page.goto(bench("c01")); + await expect(page.locator("[data-cut]")).toContainText("cut 0:03.00–0:09.00"); + // Drawn INSIDE the window on the rail, as an inner pair of markers. + await expect(page.locator("[data-cut-edge]")).toHaveCount(2); + await expect(page.locator("[data-cut-span]")).toBeVisible(); + + // The button runs the same matcher from the bench. The SCORE is a fact about + // a matching run rather than about the cut, so it appears when one has just + // happened and not on a reload -- which is why it is asserted here. + await page.locator("[data-cut-to-quote]").click(); + await expect(page.locator("[data-bench-note]")).toContainText("cut to the quote (match"); + await expect(page.locator("[data-cut]")).toContainText("quote match"); +}); + +test("a quote the transcript does not contain is reported, never guessed", async ({ request }) => { + const { token: t } = await token(request, "c02"); + await request.put("/api/report/window", { + data: { + project: PROJECT, + clip: "c02", + quote: "a sentence that appears nowhere in this recording at all", + token: t, + }, + }); + + const { token: t2 } = await token(request, "c02"); + const r = await request.post("/api/report/cut", { + data: { project: PROJECT, clip: "c02", token: t2 }, + }); + // Not an error to recover from: a quote the transcript does not contain is a + // fact about the REPORT, and a cut invented to hide it would be the worst of + // both. + expect(r.status()).toBe(200); + const j = (await r.json()) as { ok: boolean; error: string; score: number }; + expect(j.ok).toBe(false); + expect(j.error).toContain("no cut written"); + expect(j.score).toBeLessThan(0.6); + expect(readClip("c02").cutStart).toBeUndefined(); + + // Put the fixture's own quote back. + const { token: t3 } = await token(request, "c02"); + await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c02", quote: "another whole sentence", token: t3 }, + }); +}); + +test("a cut that is not inside its window is refused by the writer", async ({ request }) => { + const bad = async (patch: Record<string, unknown>) => { + const { token: t } = await token(request, "c01"); + const res = await request.put("/api/report/window", { + data: { project: PROJECT, clip: "c01", token: t, ...patch }, + }); + return { status: res.status(), error: ((await res.json()) as { error: string }).error }; + }; + + // c01 is 0.00-12.00 here. A cut outside it renders seconds nobody reviewed. + expect(await bad({ cutStart: 20, cutEnd: 25 })).toMatchObject({ status: 400 }); + expect((await bad({ cutStart: 20, cutEnd: 25 })).error).toContain("inside the window"); + // Half a pair builds as though there were no cut at all while the manifest + // reads as though it were trimmed. + expect((await bad({ cutEnd: "" })).error).toContain("both cutStart and cutEnd"); + // And the real cut is still there. + expect(readClip("c01").cutStart).toBe(3); +}); + +test("pulling the window in past the cut clears it, and says so", async ({ page }) => { + await page.goto(bench("c01")); + await keyboardLive(page); + // c01 is 0.00-12.00 with a cut at 3.00-9.00. Bring the end in to 8.5, inside + // the cut: the extent is the judgement being made right now, and the cut was + // derived from a wider one. + for (let i = 0; i < 7; i += 1) await page.locator("body").press("Shift+Comma"); + await page.getByRole("button", { name: "save window" }).click(); + await expect(page.locator("[data-bench-note]")).toContainText("cut no longer fitted"); + await expect.poll(() => readClip("c01").cutStart).toBeUndefined(); + await expect(page.locator("[data-cut]")).toContainText("no cut"); +}); + +test("the CLI pass reports every cut it can find, and writes none on a dry run", () => { + const out = execFileSync( + "node", + [path.join(UMTOOL, "report-to-video", "resolve-windows.mjs"), MANIFEST, "--cut-to-quote"], + { encoding: "utf8", env: { ...process.env, CHANNELS_DIR: path.join(FIXTURE, "channels") } }, + ); + // c04's quote is in the cue at 15-18, so its cut is proposed; nothing is + // written without --write, which is what makes the pass safe to run first. + expect(out).toMatch(/c04\s+vid1\s+15\.0–18\.0 -> cut /); + expect(out).toContain("dry run"); + expect(readClip("c04").cutStart).toBeUndefined(); +}); diff --git a/umtool/e2e/fixtures/make-fixture.mjs b/umtool/e2e/fixtures/make-fixture.mjs @@ -937,7 +937,13 @@ process.exit(r.status ?? 1); const BUILD = writeProject( "build-fixture", manifest("build-fixture", "The Build Fixture", { siteOrigin: "https://archive.example" }, [ - { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cite: 3, section: 0, lock: true, quote: "and because" }, + // c01 carries a CUT inside its extent: 3.00-6.00 is what somebody reviewed, + // 3.50-4.20 is what the quote needs, and the build must render the second + // while the fetch and the cache stay keyed on the first. The end is chosen + // to sit MORE than snapWindow (1.6 s) from the silence at 5.92 -- otherwise + // snapping would carry it back out to the extent's own edge and the test + // would pass whether or not the cut was honoured. + { type: "clip", id: "c01", video: "vid1", start: 3.0, end: 6.0, cutStart: 3.5, cutEnd: 4.2, cite: 3, section: 0, lock: true, quote: "and because" }, { type: "clip", id: "c02", video: "vid1", start: 9.0, end: 12.0, cite: 9, section: 0, lock: true, quote: "another whole sentence" }, ]), ); diff --git a/umtool/report-to-video/verify-build.mjs b/umtool/report-to-video/verify-build.mjs @@ -65,8 +65,15 @@ export async function verifyBuild(manifestPath, { outDir, variant = "sourced" } // half the expected length did not build what was asked for. const wanted = (manifest.timeline ?? []).reduce( // `seconds` covers cards and the two end-sequence kinds (scroll, chart); - // only a clip's length has to be derived from its window. - (n, e) => n + (e.type === "clip" ? Math.max(0, (e.end ?? 0) - (e.start ?? 0)) : (e.seconds ?? 0)), + // only a clip's length has to be derived from its window -- and from the + // CUT when it has one, because that is what was rendered. Measuring a cut + // clip against its whole extent would fire this floor on a build that did + // exactly what the manifest asked for. + (n, e) => + n + + (e.type === "clip" + ? Math.max(0, (e.cutEnd ?? e.end ?? 0) - (e.cutStart ?? e.start ?? 0)) + : (e.seconds ?? 0)), 0, ); if (wanted > 0 && duration < wanted * 0.5) {