commit 3b49a45d29fe553294898791542a91c79d3a1381
parent 615f3af4b6c09c8116576e95bfceaafdaaa169d8
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 8 Oct 2026 23:58:52 -0400
umtool: integration fixes, and the docs for timed notes, the guard and structural edits
- the article reader's Note button stays in the viewport
- the article video's timed notes leave errors to the reader's rail (one
shared handle, one message)
- docs/report-video.md: operator notes, timed notes, the generated-manifest
guard, structural edits; docs/sites.md: timed notes, Add to video
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 78 insertions(+), 6 deletions(-)
diff --git a/umtool/components/articles/ArticleReader.tsx b/umtool/components/articles/ArticleReader.tsx
@@ -312,7 +312,7 @@ export default function ArticleReader({
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => openComposer(pending.anchor)}
- style={{ left: Math.min(pending.x + 6, window.innerWidth - 70), top: Math.max(8, pending.y - 30) }}
+ style={{ left: Math.min(pending.x + 6, window.innerWidth - 70), top: Math.min(window.innerHeight - 32, Math.max(8, pending.y - 30)) }}
className="fixed z-50 rounded bg-[var(--color-sel)] px-2 py-0.5 text-[12px] font-medium text-[var(--color-ink)] shadow"
>
Note
diff --git a/umtool/components/articles/ArticleVideo.tsx b/umtool/components/articles/ArticleVideo.tsx
@@ -27,7 +27,7 @@ export default function ArticleVideo({
}) {
return (
<figure data-article-video className="space-y-1">
- <TimedVideo target={{ article: `${site}/${report}` }} file={file} src={src} poster={poster} testId="article-video" />
+ <TimedVideo target={{ article: `${site}/${report}` }} file={file} src={src} poster={poster} testId="article-video" showErrors={false} />
{caption && <figcaption className="text-[12px] text-[var(--color-dim)]">{caption}</figcaption>}
</figure>
);
diff --git a/umtool/components/notes/TimedNotes.tsx b/umtool/components/notes/TimedNotes.tsx
@@ -35,6 +35,7 @@ export default function TimedNotes({
video,
take,
resolveProject,
+ showErrors = true,
}: {
notes: UseNotes;
/** The file's path as the anchor stores it: project-relative (`takes/<id>/preview.mp4`), or the article's `video.mp4`. */
@@ -44,6 +45,8 @@ export default function TimedNotes({
take?: string;
/** Resolve each mark against this project's build schedule. */
resolveProject?: string | null;
+ /** Show the handle's errors here. Off when the handle is shared with a page that shows them itself (the article reader). */
+ showErrors?: boolean;
}) {
const [duration, setDuration] = useState<number | null>(null);
const [pending, setPending] = useState<number | null>(null);
@@ -134,7 +137,7 @@ export default function TimedNotes({
{marks.filter((m) => m.status === "open").length} open of {marks.length}
</span>
)}
- {notes.error && <span className="text-[var(--color-bad)]">{notes.error}</span>}
+ {showErrors && notes.error && <span className="text-[var(--color-bad)]">{notes.error}</span>}
</div>
{pending !== null && (
<div className="rounded bg-[var(--color-panel-2)] p-2">
@@ -210,6 +213,7 @@ export function TimedVideo({
notes: given,
poster,
testId,
+ showErrors,
className = "aspect-video w-full rounded border border-[var(--color-line)] bg-black",
}: {
target: NotesTarget;
@@ -220,6 +224,7 @@ export function TimedVideo({
notes?: UseNotes;
poster?: string;
testId?: string;
+ showErrors?: boolean;
className?: string;
}) {
const own = useSharedNotes(given ? null : target);
@@ -228,7 +233,7 @@ export function TimedVideo({
return (
<div className="space-y-1">
<video ref={setEl} data-testid={testId} src={src} poster={poster} controls preload="metadata" playsInline className={className} />
- <TimedNotes notes={notes} file={file} video={el} take={take} resolveProject={resolveProject} />
+ <TimedNotes notes={notes} file={file} video={el} take={take} resolveProject={resolveProject} showErrors={showErrors} />
</div>
);
}
diff --git a/umtool/docs/report-video.md b/umtool/docs/report-video.md
@@ -711,6 +711,66 @@ note empty is removed. A `verdicts.json` that does not parse is never
overwritten. The rules are `lib/report/takes.mjs`; the routes are
`/api/report/takes` (list), `/takes/preview` (ranges) and `/takes/verdict`.
+Each take also takes notes (a `take` anchor) and timed notes on its preview;
+the verdicts and those notes reach the agent through `umtool notes <project>`,
+which lists every take with its verdict and note ([notes.md](notes.md)).
+
+## Operator notes, timed notes and the generated-manifest guard
+
+Notes on a project live in `<project>/notes.json` (shape and CLI:
+[notes.md](notes.md)): **row notes** on a timeline entry (`entry` anchor, an open
+count per row on the project page and in the clip bench), **take notes**, **timed
+notes** and **edit notes**.
+
+**Timed notes.** On the built cut (On-screen → the built video), on every take
+preview and on an article's report video: `n` with the video focused, or Mark,
+writes a note at the playhead, and the tick strip under the player seeks. On a
+project the second is resolved against the build's `schedule.json` (the
+deliverable `out/<slug>.mp4` against `out/sourced/`, a variant's file against its
+own; a take's preview against `takes/<id>/out/<variant>/`) to the entry on screen:
+its title, quote, source second and archive link, stored in the note as
+`resolved`. A take's `preview.mp4` is that build's output copied, and its length
+equals the schedule's `total` exactly (all 9 takes of candace/polemic-israel), so
+a mark is exact when the lengths match within 0.25 s and `approx` when they
+differ, when the schedule is estimated, or when the second is a held frame past
+the clip's source. `lib/report/moments.mjs`, `GET /api/report/moment`.
+
+**Generated manifests.** A manifest with `generatedBy` shows one line on the
+project page, the clip bench and the On-screen section: "Generated by
+`<generatedBy>`; a rebuild of manifests overwrites edits made here." Edits are
+still allowed. Every manifest writer ROUTE goes through `withEditNotes`
+(`lib/report/guard.ts`), which diffs the manifest before and after and records
+each change as an `edit` note `{ entry?, field, from, to }`: repeated saves of one
+field coalesce into one note, and putting a field back deletes its note unless it
+has replies. The agent ports each change into the generator's inputs (BEATS,
+drafts), regenerates and resolves the note. `umtool window` is the agent's own
+tool and is not wrapped.
+
+## Structural edits
+
+The project page's timeline re-orders by drag or alt+↑/↓; each row's ⋯ menu
+duplicates it, removes it, or inserts a clip after it
+(`<channel>/<video>@<start>-<end>`). The On-screen section edits a teaser's lines,
+beat, tail, tail wait and dip; the posts (add, edit, remove); and each verdict's
+label and colour and the stamp seconds. An article's citation can be added to the
+end of a linked project's timeline ("Add to video", [sites.md](sites.md)).
+
+All of it is `POST /api/report/timeline` (`move`, `remove`, `duplicate`, `insert`,
+`teaser`, `post`, `post-remove`, `factcheck`, `undo`; `GET` gives the token), with
+the writers in `lib/report/manifest.mjs` beside the window writers: the mtime
+token, tmp + rename, 2 dp. Each op is checked by the build's own validators
+(`deck.mjs`, `factcheck.mjs`) and refused only for what it breaks, and snapshots
+the manifest first (`revisions/<stamp>-auto-before-<op>.manifest.json`, at most
+one per op every two minutes). **Undo** restores the newest such snapshot byte for
+byte (the current state is kept as `undo-saved`).
+
+A move recomputes `sectionEnter` — a clip enters when its `section` differs from
+the previous clip's, the first clip included (`lib/report/sections.mjs`) — and only
+in a manifest that already carries the flag; `section` itself never changes.
+report-to-video only READS the flag. Checked read-only on all 44 manifests under
+~/reports: no mismatch with the stored flags, and 3,490 move-and-back round trips
+byte-identical.
+
## Exports
`umtool export <project> --format toc-bbcode|toc-markdown|description|chapters`
diff --git a/umtool/docs/sites.md b/umtool/docs/sites.md
@@ -34,8 +34,9 @@ the one file it writes there is a report's `notes.json` (docs/notes.md).
## Reading and noting
The article renders in a ~70ch column with its notes in a rail (a drawer below
-1100px). Its video sits under the title in one slot
-(`components/articles/ArticleVideo.tsx`).
+1100px). Its video sits under the title (`components/articles/ArticleVideo.tsx`)
+and takes timed notes: `n` with the video focused, or **Mark**, notes the
+playhead as a `moment` anchor on the article's notes ([report-video.md](report-video.md)).
| to note | do |
|---|---|
@@ -70,6 +71,12 @@ A post shows its text and its capture screenshot. The walk
(`/evidence`) is the same panel one citation at a time: `j`/`k` (or ←/→),
`space` plays, `n` notes.
+**Add to video** (in the panel, when the article has a linked video project)
+puts the cited span at the end of that project's timeline as a clip
+(`/api/report/timeline` insert): the manifest is snapshotted first, so the
+project page's **Undo** takes it back, and on a generated manifest an `edit` note
+tells the agent to port it into the generator's inputs.
+
## Routes (read-only)
| route | serves |