commit 9d38f625e5cf7b01a99cdba857b74aa52927477e
parent 87a01399559b16038556d483f71e7c44c0b79a84
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 5 Oct 2026 02:09:14 -0400
plans: report sites — a site's reports, cited-only publishing and moment pages
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
| A | plans/report-sites.md | | | 156 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 156 insertions(+), 0 deletions(-)
diff --git a/plans/report-sites.md b/plans/report-sites.md
@@ -0,0 +1,156 @@
+# Report sites — a site built around cited reports
+
+Status: PLAN (2026-10-04), awaiting operator review. Not started.
+
+## What it is
+
+Two independent axes on a site:
+
+| Axis | Values | Today |
+|---|---|---|
+| `reports` | 0..n published reports, each a cited document rendered as pages | none |
+| `publish` | `"full"` — the searchable corpus (today); `"cited"` — ONLY the moments the site's reports cite | always full |
+
+- A **report site** is `publish: "cited"` + ≥1 report: no search, no browse, no full transcripts, no archives.
+- A **full site with reports** keeps everything it has and gains a Reports section; its moment pages also link
+ into the corpus (`/?v=…&t=`).
+- Every citation in a report opens a **moment page** on the site: the quote, a self-hosted evidence clip of the
+ cited span (720p, ± a little context), the surrounding transcript lines, record metadata (title, channel,
+ date), and a link to the original at its time. A post citation shows the captured screenshot and media
+ (capture-posts) and the post text instead of a clip.
+- A report claim can carry the reviewed source's own sentence as an image (shoot-page) and the source's
+ archive links.
+
+First user: one private, local-only report site. The machinery is generic so later sites (and later reports
+on existing full sites) need configuration, not code.
+
+## The report document — `report.json` (format `archilyzer-report`, version 1)
+
+```jsonc
+{ "format": "archilyzer-report", "version": 1,
+ "id", "kind": "factcheck" | "sweep", "title", "subtitle", "summary" /* md */, "published", "updated",
+ "subject": { "source": "s0" }, // the document under review (optional)
+ "verdicts": { "PARTLY": { "label": "…" } }, // optional label/colour overrides of the shared vocab
+ "sources": { "s0": { "kind": "article", "title", "url", "publisher", "author", "date",
+ "archives": [{ "label", "url", "context" }], // links in context, as the source had them
+ "saved": "sources/s0/page.html" } }, // input for stills only — NEVER published
+ "citations": {
+ "c01": { "kind": "video", "channel", "id", "start", "end", "quote", "pad": { "before": 5, "after": 5 } },
+ "p01": { "kind": "post", "channel", "id", "quote" },
+ "a01": { "kind": "source", "source": "s0", "quote", "image": "stills/a01.png" } },
+ "sections": [ { "id", "title", "body" /* md */,
+ "claims": [ { "id", "text", "verdict" /* optional */,
+ "sourceQuote": { "citation": "a01" },
+ "findings": "md with [label](cite:c01) refs",
+ "citations": ["c01", "p01"] } ] } ] }
+```
+
+- A fact-check is sections (chapters) → claims with a verdict. A sweep report is sections with no verdicts, or
+ bodies with inline `cite:` links.
+- The verdict vocabulary (CORROBORATED, PARTLY, CONTRADICTED, NOT_FOUND, UNTESTABLE + labels/colours) moves to
+ common; `umtool/report-to-video/factcheck.mjs` re-exports it — one copy.
+- Files per site: `transcripts/sites/<siteId>/reports/<reportId>/{report.json, stills/, sources/<sid>/…}`.
+ `site.json` `reports: [ids]` is the published, ordered list; an unlisted directory is a draft.
+- Validation (zod, `common/lib/report/`): ids resolve, every `cite:` ref exists, spans are sane (cap ~120 s),
+ stills exist; at compose time every video quote is checked against its cue window (fail loudly on drift; a
+ platform with two ids must say which).
+
+## New `site.json` keys
+
+| Key | Default | Notes |
+|---|---|---|
+| `publish` | `"full"` | `"cited"` publishes only reports + their moments. Only the non-default is written. |
+| `reports` | `[]` | Ordered report ids under `reports/`. |
+
+`channels` keeps its meaning (the pool a site's citations may resolve against). Docs in `SITE_FIELD_DOCS`,
+`SITE.md` regenerated; the editor's site form gets the publish control; a new **Reports** tab under
+`/sites/<id>/` lists reports, their validation problems, and "Prepare evidence media".
+
+## Routes (export app)
+
+| Route | What |
+|---|---|
+| `/reports/` | Report index; the HOME page of a `cited` site |
+| `/reports/<reportId>/` | The report: summary, tally (fact-check), sections → claims (verdict chip, source sentence still, findings with citation links, archive links in context) |
+| `/m/<channel>/<id>/<start>-<end>/` | Video moment page |
+| `/m/<channel>/<id>/` | Post moment page |
+
+- Server-rendered at build from JSON that compose writes under `public/reports/**` and `public/m/**`, read like
+ `export/app/lib/archives.ts`.
+- `generateStaticParams` must never be empty (Next 16 static export error E87 on an empty dynamic route): a site
+ with zero reports emits one placeholder param that renders "no reports".
+- A `cited` site's shell has no workspace/search, no `/ask`, no downloads/duplicates; the home page must not call
+ `countTranscripts()` (it throws without a summaries manifest).
+- A full site shows a **Reports** header link when it has reports.
+
+## Evidence media
+
+- Clips are cut ON THE HOST, before the per-site build, by a prepare step (`archilyzer reports prepare <siteId>`
+ and an editor job kind with `needsMedia`): the container comes from the corpus clip window
+ (`data/<id>/clips/<from>-<to>.*`) or the saved-video store pointer; cut accurately, 1280×720 H.264 crf 23, AAC.
+ Cached at `.export-index/sites/<siteId>/report-media/<hash>.mp4` (hash = source + span + profile), so the docker
+ build (read-only mounts, no ffmpeg) only copies.
+- The cutter's tier lookup and cut args move from `umtool/report-to-video/sources.mjs` / `umtool/lib/report/` into
+ `common/lib/evidenceClip-server.ts`; umtool re-exports (common may not import umtool).
+- A citation whose media is missing fails prepare with a list (the editor's fetch-window / persist fill it).
+- Published layout: `out/media/clips/<channel>/<id>/<start>-<end>.mp4`, `out/media/posts/<channel>/<id>/…`
+ (only cited captures; the post-visibility rule still applies), `out/reports/<r>/stills/*.png`.
+- Limits: refuse a file over 24 MiB (Pages: 25 MiB), keep the site under the 20k-file cap.
+- The existing guard test that forbids publish code from naming `posts-media` is amended to allow exactly the
+ one module that copies CITED captures, with a test that it copies nothing else.
+
+## Compose and the contract
+
+- `compose-site` gains a reports stage: resolve citations against the shared transcript trees, verify quotes,
+ write report + moment JSON, copy media and stills.
+- `publish: "cited"` prunes everything corpus-shaped from `public/` (summaries, stats, transcripts, subs, posts,
+ digests, archives, duplicates, tags, aliases, chart templates, service worker) — `public/` is shared by every
+ site's build in turn, so stale data from the previous site must not survive — and an **out/ allowlist audit**
+ refuses a cited build that contains anything outside reports/moments/media/stills/shell assets.
+- Always emit `site.json` (`channels: []` for a cited site) and `corpus.json` with `site.scope: "cited"`,
+ `audience`, and `reports: { index: "/reports/index.json" }` — the private-deploy guard and compose's
+ cross-site check read them. `CONTRACT.corpusSpec` → 5; readers tolerate a cited scope (an old reader sees an
+ empty corpus, which is safe). `llms.txt` and the sitemap get a cited variant listing report routes.
+- Hub and homepage exclude `cited` sites for now; MCP may later gain `list_reports` / `get_report`.
+- umtool's cue walk (`cues.mjs`) cannot resolve cues off a cited site — documented.
+
+## Converters (to make reports from what exists)
+
+- umtool video manifest / ledger → report.json.
+- Sweep markdown (`/sweep` output: `[title @ mm:ss](<origin>/?v=…&t=…)`, `[post by …](url)`) → report.json, ends
+ widened with `resolve-windows widen`.
+- `shoot-page` batch items from each claim's `sourceQuote`.
+- A bespoke report shape (any other generator) converts outside the repo and validates against the schema.
+
+## Slices
+
+| Slice | What | Size | Needs |
+|---|---|---|---|
+| R0 | `common/lib/report/`: zod schema, types, validation, moment keys, shared verdict vocab; `REPORT.md` generated | M | — |
+| R1 | site keys `publish` / `reports` + site form control | S | — |
+| R2 | evidence media: cutter port to common, `reports prepare` CLI + job kind, cited-capture copier + guard amendment | L | R0 |
+| R3 | compose: reports stage, cited prune, allowlist audit, always-emit site/corpus, llms/sitemap variants | L | R0, R2 interface |
+| R4 | export app: routes, Reports header link, cited shell, placeholder params | L | R0 (fixture JSON) |
+| R5 | consumers: corpusSpec 5, reader tolerance, hub/homepage exclusion, builtExport audit hook | M | R0 |
+| R6 | converters: manifest → report, sweep md → report, shoot-page items | M | R0 |
+| R7 | editor Reports tab: list, problems, Prepare media, private build | M | R1, R2 |
+| R8 | e2e: a cited fixture site (own playwright config), compose + audit unit tests | M | R3, R4 |
+
+```
+wave 1: R0, R1
+wave 2: R2, R4, R5, R6 (after R0)
+wave 3: R3 (after R2), R7 (after R1, R2)
+wave 4: R8 (after R3, R4)
+```
+
+## Risks
+
+1. Stale `public/` leaking corpus data into a cited build → explicit prune + out/ allowlist audit before anything
+ is served or deployed.
+2. A cited build without `site.json` / `corpus.json` silently disables the private-deploy guard → always emit.
+3. Next 16 empty-params build failure → placeholder param.
+4. Post privacy: only cited captures, the visibility rule applies, saved source pages are never published.
+5. Moment JSON carries only cited fields and a bounded cue context.
+6. Size: span cap, 24 MiB per-file refusal, 20k files.
+7. Cue drift between a corpus and an earlier publish; platforms with two ids → validated at compose, loud failure.
+8. Contract change for third-party readers → spec bump, safe degradation.