ADR-0059: Editor Engine Switches to TipTap (spike-gated; supersedes the Milkdown/Crepe choice)¶
- Status: accepted (spike-gated per §2 — a canon-breaking spike finding reopens it)
- Date: 2026-07-11
- Deciders: Nicolas Burri ("nothing is set in stone… take the path of least resistance to a full featured editor with as many convenience functions as we can provide"), after the license audit (all-MIT lane verified) and the ceilings analysis below.
- Supersedes: ADR-0055 §1 (engine: Milkdown) and ADR-0056 §1/§4 (Crepe chrome) — the mode-switch UX concept (0055 §2) carries over. ADR-0057/ADR-0058 (tree record, projection, v2-only) are UNCHANGED — they are what makes this swap cheap.
- Relates to: ADR-0007 (validation evidence), the projection module (M1) whose methodology transfers.
Context¶
Milkdown was chosen (ADR-0055) when markdown TEXT was the record: its remark-native, markdown-first architecture minimized serialization drift, and TipTap was rejected precisely because markdown is an add-on there. ADR-0057/0058 made the ProseMirror tree the record and markdown a derived projection — Milkdown's load-bearing advantage became irrelevant, while its costs kept arriving as UPSTREAM CEILINGS, verified in source: the slash-menu trigger is hard-coded to line-start (no config surface), table cells are bound to the GFM inline-only schema (block content impossible without orphaning Crepe's table widget), and the chrome splits across two inconsistent toolbars (ours + Crepe's shell). Every requested convenience (mid-line slash, lists in table cells, color, font size) is either fenced upstream or a bespoke plugin against a small ecosystem. Nicolas: no promises made, no data recorded, no infrastructure to migrate — optimize for the full-featured end state.
License audit (2026-07-11, registry-verified): @tiptap/core, @tiptap/pm, starter-kit and all needed official extensions (tables, color, text-style, font-family, task lists, bubble/floating menus, suggestion, link, image, highlight, Mathematics) are MIT; foundations (prosemirror-*, katex, prosemirror-markdown) MIT; future collab stack (yjs, y-prosemirror, extension-collaboration) MIT. TipTap Pro extensions are proprietary but live behind a private registry (cannot enter the build accidentally) and nothing needed is Pro. Contrast: CKEditor 5 and TinyMCE v7+ are GPL/commercial — the trap this project must avoid.
Decision¶
- TipTap becomes the visual-editor engine. The editor module and chrome are rebuilt on @tiptap/core + official extensions (framework-agnostic integration, exactly like the Crepe integration pattern); chrome (fixed toolbar + bubble menu + slash menu via @tiptap/suggestion) is OURS — one design language, no upstream shell, foundation for the expected visual rework.
- Verification-first spike gates the swap (house style): schema coverage of the current
corpus, projection fixed-point property (
project(import(project(t))) == project(t)) against a prosemirror-markdown-based projection, the three complaint features proven (slash anywhere, bullet list inside a table cell, text color), math via extension-mathematics, bundle measurement, and the tree→tree vocabulary mapping from Milkdown schema names. A canon-breaking finding reopens this ADR. - Everything below the editor survives: tree record + blobs + freeze (ADR-0057 §1), client-computed projection + server extraction trust model (§3/§4), v2-only enforcement (ADR-0058), the policy renderer (DOMSerializer over the new schema, same content-policy layer), corpus + property-test methodology, seed pipeline shape, e2e discipline. The Kotlin extraction walker updates its node-name vocabulary. Stored trees change vocabulary — reseed per ADR-0058 §5 (no data migration; pre-production).
- The projection remains markdown (unchanged role: human-readable copy, text diffs, AI/MCP
surface), now serialized via the prosemirror-markdown family from OUR schema; per-feature
degradation notes continue (color → plain text; math →
$...$). - A CI license gate (license-checker, permissive allow-list, production dependencies) lands with the swap so the permissive-only posture is enforced mechanically from now on. T1 spike correction: the lane is "all-PERMISSIVE", not literally all-MIT — the markdown projection path legitimately brings Python-2.0 (argparse), BSD-2 (entities) and ISC (markdown-it-task-lists), all OSI/permissive, zero copyleft. The gate's allow-list reflects that.
Consequences¶
- Milkdown/Crepe and their fidelity-harness variants retire after the swap; the corpus and property tests are re-pointed, not discarded. The projection-notes and verification tool transfer.
- Chrome is assembled from primitives (more initial work than Crepe's shell — accepted trade for no ceilings); Crepe's block-handle/table-widget polish is rebuilt incrementally where wanted.
- The feature track (math, color, block-in-cell tables, font size, mid-line slash) becomes extension assembly instead of upstream fights.
- Slices: T1 spike (facts; go/no-go), T2 engine + chrome swap behind the existing mode-switch UX, T3 feature batch (math/color/tables/slash) + docs/ADR-index closeout.
Slice status¶
- T1 — DONE (spike; verdict GO):
docs/planning/t1-tiptap-spike-report.md. - T2 — DONE: engine + OUR chrome (fixed toolbar + bubble/slash/table menus) behind the mode switch; enhanced serializer shipped; corpus 21/21 strong.
- T3 — DONE (this slice): the QMS feature batch on the T2 editor.
- Math (flagship):
@tiptap/extension-mathematics+ KaTeX authored in the editor and rendered in the reading view, both LAZY — KaTeX is in its OWN chunk (~261 kB raw), never in the initial bundle nor the base editor chunk (~258 kB, KaTeX-free), and its stylesheet is a lazily-injected asset.$…$/$$…$$input rules + slash items + click-to-edit; the shared markdown-it PARSE rules close themarkdown→tree→markdownfixed point (T1 spike condition 2). Corpus gained24-math.md(STRONG). The reader/import schema uses KaTeX-free stand-in math nodes so a math-free PM_DOC never pulls KaTeX. - Text colour + highlight: a FIXED QMS palette (
text-color-palette.ts) +<mark>highlight, applied from the bubble menu. Both project to plain text (the tree keeps the mark). XSS posture: the reading view maps a recognised palette colour to att-color-<name>CLASS and strips the inline style;sanitizeContentHtmladditionally forbidsstyle— no arbitrary CSS ever reaches the DOM. - Block content in table cells:
tableCellisblock+(default) — lists/task-lists/code work in cells; the mid-line slash triggers inside a cell. The projection flatten (block-in-cell →;-joined line) is verified on a tree fixture inrun-projection.mjs(markdown cannot express block-in-cell, so no corpus.mdcan); documented inprojection-notes.md. - QMS conveniences: slash items for inline/block math, mermaid + template-vars code blocks, and
"image from attachments" (visual-mode attachment picker, version-scoped); paste hygiene
(
transformPastedText) enabled in the live editor. - Corpus is 23/23 STRONG (
24-math.md,25-block-cells.mdadded); the KotlinPmDocTextwalker indexesinlineMath/blockMathlatex; goldens + tests regenerated. Docs (this note +projection-notes.md) and i18n (en/de) updated.