Archilyzer · Source

archilyzer

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

commit f28e7a02191214c4287a49576397b5b702cdc604
parent eea3f3327802f839df1c30f382026fb74ef69a63
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Thu,  8 Oct 2026 22:33:42 -0400

docs: operator notes (umtool/docs/notes.md, AGENTS.md, REPORT.md)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
MAGENTS.md | 15+++++++++++++++
MREPORT.md | 2+-
Mcommon/lib/report/docs.ts | 4+++-
Mumtool/docs/README.md | 2++
Aumtool/docs/notes.md | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 133 insertions(+), 2 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md @@ -140,6 +140,21 @@ better), whose clock is not the same. This tooling is on `main` as of 2026-08-20. +# Operator notes on articles and videos + +```sh +umtool notes --all --open # from the checkout: node umtool/bin/umtool.mjs notes --all --open +``` + +The operator leaves notes in umtool on articles (`/sites/<site>/<report>`) and on +report-video projects (rows, takes, moments in a cut). They live in a `notes.json` +beside the report's `report.json` (never published) or beside the project's +`video.manifest.json`. `umtool notes <site>/<report>` prints each open note with its +anchor resolved and the **source** file to edit — a draft, not the generated +`report.json` or manifest. Act, regenerate, then `umtool notes reply <id> "<what +changed>" --resolve`. Never hand-edit `notes.json`. See +[umtool/docs/notes.md](umtool/docs/notes.md). + # The runtime container `docker compose up -d` stands up a working archive: the editor plus Caddy, with diff --git a/REPORT.md b/REPORT.md @@ -2,7 +2,7 @@ <!-- GENERATED by common/bin/file-schemas-docs.ts from the schemas and their *_FIELD_DOCS records — do not edit by hand. --> -One cited report, format `"archilyzer-report"`, version 1, persisted to `transcripts/sites/<siteId>/reports/<reportId>/report.json` beside its `stills/` and `sources/<sourceId>/`; a relative path in it is relative to that directory. A site's `reports` list in `site.json` is the published, ordered list — see [SITE.md](SITE.md); a report directory it does not name is a draft. The schema is `common/lib/report/schema.ts`; its citations and sources are the citation model's — see [CITATIONS.md](CITATIONS.md). +One cited report, format `"archilyzer-report"`, version 1, persisted to `transcripts/sites/<siteId>/reports/<reportId>/report.json` beside its `stills/` and `sources/<sourceId>/`; a relative path in it is relative to that directory. A site's `reports` list in `site.json` is the published, ordered list — see [SITE.md](SITE.md); a report directory it does not name is a draft. The schema is `common/lib/report/schema.ts`; its citations and sources are the citation model's — see [CITATIONS.md](CITATIONS.md). A `notes.json` beside it holds the operator's notes on the article (umtool, `umtool notes`; [umtool/docs/notes.md](umtool/docs/notes.md)) and is never published. A **fact-check** (`"kind": "factcheck"`) is sections (chapters) of claims, each with a verdict and its findings. A **sweep** (`"kind": "sweep"`) is sections with no verdicts, or bodies that cite inline. Markdown fields (`summary`, a section's `body`, a claim's `findings`) cite with `[label](cite:<id>)`. diff --git a/common/lib/report/docs.ts b/common/lib/report/docs.ts @@ -32,7 +32,9 @@ export function renderReportMarkdown(): string { "that directory. A site's `reports` list in `site.json` is the published, " + "ordered list — see [SITE.md](SITE.md); a report directory it does not name is a " + "draft. The schema is `common/lib/report/schema.ts`; its citations and sources " + - "are the citation model's — see [CITATIONS.md](CITATIONS.md).", + "are the citation model's — see [CITATIONS.md](CITATIONS.md). A `notes.json` " + + "beside it holds the operator's notes on the article (umtool, `umtool notes`; " + + "[umtool/docs/notes.md](umtool/docs/notes.md)) and is never published.", ); out.push(""); out.push( diff --git a/umtool/docs/README.md b/umtool/docs/README.md @@ -93,6 +93,8 @@ defects that have already shipped in real videos: a manifest with no `siteOrigin | [mix-from-a-project.md](mix-from-a-project.md) | Deep-linking a clip into `/mix` | | [index.md](index.md) | The LMDB index, and why the filesystem stays the model | | [cli.md](cli.md) | `umtool ls / show / check / build / window / …` | +| [notes.md](notes.md) | Operator notes on articles and videos; `umtool notes`, the agent loop | +| [sites.md](sites.md) | `/sites`: every site's articles, their media, workspaces and notes | | [authoring.md](authoring.md) | Writing a manifest from a sweep report | | [e2e.md](e2e.md) | The fixture, the stubs, the global queue | | [quirks.md](quirks.md) | Everything that cost time to find out | diff --git a/umtool/docs/notes.md b/umtool/docs/notes.md @@ -0,0 +1,112 @@ +# Notes: the operator writes them in umtool, an agent acts on them + +A note is a sentence or two the operator leaves on an **article** (a report on a +site, `/sites/<site>/<report>`) or on a **report-video project** (a timeline row, a +take, a moment in a rendered cut). It is saved where the agent that made the thing +can read it back, act on the right SOURCE file, and reply or resolve. + +```sh +umtool notes --all --open # every notes file with an open note +umtool notes candalyzer/polemic-israel # one article's open notes, as markdown +umtool notes candace/polemic-israel # one video project's (plus every take's verdict) +umtool notes reply n_mgk3x0a1b2 "Changed 'said' to 'claimed' in drafts/israel.json" --resolve +umtool notes resolve | wontfix | reopen n_mgk3x0a1b2 +``` + +Run it from the repo checkout (or with `TRANSCRIPTS_DIR`/`SITES_DIR` and +`REPORTS_DIR` set): like `CHANNELS_DIR`, the sites directory is found by walking up +from the working directory to the checkout. + +## The agent loop + +1. `umtool notes --all --open` — what is waiting. +2. `umtool notes <target>` — each open note with its anchor resolved against what + is on disk now, and the **Source** line naming the file to edit. +3. Edit the SOURCE, not the output. An article's `report.json` is regenerated from + a draft (`<workspace>/polemics/drafts/<slug>.json`) by a generator + (`make-site.py`, `polemics.py`); a generated video manifest (`generatedBy`) is + rewritten by its `make-videos.py`. An edit to the output is lost on the next run. +4. Regenerate. +5. `umtool notes reply <id> "<what changed>" --resolve` — or reply without + `--resolve` to ask a question. The operator sees agent replies badged "agent". + +**Never hand-edit `notes.json`.** The CLI and the app write through one store that +validates, takes a lock, and refuses a stale write; a hand edit races the page and +can lose a reply. If the recorded source is wrong, correct it: +`umtool notes source <target> --draft <path> [--generator <path>] [--how <why>]`. + +The same digest is served as text at `GET /api/notes/context?article=<site>/<report>` +(or `?project=<id>`); the page's "Copy agent brief" copies it. + +## Where notes live + +| Subject | File | +|---|---| +| An article | `transcripts/sites/<site>/reports/<report>/notes.json`, beside `report.json` | +| A report-video project | `<project>/notes.json`, beside `video.manifest.json` | + +The article file is the one corpus file umtool writes, and only through +`isCorpusNotesFile` (`lib/paths.mjs`): exactly `sites/<site>/reports/<id>/notes.json`, +in an existing report directory that is not a symlink out of its site. The +generators overwrite only `report.json`, `video.mp4` and `poster.jpg` in a report +directory, so notes survive a regenerate; compose and the report history never read +it (`common/publish/composeReports.test.ts` holds that), so a note is never published. + +## The file + +```jsonc +{ "format": "umtool-notes", "version": 1, + "subject": { "kind": "article", "site": "candalyzer", "report": "polemic-israel" }, + // | { "kind": "video-project", "project": "candace/polemic-israel" } + "source": { "draft": "~/reports/candace/polemics/drafts/israel.json", + "generator": "~/reports/candace/site/polemics.py", + "how": "draft matched by id polemic-israel; generator names candalyzer" }, + "notes": [ { "id": "n_…", "status": "open", // open | resolved | wontfix + "author": "operator", "text": "…", // operator | agent + "at": "…", "updatedAt": "…", + "anchor": { … }, + "replies": [ { "author": "agent", "text": "…", "at": "…" } ], + "resolvedAt": "…", "resolvedBy": "agent" } ] } +``` + +`source` is filled on the first write. For an article it comes from +`lib/articles/sources.mjs`: a draft whose `id` or file name is the report id (allowing +a `polemic-` prefix either way; the workspace whose generator names the site wins a +tie), and the generator under `<workspace>/polemics` or `<workspace>/site` that names +the site or the report. For a video project it is the manifest and, when it has +`generatedBy`, the generator. + +The last note deleted deletes the file. A `notes.json` that does not parse is never +overwritten — fix it by hand first. + +## Anchors + +| `kind` | Fields | Resolved for the agent as | +|---|---|---| +| `text` | `section` (a section id, or `title`/`subtitle`/`summary`/`method`), `quote`, `prefix`, `suffix` | the section title and the sentence holding the quote | +| `cite` | `cite` (a citation id) | the citation's quote, speaker, date, `<channel>/<id>@start-end` | +| `section` | `section` | the section title | +| `whole` | — | the whole article or project | +| `moment` | `file` (relative mp4), `t`, `take?`, `entry?`, `resolved?` | the time, and the entry, quote and source second it resolved to when written | +| `entry` | `entry` (a timeline entry id) | the entry's title, quote and source span | +| `take` | `take` (a take id) | the take's label and summary, and its verdict | +| `edit` | `entry?`, `field`, `from`, `to` | an edit made in umtool to a GENERATED manifest — port it into the generator's inputs | + +A text anchor is a quote with 32 characters of context either side (the W3C +TextQuoteSelector), never an offset, so it survives a regenerated `report.json`: +`lib/annotations/anchor.mjs` finds the quote exactly, then with whitespace, quotes +and case normalised, then — when the quote itself was rewritten — between its +surviving context. A note it cannot place is **orphaned**: still shown, pinned to its +section, with its original quote. + +## Code + +| Module | What | +|---|---| +| `lib/annotations/shape.mjs` | the contract: constants, validation, the ops (pure, client-safe) | +| `lib/annotations/anchor.mjs` | re-anchoring (pure, client-safe) | +| `lib/annotations/store.mjs` | read, lockfile (`notes.json.lock`, stale after 30 s), token, tmp+rename | +| `lib/annotations/targets.mjs` | article / project → file, subject, source; `listNotesFiles` | +| `lib/annotations/digest.mjs`, `cli.mjs` | the markdown digest and `umtool notes` | +| `app/api/notes/route.ts` | GET / POST (operator-stamped; 409 on a stale token) | +| `lib/annotations/useNotes.ts` | the page's hook |