Archilyzer · Source

archilyzer

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

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:
Mumtool/components/articles/ArticleReader.tsx | 2+-
Mumtool/components/articles/ArticleVideo.tsx | 2+-
Mumtool/components/notes/TimedNotes.tsx | 9+++++++--
Mumtool/docs/report-video.md | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mumtool/docs/sites.md | 11+++++++++--
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 |