@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,167 @@
1
+ # ADR-079: A Seventh Export Subpath, `./nodes`, for the Headless Mapper
2
+
3
+ **Date:** 2026-08-07
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** ADR-075 (adds a seventh export subpath to the surface ADR-077 had already widened to six)
7
+ **Issue:** #954
8
+
9
+ ## Context
10
+
11
+ `@liminis/editor` already exported the markdown↔Lexical mapper functions
12
+ (`importMarkdownToLexical`, `exportLexicalToMdast`) from the root barrel, but
13
+ not the Lexical node classes — or the aggregate array of them — the production
14
+ editor configures its `LexicalComposer` with. The mapper instantiates those
15
+ node classes directly (`$createHeadingNode`, `$createCalloutNode`, …) and
16
+ Lexical throws at runtime if a `createEditor` isn't registered with every node
17
+ type an update tries to create. Without the array, a consumer could not build
18
+ a working headless editor and therefore could not exercise the mapper at all
19
+ without mounting the full `<Editor>` component — pulling in MathJax, Mermaid
20
+ and Prism for a test that needs none of them.
21
+
22
+ The array itself already existed, as an unexported `const editorNodes` local
23
+ to `Editor.tsx`. The package's own test suite had worked around the gap by
24
+ hand-maintaining a second copy in `roundtrip-test-utils.ts`, with a comment
25
+ noting it had to be kept "matching Editor.tsx" — a live instance of the exact
26
+ drift risk this ADR closes, silently depended on by roughly fifteen other
27
+ internal test files.
28
+
29
+ The issue's own text suggested `./contract` or `./markdown` as candidate
30
+ homes. Both are ruled out by their own documented and tested isolation
31
+ contracts: `./markdown`'s file header states "do not add anything to this
32
+ entry whose import graph reaches Lexical, React, MathJax, or the C4
33
+ subsystem," and `./contract`'s entire purpose is the zod host-message shapes
34
+ alone, consumed via `import type` specifically to avoid pulling in Lexical.
35
+ `./headless`'s own docstring is equally explicit that nothing there "may
36
+ transitively import Lexical," and `./annotations` (ADR-077) is contractually
37
+ Lexical-free, enforced by its own static-graph test. None of the six subpaths
38
+ declared as of ADR-077 is a legitimate home for a Lexical node array.
39
+
40
+ ## Decision
41
+
42
+ **Extract the shared node array, and add a seventh declared subpath,
43
+ `./nodes`, carrying both it and the mapper functions.**
44
+
45
+ ### The array moves to its own module
46
+
47
+ `editorNodes` is extracted from `Editor.tsx` into
48
+ `packages/editor/src/app/editor/editorNodes.ts`. `Editor.tsx`'s
49
+ `LexicalComposer` and the new public export both import this one array —
50
+ neither holds its own copy. `roundtrip-test-utils.ts`'s hand-maintained
51
+ duplicate is deleted and replaced with an import of the same module, closing
52
+ the internal drift risk the array's own comment already flagged. Wherever the
53
+ production editor's node set changes, both consumers see the change with no
54
+ second list to edit — this is what makes the export "derived from the
55
+ editor's own configuration" rather than a snapshot of it.
56
+
57
+ ### A seventh, considered subpath: `./nodes`
58
+
59
+ ADR-075 fixed the surface at five subpaths and permitted adding a *considered*
60
+ export when no existing one fits; ADR-077 exercised that rule once already for
61
+ `./annotations`. This is the same rule, exercised again.
62
+
63
+ `./nodes` re-exports `editorNodes` from the shared module, plus
64
+ `importMarkdownToLexical`/`exportLexicalToMdast` from the mapper. Both live on
65
+ the same subpath because they cannot be usefully separated: the mapper
66
+ directly imports the node classes' `$create*`/`$is*` helpers, so an entry
67
+ point exporting the functions without the matching node array would still
68
+ leave a consumer unable to build a working `createEditor` for them, and vice
69
+ versa. Putting both in one place also means Acceptance Scenario 1/2's
70
+ "importing only from `@liminis/editor`'s declared entry points" is satisfied
71
+ by a single import.
72
+
73
+ ### The MathJax-lite exception is inherited, not re-litigated
74
+
75
+ `EquationNode.tsx` imports `mathjax-config.ts` at module scope, which
76
+ statically imports MathJax's lite *and* browser adaptors — around 90
77
+ side-effectful TeX-configuration imports evaluate whenever the node set loads,
78
+ with no DOM, no rendering, and no MathJax document instantiation. `./headless`
79
+ already carries and documents this identical exception (see ADR-075 §1).
80
+ `./nodes` inherits it rather than attempting to fix `EquationNode.tsx`'s
81
+ import laziness, which is a separate refactor out of scope for a purely
82
+ additive change (FR-008).
83
+
84
+ Verified directly (not assumed) that `./nodes`'s import graph is otherwise
85
+ narrow: `Mermaid`'s and `C4`'s render components are `lazy(() =>
86
+ import(...))`-loaded from their node class files, so a non-rendering headless
87
+ editor never evaluates `mermaid`, `@dagrejs/dagre`, or Prism — none of those
88
+ packages, nor `zustand` or `lucide-react`, appear anywhere in the static
89
+ graph. This is pinned by `nodes-subpath.test.ts`, modeled on
90
+ `annotations-subpath.test.ts`'s static-graph-walker pattern but adapted in two
91
+ ways: comments are stripped before scanning for `from '...'` specifiers
92
+ (`c4/types.ts` has a doc comment containing `from "Name"`, which the naive
93
+ regex would otherwise misread as a bare package import), and dynamic
94
+ `import('./Component')` targets are not followed as graph edges, since
95
+ treating a lazy boundary as an eager one would defeat the point of measuring
96
+ it.
97
+
98
+ ### The round-trip test asserts on mdast, not stringified markdown
99
+
100
+ `nodes-subpath-roundtrip.test.ts` (FR-006/SC-005) proves the export is
101
+ sufficient on its own: it imports only from `./nodes` and `./markdown`,
102
+ builds a `createEditor` from `editorNodes`, and drives a markdown → Lexical →
103
+ mdast round trip covering headings, links, lists, tables, code, images and a
104
+ callout. It asserts against the resulting mdast tree's shape rather than
105
+ re-stringified markdown, because `./markdown` does not export
106
+ `stringifyMarkdown` — reaching for the root barrel to get it would reintroduce
107
+ the `<App>`/`<Editor>`/Prism weight this subpath exists to avoid. This is a
108
+ scope decision for what this one test checks, not a lowering of FR-005's
109
+ fidelity bar, which the package's existing internal round-trip suite still
110
+ enforces at full strength.
111
+
112
+ One calling-convention detail worth recording because it is easy to get
113
+ wrong: `importMarkdownToLexical` schedules its own `editor.update()`, which by
114
+ default reconciles on a microtask. Calling `exportLexicalToMdast` immediately
115
+ afterward races that reconciliation and reads a stale (often empty) tree. The
116
+ fix — wrapping the call in an outer `editor.update(fn, { discrete: true })` —
117
+ is the same convention the package's own internal `roundtrip-test-utils.ts`
118
+ already relies on; it is now also shown in `docs/markdown-pipeline.md`'s
119
+ headless-editor example, so external consumers don't have to discover it by
120
+ reading the mapper's implementation.
121
+
122
+ ## Consequences
123
+
124
+ **Good:**
125
+
126
+ - A consumer (Zusammen's own round-trip harness is the motivating case) can
127
+ build a headless editor and exercise the real mapper without a fork, without
128
+ mounting `<Editor>`, and without pulling in MathJax's browser adaptor,
129
+ Mermaid, or Prism.
130
+ - The internal drift risk `roundtrip-test-utils.ts`'s comment already named is
131
+ closed: one array, two consumers, both inside and outside the package.
132
+ - Adding a node type to the production editor's configuration requires editing
133
+ exactly one file; both `Editor.tsx` and `./nodes` see it automatically.
134
+
135
+ **Bad / accepted:**
136
+
137
+ - `./nodes`'s isolation contract is strictly weaker than
138
+ `./markdown`/`./contract`/`./annotations`: it requires Lexical and React,
139
+ and carries the same MathJax-lite exception as `./headless`. This is stated
140
+ explicitly here and in the subpath's own docstring so it isn't later
141
+ mistaken for an oversight.
142
+ - The surface ADR-075 fixed at five subpaths is now seven. Each widening is
143
+ small and individually justified, but the "closed set" is closed only in the
144
+ sense that widening it requires this documented process, not that it never
145
+ grows.
146
+
147
+ **Neutral:**
148
+
149
+ - `verify-package.mjs`'s `EXPECTED_SUBPATHS` and
150
+ `examples/external-consumer/src/subpaths.ts`'s per-subpath import block are
151
+ both hand-maintained lists; both were extended in the same change so the new
152
+ subpath's publishability is actually verified against a packed tarball,
153
+ rather than silently going unchecked the way ADR-078 built that script to
154
+ prevent.
155
+
156
+ ## References
157
+
158
+ - Issue #954 (this phase)
159
+ - ADR-075 (the `@liminis/editor` package boundary; §1's closed-subpath rule
160
+ this ADR exercises again)
161
+ - ADR-077 (comments and corrections as one annotation mechanism; the prior
162
+ exercise of the same "considered export" rule, for `./annotations`)
163
+ - ADR-078 (non-bundling `tsc` emit + `publishConfig` delivery; the mechanism a
164
+ new subpath plugs into)
165
+ - `verveguy/zusammen#102` (the private adoption this export unblocks),
166
+ `verveguy/zusammen#62` and `#50`, `#943` (the mapper fidelity bugs the
167
+ resulting headless round-trip guard is meant to catch)
@@ -0,0 +1,325 @@
1
+ # ADR-080: Comments Are Folded State in a Workspace Dotfile, Owned by the Main Process
2
+
3
+ **Date:** 2026-08-08
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** ADR-077 (supplies the Liminis consumer it anticipated; corrects two statements it made about the app's wiring)
7
+ **Issue:** #964
8
+
9
+ ## Context
10
+
11
+ ADR-077 established comments and corrections as two *configurations* of one
12
+ annotation mechanism inside `@liminis/editor`, and put storage, lifecycle and
13
+ presentation deliberately on the **host** side of the ADR-075 persistence seam.
14
+ It then noted that Liminis had none of that host side: it configured the
15
+ `correction` kind, supplied no `onCreateAnnotation`, and never called
16
+ `captureAnchor` or `resolveAnchors` at all.
17
+
18
+ This ADR records the decisions that fill that gap — where comment data lives,
19
+ what shape it takes, and who owns it.
20
+
21
+ Two properties of the Liminis environment drove the answers, and both differ
22
+ from Zusammen, whose implementation of this feature is the nearest prior art:
23
+
24
+ - **A Liminis Notebook is a plain, human-browsable folder of markdown that is
25
+ also a git repository.** `workspace-init` runs `git init`, and the workspace
26
+ `.gitignore` template excludes only `.liminis/context-graph.pid` and
27
+ `.liminis/mcp-oauth.json` — so anything else written under `.liminis/` is
28
+ tracked, diffable, and swept into `createCheckpoint`'s `git add -A` for free.
29
+ - **Liminis is single-user and single-machine.** Concurrency is several windows
30
+ of one app instance plus in-process agent activity — not concurrent reviewers
31
+ on branches, which is the problem Zusammen's storage design exists to solve.
32
+
33
+ ## Decision
34
+
35
+ ### 1. Comments live in `.liminis/comments.json`, as folded state
36
+
37
+ One JSON index for the whole workspace, keyed by workspace-relative POSIX path,
38
+ written atomically (temp file + rename) by the main process.
39
+
40
+ **Not embedded in the markdown.** That would defeat the "transparent on export"
41
+ property the mark renderer was built for and make every comment a document
42
+ edit — the thing FR-007 forbids outright.
43
+
44
+ **Not a sidecar beside each document.** It travels with the file, but it
45
+ litters a notebook that is meant to read as a plain folder of notes, and
46
+ Liminis already has a dotfile precedent in
47
+ `.liminis/knowledge-corrections.yaml`.
48
+
49
+ **Not a dedicated git ref (Zusammen ADR-003's WAL).** That model is correct for
50
+ Zusammen's concurrent-reviewer, branch-per-review workflow, but it needs the
51
+ scratch-index/CAS plumbing Liminis does not have, and buys conflict-freedom
52
+ Liminis does not need. A notebook is already a git repo, so the working tree
53
+ gives versioning, diffability and checkpoint participation without a second
54
+ write path.
55
+
56
+ **Folded state, not an event log.** Zusammen's append-only `CommentEvent` log
57
+ with a pure `reconstructComments` fold buys conflict-free merge and a complete
58
+ audit trail. Liminis needs neither. The two properties it *does* need from that
59
+ model are one field (`editedAt`, for FR-023's "marked as edited") and one store
60
+ invariant (never auto-delete, FR-018) — neither of which justifies replay
61
+ machinery. The notebook's git history is the audit trail.
62
+
63
+ **One index, not one file per document.** `rollbackToCommit` is
64
+ `git reset --hard`, which is workspace-wide, so per-document granularity buys
65
+ nothing for checkpoint alignment. A single index makes a rename remap a key
66
+ change rather than a file move, and sidesteps path→filename escaping entirely.
67
+
68
+ **JSON, not YAML** — unlike the corrections precedent. Anchors carry exact
69
+ whitespace in `targetText`, `prefixContext` and `suffixContext`, which YAML
70
+ block scalars round-trip badly, and the package's `ANCHOR_SCHEMA` is already
71
+ zod.
72
+
73
+ **Deletion is a hard delete.** FR-018 forbids only *automatic* deletion. A user
74
+ delete removes the record; git history preserves it; and FR-023 requires the
75
+ thread to actually leave the comment list, which a tombstone would complicate
76
+ for no gain.
77
+
78
+ **Accepted consequence:** comments are commit-worthy working-tree changes. They
79
+ appear in `git status`, in checkpoint diffs, and move with a rollback. That is
80
+ the intended behaviour — a rollback that restores a document should restore the
81
+ comments that were on it — and it is the single most surprising thing a user
82
+ who inspects their notebook will find.
83
+
84
+ ### 2. `docVersion` is a content hash
85
+
86
+ `AnchorFields` carries no `docVersion` and `ANCHOR_SCHEMA` requires a non-empty
87
+ one, so the host must stamp it. Liminis resolves anchors against *working-tree*
88
+ text — the file as it is now, which may never have been committed and may have
89
+ been edited outside the app — so Zusammen's git-HEAD-SHA answer does not
90
+ transfer.
91
+
92
+ `sha256(documentText).slice(0, 16)`, computed in the main process. It is audit
93
+ metadata the resolver never reads; what matters is that capture, re-capture and
94
+ resolution all compute it the same way, which one main-process implementation
95
+ guarantees.
96
+
97
+ ### 3. The store is main-process-owned, behind IPC, and broadcasts
98
+
99
+ Per ADR-032 the main process owns all state; per ADR-007 renderer traffic goes
100
+ through `ipcMain.handle` + `contextBridge`; per ADR-025 window events are not a
101
+ state channel. The comment store is therefore a main-process module with a
102
+ `comments:*` IPC surface, and every change is pushed to every window over
103
+ `comments:changed`.
104
+
105
+ The renderer store holds nothing of its own beyond presentation state. Writes
106
+ are serialized inside main by a per-workspace promise chain — the corrections
107
+ precedent does unlocked read-modify-write, which is fine at its write rate and
108
+ would not be at a comment editor's.
109
+
110
+ **SC-005 is forward-looking.** Only one window hosts an editor today, so
111
+ "a comment created in one window appears in another" is evidenced by a unit
112
+ test on the broadcast fan-out rather than end-to-end. The fan-out is
113
+ architecturally required regardless, and costs nothing.
114
+
115
+ ### 4. Resolution runs off the document-open path, and never auto-deletes
116
+
117
+ `resolveAnchor` performs one full `parseBlocks` — an mdast parse of the entire
118
+ document — *per anchor*, and `resolveAnchors`' `Promise.all` does not make that
119
+ concurrent because the work is synchronous CPU. A hundred comments is a hundred
120
+ parses on the main process's only thread.
121
+
122
+ The host cannot hoist that parse (FR-033 bars touching the package), so
123
+ `comments:open` answers immediately from the store with each comment's *last
124
+ persisted* outcome and schedules the re-resolution pass, which yields to the
125
+ event loop every five anchors and broadcasts when it lands. This is Zusammen's
126
+ best-effort pattern. Hoisting `parseBlocks` out of the per-anchor loop is a
127
+ separate upstream issue against `packages/editor`.
128
+
129
+ The graded outcomes map to graded persistence, and to graded UI:
130
+
131
+ | Outcome | Persisted | Marker | Presented as |
132
+ | --- | --- | --- | --- |
133
+ | `unchanged` | nothing (FR-015) | yes | Open |
134
+ | `re-attached` | outcome + new anchor (FR-014) | yes | Open |
135
+ | `flagged` | outcome only | no | Location uncertain, with the original quote |
136
+ | `orphaned` | outcome only | no | Detached, with the original quote |
137
+
138
+ No comment is ever deleted as a consequence of an outcome (FR-018), and no
139
+ semantic-relocation strategy is supplied (FR-021) — filling
140
+ `proposeSemanticRelocation` needs a model call on the document-open path, with
141
+ the cost, latency and failure modes that implies, and is a follow-up.
142
+
143
+ Two idempotence guards keep merely *opening* a document from dirtying the
144
+ working tree: a `re-attached` anchor identical to the stored one is not
145
+ rewritten, and an `unchanged` outcome clears a previous verdict only when that
146
+ verdict was actually suppressing a marker.
147
+
148
+ ### 5. A degraded store refuses writes rather than overwriting
149
+
150
+ A store file that exists but will not parse puts that workspace into a
151
+ **degraded** state: reads answer empty so documents still open, every write is
152
+ refused with a distinct error, the renderer shows a persistent banner, and the
153
+ file is never touched.
154
+
155
+ This deliberately departs from the corrections precedent, which logs malformed
156
+ YAML and treats it as absent — and whose writer would then happily overwrite
157
+ it. FR-010 forbids exactly that for comments, because the file being
158
+ overwritten is the user's own data.
159
+
160
+ Two corollaries, both found in review and both load-bearing:
161
+
162
+ **Degradation is re-checked, not remembered.** A degraded entry is re-read on
163
+ every access rather than cached. The banner tells the user their comment file is
164
+ unreadable and the obvious response is to go and repair it; caching the verdict
165
+ would keep refusing writes until the app restarted, with nothing explaining why
166
+ the repair had no effect. A healthy store is still cached on first load.
167
+
168
+ **The store validates on the way out as well as in.** Every field reaching a
169
+ mutator arrives over IPC, where the types are a compile-time promise only.
170
+ Validating only on read meant an unstorable record — an empty `id`, an author
171
+ with an empty `name` — wrote successfully and failed on the *next* load, at
172
+ which point the whole workspace flipped degraded: every comment invisible, every
173
+ write refused. The store could corrupt itself into the state this decision
174
+ exists to prevent. `writeToDisk` now parses the file it is about to write and
175
+ refuses on failure, keeping the failure local to the bad record.
176
+
177
+ ### 6. The correction path now runs the package's capture cycle
178
+
179
+ `AnnotationPlugin` registers a single command handler for *every* configured
180
+ kind, gated only on the kind having a `createAffordance`. `correction` has one.
181
+ So once Liminis supplies `onCreateAnnotation` — which FR-003 requires —
182
+ clicking "Correction…" runs the package's wrap → export → `readAnchorFields` →
183
+ unwrap cycle in addition to opening the legacy correction panel, and the host
184
+ handler discards the result.
185
+
186
+ This is precisely the cost ADR-077 declined to pay. It is accepted here because
187
+ both alternatives are barred: removing the correction affordance would be a
188
+ correction behaviour change (FR-034), and suppressing the capture would need a
189
+ package change (FR-033). User-visible behaviour is unchanged —
190
+ `markerStyle: 'none'` plus `livemarkPolicy: () => false` plus
191
+ `retainMarkOnCreate: false` keep the correction path paint-free — and all three
192
+ are guarded in `editor-package-wiring.test.ts`.
193
+
194
+ ### 7. Commenting is enabled on `EditorColumn` only
195
+
196
+ Since #961 all five `<App>` mount sites configure annotation kinds. Adding
197
+ `comment` to the value they *shared* would have given the canvas card bodies,
198
+ `ActionViewer` and `MarkdownEditorPanel` a "Comment…" entry with no composer
199
+ behind it. Those four are pinned to `CORRECTION_ONLY_ANNOTATION_KINDS` —
200
+ byte-identical to what they had — and `ANNOTATION_KINDS` (comment + correction)
201
+ is passed by `EditorColumn` alone (FR-032).
202
+
203
+ ### 8. Anchoring inside a code fence or frontmatter is refused up front
204
+
205
+ The spec's structured-content edge case permits "work or be refused up front".
206
+ Refusing is the safer half: a mark inside a code fence or a frontmatter block
207
+ is the least-exercised region of the package's mark-transparency corpus, and a
208
+ silent round-trip corruption there would damage the user's document — the one
209
+ thing FR-007 exists to prevent. On refusal the transient mark is retracted, a
210
+ toast explains, and nothing is persisted.
211
+
212
+ ## Consequences
213
+
214
+ **Good**
215
+
216
+ - Comments version, diff and roll back with the notebook, with no second git
217
+ write path and no new plumbing.
218
+ - The difference between the comment feature and the correction feature stays
219
+ expressible as two `AnnotationKindConfig` values plus host code; nothing
220
+ inside `@liminis/editor` changed (SC-007).
221
+ - Agent authorship is representable and renderable from day one, so the later
222
+ agent-authoring path needs no schema migration (FR-027). The anchor resolver
223
+ already exercises it — its automatic resolutions are attributed to
224
+ `{ name: 'Liminis Anchor Resolver', kind: 'agent' }`.
225
+ - A corrupt store degrades commenting without endangering either the document
226
+ or the existing comment data.
227
+
228
+ **Bad, or at least surprising**
229
+
230
+ - `git status` in a notebook now shows `.liminis/comments.json` changing as the
231
+ user comments. This is intended, and is documented in the release notes.
232
+ - The single index grows unbounded across a large notebook. Acceptable at
233
+ Liminis's scale — comments are small and the write is atomic — and the
234
+ migration to per-document files is mechanical if it ever stops being.
235
+ - Correction authoring pays for a capture cycle whose result is thrown away.
236
+ - The right rail now has three mutually-exclusive occupants (history >
237
+ comments > TOC) and a correspondingly fiddlier `paddingRight`.
238
+
239
+ ## Amendments to ADR-077
240
+
241
+ Two statements in ADR-077 are true no longer, and this ADR supersedes them:
242
+
243
+ 1. *"the resolver currently has no Liminis consumer … stated here so it isn't
244
+ later mistaken for dead code"* — `src/main/comments/comment-anchoring.ts` is
245
+ that consumer.
246
+ 2. *"`liminis-app` supplies `annotationKinds` but **not** `onCreateAnnotation`"*
247
+ — `EditorColumn` supplies both. See §6 for what that changed on the
248
+ correction path.
249
+
250
+ ## Known limitations at first release
251
+
252
+ > **Historical — both were fixed by #970 (PR #971) on 2026-08-13.** The rest of
253
+ > this section is written in the present tense of the first release and is kept
254
+ > as the record of what shipped and why. For what is true now, read the
255
+ > resolution note at the end of the section: neither limitation survives, and
256
+ > neither mitigation described below still exists in the codebase.
257
+
258
+ Two `@liminis/editor` mark-placement gaps were measured while reviewing this
259
+ issue and filed as #970. FR-033 forbids patching either from the host, so they
260
+ ship as follows — the two were deliberately *not* treated the same way, because
261
+ one degrades an experience and the other damages data.
262
+
263
+ **Overlapping passages place only one mark.** `placeMarksForAnchors` works
264
+ back-to-front because a placed mark splits its TextNode and invalidates
265
+ `offsetSpans` for everything after it — sound for disjoint ranges, but for
266
+ overlapping ones the later placement invalidates the span the earlier entry
267
+ still needs. Strict nesting fails the same way. Accepted as a first-release
268
+ limitation: both comments persist, both are listed, and the unmarked one is
269
+ reachable from the comment list, which flags the threads affected
270
+ (`findOverlappingThreadIds`). The release notes say so plainly. The set that
271
+ actually places is pinned by an assertion in
272
+ `comment-markdown-integrity.test.tsx` so the behaviour cannot drift unnoticed.
273
+
274
+ **A mark spanning an inline link reports a truncated span**, and that one
275
+ *writes*. An anchor stored as `it [rests](https://example.com)` comes back from
276
+ `collectLiveAnchorSnapshots` as `it [rests`; the FR-019 refresh sees a
277
+ difference no user caused and would persist an anchor ending mid-syntax,
278
+ silently and cumulatively. That is data damage rather than a limitation, so it
279
+ is guarded host-side: `canRefreshAnchorText` refuses to replace a stored
280
+ `targetText` with text *contained within* it. The rule is deliberately blunt —
281
+ sharpening it would mean re-implementing the mapper's knowledge of markdown in
282
+ the host, which is #970's job. Its accepted cost is that a user who genuinely
283
+ deletes the tail of an anchored passage gets no re-capture; a stale-but-valid
284
+ anchor still re-resolves through the fuzzy resolver, whereas a truncated one is
285
+ corrupt and does not.
286
+
287
+ **When #970 lands**, the guard and the pinning assertion come out together —
288
+ they are two halves of the same standing mitigation.
289
+
290
+ > **Resolved 2026-08-13 (#970, PR #971).** Both limitations above are fixed
291
+ > upstream and their mitigations are gone from this codebase.
292
+ >
293
+ > - Overlapping and nested marks now *all* place: placement resolves every
294
+ > entry against the pristine tree into absolute text offsets before applying
295
+ > any of them, so entries no longer invalidate each other. The
296
+ > `comment-markdown-integrity.test.tsx` assertion now pins all seven ids.
297
+ > - A mark spanning an inline link recovers the whole construct, so
298
+ > `canRefreshAnchorText` and both its call sites are **deleted**. The refresh
299
+ > now skips only a genuinely unchanged span, which restores re-capture for the
300
+ > suffix deletions the guard used to decline.
301
+ >
302
+ > Still outstanding: the comment list's `findOverlappingThreadIds` notice
303
+ > ("Shares a passage with another comment — only one of them shows a
304
+ > highlight") is now untrue and should be removed. It is app-side presentation,
305
+ > which #970's spec puts out of its own scope, so it did not travel with this
306
+ > change.
307
+
308
+ ## Follow-ups
309
+
310
+ Deliberately out of scope here, each worth its own issue:
311
+
312
+ - Hoist `parseBlocks` out of `resolveAnchors`' per-anchor loop (upstream,
313
+ `packages/editor`).
314
+ - Fill the `proposeSemanticRelocation` seam for orphaned anchors.
315
+ - The agent-facing path for *creating* comments — an MCP tool or agent-SDK
316
+ surface, and anchoring from outside a mounted editor. This issue makes agent
317
+ authorship representable; it does not make it reachable.
318
+
319
+ ## References
320
+
321
+ - ADR-075 — the `@liminis/editor` package boundary at persistence
322
+ - ADR-077 — comments and corrections as two kinds of one mechanism
323
+ - ADR-032 / ADR-025 / ADR-007 / ADR-024 — state ownership, renderer state, IPC, dialogs
324
+ - ADR-057 — the `.liminis/` knowledge-corrections file-format precedent
325
+ - Zusammen ADR-003, ADR-008, ADR-012 — the prior art this decision diverges from
@@ -0,0 +1,108 @@
1
+ # ADR-081: The Web Shell Reads the Fixture Corpus Through Generated Data, Not a `src/` Import
2
+
3
+ **Date:** 2026-08-15
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #2 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ verveguy/liminis-editor#1 landed a round-trip fixture corpus at
12
+ `src/app/mapper/__tests__/fixtures/roundtrip/`, covering every Lexical node
13
+ class `editorNodes.ts` declares (`node-class-completeness.test.ts` enforces
14
+ this). This issue grows `examples/demo` from a single hand-written markdown
15
+ string into a shell that renders every fixture-representable node class, and
16
+ requires (FR-006/FR-007) that its content come from that same corpus rather
17
+ than a second, hand-maintained set of sample documents.
18
+
19
+ `examples/demo` is a structurally external consumer of `@liminis/editor` — its
20
+ own `package.json`, no workspace linkage, and it installs the packed tarball
21
+ the same way a real adopter would (`scripts/run-demo.mjs`). FR-008 makes that
22
+ literal for imports: neither shell may import from `@liminis/editor/src/...`
23
+ or any relative path reaching into the package's `src/`. The fixture corpus
24
+ lives inside `src/`, so a direct `import.meta.glob('../../../src/app/mapper/
25
+ __tests__/fixtures/roundtrip/**/*.md')` from the demo would be exactly the
26
+ thing FR-008 prohibits — even though the fixtures are markdown data, not
27
+ package code, the path itself crosses the boundary the rule polices.
28
+
29
+ Two shapes were available to resolve this, both surfaced during Research:
30
+
31
+ 1. Move the corpus to a repository-root `fixtures/` directory, sibling to
32
+ `src/`, and update `roundtrip-test-utils.ts`'s `discoverFixtures` path and
33
+ `fixtures/roundtrip/README.md` accordingly.
34
+ 2. Leave the corpus where #1 put it, and add a build-time script that reads
35
+ it and emits a generated data module inside `examples/demo`, which the
36
+ demo then imports as an ordinary local file.
37
+
38
+ #1's own Out of Scope section reserves "the content and structure of the
39
+ shared fixture corpus itself" for that issue. Moving the corpus is not a
40
+ content or structure change, but it touches already-merged, extensively
41
+ documented test infrastructure (`fixtures/roundtrip/README.md`'s per-fixture
42
+ provenance notes, `roundtrip-test-utils.ts`'s `discoverFixtures`) for a rule
43
+ that belongs to a different issue. That is a bigger, riskier change than
44
+ generating one file, for a boundary #1 drew around itself deliberately.
45
+
46
+ ## Decision
47
+
48
+ **The corpus stays at `src/app/mapper/__tests__/fixtures/roundtrip/`.**
49
+ `scripts/generate-demo-fixtures.mjs` reads it directly (a build-time Node
50
+ script, not a runtime import, so it is not itself bound by FR-008 — nothing
51
+ in the demo's own source or bundle reaches into `src/`) and emits
52
+ `examples/demo/src/fixtures.generated.js`, a plain JS module listing each
53
+ fixture's corpus-relative name, group, and markdown text.
54
+
55
+ Fixtures with a `.error.txt` sidecar (expected to throw on import) are
56
+ excluded, mirroring `node-class-completeness.test.ts`'s own skip logic —
57
+ `discoverFixtures` and the exclusion rule are the only pieces of corpus
58
+ knowledge this script needs, and both already exist in
59
+ `roundtrip-test-utils.ts`'s shape.
60
+
61
+ The generated file is gitignored. `pnpm demo` (`scripts/run-demo.mjs`) and
62
+ the new `scripts/build-examples.mjs` (CI) both regenerate it before the demo
63
+ is built or run, so there is no code path that consumes a stale copy, and the
64
+ demo's own source tree never contains a checked-in duplicate of #1's corpus
65
+ to drift out of sync with it.
66
+
67
+ ## Consequences
68
+
69
+ **Good:**
70
+
71
+ - `examples/demo` satisfies FR-006/FR-007 (fixture-driven content, same
72
+ corpus as #1) and FR-008 (no `src/` reach-in) simultaneously, without
73
+ touching a single file #1 already merged.
74
+ - The corpus has exactly one location and one owner (#1's own scope), for
75
+ both the completeness test and the web shell — the generated file is a
76
+ derived artifact, not a second source of truth.
77
+ - Adding a fixture to the corpus (or a class of fixture, as future issues
78
+ extend #1's coverage) makes it appear in the demo on the next regeneration,
79
+ with no change to the demo's own source.
80
+
81
+ **Bad / accepted:**
82
+
83
+ - A codegen step is now part of both `pnpm demo` and the CI build path for
84
+ `examples/demo` — one more script in the pack/install/build pipeline
85
+ Research already flagged as having duplicated orchestration logic across
86
+ `run-demo.mjs`, `verify-package.mjs`, and now `build-examples.mjs`. Left
87
+ unconsolidated; not required by this issue.
88
+ - The generated file, being gitignored, does not appear in a diff when a
89
+ fixture changes — a reviewer checking "does the demo still render
90
+ correctly" has to run the generator locally rather than reading the PR.
91
+
92
+ **Neutral:**
93
+
94
+ - The repo-root-`fixtures/` alternative remains available if a future issue
95
+ needs the corpus to be reachable from outside `src/` for reasons beyond
96
+ this one shell (e.g. a second consumer). Nothing here forecloses it; it was
97
+ simply not the lowest-risk answer to *this* issue's requirement.
98
+
99
+ ## References
100
+
101
+ - Issue #2 (this decision), verveguy/liminis-editor#1 (the corpus this
102
+ decision reads from, and whose Out of Scope section this decision respects)
103
+ - `src/app/mapper/__tests__/fixtures/roundtrip/README.md` (the corpus's own
104
+ documentation, unchanged by this decision)
105
+ - `docs/decisions/adr-078.md` (packed-tarball delivery; the reason
106
+ `examples/demo` cannot resolve `@liminis/editor/src/...` even in
107
+ development, which is the same structural constraint this decision applies
108
+ to the corpus)