@liminis/editor 0.1.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/LICENSE +34 -0
  2. package/README.md +376 -0
  3. package/dist/annotations/anchor-align.d.ts +50 -0
  4. package/dist/annotations/anchor-align.js +74 -0
  5. package/dist/annotations/anchor-model.d.ts +81 -0
  6. package/dist/annotations/anchor-model.js +80 -0
  7. package/dist/annotations/anchor-resolver.d.ts +92 -0
  8. package/dist/annotations/anchor-resolver.js +231 -0
  9. package/dist/annotations/block-structure.d.ts +65 -0
  10. package/dist/annotations/block-structure.js +113 -0
  11. package/dist/annotations/types.d.ts +159 -0
  12. package/dist/annotations/types.js +50 -0
  13. package/dist/annotations.d.ts +25 -0
  14. package/dist/annotations.js +27 -0
  15. package/dist/app/App.d.ts +64 -0
  16. package/dist/app/App.js +180 -0
  17. package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
  18. package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
  19. package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
  20. package/dist/app/editor/AnchorScrollPlugin.js +134 -0
  21. package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
  22. package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
  23. package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
  24. package/dist/app/editor/AnnotationPlugin.js +135 -0
  25. package/dist/app/editor/AnnotationSurface.d.ts +34 -0
  26. package/dist/app/editor/AnnotationSurface.js +115 -0
  27. package/dist/app/editor/AssetContext.d.ts +20 -0
  28. package/dist/app/editor/AssetContext.js +81 -0
  29. package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
  30. package/dist/app/editor/BlockClickPlugin.js +126 -0
  31. package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
  32. package/dist/app/editor/CodeBlockPlugin.js +219 -0
  33. package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
  34. package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
  35. package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
  36. package/dist/app/editor/DragHandlePlugin.js +300 -0
  37. package/dist/app/editor/Editor.d.ts +84 -0
  38. package/dist/app/editor/Editor.js +539 -0
  39. package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
  40. package/dist/app/editor/FrontmatterPlugin.js +301 -0
  41. package/dist/app/editor/ImageModal.d.ts +13 -0
  42. package/dist/app/editor/ImageModal.js +267 -0
  43. package/dist/app/editor/ImagePlugin.d.ts +3 -0
  44. package/dist/app/editor/ImagePlugin.js +187 -0
  45. package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
  46. package/dist/app/editor/LinkClickPlugin.js +90 -0
  47. package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
  48. package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
  49. package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
  50. package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
  51. package/dist/app/editor/SearchPlugin.d.ts +1 -0
  52. package/dist/app/editor/SearchPlugin.js +379 -0
  53. package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
  54. package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
  55. package/dist/app/editor/SlashMenu.d.ts +11 -0
  56. package/dist/app/editor/SlashMenu.js +494 -0
  57. package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
  58. package/dist/app/editor/SlashMenuPlugin.js +168 -0
  59. package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
  60. package/dist/app/editor/TableActionsPlugin.js +398 -0
  61. package/dist/app/editor/TogglePlugin.d.ts +1 -0
  62. package/dist/app/editor/TogglePlugin.js +159 -0
  63. package/dist/app/editor/Toolbar.d.ts +10 -0
  64. package/dist/app/editor/Toolbar.js +263 -0
  65. package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
  66. package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
  67. package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
  68. package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
  69. package/dist/app/editor/annotation-marks.d.ts +177 -0
  70. package/dist/app/editor/annotation-marks.js +750 -0
  71. package/dist/app/editor/annotationCommands.d.ts +13 -0
  72. package/dist/app/editor/annotationCommands.js +11 -0
  73. package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
  74. package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
  75. package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
  76. package/dist/app/editor/c4/edge-clipping.js +139 -0
  77. package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
  78. package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
  79. package/dist/app/editor/c4/layout.d.ts +25 -0
  80. package/dist/app/editor/c4/layout.js +839 -0
  81. package/dist/app/editor/c4/parser.d.ts +19 -0
  82. package/dist/app/editor/c4/parser.js +410 -0
  83. package/dist/app/editor/c4/render-to-string.d.ts +24 -0
  84. package/dist/app/editor/c4/render-to-string.js +34 -0
  85. package/dist/app/editor/c4/renderer.d.ts +64 -0
  86. package/dist/app/editor/c4/renderer.js +569 -0
  87. package/dist/app/editor/c4/types.d.ts +203 -0
  88. package/dist/app/editor/c4/types.js +43 -0
  89. package/dist/app/editor/correction-yaml.d.ts +10 -0
  90. package/dist/app/editor/correction-yaml.js +40 -0
  91. package/dist/app/editor/editorNodes.d.ts +9 -0
  92. package/dist/app/editor/editorNodes.js +36 -0
  93. package/dist/app/editor/index.d.ts +1 -0
  94. package/dist/app/editor/index.js +1 -0
  95. package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
  96. package/dist/app/editor/mark-overlap-resolver.js +45 -0
  97. package/dist/app/editor/nodes/C4Component.d.ts +14 -0
  98. package/dist/app/editor/nodes/C4Component.js +284 -0
  99. package/dist/app/editor/nodes/C4Node.d.ts +32 -0
  100. package/dist/app/editor/nodes/C4Node.js +112 -0
  101. package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
  102. package/dist/app/editor/nodes/CalloutNode.js +94 -0
  103. package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
  104. package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
  105. package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
  106. package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
  107. package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
  108. package/dist/app/editor/nodes/CustomListNode.js +54 -0
  109. package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
  110. package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
  111. package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
  112. package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
  113. package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
  114. package/dist/app/editor/nodes/EquationComponent.js +130 -0
  115. package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
  116. package/dist/app/editor/nodes/EquationNode.js +179 -0
  117. package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
  118. package/dist/app/editor/nodes/FootnoteNode.js +151 -0
  119. package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
  120. package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
  121. package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
  122. package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
  123. package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
  124. package/dist/app/editor/nodes/HtmlNode.js +121 -0
  125. package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
  126. package/dist/app/editor/nodes/ImageComponent.js +228 -0
  127. package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
  128. package/dist/app/editor/nodes/ImageNode.js +183 -0
  129. package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
  130. package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
  131. package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
  132. package/dist/app/editor/nodes/MermaidComponent.js +216 -0
  133. package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
  134. package/dist/app/editor/nodes/MermaidNode.js +96 -0
  135. package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
  136. package/dist/app/editor/nodes/ToggleNode.js +237 -0
  137. package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
  138. package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
  139. package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
  140. package/dist/app/editor/nodes/diagram-utils.js +29 -0
  141. package/dist/app/editor/nodes/index.d.ts +30 -0
  142. package/dist/app/editor/nodes/index.js +17 -0
  143. package/dist/app/editor/utils.d.ts +2 -0
  144. package/dist/app/editor/utils.js +17 -0
  145. package/dist/app/mapper/index.d.ts +3 -0
  146. package/dist/app/mapper/index.js +2 -0
  147. package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
  148. package/dist/app/mapper/lexicalToMdast.js +1937 -0
  149. package/dist/app/mapper/mdastToLexical.d.ts +31 -0
  150. package/dist/app/mapper/mdastToLexical.js +1075 -0
  151. package/dist/contract.d.ts +8 -0
  152. package/dist/contract.js +8 -0
  153. package/dist/headless.d.ts +40 -0
  154. package/dist/headless.js +45 -0
  155. package/dist/host/context.d.ts +20 -0
  156. package/dist/host/context.js +24 -0
  157. package/dist/host/defaults.d.ts +21 -0
  158. package/dist/host/defaults.js +47 -0
  159. package/dist/host/messages.d.ts +26 -0
  160. package/dist/host/messages.js +47 -0
  161. package/dist/host/types.d.ts +75 -0
  162. package/dist/host/types.js +13 -0
  163. package/dist/index.d.ts +35 -0
  164. package/dist/index.js +51 -0
  165. package/dist/markdown/annotate-sentinels.d.ts +42 -0
  166. package/dist/markdown/annotate-sentinels.js +86 -0
  167. package/dist/markdown/parse.d.ts +56 -0
  168. package/dist/markdown/parse.js +516 -0
  169. package/dist/markdown/stringify.d.ts +7 -0
  170. package/dist/markdown/stringify.js +439 -0
  171. package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
  172. package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
  173. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
  174. package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
  175. package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
  176. package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
  177. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
  178. package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
  179. package/dist/markdown.d.ts +28 -0
  180. package/dist/markdown.js +44 -0
  181. package/dist/mathjax-config.d.ts +89 -0
  182. package/dist/mathjax-config.js +133 -0
  183. package/dist/nodes.d.ts +34 -0
  184. package/dist/nodes.js +34 -0
  185. package/dist/stores/correctionStore.d.ts +15 -0
  186. package/dist/stores/correctionStore.js +12 -0
  187. package/dist/stores/editorStore.d.ts +16 -0
  188. package/dist/stores/editorStore.js +27 -0
  189. package/dist/styles.css +2477 -0
  190. package/dist/types.d.ts +296 -0
  191. package/dist/types.js +91 -0
  192. package/dist/utils/file-types.d.ts +10 -0
  193. package/dist/utils/file-types.js +46 -0
  194. package/docs/annotations.md +134 -0
  195. package/docs/decisions/README.md +72 -0
  196. package/docs/decisions/adr-002.md +27 -0
  197. package/docs/decisions/adr-007.md +27 -0
  198. package/docs/decisions/adr-010.md +26 -0
  199. package/docs/decisions/adr-024.md +124 -0
  200. package/docs/decisions/adr-024b.md +0 -0
  201. package/docs/decisions/adr-025.md +103 -0
  202. package/docs/decisions/adr-027.md +66 -0
  203. package/docs/decisions/adr-057.md +67 -0
  204. package/docs/decisions/adr-070.md +131 -0
  205. package/docs/decisions/adr-075.md +410 -0
  206. package/docs/decisions/adr-076.md +183 -0
  207. package/docs/decisions/adr-077.md +410 -0
  208. package/docs/decisions/adr-078.md +243 -0
  209. package/docs/decisions/adr-079.md +167 -0
  210. package/docs/decisions/adr-080.md +325 -0
  211. package/docs/decisions/adr-081.md +108 -0
  212. package/docs/decisions/adr-082.md +123 -0
  213. package/docs/decisions/adr-083.md +204 -0
  214. package/docs/decisions/adr-084.md +179 -0
  215. package/docs/editor-api.md +106 -0
  216. package/docs/markdown-pipeline.md +282 -0
  217. package/docs/provenance.md +106 -0
  218. package/docs/zusammen-editor-capability-map.md +232 -0
  219. package/package.json +175 -0
@@ -0,0 +1,410 @@
1
+ # ADR-075: The `@liminis/editor` Package Boundary Is Drawn at Persistence
2
+
3
+ **Date:** 2026-08-03
4
+ **Status:** Accepted
5
+ **Issue:** #938
6
+
7
+ ## Context
8
+
9
+ The Lexical markdown WYSIWYG editor — the controlled `<Editor>` component, ~18
10
+ custom node types, ~25 plugins, the C4 diagram subsystem, the mdast⇄Lexical
11
+ mappers, and a 100-fixture round-trip corpus — lived embedded in
12
+ `liminis-app/src/editor/**`.
13
+
14
+ A near-identical *copy* of that tree was grafted into the Zusammen project and
15
+ the two have been diverging: Zusammen accreted a review-commenting feature and
16
+ markdown idempotency fixes Liminis lacks, while Liminis accreted an AI
17
+ **correction** feature and a post-#896 fixture corpus Zusammen lacks. Every
18
+ future fix to the shared core had to be hand-ported in both directions or
19
+ silently lost.
20
+
21
+ Making the editor one package with a curated public API is what stops that. But
22
+ a move that leaves the host reach-ins in place is a directory rename, not an
23
+ extraction — and the reach-ins were substantial:
24
+
25
+ - `messaging-electron` imported directly by `App.tsx`, `ImagePlugin`,
26
+ `LinkClickPlugin` and `MermaidComponent`.
27
+ - `window.api.*` reached into by `AnchorScrollPlugin`,
28
+ `WikiLinkExistencePlugin`, and `CorrectionPanelPlugin` (7 sites).
29
+ - `shared/logger`, which branches on `process.type` and imports `node:fs`.
30
+ - `toast` from `sonner`.
31
+
32
+ The question this ADR settles is **where the seam goes**, because everything
33
+ else — which files move, what the public entry exposes, what gets injected —
34
+ follows from it.
35
+
36
+ ## Decision
37
+
38
+ **The package/app seam is drawn at persistence.**
39
+
40
+ Anything about text ranges, marks, rendering, and in-document UX is
41
+ **package-side**. Storage, lifecycle, identity, and higher-level panels are
42
+ **app-side**. The editor package reaches the host only through injected
43
+ services; it never reaches for a global.
44
+
45
+ Four consequences, each load-bearing:
46
+
47
+ ### 1. A closed set of declared export subpaths — the `exports` map *is* the public surface
48
+
49
+ `packages/editor/package.json` declared exactly five entries when this ADR was
50
+ written. ADR-077 added a sixth, `./annotations`, and ADR-079 added a seventh,
51
+ `./nodes`, both as *considered* exports under the rule stated below; the table
52
+ is kept current here so this record stays the single inventory of the public
53
+ surface:
54
+
55
+ | Subpath | Purpose |
56
+ |---|---|
57
+ | `.` | The React surface: `<App>`, `<Editor>`, host provider/hook, markdown pipeline, message contract, stores, `findSvgElement` |
58
+ | `./markdown` | Markdown parsing alone: `parseMarkdown`, the mdast type guards, `getFileType`. No DOM, no Lexical, no MathJax |
59
+ | `./headless` | DOM-free: C4 parse/layout/`renderC4DiagramToSVG`, the MathJax **lite** document factory, `getFileType` |
60
+ | `./annotations` | DOM-, React- and Lexical-free annotation mechanics: anchor model, resolver, block structure, span alignment, annotation/kind types (added by ADR-077) |
61
+ | `./contract` | The zod host-message contract alone, for `import type` at the preload boundary |
62
+ | `./nodes` | `editorNodes` (the exact Lexical node array `<Editor>` configures itself with) plus `importMarkdownToLexical`/`exportLexicalToMdast`, for building a headless Lexical editor (added by ADR-079) |
63
+ | `./styles.css` | The stylesheet |
64
+
65
+ FR-002's "single curated public entry" is read as *nothing may be imported that
66
+ is not declared in `exports`*. Several subpaths rather than one is forced, not
67
+ stylistic:
68
+
69
+ - The Electron **main** process declares `lib: ["ES2024"]` and no DOM
70
+ (`tsconfig.main.json`). It consumes `renderC4DiagramToSVG` (for the
71
+ `app_render_c4_diagram` MCP tool and the remote-session route) and the MathJax
72
+ lite adaptor. A single React barrel would make `tsc` follow the whole graph and
73
+ typecheck Lexical against a DOM-free lib set. **If `./headless` ever fails to
74
+ compile under `tsconfig.main.json`, prune that entry's import graph — never
75
+ widen main's `lib`.**
76
+
77
+ **The DOM-free contract is a convention, not a compiler-enforced invariant, and
78
+ reviewers should not assume otherwise.** Main's declared `lib` does not actually
79
+ make `Document`/`HTMLElement` unresolvable: something already in `src/main`'s own
80
+ dependency graph pulls the DOM lib in, so those globals resolve inside the main
81
+ program today. (Verified two ways: a `(d: Document) => d.body` probe compiles
82
+ under `tsconfig.main.json`, while the same probe against `types: ["node"]` alone
83
+ fails with `TS2304`. This predates the extraction — main's include list and
84
+ dependencies are unchanged by it.) The practical consequence is that a DOM-typed
85
+ export added to `./headless` would compile silently instead of failing the build.
86
+ For that reason `createBrowserAdaptorDocument` and `BrowserMathJaxInstance` —
87
+ typed over `Document` and `MathDocument<HTMLElement>` — are deliberately **not**
88
+ on this entry; their only consumer is `EquationComponent.tsx` inside the package,
89
+ which imports them by relative path. Restoring real enforcement (a separate
90
+ DOM-free tsconfig project over `./headless`) is still worth doing. It was
91
+ originally deferred to #940; #940 gave the package a build but deliberately
92
+ scoped itself to delivery and did not take this on. It needs a follow-up.
93
+
94
+ One **known exception** is worth naming rather than leaving to be discovered:
95
+ `./headless` re-exports `createLiteAdaptorDocument` from `mathjax-config.ts`,
96
+ and that module statically imports `browserAdaptor` alongside `liteAdaptor` at
97
+ top level. So loading this entry does evaluate a DOM-adaptor module — the
98
+ convention is already bent, not merely bendable. It is *not* a regression from
99
+ the extraction: before it, `main/remote-session/routes.ts` imported
100
+ `shared/mathjax-config` directly and pulled in the identical graph, so main's
101
+ runtime imports are unchanged. Splitting the module into lite and browser
102
+ halves is the real fix, alongside the enforcement above — doing it here would
103
+ change main's module graph under a parity constraint. Originally deferred to
104
+ #940, which scoped itself to delivery and did not take it on; it needs the same
105
+ follow-up.
106
+ - `preload/index.ts` and `types/window-api.d.ts` need only the message shapes.
107
+ `./contract` is often shorthanded as "the zod-free entry"; it is not. It
108
+ re-exports `./types`, which imports zod and uses it for real runtime validation
109
+ (`validateHostToUIMessage`). What actually keeps zod out of the preload bundle
110
+ is that both consumers use `import type`, which erases at build time — the
111
+ narrow subpath only limits how much would come along if one of them ever
112
+ didn't. `editor-package-wiring.test.ts` pins that `import type` discipline,
113
+ since nothing in the exports map can.
114
+ - `styles.css` is not code.
115
+ - `./markdown` exists because the root barrel exports `<App>`/`<Editor>`, so
116
+ importing *anything* from it drags Lexical, MathJax, Mermaid and the C4
117
+ subsystem into the importing window. `MarkdownSnippet` needs only
118
+ `parseMarkdown` and `getFileType`, and is reachable from the `graph` and
119
+ `workflow` renderer entry points, which host no editor at all. Measured on the
120
+ emitted bundles: barrel ≈ 4.76 MB / 4.35 MB of eagerly `modulepreload`ed
121
+ script for those two windows, versus 2.49 MB / 2.08 MB through `./markdown`.
122
+ `./headless` is *not* an adequate home for these helpers — it also re-exports
123
+ `mathjax-config`, whose ~90 bare `import '@mathjax/src/…Configuration.js'`
124
+ lines are genuinely side-effectful and unshakeable by any consumer, leaving a
125
+ ~1.94 MB `mathjax-config` chunk in place. Enforced by
126
+ `editor-package-wiring.test.ts`, which fails if any app file's editor imports
127
+ are wholly covered by `./markdown` yet come from the barrel.
128
+
129
+ ### 2. Host services are injected via React context, with a safe default for every member
130
+
131
+ `EditorHostServices` (in `src/host/types.ts`) carries an optional `bridge`,
132
+ `logger`, `resolveWikiLinks`, `onScrollToAnchor`, `notifyError`, and
133
+ `corrections`. It is supplied through `<EditorHostProvider services={…}>` and
134
+ read with `useEditorHost()`.
135
+
136
+ Context rather than prop drilling: there are five separate editor mount sites in
137
+ `liminis-app` and ~20 deeply nested plugins; threading a services object through
138
+ `App` → `Editor` → every plugin would be a large, error-prone diff. A
139
+ module-level `configureEditorHost()` singleton was rejected as untestable in
140
+ parallel and hostile to a second consumer.
141
+
142
+ Every member has a safe default (`src/host/defaults.ts`), so `<Editor>` renders
143
+ in a host that supplies nothing. Two default *shapes* are used deliberately:
144
+
145
+ - `bridge`, `logger` and `notifyError` fall back to a concrete no-op/console
146
+ implementation.
147
+ - `resolveWikiLinks`, `onScrollToAnchor` and `corrections` stay **undefined**
148
+ when absent, because their consumers treat "service missing" as "feature
149
+ unavailable, leave the document alone". That is exactly what the pre-extraction
150
+ `if (!window.api?…) return` guards did; substituting a stub that returns `{}`
151
+ would, for instance, mark every wiki-link broken.
152
+
153
+ ### 3. The host bridge has two methods; the message helpers are package-side
154
+
155
+ `EditorHostBridge` is only `postMessage` + `addMessageHandler`. The helpers
156
+ (`requestInit`, `requestSettings`, `applyTextEdits`, `writeAsset`, `openLink`)
157
+ were moved *into* the package and rebuilt on `postMessage`.
158
+
159
+ This makes parity structural rather than hoped-for: every host adapter emits
160
+ byte-identical wire payloads because there is only one place that constructs
161
+ them. `liminis-app/src/renderer/editor-host.ts` is now a thin adapter over
162
+ `window.api.editor`, and it is the successor to the deleted
163
+ `src/editor/messaging-electron.ts`.
164
+
165
+ **One deliberate non-parity change came with that move.** The adapter's
166
+ handler-leak guard was carried over verbatim at first, and review found it
167
+ unsound in two ways that only bite at scale, so it was repaired rather than
168
+ preserved:
169
+
170
+ - It counted **unique function identities** in a `Set`, but every call opens its
171
+ own `onMessage` subscription. Two registrations of one reference were two
172
+ subscriptions counted as one, and the first disposer released both.
173
+ - Past the limit it **threw**. The only caller is a React effect, so the throw
174
+ propagated into render and unmounted the editor. This was reachable in ordinary
175
+ use, not just in a leak: each mounted `<App>` registers two handlers (`App.tsx`
176
+ and `ImagePlugin.tsx`), and `canvasStore.expandCard(…, multiExpand)` expands an
177
+ unbounded number of note cards simultaneously — so six expanded cards exceeded
178
+ the budget of ten and took the tree down.
179
+
180
+ A first repair replaced the throw with a log and a refused registration. Review
181
+ caught that this only traded a loud failure for a silent one: refusing means
182
+ returning a no-op disposer *without opening the subscription*, so that editor goes
183
+ deaf to host messages — including `ASSET_WRITTEN`, which makes image paste fail
184
+ with no error anywhere. The same six-expanded-cards path reaches it.
185
+
186
+ **The budget is therefore a diagnostic, not admission control.** The guard counts
187
+ live subscriptions, releases idempotently, holds no slot when there is no preload
188
+ API, and — past the budget — logs and *still registers*. A genuine leak is still
189
+ loud in the log; it just no longer degrades the editor to say so.
190
+
191
+ This is a behaviour change against FR-005's parity constraint, taken knowingly: the
192
+ prior behaviour was a crash, the defect is pre-existing on `main` rather than
193
+ introduced here, and the guard is diagnostic rather than a product behaviour.
194
+ Covered by `liminis-app/src/renderer/__tests__/editor-host.test.ts`, whose
195
+ delivery-past-the-budget and idempotent-release cases both fail against the refusing
196
+ implementation.
197
+
198
+ ### 4. Reconciliation with existing ADRs
199
+
200
+ **ADR-057 (Knowledge Corrections — Renderer→Filesystem→MCP Pipeline).** The
201
+ correction *panel* is in-editor UI and therefore package-side; the pipeline
202
+ ADR-057 describes is persistence and therefore app-side. ADR-057's flow is
203
+ unchanged in behaviour but now **crosses the package boundary** via
204
+ `CorrectionHostServices`:
205
+
206
+ ```ts
207
+ interface CorrectionHostServices {
208
+ readCorrections(): Promise<string | null> // raw YAML text, or null
209
+ writeCorrections(yaml: string): Promise<void> // host owns mkdir + atomic write
210
+ suggestEntities(query: string, numResults: number): Promise<string[]>
211
+ suggestPassages(query: string, numResults: number, minScore: number): Promise<string[]>
212
+ applyCorrections(): Promise<boolean>
213
+ }
214
+ ```
215
+
216
+ `.liminis/knowledge-corrections.yaml`, the atomic write, and both
217
+ `knowledge-reader`/`knowledge-writer` MCP calls all still live in
218
+ `liminis-app` — read ADR-057's "renderer" as "`liminis-app`'s renderer host
219
+ adapter", not "the editor component". Two orderings ADR-057 and ADR-042 depend
220
+ on are preserved verbatim inside the package and must stay that way:
221
+ `suggestEntities` is awaited *before* `suggestPassages` (the MCP stdio invoker
222
+ rejects concurrent calls to one server), and the confirm path re-reads, merges,
223
+ then writes atomically.
224
+
225
+ **ADR-025 (Zustand for renderer state).** Two zustand stores now live inside a
226
+ package rather than under `liminis-app/src/renderer/stores/`. ADR-025's
227
+ substance is unchanged — state is still zustand, not window events — but the
228
+ store's *location* may now be a package. The ADR-024/025/032 guidance in
229
+ `.claude/rules/electron-renderer.md` is scoped to `liminis-app` renderer paths
230
+ the editor has left, so it needs to be carried across to `packages/editor/` as a
231
+ path-scoped rule per ADR-034 — see "Outstanding" below.
232
+
233
+ **ADR-002, ADR-010, ADR-070, ADR-007, ADR-027** are unaffected: the editor is
234
+ still Lexical, the mapper design is unchanged, the C4/node types are unchanged,
235
+ the contextBridge remains the app-side adapter, and the package stays inside the
236
+ repo's vitest story with its own config.
237
+
238
+ ## Deviations from the spec's letter, and why
239
+
240
+ Three of the spec's file-placement instructions did not survive contact with the
241
+ code. Each deviation follows the persistence seam this ADR establishes, or the
242
+ curated-surface principle, which are the spec's own stated principles.
243
+
244
+ **`correctionStore` moves package-side** (FR-006 says it stays in
245
+ `liminis-app`). It holds only ephemeral panel UI state — `isOpen`, `position`,
246
+ `selectedText` — persists nothing, and has zero consumers outside the two editor
247
+ plugins. Injecting it would be prop-drilling purely to satisfy a file location.
248
+ FR-006's *substance* — that correction persistence and lifecycle stay app-side —
249
+ is honoured in full by `CorrectionHostServices`.
250
+
251
+ Because that justification rests entirely on "no consumer outside the two editor
252
+ plugins", `useCorrectionStore` is deliberately **not** re-exported from `.`.
253
+ Publishing it would invite the app-side consumer that invalidates the reasoning,
254
+ with nothing at the package boundary to catch it. `useEditorStore` *is* public —
255
+ `EditorColumn` drives the frontmatter tray from the app toolbar — so the two
256
+ stores are not symmetric, and that asymmetry is the point.
257
+
258
+ **`mathjax-config.ts` moves package-side but is exposed on `./headless`**, and
259
+ `fileTypes.ts` **splits**. `getFileType` (used by `mdastToLexical` and
260
+ `MarkdownShortcutsPlugin`) moved to `packages/editor/src/utils/file-types.ts`;
261
+ `toWorkspaceRelativePath` has no editor consumer at all and stayed in
262
+ `liminis-app/src/renderer/utils/fileTypes.ts`. `mathjax-config`'s existing test
263
+ moved with it — the spec's manifest omitted it, and dropping it would have been
264
+ silent coverage loss.
265
+
266
+ **`messaging.ts` was deleted rather than moved.** The spec listed the vestigial
267
+ VS Code webview adapter among the files that move, and the Plan kept it as a
268
+ notional second `EditorHostBridge` implementation. It is neither. It has no
269
+ importer anywhere in the repo, it implements the *old* ad-hoc API rather than
270
+ `EditorHostBridge`, it self-invokes `initMessageListener()` at module load
271
+ (attaching a `window` listener the moment anything imports it), and it carries
272
+ the very throw-on-overflow handler logic that decision 3 above deliberately
273
+ replaced with a diagnostic counter. Shipping it would have contradicted this
274
+ ADR's claim that `host/messages.ts` is the single place wire payloads are
275
+ constructed.
276
+
277
+ Its `VsCodeApi` interface and the `declare global { interface Window {
278
+ acquireVsCodeApi } }` augmentation in `types.ts` went with it. That augmentation
279
+ was the concrete harm: `export * from './types'` in both `src/index.ts` and
280
+ `src/contract.ts` published it, so every consumer of `.` **and** `./contract` —
281
+ including the Electron preload, which uses `./contract` precisely to avoid
282
+ unrelated surface — had `window.acquireVsCodeApi` typed as globally available in
283
+ a host where it never exists. Verified by probe: before the removal a consumer
284
+ calling `window.acquireVsCodeApi()` typechecked cleanly; after it, the same
285
+ probe fails with `TS2339`.
286
+
287
+ ## Consequences
288
+
289
+ ### Positive
290
+
291
+ - One implementation of the editor, with a curated API, ready for Zusammen to
292
+ adopt in place of its copy — which is what ends the divergence.
293
+ - The package can be reasoned about, and tested, with no knowledge of Electron.
294
+ `<Editor>` renders with zero host services supplied.
295
+ - The correction feature's persistence contract is now explicit and stubbable,
296
+ where previously it was seven scattered `window.api` calls.
297
+ - #939 (unified annotation mechanism) becomes a contained design problem rather
298
+ than a design problem tangled up with a 182-file relocation.
299
+
300
+ ### Negative / costs
301
+
302
+ - **Four build-config landmines had to be defused, and they stay defused only
303
+ by intent:**
304
+ 1. Tailwind v4 automatic source detection always skips `node_modules`, and the
305
+ package resolves through the `node_modules/@liminis/editor` symlink. Without
306
+ an explicit `@source "../../../../packages/editor/src"` in
307
+ `src/renderer/styles/main.css`, the editor's ~165 utility classes silently
308
+ stop being generated — with no typecheck, lint, build, or test signal.
309
+ 2. `externalizeDepsPlugin()` on the `main`/`preload` targets must exclude
310
+ `@liminis/editor`, or the emitted bundle keeps a bare
311
+ `import '@liminis/editor/headless'` that resolves to unbuilt TypeScript at
312
+ runtime.
313
+ 3. The package needs its own `typecheck`/`lint`/`test` scripts or `pnpm -r`
314
+ skips 74 source files, and CI needs its own test step or the 100 relocated
315
+ fixtures stop running while the suite stays green.
316
+ 4. **The package must NOT declare `"sideEffects"`.** This one is a trap rather
317
+ than a chore, and it was walked into during review before being walked back
318
+ out. Barrel weight (see subpath `./markdown` above) makes a `sideEffects`
319
+ declaration look like the obvious one-line fix. It is not: declaring it
320
+ breaks the renderer outright.
321
+
322
+ Marking our modules side-effect-free does not merely prune our own graph —
323
+ it changes rolldown's whole-graph chunking. Under it, `prismjs` (reached
324
+ via `@lexical/code`) has its core separated from `prism-clike.js`, which
325
+ mutates a bare `Prism` global that is by then undefined:
326
+
327
+ ```
328
+ ReferenceError: Prism is not defined (prism-clike.js, renderer entry)
329
+ ```
330
+
331
+ The renderer never mounts. `document.body.innerText` is empty, and e2e went
332
+ from green to 138 failures spanning chat, command palette, content editing
333
+ and semantic search — nothing editor-specific, because every locator misses
334
+ when nothing renders. Bisected to the commit that added the key; removing
335
+ only that key restores a green suite. The hazard lives in a **transitive
336
+ dependency**, so it cannot be expressed in our own `sideEffects` field at
337
+ all: `["**/*.css"]` and `false` fail identically. Reduce barrel weight with
338
+ a narrow entry point instead.
339
+
340
+ **Closed by #940, in favour of the prohibition.** The theory behind the
341
+ deferral was that a consumer of *built* output would have a different module
342
+ graph and might legitimately need the field. It does not: #940's build is a
343
+ structure-preserving `tsc` emit that does no chunking of its own, so an
344
+ external consumer's bundler sees the same graph liminis-app's does. The
345
+ hazard is identical, not different. The measurement the field was wanted for
346
+ targets the `React.lazy()` boundary on `AnnotationSurface` instead and does
347
+ not depend on it. See ADR-078 §5. This prohibition is permanent unless the
348
+ build strategy itself changes.
349
+
350
+ Landmines 1, 2 and 4 are guarded by
351
+ `liminis-app/src/shared/__tests__/editor-package-wiring.test.ts`. All three are
352
+ invisible to typecheck, lint, build and the rest of the suite — the build
353
+ reports success while the app degrades (landmine 4 being the exception that
354
+ proves the rule: it *is* caught, but only by e2e, an hour downstream) — which
355
+ is why they get a test that asserts the *configuration* rather than the
356
+ symptom.
357
+ - ~~The package is consumed as raw TypeScript in-workspace. A standalone ESM +
358
+ `.d.ts` build, peer dependencies, and resolving the patched
359
+ `mdast-util-wiki-link@0.1.2` are deferred to #940.~~ **Landed in #940 — see
360
+ ADR-078.** The package now builds to `dist/`; `publishConfig` points external
361
+ consumers at it while the checked-in manifest still resolves to `src/`, so
362
+ liminis-app's consumption — and both build guards above — are unchanged.
363
+ `react`/`react-dom`/`lexical` and all eleven `@lexical/*` are peers, and
364
+ `mdast-util-wiki-link` is vendored into the package with its patch applied
365
+ inline, so no pnpm patch survives.
366
+ - Ambient declarations (`jsx.d.ts` for React 19's non-global JSX namespace, and
367
+ the `micromark-extension-wiki-link` shim) are duplicated in
368
+ `packages/editor/src/ambient/` and `liminis-app/src/types/`, since both
369
+ programs need them and neither may depend on the other.
370
+
371
+ ## Outstanding
372
+
373
+ **`.claude/rules/editor-package.md` has not been written.** The Fabrik worker
374
+ environment denies writes under `.claude/`, so the path-scoped rule that carries
375
+ ADR-024/025/032 across to `packages/editor/**` (per ADR-034) could not land with
376
+ this ADR. Its drafted content is in PR #941's body and needs a human to commit
377
+ it. Until it exists, the guidance lives only here and in this ADR's "Decision"
378
+ section.
379
+
380
+ ## Alternatives Considered
381
+
382
+ **A single root barrel export.** Rejected: it forces the DOM-free main process
383
+ program to typecheck Lexical and `react-dom/client`.
384
+
385
+ **Leaving `messaging-electron.ts` in the package and stubbing `window.api` in
386
+ tests.** Rejected: it is precisely the coupling the extraction exists to remove,
387
+ and it would have kept the package's test setup mocking a host global.
388
+
389
+ **Keeping the message helpers app-side, with a six-method bridge.** Rejected:
390
+ six adapter methods per host is six opportunities to drift from the wire format,
391
+ where two methods plus package-side helpers cannot drift at all.
392
+
393
+ **A general-purpose third-party plugin API.** Explicitly out of scope. The
394
+ public entry is a curated export surface, not an extension mechanism; #939 bakes
395
+ annotations *into* the package rather than exposing hooks for hosts to extend.
396
+ See ADR-077, which carries that out and adds `./annotations` as a sixth,
397
+ considered subpath under the rule stated above.
398
+
399
+ ## References
400
+
401
+ - ADR-079 (headless mapper/node export) — adds `./nodes` as a seventh,
402
+ considered subpath under the rule stated above
403
+ - ADR-077 (comments and corrections as two kinds of one annotation mechanism) —
404
+ extends this seam and adds the `./annotations` subpath
405
+ - Issue #938 (this phase), #939 (unified annotation mechanism), #940 (OSS hardening)
406
+ - `specs/938-extract-the-lexical-markdown/spec.md`
407
+ - ADR-002 (Lexical), ADR-010 (bidirectional markdown conversion), ADR-025
408
+ (zustand for renderer state), ADR-042 (MCP stdio serialization), ADR-057
409
+ (knowledge corrections pipeline), ADR-070 (dedicated Lexical node types)
410
+ - `.claude/rules/editor-package.md` (not yet written — see "Outstanding")
@@ -0,0 +1,183 @@
1
+ # ADR-076: Markdown Serialization Is a Fixed Point, at the Cost of a One-Time Canonicalization
2
+
3
+ **Date:** 2026-08-04
4
+ **Status:** Accepted
5
+ **Issue:** #943
6
+
7
+ ## Context
8
+
9
+ Liminis's markdown pipeline —
10
+ `parseMarkdown → importMarkdownToLexical → exportLexicalToMdast → stringifyMarkdown` —
11
+ runs on every open and save. Until now the round-trip corpus asserted only that
12
+ one pass was faithful: input → output matched either the input itself or a
13
+ recorded `.expected.md` sidecar.
14
+
15
+ That guarantee is weaker than it looks. It says nothing about whether the
16
+ *output* is stable. A serializer can be perfectly faithful on the first pass and
17
+ still drift on the second, which in practice means a document nobody edited
18
+ keeps changing on disk every time it is opened and saved — invisible in the
19
+ WYSIWYG view, visible in git diffs, and cumulative.
20
+
21
+ Two genuine defects had been living in `stringify.ts` behind exactly that gap.
22
+ Both were found and fixed in Zusammen's forked copy of the file
23
+ ([verveguy/zusammen#62](https://github.com/verveguy/zusammen/pull/62), closing
24
+ its #50) and were still live in Liminis:
25
+
26
+ 1. **A blank line before a list was silently dropped.** A `join` override
27
+ implements a "label:" convention — a paragraph ending in `:` gets zero blank
28
+ lines before the block that follows, so `Fenced code:` hugs its fence.
29
+ `list` was wrongly in that set, so `Steps to reproduce:` followed by a blank
30
+ line and a bulleted list lost the blank line, unlike every other
31
+ preceding-block type.
32
+
33
+ 2. **A post-process regex corrupted verbatim content.** `stringify.ts` ended
34
+ with `result.replace(/\n{3,}/g, '\n\n')`. `mdast-util-to-markdown`'s own join
35
+ logic already never emits 3+ newlines between sibling blocks, so this had no
36
+ legitimate job left. Its only real effect was destroying intentional
37
+ multi-blank-line runs *inside fenced code blocks and frontmatter YAML block
38
+ scalars* — content that must survive byte-for-byte.
39
+
40
+ ADR-075 drew the `@liminis/editor` package boundary and named "markdown
41
+ idempotency fixes Liminis lacks" as precisely the divergence the extraction
42
+ existed to stop. Because the serializer now has a single home, this is a
43
+ one-site fix rather than a hand-port to two repositories.
44
+
45
+ ## Decision
46
+
47
+ **Serialization output is a fixed point.** Every fixture's own first-pass output
48
+ is round-tripped a second time and must be byte-identical. This applies to
49
+ fixtures whose first pass legitimately changes bytes (`.expected.md` sidecars)
50
+ as well as byte-identical ones — matching a sidecar says nothing about whether
51
+ that sidecar's content is itself stable.
52
+
53
+ **We accept a one-time canonicalization to get there.** Removing `list` from the
54
+ colon-join override is a user-visible behaviour change: a document containing a
55
+ colon-ending paragraph immediately followed by a list *gains* a blank line on its
56
+ first save after this ships.
57
+
58
+ **Exemptions are individually documented, never batched.** A fixture with a
59
+ known, accepted non-idempotency carries a `<name>.idempotence-exempt.txt`
60
+ sidecar whose contents are the written reason. An exemption file that exists but
61
+ is empty is a hard error in fixture discovery — the reason is mandatory, which
62
+ makes "skip it to get green" impossible to do silently.
63
+
64
+ **No repo-wide normalization pass.** Documents canonicalize as they are saved.
65
+
66
+ ### Which document shapes change on first save
67
+
68
+ Only one, and only in one direction:
69
+
70
+ | Shape | Before | After |
71
+ |---|---|---|
72
+ | Colon-ending paragraph immediately followed by a list (no blank line) | stays zero blank lines | gains one blank line |
73
+ | Colon-ending paragraph, blank line, then a list | blank line **dropped** | blank line preserved |
74
+ | Colon-ending paragraph before a table, fenced code, or block math | zero blank lines | **unchanged** — zero blank lines |
75
+ | 2+ consecutive blank lines inside a fenced code block or YAML block scalar | **collapsed to one** | preserved verbatim |
76
+ | 2+ consecutive blank lines between sibling blocks | collapsed to one | unchanged — still collapsed, by the library's own join logic |
77
+
78
+ The last row is the one worth stating explicitly: removing the regex did *not*
79
+ let 3+ newlines through. Blank-line runs between flow siblings are discarded at
80
+ *parse* time — mdast has no representation for them — so the collapse still
81
+ happens without any post-process help. This is pinned by fixtures
82
+ (`50-multiple-blank-lines-between-paragraphs`,
83
+ `50-multiple-blank-lines-before-list`, and the three-blank-line runs inside
84
+ `50-reference-readme`).
85
+
86
+ ## Consequences
87
+
88
+ ### Positive
89
+
90
+ - A saved document that nobody edits stops changing on disk. This is the
91
+ structural guarantee that would have caught both defects, and it now catches
92
+ the next one automatically for every fixture in the corpus.
93
+ - Code and configuration inside fenced blocks and YAML block scalars survive
94
+ verbatim. This was the more serious of the two defects: silently corrupting
95
+ the code a note documents is worse than a cosmetic diff.
96
+ - The fix lands in `@liminis/editor`, so when Zusammen adopts the package (#940)
97
+ it stops being something anyone has to hand-port.
98
+
99
+ ### Negative
100
+
101
+ - **A visible one-time diff in users' notebooks.** Any real document with a
102
+ colon-ending paragraph immediately followed by a list gains a blank line the
103
+ first time it is saved after this ships. This is accepted deliberately: it
104
+ makes lists behave like every other preceding-block type, and the alternative
105
+ — preserving the defect to avoid the diff — means the drift never stops.
106
+ - The round-trip corpus is now the slowest suite in the workspace: the second
107
+ pass roughly doubles its work across the 100 non-exempt fixtures (105 inputs
108
+ less the 5 exemptions below). "Non-exempt" means exactly one thing — the
109
+ fixture runs the second-pass assertion. It is not a claim about fidelity: the
110
+ 23 `known-defects/` fixtures are all non-exempt and all pass, because a round
111
+ trip that loses content still reaches a fixed point (see the Neutral note
112
+ below). The only other category outside the assertion is `.error.txt`
113
+ fixtures, which reject on the first pass and so have no output to feed back;
114
+ the corpus has none today.
115
+
116
+ ### Neutral
117
+
118
+ - Five fixtures are exempt, all one pre-existing cause: #902's double-hard-break
119
+ marker serializes as a whitespace-only line, which CommonMark re-reads as
120
+ blank, splitting the paragraph on the second pass. Fixing that hard-break
121
+ normalization is out of scope here and needs its own issue. The exemption set
122
+ was determined by observation — the harness landed before the fixes so the
123
+ pre-fix failure set could be recorded — not by prediction.
124
+ - The `known-defects/` fixtures continue to assert current defective behaviour.
125
+ None of them needed an exemption: asserting a *defective* output does not stop
126
+ that output from being a stable fixed point.
127
+ - **A fixed point is a guarantee about stability, not fidelity.** This is the
128
+ limit of what the second pass buys, and it is worth stating plainly so the
129
+ guarantee is not over-read. A round trip that silently *drops* content reaches
130
+ a fixed point immediately and passes — `known-defects/other-blockquote-list-
131
+ content-lost`, found during this issue's review, is the live example: a list
132
+ inside a blockquote is lost entirely, and the lossy output is perfectly
133
+ stable. The first-pass `.expected.md` comparison is what catches fidelity
134
+ loss. That defect is pre-existing, lives in the mapper rather than the
135
+ serializer, and needs its own issue.
136
+ - `stringify.test.ts`'s "collapse excessive blank lines" case still passes with
137
+ the post-process deleted, since its input is two plain paragraphs. It was
138
+ retitled rather than removed — it now pins the `mdast-util-to-markdown` join
139
+ guarantee that removing the regex rests on, so a library upgrade that breaks
140
+ that guarantee fails loudly instead of silently letting blank-line runs
141
+ through. Review broadened it from that single paragraph pair to 14 sibling-
142
+ block pairs (heading, fenced code, list, table, blockquote, thematic break,
143
+ block math, HTML block, nested list, frontmatter), so the guarantee is pinned
144
+ where it is actually load-bearing.
145
+ - The colon detection only fires when the paragraph's final inline node is a
146
+ plain `text` node, so `**Steps:**` is not treated as colon-ending. After this
147
+ change that distinction is unobservable for lists — both shapes get one blank
148
+ line — but it remains observable for `table`/`code`/`math`, where
149
+ `**Fenced code:**` keeps its blank line and `Fenced code:` does not. That
150
+ asymmetry is pre-existing and deliberately unchanged; both sides are pinned by
151
+ the `943-non-text-colon-paragraph-*` fixtures.
152
+
153
+ ## Alternatives Considered
154
+
155
+ **Make the regex verbatim-span-aware instead of removing it.** Rejected: it
156
+ would have to know about fenced code, inline code, math spans and frontmatter
157
+ block scalars — the same span-tracking complexity that already makes the
158
+ neighbouring post-processes fragile — to implement a guarantee the library
159
+ already provides for free.
160
+
161
+ **Keep `list` in the colon-join override and exempt the fixtures.** Rejected:
162
+ this is the reported defect, not an accepted normalization. Exempting it would
163
+ be exactly the "batch-exempt to get green" failure the sidecar convention exists
164
+ to prevent.
165
+
166
+ **A repo-wide normalization pass over existing `.md` documents.** Rejected: it
167
+ would bury the actual fix in a large, undifferentiated diff, and it forces the
168
+ one-time change on documents the user may not have opened in months. Letting
169
+ documents canonicalize as they are saved keeps each diff small and attributable.
170
+
171
+ **Amend ADR-075 rather than write a new ADR.** Rejected: ADR-075 is Accepted and
172
+ scoped to the package boundary. This is a distinct, user-visible behaviour
173
+ decision; folding it into an accepted structural ADR muddies both records.
174
+
175
+ ## References
176
+
177
+ - Issue #943; [verveguy/zusammen#62](https://github.com/verveguy/zusammen/pull/62) (the original fix)
178
+ - ADR-075 — the `@liminis/editor` package boundary that made this a single-site fix
179
+ - ADR-070 — dedicated Lexical node types for lossless round-tripping; this extends
180
+ its guarantee from "one pass is faithful" to "one pass is a fixed point"
181
+ - `packages/editor/src/markdown/stringify.ts` — both fixes
182
+ - `packages/editor/src/app/mapper/__tests__/fixtures/roundtrip/README.md` — the
183
+ fixture, sidecar and exemption conventions