# Release 22 — one article, two shapes: slides, and the isometric overview Written 2026-10-09. An article (a `report.json`) can be read as the article it is, as a slide deck, or in an **isometric overview** that lays the two side by side in one tilted 3-D plane — each section beside its slide — so a reader sees the same article both ways and enters whichever they prefer at the same place. ## Rulings (operator, 2026-10-09) - Articles may be **built a bit differently** to make this work: `report.json` gains authoring fields for slides. - **"Isometric render" is the overview described here** (operator confirmed 2026-10-09): a tilted plane pairing each article section with its slide; click either to enter that view. ## What is there (verified) - An article is `report.json` (`common/lib/report/schema.ts`, `validate.ts`, REPORT.md): `kind` `factcheck | sweep`, title/subtitle/summary/method, `sections[]` → `claims[]` (`text`, `verdict`, `gist`, `findings`, `citations`), markdown bodies citing `[label](cite:)`, `video`, `sources`, `citations`. - Two renderers of one view model (`ReportPageView`, `common/lib/report/views.ts`): the export site's `export/app/components/reports/ReportArticle.tsx` (route `export/app/reports/[reportId]/page.tsx`, static) and umtool's `ArticleReader.tsx`/`ArticleBody.tsx` (`umtool/app/sites/[site]/[report]/page.tsx`, with its own lenient resolver `umtool/lib/articles/article.ts` `articleView` for drafts). - Shared citation components: `common/components/citations/*` (`CitedMarkdown`, `InlineCite`, `CitationCard`, `ReferenceList`), `common/components/report/VerdictChip.tsx`; markdown via `markdown-to-jsx`. - Exports: `REPORT_EXPORT_FORMATS` (`views.ts`), `common/publish/reportExports.ts` (`report.html` self-contained via `common/lib/report/exportHtml.ts`, `report.pdf` by Chromium), `reportExportFiles.ts`, the downloads table in `ReportArticle.tsx`, the revision-history commit (`reportHistory.ts`). - View toggles: the transcript `vm`/`vt` URL params (`common/components/urlState.ts`; absent = the default view). - **Naming hazard:** "deck" means umtool's video chrome overlay (`umtool/report-to-video/deck.mjs`, `chrome-deck.mjs`) and several editor card stacks. This feature is **slides** everywhere: `slides`, `SlideView`, `?rv=slides`. ## Design ### The article gains slide fields (schema, backward-compatible, `version` stays 1) - `report.slides?`: `{ hide?: boolean, title?: string, closing?: string }` — the deck's own title-slide line and closing line; `hide: true` publishes no slides view. - `section.slide?` and `claim.slide?`: `{ title?: string, points?: string[], cite?: , layout?: "points" | "evidence" | "quote" | "statement", hide?: boolean }`. `points` are the slide's own words (≤ 5, each ≤ 140 chars, may cite inline); `cite` picks the evidence the slide shows (a still, a post shot, a clip's poster and quote); `layout` overrides the default. - The validator checks them (counts, lengths, a `cite` that names a citation of that section/claim, inline cites that resolve) and REPORT.md is regenerated (`docs files`). An article with no slide fields still gets slides, derived as below; the fields make them good. ### One builder: `buildReportSlides(view): SlideView[]` (`common/lib/report/slides.ts`, pure) Every slide carries an **anchor** (`{kind: "head" | "summary" | "found" | "section" | "claim" | "sources", id?}`) that is also an id in the article's DOM — the one mapping the toggle, the overview and notes all use. | slide | from | default layout | |---|---|---| | title | series, title, subtitle, dates, subject, `report.slides.title` | `title` | | the quick take | `summary`'s first paragraph (or `points`) | `statement` | | what the check found | the verdict tally (`foundGroups`) — factcheck only | `found` | | one per section | `section.slide.points`, else the body's first two sentences | `points` | | one per claim | claim `text`, `VerdictChip`, `gist`, the `cite`d (else first) citation's evidence | `evidence` | | sources | the reference count, the site link, `report.slides.closing` | `sources` | `hide` drops a slide; a claim with no citation renders `statement`. Long markdown never lands on a slide: a slide shows `points`/`gist`/one quote, and every slide links "Read this in the article" (its anchor). ### The reader (export site and umtool, shared components in `common/components/report/slides/`) - **View switch** on the article header: `Article | Slides | Overview`, a segmented control in the transcript `modeStrip` pattern. URL param `rv` (`slides`, `iso`; absent = article) plus the anchor in the hash, so a shared link opens the same view at the same place. Switching keeps the place: the article's section in view ↔ that section's slide. The article HTML stays the statically rendered page (search engines and no-JS readers get the article); slides and the overview render client-side from the same `page.json`, loaded on demand. - **Slides** (`ReportSlides.tsx`): one 16:9 stage letterboxed in the viewport, scaled type; ←/→, PgUp/PgDn, Home/End, Esc (back to the article at this slide's anchor), swipe on touch; a progress rail; slide number in the hash (`#s-4`). Citations stay live (the `CitationsProvider` of the page): a cite opens its card. Per-site accent and light/dark from the existing tokens. Phones: the stage fits the width; portrait shows the slide's text at readable size with its evidence below. - **Overview** (`ReportOverview.tsx`): one plane tilted isometrically (CSS `rotateX(55deg) rotateZ(-45deg)`), the article's sections as a column of page-blocks on the left, the slides as a column of 16:9 cards on the right, a connector between each pair. Hover/focus a pair lights both; click the block to open the article at it, the card to open the slides at it; arrow keys walk pairs. `prefers-reduced-motion`, narrow screens and keyboard-first users get the same pairs flat (two columns, or stacked pairs on a phone) — the tilt is never the only way to read it. - **umtool** `ArticleReader` gets the same switch over its lenient `articleView`, so a draft can be checked in all three shapes before publishing; a slide shows the note count of its anchor, and the source tab's lint lists slide problems (a section with no `points` whose body's first sentences run long, an over-long point). ### Exports - `slides.html` (self-contained like `report.html`: inline style + a few lines of inline script for keys, images inlined) and `slides.pdf` (Chromium, one 16:9 page per slide) join `REPORT_EXPORT_FORMATS`, the downloads table, `evidence-pack.zip` and the revision commit's `exports.json`. Same 24 MiB rule. ## Slices (Track E) | slice | class | what | |---|---|---| | **E1** schema + builder | `[unit]` | the slide fields in `schema.ts`/`validate.ts`, REPORT.md regenerated; `slides.ts` `buildReportSlides` with a table of tests (every default, every override, `hide`, a sweep with no tally, a claim with no citation, anchors unique and matching the article's ids); the demo fixture report gains slide fields | | **E2** slides view | `[unit]` + `[spec]` | `common/components/report/slides/` (`ReportSlides`, slide layouts, `ViewSwitch`), wired into `ReportArticle.tsx` and the export route; article anchors on sections/claims; `rv` + hash state | | **E3** overview | `[unit]` + `[spec]` | `ReportOverview` (isometric + flat fallbacks), pair navigation | | **E4** umtool | `[spec umtool]` | the switch in `ArticleReader`, note counts per slide, slide lint in the source tab | | **E5** exports | `[unit]` | `slides.html`, `slides.pdf`, downloads table, evidence pack, history | | **E6** records | `[none]` | this file's record, changelogs (export, umtool), REPORT.md, PUBLISH.md's exports paragraph | E1 → E2 → E3; E1 → E4; E1 → E5. e2e: the export report suite (`pnpm --filter export run e2e:report`) gains slides and overview cases over the fixture (switch keeps the place, keys, a cite opens its card, the reduced-motion overview, a phone viewport) — run once at E3 and once at E5; umtool's article specs once at E4. ## Do not touch `umtool/report-to-video/**` (the Candace session's); the editor; `transcripts/**`. Published reports change only when a site is rebuilt — nothing here rewrites a report.json in the corpus. ## Record ### Track E Branch `worktree-agent-a4ec71c7c593a80e5`, off `fdbdd370` (`r19/integration` with this plan). Not pushed, not merged. #### E1, as shipped — the slide fields and the builder `report.json` gains `slides` (`hide`, `title`, `points`, `closing` — `points` added to the plan's three: the plan's "the quick take: summary's first paragraph (or `points`)" needs a place for them) and a section's or claim's `slide` (`title`, `points`, `cite`, `layout`, `hide`); `version` stays 1. The limits are one pure module, `common/lib/report/slideRules.ts` (5 points, 140 visible characters each — a citation counts as its label —, 160 for a line), with the report page's own anchor ids (`REPORT_PAGE_ANCHOR_IDS`: `report-head`, `in-brief`, `found`, `claims`, `references`, `downloads`, `report-end`). The validator checks lines (one line, not blank, not too long), points (count, length, inline cites that name a citation the report cites ELSEWHERE — a slide never brings evidence of its own, which would have no number), a `cite` of the slide's own section or claim, an `evidence`/`quote` layout with something to show, and refuses a section or claim id that is one of the page's own anchors (a counts-only scan of the 26 published `report.json` files: 0 of 322 ids collide). `lib/report/slides.ts` `buildReportSlides(view)` is the deck (title, In brief, What the check found, each section and claim, sources), each slide anchored to an id the article page carries (`reportPageAnchorIds`), plus `slideForAnchor`, `slideIndexForPlace`, `slideLint` (umtool) and the URL place (`parseReaderPlace`/`readerPlaceUrl`: `rv`, `#s-` or an anchor). The page view carries the fields through `buildReportPageView` (compose) and umtool's lenient fallback (which also now gives a claim's `sourceQuote` as its id, as the strict view does). REPORT.md regenerated (a Slides section, the two key tables); the demo fixture report gains `slides`, a section's points and cite, a claim's title and points, a claim's quote layout. #### E2, as shipped — the slides view `common/components/report/slides/`: `Slide` (one slide per kind and layout; its left edge the colour of what it says — a claim's verdict, else the accent; "Read this in the article" to its anchor), `ReportSlides` (a 1280 × 720 canvas scaled as one piece to its box, reflowed on a narrow portrait box; ← → PgUp PgDn Home End, Esc, swipe; a region with an `aria-live` "Slide n of N: — " label; a rail), `ViewSwitch` (a `role="group"` of `aria-pressed` toggles), `SlidesHost` (what differs by host: markdown, a citation's marker, the way back, a badge), `useReaderPlace` (`rv` + hash; a switch pushes, a step replaces). Styles are plain CSS, `common/styles/slides.css` (`.rs-*`, unlayered so it wins over a shared component's utilities), coloured from the host's tokens through `--rs-*`. The export site's report header carries `ReportReader`: the other views are laid over the static page, built in the browser from `page.json` (fetched on demand); the article behind is `inert` and still; the place is kept both ways (the part in view opens its slide; Esc / "Read this" land on the anchor, scrolled to and focused); a citation number's click inside a slide opens its card instead of jumping to the reference list behind. `CitationCard`'s `heading` is exported as `citationHeading`. #### E3, as shipped — the overview `ReportOverview`: the pairs as one ordered list; tilted (`rotateX(55deg) rotateZ(-45deg)`, the plane's box sized from the tilt's geometry so it scrolls whole, the scroller alone moved to the pair in force — never the article behind) or flat (two columns; stacked under 780 px of box, a container query so umtool's column stacks too). Flat for reduced motion, a box under 900 px, a keyboard arrival, or the **Flat** toggle (localStorage, try/catch). ↑ ↓ Home End walk, ← → block/card, Enter opens, Esc back to the article; roving tabindex (only the pair in force is tabbable); a thumbnail is the real slide, `inert`, its words as text. The e2e specs were split: `slides.spec.ts`, `overview.spec.ts`. #### E4, as shipped — umtool `ArticleReader` gets the switch over its lenient view; `ArticleShapes` renders the shared slides/overview in the reading column (the text stays mounted, hidden, its note marks with it; the notes rail beside). A slide's markdown is umtool's `Md` (a citation opens the evidence rail), its badge the open notes on its part (`openNotesBySlide`: a section's on its slide, title/subtitle on the title slide, a citation's on each slide showing it). The source tab lists slide lint and the validator's slide problems (`data-slide-lint`), each linked to its slide. umtool's globals.css imports slides.css, sources `common/components/report`, and maps `--rs-*` to its bench palette (`sel` the accent). The sites fixture's draft `polemic-beta` gains a long body (its slide lint's case). `umtool/e2e/article-slides.spec.ts` (4 tests). #### E5, as shipped — exports `slides` (`slides.html`) and `slides-pdf` (`slides.pdf`) join `REPORT_EXPORT_FORMATS` (order html, pdf, md, slides, slides-pdf, zip), so the downloads line, compose, the cited audit's allowlist (`REPORT_EXPORT_FILENAMES`), the revision commit's `exports.json`, the CLI and the ops job take them unchanged. `lib/report/exportSlidesHtml.ts` is the slides as one file (slides.css inline, the light base in the site's accent — `resolveAccent(site.accent) .light` —, every picture a data: URI, a few lines of script to fit and step, numbers to a reference list under the deck, each slide's way back to the site, the footer line on the sources slide). It is a STRING mirror of `Slide.tsx`: reportExports.ts is bundled into the editor's server (its "use server" export action), where `react-dom/server` is not imported; `exportSlides.test.ts` holds the two to the same `rs-` classes per slide and every class to a rule in the stylesheet. `slides.pdf` prints it at 1280 × 720 (`PdfPrinter.print(html, size?)`, `playwrightRuntime`'s `pdf` type gains width/height). A poster only the slides show is inlined or packed when local, never a problem. A report with `slides.hide` exports neither (a log line). CLI usage, ops usage and COMMANDS.md updated. #### E6 — records This section; `export/CHANGELOG.md` `[Unreleased]` (two bullets); PUBLISH.md's exports paragraphs and table row; the ruling line above reworded as on `r19/integration`. umtool has no CHANGELOG.md (none written); the editor is unchanged (no editor changelog). | commit | what | |---|---| | `2734b22b` | E1 schema, validator, slideRules, buildReportSlides + tests, REPORT.md, fixture | | `12691d03` | E2 shared slides components, slides.css, ReportReader, article anchors, slides.spec | | `75f0205d` | E3 ReportOverview, the switch's Overview, overview.spec | | `5d0b4fbc` | E4 umtool switch, ArticleShapes, note counts, source-tab slide lint, article-slides.spec | | `e9ea4cdc` | E5 slides.html / slides.pdf exports, downloads, pack, tests, CLI text | | (this) | E6 record, changelog, PUBLISH.md | **Gates.** `pnpm -r --no-bail --workspace-concurrency=1 exec tsc --noEmit` clean at E1 and at E5 (E2–E4 per package: common, export, umtool `tsc --noEmit` clean). Unit: `common/lib/report/*.test.ts` 111/111 (slides.test 26, exportSlides.test 6, report.test 17 with the new slide/anchor cases); `publish/composeReports.test.ts` + `reportMedia.test.ts` 23/23 (export expectations updated for the two new files and the second skipped-PDF note); `lib/builtExport.test.ts` + `bin/cli-docs.test.ts` + `bin/_cli.test.ts` + exportSlides 62/62; `pnpm --filter export test` 116/116; mcp 292/292 (at E1), `mcp/src/reports.test.ts` 5/5 (at E5); `scripts/archilyzer-ops.test.mjs` 46/46. **Not run — the operator's P1 hold (no e2e, no builds until the all-clear):** the export report suite (`e2e:report`, planned at E3 and E5: `slides.spec.ts` 10, `overview.spec.ts` 3, the downloads/audit changes), umtool's article specs (`article-slides.spec.ts` + the three existing), the umtool capped build, and the full common suite. The views were checked visually instead, with no build: slides.html of the fixture rendered and screenshotted in headless Chromium (every slide kind), and the shared components bundled with esbuild over the fixture's page.json (slides at 1440 × 900 and 390 × 844, the overview tilted, flat under reduced motion, stacked on a phone, light and dark). **Found and left.** (1) A claim's slide shows its gist when it has no points; a claim with neither shows only its title and text. (2) The overview's thumbnails render every slide (fine at the fixture's 11; a report of 100+ claims would render 100+ small slides — not measured). (3) The keyboard-arrival heuristic is the last input before the overview mounted (a keydown anywhere counts). (4) `report.json`'s change summary (revision history) does not name a slide-field edit ("Other edits"). (5) slides.html's light base only (no dark variant), as report.html. #### Fix round, as it went Branch `r19/shapes-fix`, off `acda58f9`. The specs' first run (export report suite 42/45, umtool article specs 18/21) and the operator's ruling of 2026-10-09: **the view switch is sticky** — Article | Slides | Overview stays in reach while the article scrolls, on desktop and phones, on the export site and in umtool. On the export site the switch leaves the report's `<header>` for a strip right after it (`[data-report-views]`, `sticky top-14` under the site header, within the article's box; frosted like the site header, no label); in umtool it sticks to the top of the reading column (`[data-article-views]`). Every anchor lands below the bars (`[&_[id]]:scroll-mt-32` on the export article, `scroll-mt-16` on umtool's blocks and claims). The failures: - `overview.spec.ts:15` and `slides.spec.ts:39` (the switch in the header scrolled away; a click scrolled to the top, so the head was the place) — fixed by the sticky switch. Under it a second cause showed: the part being read was the last place above the top third, so a jump to a section with a short lead (`#bridge`) and a switch opened its first claim. The line is now the top of the reading area — where a jump lands a place (its `scroll-margin-top`) plus 32 px — in both readers. Then `overview.spec.ts` pressed Enter before ArrowRight's focus move (next frame) had landed; it now waits for the card's focus, as after every other key. - `overview.spec.ts:68` (phone) — passes as fixed in `acda58f9`. - `article-slides.spec.ts:30` and `:62` — umtool's `ArticleShapes` never rewrote a slides URL naming a place to the slide's hash; it now does, replacing the entry, as `ReportReader` does. - `article-evidence.spec.ts:51` — not a regression (the walk is unchanged in release 22): the trace shows `j` pressed ~30 ms before the walk hydrated. The spec repeats the press until the walk moves (safe: `j` at the last citation stays there). Specs: `slides.spec.ts` asserts the switch in the viewport and the claim below it after the jump to `#claim-3`; `article-slides.spec.ts` the same after Esc lands on a section; `reports.test.ts` holds the switch after `</header>`, sticky. Export `CHANGELOG.md` one bullet. **Final: export report suite 45/45, umtool article specs 21/21**, `pnpm --filter export test` 118/118, tsc clean (common, export, umtool). Merged into `r19/integration` at `3c445f6e`. Then, on the tip: near the end of a report the last places cannot scroll up to the reading line, so a switch from a jump to the last claim opened the slide before it. In both readers the place the URL names (a jump, Esc from a slide) now wins while it is on screen below the line; a reader scrolling by hand without one keeps the line rule. `slides.spec.ts` covers it (`#claim-5` in a 2400 px window → `#s-12`). The changelog's two bullets fold into E2's (the switch is unreleased; it reads as shipped). ## Rollout Rebuild and deploy each site with reports (the slides view is in the bundle, and `reports prepare` re-exports to add `slides.html`/`slides.pdf`). Authors add `slide` fields at their own pace; an article without them still has derived slides.