From 5b185a86bc3d2c118aebda29813c1d27758561a9 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 11 Jun 2026 08:29:03 -0700 Subject: [PATCH] add spec ./specs/coauthoring-rendered-preview.md (status: graduated) --- specs/coauthoring-rendered-preview.md | 471 ++++++++++++++++++++++++++ 1 file changed, 471 insertions(+) create mode 100644 specs/coauthoring-rendered-preview.md diff --git a/specs/coauthoring-rendered-preview.md b/specs/coauthoring-rendered-preview.md new file mode 100644 index 0000000..c8cbd03 --- /dev/null +++ b/specs/coauthoring-rendered-preview.md @@ -0,0 +1,471 @@ +--- +status: graduated +--- +# Solution Design: Rendered Track-Changes Preview (F7) + +| | | +| --- | --- | +| **Author(s)** | Ben Stull (with Claude) | +| **Reviewers / approvers** | Ben Stull | +| **Status** | `draft` | +| **Version** | v0.1.0 | +| **Source artifacts** | Feature `benstull/vscode-cowriting-plugin#21` (F7) · Deferred task `#22` (intra-diagram mermaid diffing) · Epic `#1` · Builds on F6 `#17` (PR #18) + `#19` (PR #20) · Parent specs: `coauthoring-diff-view.md` (F6, graduated), `coauthoring-inner-loop.md`, `coauthoring-attribution.md`, `coauthoring-propose-accept.md` · Lineage: `ben.stull/rfc-app#48` | + +**Change log** + +| Date | Version | Change | By | +| --- | --- | --- | --- | +| 2026-06-11 | v0.1.0 | Initial draft — brainstorming session 0020 (from the F6-follow-up ideation in session 0019) | Ben Stull + Claude | + +--- + +## 1. Business Context + +### 1.1 Executive Summary + +F6 gave the human coauthor a *diff* of their own changes since a coauthoring +baseline — but as a two-pane native `vscode.diff` of **raw text**, with only the +right pane editable. For prose, that's the wrong altitude: a writer wants to see +the **rendered** document and what changed in it, the way "track changes" / +"suggesting mode" works in Word or Google Docs — not a raw-markdown split-diff. +F7 adds a **rendered track-changes preview**: a webview, opened beside the normal +(fully editable) source editor, that renders the markdown document and marks +**what changed since the F6 baseline** — additions highlighted, deletions struck +through — updating live as the human and Claude edit. It has **full mermaid +support**: fenced `mermaid` diagrams render as diagrams, and a diagram that +changed since the baseline carries a "changed" badge (intra-diagram diffing is a +deferred follow-up, #22). F7 **reuses the F6 baseline** as its "before" and adds +no new persistence: it is a pure, read-only view (INV-20). + +### 1.2 Background + +The inner loop shipped F2–F5 (threads · attribution · propose/accept · +cross-rung), F6 added the baseline + a native diff toggle (#17), and #19 +broadened F6 to any file. In manual testing the operator found the raw two-pane +diff unintuitive for prose ("the diff view isn't intuitive, and only the right +document being editable isn't either") and asked for a single, intuitive surface +that shows the rendered doc and its changes. The ideation (session 0019) compared +a single-pane change gutter, inline track-changes marks, and a **rendered +preview with change highlights**; the operator chose the rendered preview and +locked the architecture (custom webview; `markdown-it` + `mermaid` + `diff` +libraries; block-level diff with atomic code/mermaid blocks). Those decisions are +recorded in #21 and are the inputs to this design. + +### 1.3 Business Actors / Roles + +- **Coauthor (human)** — the writer/engineer (PP-1); F7's sole user. +- **Coauthor (machine)** — Claude via `@cline/sdk`; not a user of F7, but its + landings advance the F6 baseline F7 renders against. + +### 1.4 Problem Statement + +A markdown writer cannot see, in the *rendered* document, what they (and Claude) +changed since the last coauthoring moment. F6's diff is raw text in a split pane; +the only "rendered" view (VS Code's built-in preview) shows the current document +with no notion of a baseline or what changed. + +### 1.5 Pain Points + +- Reviewing prose changes as raw-markdown diff hunks is low-altitude and noisy + (markup syntax, reflowed lines) — you read the source, not the document. +- Mermaid diagrams, tables, headings, lists — the things a render *clarifies* — + are exactly where a raw diff is least legible. +- The two-pane, right-only-editable diff is an unfamiliar editing posture. + +### 1.6 Targeted Business Outcomes + +Open one preview beside the document; see the rendered doc with your changes +since the baseline marked inline (additions highlighted, deletions struck); +keep editing the normal source pane (where you and Claude work) and watch the +preview update live. Self-review of prose becomes a glance at a familiar +"track-changes" rendering. + +### 1.7 Scope (business) + +**In scope:** a command that opens a **rendered track-changes preview** webview +beside the active **markdown** document; rendering the document via `markdown-it` +with **full mermaid** diagram support; computing a **block-level** diff against +the F6 baseline (word-level refinement inside changed prose blocks); marking +added / removed / changed blocks and inline ins/del; a **whole-diagram "changed" +badge** for changed/added mermaid (and code) blocks; live update on edit +(debounced) and on baseline advance/pin; bundling mermaid as a **webview-local** +asset (no CDN); unit + host-E2E coverage of the pure render model; manual smoke. + +**Out of scope (deferred, not forgotten):** **intra-diagram mermaid diffing** +(#22 — v1 uses the whole-diagram badge); preview→source **scroll-sync**; +non-markdown documents (the preview is offered only for markdown); export / +print / copy-as-clean; editing *in* the preview; per-author coloring in the +preview (F3 attribution lives in the editor, not here); replacing F6 (F7 +**coexists** — see §6.7). + +**Non-goals (firm):** a general-purpose markdown previewer to rival the built-in +one (F7 is specifically the *track-changes* render); a WYSIWYG editor; any change +to the sidecar, the cross-rung contract, or `SCHEMA_VERSION`. + +### 1.8 Assumptions · Constraints · Dependencies + +- **Anchor:** Feature #21; deferred mermaid task #22. Builds on F6 (#17/#19) and + the shipped inner loop. +- **Reuses the F6 baseline** (`DiffViewController` / `BaselineStore`, global + storage keyed by URI hash, INV-18/19) as the "before" text; F7 adds **no** + persistence and never writes the baseline (INV-20). +- **New library dependencies (npm, runtime):** `markdown-it`, `mermaid`, + `diff` (jsdiff). **No** VS Code `extensionDependencies`; **no** CDN — mermaid + is bundled as a webview-local asset (CSP-safe), never in the extension-host + bundle (the `@cline/sdk` precedent: heavy deps stay out of the host bundle). +- No LLM anywhere in F7 or its tests; no network; no new credential surface + (INV-8 untouched). +- Nothing persisted touches `.threads/`; INV-14..17 + `SCHEMA_VERSION` untouched. + +### 1.9 Business Use Cases + +- **BUC-1 (prose review)** Mid-session the writer opens the preview and sees the + rendered chapter with their additions highlighted and deletions struck since + Claude last landed — then keeps writing, watching it update. +- **BUC-2 (diagram review)** A doc with a mermaid flowchart: after editing the + diagram, the preview renders the new diagram with a "changed since baseline" + badge, so the writer notices the diagram moved without hunting the fence. + +--- + +## 2. Solution Proposal + +A new **webview panel** (`cowriting.trackChangesPreview`) opened beside the +active markdown editor by a command. On open and on every (debounced) document +change or baseline epoch change, the **extension host** computes a **block-level +diff** between the F6 baseline text and the current buffer, renders it to +annotated HTML with `markdown-it` (prose blocks carry inline ``/`` from +a word-level refinement; code and mermaid fences are **atomic** — whole block +added/removed/changed), and **posts the HTML to the webview**. The webview swaps +in the HTML and runs **mermaid** over any `
` blocks (mermaid
+needs a DOM, so it runs client-side), applying CSS for added/removed/changed
+marks and the diagram "changed" badge. The document, the sidecar, and the F6
+baseline are never mutated (INV-20). The whole render pipeline downstream of "get
+baseline + current text" is a **pure function** (`trackChangesModel.ts`),
+unit-testable with no vscode and no webview (INV-22).
+
+---
+
+## 3. Product Personas
+
+- **PP-1 Inner-loop coauthor** — the human markdown writer/engineer (as F2–F6);
+  the only persona F7 serves.
+
+## 4. Product Use Cases
+
+- **PUC-1 (open / close)** In a markdown editor, run **"Cowriting: Open
+  Track-Changes Preview"** → a preview opens in the column beside the editor,
+  rendering the doc with changes-since-baseline marked. Closing the panel (or
+  re-running the command) dismisses it. The document is untouched.
+- **PUC-2 (live edit)** Type in the source editor → the preview updates
+  (debounced) with the new additions highlighted; nothing in the document,
+  sidecar, or baseline changes.
+- **PUC-3 (epoch follows the machine)** Accept a Claude proposal → the F6
+  baseline advances (INV-18) → the preview re-renders: the accepted text is **no
+  longer** marked as a change; the writer's subsequent edits still are.
+- **PUC-4 (pin)** Run "Cowriting: Pin Diff Baseline to Now" (the F6 command) →
+  the preview empties of marks (baseline = now); edits from here are marked.
+- **PUC-5 (mermaid)** A changed or added `mermaid` block renders as the **new**
+  diagram with a **"changed since baseline"** badge; a removed diagram renders
+  struck/ghosted at the block level. (Intra-diagram diffing → #22.)
+- **PUC-6 (graceful edges)** Non-markdown active editor → the command warns and
+  opens nothing (F7 is markdown-only; F6 covers any file). A malformed
+  mermaid/markdown block renders an inline error chip in place of that block, not
+  a broken preview. Storage/baseline absent → the preview shows the doc with
+  *no* marks (everything is "since opened") and a one-line note.
+
+## 5. UX Layout
+
+A webview in the editor column **beside** the source (the `ViewColumn.Beside`
+convention of the built-in preview). The source editor stays the normal, fully
+editable editor — the writer and Claude work there; F3 attribution tint / F4
+proposals keep working there unchanged. The preview is **read-only rendered
+output**: the document as markdown, with:
+
+- **Added** text/blocks — a soft green background (``), themeable.
+- **Removed** text/blocks — struck-through, muted (``).
+- **Changed atomic block** (code / mermaid) — rendered new, with a small
+  **"changed"** badge at the block's top-right corner.
+- A compact **header bar**: `Track changes since ` (`opened` / `Claude
+  landed` / `pinned`, reusing F6's epoch label) and a `+N −M` summary.
+
+Colors derive from the active VS Code theme via webview CSS variables
+(`--vscode-diffEditor-insertedTextBackground`, `…removedTextBackground`, etc.)
+so it matches light/dark/high-contrast. No bespoke chrome beyond the header.
+
+---
+
+## 6. Technical Design
+
+### 6.1 Invariants
+
+Parent invariants INV-1..INV-19 carry over unchanged. F7 adds:
+
+- **INV-20 (pure read-only preview)** F7 never mutates the document, the
+  sidecar, the attribution state, or the F6 baseline. It only *reads* the
+  baseline text (via the F6 controller) and the current buffer.
+- **INV-21 (sealed webview)** The webview loads only **local** bundled assets
+  under a strict CSP (no remote scripts/styles, no CDN); mermaid is a bundled
+  webview asset. No network, no LLM, no credential surface (INV-8 untouched).
+- **INV-22 (deterministic, testable render model)** Everything downstream of
+  `(baselineText, currentText) → annotated HTML` is a **pure, vscode-free**
+  function (`trackChangesModel.ts`): same inputs → same HTML, unit-testable with
+  no editor and no webview.
+- **INV-23 (atomic non-prose blocks)** Code and mermaid fences are diffed at the
+  **whole-block** level only — never word-refined, never partially rendered. A
+  changed/added one renders fully (with a "changed" badge); a removed one
+  renders struck at block level. (Guards against rendering half a diagram.)
+
+### 6.2 High-level architecture
+
+Two new units plus a webview asset; the F6 controller is reused for the baseline.
+
+- **`trackChangesModel.ts`** (`src/`, **vscode-free, pure** — INV-22) — the
+  engine: `renderTrackChanges(baselineText, currentText): string` (annotated
+  HTML). Internals: split both texts into top-level markdown **blocks**; LCS-diff
+  the block sequences by normalized text → `unchanged | added | removed |
+  changed` block ops; for a `changed` **prose** pair, compute a **word-level**
+  `diff` and emit inline ``/``; **code/mermaid** blocks stay atomic
+  (INV-23); render each block's markdown via `markdown-it` (mermaid fences →
+  `
` for client-side rendering), wrapping each block + in a `
`. Pure: `markdown-it` and + `diff` are libraries; no vscode, no DOM. +- **`trackChangesPreview.ts`** (`src/`, vscode layer) — the webview controller: + registers `cowriting.showTrackChangesPreview`; owns one panel per document + (`Map`); on open / debounced `onDidChangeTextDocument` / + F6 `onDidChange*` epoch change, reads the **baseline** (from + `DiffViewController`) + the current buffer, calls `renderTrackChanges`, and + `postMessage`s the HTML; builds the sealed HTML shell (CSP, nonce, local + asset URIs). Markdown-only guard; disposes panels on close. +- **Webview asset** (`media/preview.js` + `media/preview.css` + bundled + `mermaid`) — receives `{html}` messages, swaps `innerHTML`, runs + `mermaid.run()` over `.mermaid` blocks, applies theme CSS variables. Built as a + **separate esbuild bundle** (`out/media/preview.js`) so mermaid never enters + the extension-host bundle. +- **F6 reuse:** `DiffViewController` exposes the baseline (`getBaseline(uriString)` + already returns `{text,…}`); F7 subscribes to baseline epoch changes. A small + additive event (`onDidChangeBaseline`) may be added to the F6 controller so the + preview refreshes on advance/pin without polling (additive, like + `onDidApplyAgentEdit`). + +```mermaid +flowchart LR + edit["source editor\n(human + Claude edits)"] -- onDidChangeTextDocument (debounced) --> ctl + base["F6 baseline\n(DiffViewController, INV-18/19)"] -- onDidChangeBaseline --> ctl["trackChangesPreview\n(vscode layer)"] + ctl -- "(baselineText, currentText)" --> model["trackChangesModel\n(pure, vscode-free)\nblock diff + markdown-it"] + model -- annotated HTML --> ctl + ctl -- postMessage{html} --> wv["webview\n(sealed CSP, local assets)\nmermaid.run() on .mermaid"] +``` + +### 6.3 Data model & ownership + +**No persisted artifact of any kind** (INV-20). F7 holds only in-memory webview +panels keyed by document URI; the baseline it reads is owned by F6. The only +on-the-wire model is the **annotated block** the engine produces (host→webview), +which is transient HTML, not stored: + +```ts +// internal to trackChangesModel.ts (illustrative — not persisted) +type BlockOp = + | { kind: "unchanged"; html: string } + | { kind: "added"; html: string } + | { kind: "removed"; html: string } // rendered struck at block level + | { kind: "changed"; html: string; // prose: inline /; atomic: badge + atomic: boolean }; // true for code/mermaid (INV-23) +``` + +### 6.4 Interfaces & contracts + +- **`trackChangesModel`** (vscode-free): `renderTrackChanges(baselineText: string, + currentText: string, opts?: { mermaid?: boolean }): string` — returns the inner + HTML for the preview body. Deterministic (INV-22). Also exports + `diffBlocks(baselineText, currentText): BlockOp[]` (the pre-render model) for + fine-grained unit tests. +- **`TrackChangesPreviewController`** (vscode layer): + `show(document)` — open/reveal the panel for a markdown doc (warn + no-op for + non-markdown); `refresh(document)` — recompute + post (debounced internally); + `isOpen(uriString): boolean` and `getLastModel(uriString): BlockOp[] | undefined` + (test seam — the last computed model, so host E2E can assert marks without + reading webview DOM). Disposable. +- **`DiffViewController`** (F6, additive): `readonly onDidChangeBaseline: + vscode.Event<{ uri: string }>` fired on every capture (open/advance/pin) so the + preview refreshes; mirrors the existing internal `onDidChangeEmitter`. +- **Command / keybinding** (`package.json`): `cowriting.showTrackChangesPreview` + ("Cowriting: Open Track-Changes Preview", palette; default keybinding + `ctrl+alt+r`, `when: editorLangId == markdown`). Registered as a warning stub in + the no-folder path is **not** required — like F6 (#19) it is workspace- + independent (the baseline is global storage), so it is registered for real. +- **Build:** `esbuild.mjs` gains a second entry building `media/preview.ts` → + `out/media/preview.js` (IIFE, bundles `mermaid`). `markdown-it` + `diff` bundle + into the host `out/extension.cjs` as today. + +### 6.5 Per–Product-Use-Case design + +- **PUC-1 (open/close):** command → markdown guard → create or reveal a + `WebviewPanel` (`viewType "cowriting.trackChangesPreview"`, `ViewColumn.Beside`, + `retainContextWhenHidden: false`, `localResourceRoots = [out/media]`). Build the + sealed shell (CSP `default-src 'none'; script-src 'nonce-…'; style-src + 'nonce-…' …; img-src data:`), then `refresh`. +- **PUC-2 (live edit):** subscribe to `onDidChangeTextDocument` filtered to the + panel's doc; **debounce** ~150 ms; recompute + post. Pure model keeps this + cheap; O(document) per render at inner-loop scale. +- **PUC-3 / PUC-4 (epoch follows machine / pin):** subscribe to F6 + `onDidChangeBaseline`; on fire for the panel's doc, `refresh`. Accepted text is + in the new baseline, so it renders unmarked. +- **PUC-5 (mermaid):** the engine emits `
SRC
` for a + `mermaid` fence and tags the wrapping block `cw-changed`/`cw-added` when the + fence text differs from / is absent in the baseline; the webview runs + `mermaid.run()` and CSS draws the badge. Atomic (INV-23) — never word-diffed. +- **PUC-6 (graceful edges):** non-markdown → `showWarningMessage`, no panel. + A block that throws in `markdown-it`/`mermaid` → the engine/webview substitutes + an inline error chip for that block; the rest renders. No baseline yet → render + current doc with zero marks + a header note. + +### 6.6 Non-functional requirements & cross-cutting concerns + +Render cost is O(document) on a debounced edit — fine at inner-loop scale; large +docs (>~a few hundred KB) can cap the live debounce or render on idle (a tunable, +not v1-critical). The webview is **sealed** (INV-21): local assets only, strict +CSP with a per-load nonce, no network. mermaid (~MB) lives only in the webview +bundle, never the host bundle (the `@cline/sdk` size discipline). Theme-aware via +CSS variables. No telemetry, no LLM, no credentials. + +### 6.7 Key decisions & alternatives considered + +| Decision | Chosen | Alternatives rejected | +| --- | --- | --- | +| **Render surface** | **Custom webview** we own | **Augment the built-in markdown preview** (markdown-it plugin + CSS) — the built-in preview is hardwired to render the *active document's text*, not a diff artifact; can't cleanly feed it a baseline-vs-current delta. **Decoration-only in the editor** — that's the F6/single-pane path, not a *rendered* view | +| **Libraries vs roll-our-own** | **Depend on `markdown-it` + `mermaid` + `diff`** (npm libs), **roll the track-changes integration** (no off-the-shelf "rendered markdown diff w/ mermaid") | **roll a markdown/mermaid renderer** (months of work, worse); **depend on a 3rd-party VS Code *extension*** (e.g. a mermaid-preview extension) — fragile `extensionDependencies`, install-coupled, quality risk | +| **Where each layer runs** | markdown-it + diff in the **host** (post HTML); **mermaid in the webview** (DOM-bound) | mermaid in the host (no DOM; headless mermaid is heavy/brittle); everything in the webview (ship the diff engine into the sandbox — harder to unit-test, INV-22 lost) | +| **Diff granularity** | **Block-level + word refinement in prose; code/mermaid atomic** (INV-23) | **pure inline source ins/del** — breaks on structural edits, renders half a mermaid fence; **DOM/SVG diffing** — complex, mermaid SVGs diff poorly | +| **Mermaid changes (v1)** | **Whole-diagram "changed" badge**; intra-diagram diff **deferred (#22)** | **intra-diagram node/edge diff now** — large unscoped design (source vs parsed-graph vs SVG; layout reflow) — explicitly deferred by the operator | +| **Relationship to F6** | **Coexist** — F7 is the markdown track-changes view; F6's native diff/gutter still serves **any file incl. code** | **replace F6** — F6 works on non-markdown where a rendered preview is meaningless; removing it loses coverage | +| **Asset delivery** | **Bundle mermaid as a webview-local asset** (esbuild second entry), strict CSP | **CDN/remote mermaid** — violates INV-21 (offline/CSP); **host-bundle mermaid** — bloats `extension.cjs` for code that only runs in the webview | +| **Open keybinding** | `ctrl+alt+r` (`when: editorLangId == markdown`) — "r" for *rendered*; unbound in stock VS Code | `ctrl+k v` (taken by the built-in preview); `cmd+…` (macOS-specific, the F6 §6.7 reasoning) | + +### 6.8 Testing strategy + +- **Unit (vitest, vscode-free):** `trackChangesModel` — `diffBlocks` / + `renderTrackChanges` over fixtures: pure-addition, pure-deletion, prose + modification (asserts inline ``/``), block insert/remove/reorder, + a **code** fence change (atomic — no inline ins/del, whole block `cw-changed`, + INV-23), a **mermaid** fence change (atomic + emits `
` +
+  `cw-changed` badge class), unchanged doc (no marks), malformed block (error
+  chip, rest renders), determinism (same inputs → identical HTML, INV-22).
+- **Host E2E (`@vscode/test-electron`, the F2–F6 pattern, no LLM):** open a
+  markdown fixture → run `cowriting.showTrackChangesPreview` → assert a panel is
+  open (`isOpen` true, viewType matches) and `getLastModel()` shows the expected
+  block ops (e.g. `opened` baseline → no changes; type → an `added`/`changed`
+  block; programmatic propose+accept → baseline advanced, accepted block now
+  `unchanged`; pin → no marks). Webview **DOM/mermaid rendering** is *not* asserted
+  in E2E (sealed sandbox) — covered by manual smoke. Non-markdown doc → command
+  warns, no panel.
+- **Live smoke (manual — `docs/MANUAL-SMOKE-F7.md`):** open a markdown doc with
+  prose + a mermaid diagram + a code block → edit prose (see ins/del) → edit the
+  mermaid (see the diagram re-render with the "changed" badge) → ask Claude to
+  edit a selection + accept (the accepted text drops its marks) → pin (marks
+  clear). Verify light/dark theming and that `git status` shows nothing.
+
+### 6.9 Failure modes, rollback & flags
+
+`markdown-it`/`mermaid` throwing on a block → inline error chip for that block,
+preview still renders (PUC-6). Baseline missing → render with no marks + a note.
+Webview disposed by the user → controller drops the panel; re-run the command to
+reopen. No feature flag: the preview is a pure read-only view (INV-20) — not
+opening it is the off state; rollback is reverting the PR with zero data
+migration (nothing persisted). Large-doc perf → debounce/idle cap (tunable),
+never a correctness issue.
+
+---
+
+## 7. Delivery Plan
+
+### 7.1 Approach / strategy
+
+One planning-and-executing session (F7 = #21), plan written just-in-time from
+this spec — the F2–F6 precedent. Host-E2E tier (a VS Code extension has no
+browser/deploy stage); no LLM in CI. The webview's *visual* rendering (mermaid,
+theming) is verified by the manual smoke, not automated — the automated seam is
+the pure render **model**.
+
+### 7.2 Slicing plan
+
+- **SLICE-1** `trackChangesModel` (vscode-free): block diff + word refinement +
+  `markdown-it` render + atomic code/mermaid (INV-22/23) + vitest suite. Add
+  `markdown-it` + `diff` deps.
+- **SLICE-2** Webview asset + build: `media/preview.ts` (receive HTML, run
+  mermaid, theme CSS), esbuild second entry, bundle `mermaid`; sealed CSP shell.
+- **SLICE-3** `TrackChangesPreviewController` + F6 `onDidChangeBaseline` event +
+  `package.json` command/keybinding + `CowritingApi` handle + test seam
+  (`isOpen`/`getLastModel`); markdown guard; debounced live update.
+- **SLICE-4** Host E2E (open/live/epoch/pin/non-markdown per §6.8) +
+  `docs/MANUAL-SMOKE-F7.md` + README F7 section.
+
+E2E are first-class plan tasks (handbook §9/§4); this app's required tier is host
+E2E (the F2–F6 precedent).
+
+### 7.3 Rollout / launch plan
+
+Non-shippable (no marketplace publish). "Done" = #21 acceptance: a rendered
+track-changes preview opens beside a markdown doc, marks changes-since-baseline
+(prose ins/del + atomic code/mermaid "changed" badge), updates live and on epoch
+change; unit + host E2E green; live smoke performed once. #22 (intra-diagram
+mermaid diff) remains open.
+
+### 7.4 Risks & mitigations
+
+| Risk | Mitigation |
+| --- | --- |
+| Rendering a *diff* of markdown breaks block structure (half a fence, broken list) | Block-level diff with **atomic** code/mermaid (INV-23); render per-block; malformed → error chip |
+| mermaid bundle size / load | Webview-only bundle, never the host bundle; lazy `mermaid.run()` after HTML swap |
+| Webview security | Sealed CSP, nonce, local-only assets (INV-21); no network/LLM |
+| Live re-render cost on large docs | Debounce + optional idle cap; pure O(document) model |
+| Scope creep into intra-diagram mermaid diff | Explicitly deferred to #22; v1 is the whole-diagram badge |
+| Two change-views (F6 + F7) confuse | Distinct commands + titles; F7 markdown-only, F6 any-file; README explains when to use which |
+
+---
+
+## 8. Traceability matrix
+
+| Requirement (#21) | Use case | Design | Slice |
+| --- | --- | --- | --- |
+| Rendered preview beside the editable source | PUC-1 | §6.2, §5 | SLICE-3 |
+| Marks what changed since the F6 baseline | PUC-2/3 | INV-18 reuse, §6.2 | SLICE-1 |
+| Full mermaid support | PUC-5 | §6.2, §6.4 | SLICE-1/2 |
+| Mermaid change = whole-diagram badge (intra-diagram → #22) | PUC-5 | INV-23 | SLICE-1 |
+| Prose additions/deletions inline | PUC-2 | §6.2 word refinement | SLICE-1 |
+| Live update on edit + epoch change | PUC-2/3/4 | §6.5 | SLICE-3 |
+| Pure read-only; no sidecar/contract impact | — | INV-20, §6.3 | all |
+| Sealed webview, no network/LLM, local mermaid | — | INV-21 | SLICE-2 |
+| Unit + host E2E, no LLM in CI | — | §6.8 | SLICE-1/4 |
+
+## 9. Open Questions & Decisions log
+
+- **RESOLVED (session 0019 ideation, recorded in #21):** render surface =
+  custom webview; libraries = `markdown-it` + `mermaid` + `diff` (no
+  extension-deps, no CDN); layer split = host renders/diffs, webview runs
+  mermaid; diff granularity = block-level + prose word-refinement, code/mermaid
+  atomic; mermaid v1 = whole-diagram "changed" badge; F7 **coexists** with F6.
+- **RESOLVED (this spec):** open command `cowriting.showTrackChangesPreview`
+  (`ctrl+alt+r`, markdown-only); test seam = pure model + `getLastModel`
+  (webview DOM not E2E-asserted); additive F6 `onDidChangeBaseline` event.
+- **OPEN → later:** intra-diagram mermaid diffing (**#22**); preview→source
+  scroll-sync; non-markdown rendered views; per-author coloring in the preview;
+  whether F7 should eventually subsume F6's prose path.
+- **Deferred decisions (autonomous calls for operator review):** **F7 coexists
+  with F6** rather than replacing it (markdown-only vs any-file); the `ctrl+alt+r`
+  keybinding; webview DOM/mermaid rendering verified by manual smoke rather than
+  automated E2E (sealed-sandbox constraint). All cheap to revisit.
+
+## 10. Glossary & References
+
+- **Track-changes preview** — a rendered (not raw) view of the markdown document
+  marking what changed since the F6 baseline. **Baseline / epoch / advance / pin**
+  — as F6 (`coauthoring-diff-view.md`). **Atomic block** — a code or mermaid
+  fence diffed whole, never word-refined (INV-23). **Changed badge** — the
+  whole-diagram mark on a changed/added mermaid (or code) block; intra-diagram
+  diffing is #22.
+- Feature #21 (F7) · deferred #22 · Epic #1 · F6 #17 (PR #18) + #19 (PR #20) ·
+  parent specs `coauthoring-diff-view.md`, `coauthoring-inner-loop.md`,
+  `coauthoring-attribution.md`, `coauthoring-propose-accept.md` · lineage
+  `ben.stull/rfc-app#48`.