spec(f7.1): intra-diagram mermaid diffing design + ship note (#22)

Refine the F7 whole-diagram "changed" badge into a node/edge-level diff inside
flowchart and sequence diagrams against the F6 baseline. Parsed-graph diff
re-emitted with mermaid's own styling directives (classDef/class/linkStyle for
flowcharts; rect-tinted runs for sequence), removed elements ghosted in place,
layout reflow accepted, all else (other types, parse failure) falling back to
the v1 badge. New §11 + INV-29..31. Shipped via PR #28 (session 0027).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 16:39:13 -07:00
parent 3875a60d71
commit 1c2844d12b
+137 -3
View File
@@ -449,9 +449,12 @@ mermaid diff) remains open.
- **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.
- **RESOLVED (session 0027, §11):** intra-diagram mermaid diffing (**#22**)
parsed-graph diff, flowchart + sequence this increment, removed elements
ghosted in place, layout reflow accepted; all else falls back to the v1 badge.
- **OPEN → later:** preview→source scroll-sync; non-markdown rendered views;
per-author coloring in the preview; whether F7 should eventually subsume F6's
prose path; further mermaid diagram types for #22 (class/state/ER/…).
- **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
@@ -469,3 +472,134 @@ mermaid diff) remains open.
parent specs `coauthoring-diff-view.md`, `coauthoring-inner-loop.md`,
`coauthoring-attribution.md`, `coauthoring-propose-accept.md` · lineage
`ben.stull/rfc-app#48`.
---
## 11. F7.1 — Intra-diagram mermaid diffing (#22)
> **Status:** shipped (session 0027, 2026-06-11; PR #28). Addendum to F7 (#21):
> the v1 whole-diagram "changed" badge (PUC-5, INV-23) is refined into a
> **node/edge-level diff inside the diagram** for the two most common diagram
> types. Anchor: task `benstull/vscode-cowriting-plugin#22` (`type/task`). Builds
> on the §6 render engine; reuses the F6 baseline as "before". No new persistence
> (INV-20 holds), no webview-security change (INV-21 holds), host stays pure &
> deterministic (INV-22 holds).
### 11.1 What changes
When a **`changed` mermaid block** of a **supported diagram type** is diffed
against the F6 baseline, instead of rendering the new diagram whole with a single
"changed" badge, F7.1 renders the new diagram **with its individual elements
colored by what changed**:
- **flowchart** (`graph` / `flowchart`) — added / removed / changed **nodes and
edges**;
- **sequence** (`sequenceDiagram`) — added / removed / changed **participants and
messages**.
Every other case is **unchanged from v1**: a wholly `added` / `removed` mermaid
block, a `changed` block of any **other** type (class, state, ER, gantt, …), and
**any parse failure** all keep the v1 whole-block badge. This is a pure
refinement of one branch of `renderOp` — nothing else in F7 moves.
### 11.2 The three forks, resolved
| Fork | Decision | Why |
| --- | --- | --- |
| **Diff level** — source-text vs parsed-graph vs SVG | **Parsed-graph.** Parse the diagram source into a typed element model (nodes/edges or participants/messages), diff that, then **re-emit the new diagram source augmented with mermaid's own styling directives** so mermaid renders a colored diagram. | Source-text diff shows a *text* diff in a *rendered* view (defeats F7). SVG diff is layout-brittle and has no stable semantic mapping (§6.7 already rejected it). Parsed-graph keeps the host **pure** (parse + diff + string-augment — fully unit-testable, INV-22) and the **webview unchanged** (it still just runs `mermaid.run()` over `<pre class="mermaid">`). |
| **Visual marking of removed elements** | **Ghost in place.** Removed nodes/edges/messages are **re-injected** into the emitted diagram, styled faded/dashed, so the deletion is visible in its original context. | The point of an intra-diagram diff is to *see* what left, where it was. A caption-only list loses position. Accepted cost: ghosts add to layout reflow. |
| **Diagram types this increment** | **Flowchart + sequence.** All other types fall back to the v1 badge. | The two most common types. Each later type is an additive follow-up on #22 (same dispatch seam). |
**Layout reflow (the fourth open question):** **accepted, not fought.** The
augmented new diagram is laid out fresh by mermaid; we do **not** attempt to pin
node positions to the baseline layout (mermaid exposes no stable layout pinning;
position-matching is a large unscoped effort). Ghosting keeps removed elements
present so the diff still reads; we do not promise the before/after diagrams are
spatially aligned.
### 11.3 Styling-hook asymmetry (a real mermaid constraint)
Flowcharts and sequence diagrams give very different styling surfaces, so the two
emitters differ:
- **Flowchart** — crisp per-element styling. Emit `classDef cwAdded/cwChanged/cwRemoved …`
once, then `class <ids> cwAdded` for nodes and `linkStyle <indices> stroke:…` for
edges (edges are addressed by their **declaration-order index**, which the parser
tracks). Ghost removed nodes/edges are appended to the source with the `cwRemoved`
class / a dashed `linkStyle`.
- **Sequence** — mermaid has **no per-message color directive**; its only
per-message styling hook is the **`rect rgb(r,g,b) … end`** background block. So
the sequence emitter rebuilds the message stream (ghosted-removed messages
re-inserted at their baseline position) and wraps each added / changed / removed
message in a one-message `rect` tinted green / amber / grey. Participants are
re-emitted (removed ones re-declared so they still appear).
Colors are **fixed, theme-neutral** values baked into the emitted source (mermaid
source can't read VS Code CSS variables): added ≈ green (`#2ea043`), changed ≈
amber (`#d29922`), removed ≈ muted grey + dashed (`#808080`), each chosen to read
on both light and dark mermaid themes. A small **legend** (`+ added · ~ changed ·
removed`) is shown beneath a diffed diagram (host-emitted markup, not part of
the mermaid source).
### 11.4 Architecture & seam
A new **pure, vscode-free, DOM-free** host module tree (INV-22), dispatched from
the existing `changed`+atomic+mermaid branch of `renderOp`:
- **`src/mermaidDiff.ts`** — `diffMermaid(beforeSrc, currentSrc): MermaidDiffResult` —
detects the diagram type (`detectDiagramType`); routes to the flowchart or
sequence differ; returns `{ kind: "augmented"; source }` on success or
`{ kind: "fallback" }` for unsupported types / parse failure. Wraps the differ
in try/catch so **any** parser surprise degrades to the v1 badge (never throws —
the §6.9 error-chip philosophy). Owns the shared `CW_COLORS` palette.
- **`src/mermaidFlowchartDiff.ts`** — `parseFlowchart` (nodes by id, edges by
declaration order) + `diffFlowchart` (emits the augmented source).
- **`src/mermaidSequenceDiff.ts`** — `parseSequence` (participants + statements) +
`diffSequence` (LCS over statements via jsdiff `diffArrays`; `rect` tints + ghost
re-insertions).
`renderOp`'s changed-mermaid branch (`src/trackChangesModel.ts`) extracts the
fence body, calls `diffMermaid`; on `augmented` it emits `<pre class="mermaid">AUGMENTED</pre>`
+ the legend and drops the single-badge; on `fallback` it does exactly what it did
before. **The webview (`media/preview.ts`) needs no change** — the styling rides
inside the mermaid source. `media/preview.css` gains the legend swatch styles only.
### 11.5 Invariants (continuing the project sequence; F8 took 2425, F9 2628)
- **INV-29 (supported-type intra-diff)** A `changed` mermaid block whose type is
**flowchart or sequence** is diffed at element granularity (node/edge resp.
participant/message) against the F6 baseline and re-emitted as a single mermaid
diagram whose elements are styled by change kind. It remains **one rendered
diagram**, never a split/word-diff (INV-23's atomicity is *refined* here, not
abandoned — the block is still rendered whole, just self-colored).
- **INV-30 (graceful fallback is total)** Any unsupported diagram type, any
wholly added/removed mermaid block, and **any** parse/emit failure fall back to
the exact v1 whole-block badge. Intra-diagram diffing **never** produces an
error chip or a broken diagram where v1 would have rendered.
- **INV-31 (ghost completeness)** Every element present in the baseline but absent
from the current diagram appears in the rendered diff as a faded/dashed ghost in
its baseline position; no removed element silently vanishes.
- **INV-22/-20/-21 preserved** The differ is pure & deterministic (same inputs →
identical augmented source); F7.1 persists nothing and reads only the existing
F6 baseline; the webview stays sealed and unchanged (styling travels in-source).
### 11.6 Testing (as shipped)
- **Unit (vitest, host, no DOM/LLM):** dispatcher + type detection
(`mermaidDiff.test.ts`); flowchart parser + node/edge diff + emission
(`mermaidFlowchartDiff.test.ts`); sequence parser + participant/message diff +
`rect` emission (`mermaidSequenceDiff.test.ts`); `renderTrackChanges` augments a
changed flowchart/sequence and falls back for unsupported types
(`trackChangesModel.test.ts`). Determinism asserted on each differ.
- **Host E2E:** a changed flowchart in the fixture doc → `getLastModel` shows the
mermaid op is `changed`+atomic and the emitted HTML (via a `renderHtmlFor` test
seam) carries the augmenting directives for the added node. Webview DOM rendering
stays manual-smoke (`docs/MANUAL-SMOKE-F7.1.md`).
- **Counts at ship:** 189 unit + 38/5 host E2E green; typecheck clean.
### 11.7 Out of scope (still deferred)
Class / state / ER / gantt / other diagram types (additive #22 follow-ups, same
seam); pinning layout so before/after align spatially; intra-label word-diffing
inside a single changed node's text; animating the transition. These are
explicitly **not** in this increment.