commit 6651ce562afc92cc0f460b8e4a51adfd435da752
parent 21d5f923c480d7c773283fe9fff22e6beeaf03bc
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 1 Oct 2026 16:16:12 -0400
umtool: a window save clamps a mute mark within 0.02 s of the moved edge onto it and stores it, so the build accepts every manifest updateClip saved
The writer's tolerance stays; the build's check stays strict. Clamping is
what a patched mark already got: a mark the new start passed still mutes
the whole clip, and one the new end passed moves at most 0.02 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/umtool/lib/report/manifest.mjs b/umtool/lib/report/manifest.mjs
@@ -295,8 +295,13 @@ export async function updateClip(dir, clipId, patch, { token = null } = {}) {
if (v == null) delete entry.muteFrom;
else entry.muteFrom = v;
} else if ((patch.start !== undefined || patch.end !== undefined) && entry.muteFrom != null) {
+ // Clamped and STORED, as a patched mark is: a mark within the writer's
+ // 0.02 s of the moved edge lands on it (past the new start it still
+ // mutes the whole clip), and the build (validateMuteFrom, strict)
+ // accepts every manifest saved here. Left as it was, a start moved from 10.00 to 10.01 under a mark at
+ // 10.00 saved, and the next build refused the whole manifest.
try {
- parseMuteFrom(entry.muteFrom, entry.start, entry.end);
+ entry.muteFrom = parseMuteFrom(entry.muteFrom, entry.start, entry.end);
} catch {
throw new Error(
`the mute mark ${entry.muteFrom} must lie inside the window ` +
diff --git a/umtool/lib/report/manifest.test.mjs b/umtool/lib/report/manifest.test.mjs
@@ -22,6 +22,7 @@ import {
updateOnscreen,
updatePosts,
} from "./manifest.mjs";
+import { validateCutEdits } from "umtool-report-to-video/deck";
const base = () => ({
slug: "t",
@@ -217,6 +218,28 @@ test("updateClip: muteFrom is a number inside the clip, rounded; empty or null d
}
});
+test("updateClip: a window edge moved within 0.02 s past the mute mark clamps the mark onto it, so the build accepts what was saved", async () => {
+ const dir = await project();
+ try {
+ // c01 is 10–20. A mark on the start, then the start moved a hundredth on.
+ await updateClip(dir, "c01", { muteFrom: 10 });
+ await updateClip(dir, "c01", { start: 10.01 });
+ let m = await read(dir);
+ assert.equal(entry(m, "c01").muteFrom, 10.01, "the mark moves onto the new start");
+ assert.deepEqual(validateCutEdits(m), [], "and the build's strict check takes it");
+ // The same at the end.
+ await updateClip(dir, "c01", { muteFrom: 20 });
+ await updateClip(dir, "c01", { end: 19.99 });
+ m = await read(dir);
+ assert.equal(entry(m, "c01").muteFrom, 19.99);
+ assert.deepEqual(validateCutEdits(m), []);
+ // Further than the tolerance is still refused.
+ await assert.rejects(updateClip(dir, "c01", { end: 19.9 }), /mute mark 19.99 must lie inside the window/);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+});
+
const DECK = { engine: "hyperframes", layout: "deck", deck: { height: 180, title: { size: 60 } } };
test("updateChrome: round trip stores the block as given; null removes it", async () => {