commit 1bedff04c0f1becca0c1d76d2b045cc196667dbb
parent 3da6cc254d868f17c66df2363a6f540290a6070b
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 19 Sep 2026 01:34:20 -0400
umtool: `verdict` — whether the walk has looked at this clip
Three states out of two keys, and an absent key is the one that matters: a
manifest nobody has walked says so by carrying neither, rather than by
carrying `"verdict": "unreviewed"` on every entry -- which would read like a
decision and would have to be written before the walk could start.
correction non-empty -> corrected (the "no" answer IS the note)
verdict === "confirmed" -> confirmed
neither -> not yet reviewed
A clip cannot be both, so updateClip() keeps the two exclusive rather than
asking every reader to pick a winner: writing a correction drops a stale
confirmation, and confirming a clip that still carries one is refused.
Through the one writer, so the CLI and the app cannot disagree: the route
whitelists `verdict`, `umtool window --verdict` sets it, and reviewOf() is the
one definition of coverage -- `umtool corrections` now opens with it, because
three corrections mean something different when sixty clips have never been
looked at.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
4 files changed, 92 insertions(+), 5 deletions(-)
diff --git a/umtool/app/api/report/window/route.ts b/umtool/app/api/report/window/route.ts
@@ -43,6 +43,9 @@ export async function PUT(request: Request) {
"citeUrl",
"quote",
"correction",
+ // Whether the walk has looked at this clip: "confirmed", or empty to clear
+ // it. A non-empty `correction` is the other answer and needs no value.
+ "verdict",
]) {
if (body[k] !== undefined) patch[k] = body[k];
}
diff --git a/umtool/bin/umtool.mjs b/umtool/bin/umtool.mjs
@@ -19,7 +19,7 @@
// umtool kinds [--json]
// umtool window <project> <clip> [--start S] [--end E] [--lock] [--lock-end]
// [--title T] [--date YYYY-MM-DD] [--cite S] [--cite-url U] [--quote Q]
-// [--correction TEXT]
+// [--correction TEXT] [--verdict confirmed|'']
// umtool corrections <project> what the report got wrong, as markdown for the next pass
// umtool build <project> [--preset preview|fast|final] [--only ID] [--dry]
// umtool index [--rebuild] [--prune] [--since MS] [--json]
@@ -41,7 +41,7 @@ import {
resolveProject,
summarise,
} from "../lib/projects/core.mjs";
-import { correctionsOf, readClipDetail, readManifest, sourcesOf } from "../lib/projects/report.mjs";
+import { correctionsOf, readClipDetail, readManifest, reviewOf, sourcesOf } from "../lib/projects/report.mjs";
import { createSnapshot, listSnapshots, readSnapshot } from "../lib/report/snapshots.mjs";
import { diffManifests, formatChange } from "../lib/report/manifest-diff.mjs";
import { EXPORT_FORMATS, exportProject } from "../lib/report/export.mjs";
@@ -437,8 +437,9 @@ function usage() {
" umtool window <project> <clip> [--start S] [--end E] [--lock|--lock-end|…]",
" attribution too: --title, --date YYYY-MM-DD, --cite, --cite-url, --quote",
" --correction TEXT what the REPORT got wrong here (never rendered)",
+ " --verdict confirmed the walk agrees this clip is what it says it is",
" an empty value (--title '') deletes the field",
- " umtool corrections <project> what the report got wrong, as markdown",
+ " umtool corrections <project> what the report got wrong + the walk's coverage",
" umtool build <project> [--preset preview|fast|final] [--only ID]",
" umtool index [--rebuild] [--prune] [--since MS] [--json]",
" umtool new <slug> [--from <report.md>|<share URL>|<channel>/<id>] [--site-origin URL] [--seed chapters]",
@@ -518,6 +519,9 @@ async function cmdWindow() {
["--cite-url", "citeUrl"],
["--quote", "quote"],
["--correction", "correction"],
+ // Not attribution: whether the walk has looked at this clip. "confirmed",
+ // or '' to clear it. A non-empty correction is the other answer.
+ ["--verdict", "verdict"],
]) {
if (val(flag) !== undefined) patch[key] = val(flag);
}
@@ -546,6 +550,9 @@ async function cmdWindow() {
if (patch.correction !== undefined) {
console.log(res.entry.correction ? ` correction: ${res.entry.correction}` : " correction cleared");
}
+ if (patch.verdict !== undefined || patch.correction !== undefined) {
+ console.log(` review: ${clipVerdict(res.entry)}`);
+ }
console.log(`\nRun resolve-windows to see whether the widener agrees:`);
console.log(` node umtool/report-to-video/resolve-windows.mjs ${p.dir}/video.manifest.json`);
} catch (e) {
@@ -564,11 +571,21 @@ async function cmdCorrections() {
const manifest = await readManifest(p.dir);
if (!manifest) die("no manifest");
const rows = correctionsOf(manifest);
+ // The walk's coverage rides along with the corrections, because the two
+ // questions are one question: a list of three corrections means something
+ // different when sixty clips have never been looked at.
+ const review = reviewOf(manifest);
+
+ if (json) return out({ project: p.id, corrections: rows, review });
- if (json) return out({ project: p.id, corrections: rows });
- if (!rows.length) return console.log("no corrections");
+ const summary =
+ `${review.total} clips: ${review.corrected} corrected, ` +
+ `${review.confirmed} confirmed, ${review.unreviewed} not yet reviewed` +
+ (review.unreviewedIds.length ? ` (ids: ${review.unreviewedIds.join(", ")})` : "");
console.log(`# Corrections for the next pass — ${manifest.title ?? p.id}\n`);
+ console.log(`${summary}\n`);
+ if (!rows.length) return console.log("No corrections: nobody has said the report got anything wrong here.");
for (const c of rows) {
console.log(`- **${c.id}** · ${c.channel}/${c.video} @ ${hms(c.at)} · <${c.href}>`);
console.log(` ${c.text}`);
diff --git a/umtool/lib/projects/report.mjs b/umtool/lib/projects/report.mjs
@@ -128,6 +128,45 @@ export function correctionsOf(m) {
}));
}
+/**
+ * Has anybody looked at this clip, and did they agree with the description?
+ *
+ * Three answers out of two keys, and the absent key is the point: a manifest
+ * nobody has walked says so by carrying neither, rather than by carrying
+ * `"verdict": "unreviewed"` on every entry -- which would read like a decision
+ * and would have to be written before the walk could start.
+ *
+ * A `correction` IS the "no" answer, so it wins on its own. The writer keeps the
+ * two mutually exclusive (see updateClip), so this never has to guess.
+ */
+export const clipVerdict = (e) =>
+ String(e?.correction ?? "").trim()
+ ? "corrected"
+ : e?.verdict === "confirmed"
+ ? "confirmed"
+ : "unreviewed";
+
+/**
+ * The walk's coverage: how much of the cut has been looked at, and what is left.
+ *
+ * One definition, read by the bench header, the project page and `umtool
+ * corrections` -- the same reason correctionsOf() is one definition.
+ */
+export function reviewOf(m) {
+ const rows = clipsOf(m).map((e) => ({ id: e.id, verdict: clipVerdict(e) }));
+ const of = (v) => rows.filter((r) => r.verdict === v);
+ return {
+ total: rows.length,
+ clips: rows,
+ confirmed: of("confirmed").length,
+ corrected: of("corrected").length,
+ unreviewed: of("unreviewed").length,
+ unreviewedIds: of("unreviewed").map((r) => r.id),
+ /** Confirmed or corrected: somebody has been here and said something. */
+ reviewed: rows.length - of("unreviewed").length,
+ };
+}
+
/** The recorded preflight, and when it ran. Never a live probe. */
export async function readAvailability(dir) {
const file = path.join(dir, "out", "availability.json");
diff --git a/umtool/lib/report/manifest.mjs b/umtool/lib/report/manifest.mjs
@@ -190,6 +190,34 @@ export async function updateClip(dir, clipId, patch, { token = null } = {}) {
else delete entry[k];
}
+ // ---- the walk's verdict -------------------------------------------------
+ //
+ // Whether somebody has LOOKED at this clip and said the description is what
+ // the clip actually is. Three states and only two keys, because an absent
+ // key is the honest way to say "nobody has been here":
+ //
+ // correction non-empty -> corrected (the no answer IS the note)
+ // verdict === "confirmed" -> confirmed
+ // neither -> not yet reviewed
+ //
+ // A clip cannot be both, so the two keys are kept mutually exclusive HERE
+ // rather than by asking every reader to pick a winner: writing a correction
+ // drops a stale confirmation, and confirming a clip that still carries one
+ // is refused rather than quietly overwriting the note.
+ if (String(entry.correction ?? "").trim()) delete entry.verdict;
+
+ if (patch.verdict !== undefined) {
+ const v = String(patch.verdict ?? "").trim();
+ if (!v) delete entry.verdict;
+ else if (v !== "confirmed") {
+ throw new Error(`verdict must be "confirmed", or empty to clear it (got \`${v}\`)`);
+ } else if (String(entry.correction ?? "").trim()) {
+ throw new Error(
+ "this clip has a correction; clear it first if the description is actually accurate",
+ );
+ } else entry.verdict = "confirmed";
+ }
+
if (patch.date !== undefined) {
const v = String(patch.date ?? "").trim();
if (!v) delete entry.date;