ADR-0055: WYSIWYG Editing Mode over Markdown (editor step R4)¶
- Status: accepted
- Superseded further by: ADR-0059 — §1 (engine: Milkdown) is replaced by TipTap; the mode-switch concept (§2) carries over.
- Retired in part by: ADR-0057 — §3 (fidelity gate as adoption criterion) and §4 (per-document entry guard incl. ALL amendments below) apply only to legacy MARKDOWN (v1) versions; tree-format versions have no round trip to guard. §1/§2 stand.
- Date: 2026-07-08
- Deciders: Nicolas Burri ("let's do it in two steps" → step 2 now: "go on with the milkdown")
- Realizes the DECIDED checkpoint editor-r4-design.md; relates to
ADR-0045 (rich content: markdown source of record), ADR-0050 (CodeMirror 6 source editor),
ADR-0051 (
doc:links).
Context¶
The markdown source IS the controlled record; the version diff view feeds reviews. Any editor whose serializer normalizes formatting produces PHANTOM DIFFS — a whole document reads as changed when one sentence was edited — degrading review quality and auditor trust. Round-trip fidelity is therefore a compliance property, not a nicety. Nicolas's ask ("render the preview and have the editor work on the content") is WYSIWYG-over-markdown; step 1 (C+ navigable preview, click-to-edit with word precision, scroll sync) has shipped.
Decision¶
- Engine: Milkdown (ProseMirror + remark). Markdown-first by construction — remark AST in and
out — so serialization drift is a bug class the engine's architecture minimizes. TipTap rejected:
markdown is an add-on there, higher drift risk. LQMS custom syntax becomes Milkdown plugins:
doc:links (incl. the[[autocomplete),attachment:images,mermaidfences (rendered diagram, source untouched),template-varsfences (definition-table render, content untouched), GFM tables. - Mode switch, not replacement. WYSIWYG and the CM6 source editor are two views over the same
markdown
contentstring (IDE-style toggle in the shared editor component). Storage, preview, save/freeze, and diff flows see only markdown, unchanged. The chosen mode is a persisted user preference (localStorage, like the preview toggle). - The fidelity gate precedes the engine (this arc's slice 1, R4a). A repeatable round-trip suite proves parse→serialize is BYTE-IDENTICAL across:
- a committed synthetic corpus covering every construct we render (headings, lists incl.
nesting, tables, blockquotes, fences incl. mermaid/template-vars,
doc:/attachment:refs, hard wraps, trailing-newline variants, mixed emphasis), and - an optional local corpus — the real customer SOPs under git-ignored
deploy/seed-local.d/(licensed/company content never reaches the repo; the suite picks the directory up when present and skips silently in CI). Verdict rule (agreed in the checkpoint): byte-identity across the full corpus → WYSIWYG may become the DEFAULT editing mode; otherwise WYSIWYG is offered for NEW documents only and revisions default to source, keeping review diffs clean. - Per-document entry guard, always. The corpus can only sample reality. On every switch INTO
WYSIWYG the editor round-trips the CURRENT document first; if parse→serialize does not reproduce
the exact bytes, the switch is refused with a visible explanation and the document stays in
source mode. A document that enters WYSIWYG mode is thus one whose serialization is proven
lossless — no normalize-on-save, ever, and no phantom diffs even for documents outside the
corpus's coverage.
Amended 2026-07-09 (live feedback): a missing FINAL newline is the one tolerated divergence
(fidelity report D8) — hand-typed source never ends in
\n, so strict equality refused nearly everything typed by hand. The guard passes whenserialized == content + "\n"and canonicalizes the buffer at mode ENTRY (an explicit single-byte act, invisible in the diff view, which normalizes trailing newlines) — still never at save. Amended 2026-07-09 (canonicalize-on-import decided, after three refusal-friction reports): a byte-divergence refusal now (a) NAMES the first divergence (line + what would change) and (b) offers "Convert for visual editing" — one explicit click applies the serializer's canonical form as a visible, unsaved authoring edit (the reviewer sees exactly that diff), after which the document enters visual mode and stays eligible. Strictness is unchanged; the escape hatch is explicit, user-initiated and auditable. Refusals for renderer-unsupported constructs (footnotes) offer NO conversion — canonical bytes would not fix unrendered content. - Slices: R4a fidelity corpus + gate (verdict report decides the default), R4b engine integration + entry guard, R4c mode UI + custom-syntax plugins. Each lands CI-green behind the existing e2e discipline; the gate suite runs in CI over the synthetic corpus.
Consequences¶
- New webui dependency surface:
@milkdown/*(lazy-loaded chunk, CM6/mermaid pattern — the initial bundle stays clean). - The gate suite becomes standing validation evidence for the editor (ADR-0007 spirit): any future Milkdown upgrade must keep the corpus byte-identical before it ships.
- If the verdict is "not byte-identical", the default-mode question returns to Nicolas with the failing constructs enumerated — the architecture (mode switch + entry guard) is unchanged either way.
- Deferred: collaborative editing on top of ProseMirror (CRDT arc is parked separately); WYSIWYG table editing niceties beyond GFM.