Archilyzer · Source

archilyzer

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

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:
Mumtool/app/api/report/window/route.ts | 3+++
Mumtool/bin/umtool.mjs | 27++++++++++++++++++++++-----
Mumtool/lib/projects/report.mjs | 39+++++++++++++++++++++++++++++++++++++++
Mumtool/lib/report/manifest.mjs | 28++++++++++++++++++++++++++++
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;