@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,282 @@
1
+ # The markdown pipeline
2
+
3
+ Markdown ↔ mdast ↔ Lexical, usable with or without an editor mounted.
4
+
5
+ ```text
6
+ markdown text ──parseMarkdown──▶ mdast Root ──importMarkdownToLexical──▶ Lexical
7
+ markdown text ◀─stringifyMarkdown── mdast Root ◀──exportLexicalToMdast── Lexical
8
+ ```
9
+
10
+ Everything on the mdast side is available from `@liminis/editor/markdown`,
11
+ which has no React, no Lexical, no DOM and no MathJax in its graph. The
12
+ Lexical mappers live on the root entry, because they need Lexical.
13
+
14
+ ## `parseMarkdown`
15
+
16
+ ```ts
17
+ import { parseMarkdown } from '@liminis/editor/markdown'
18
+
19
+ const { root } = parseMarkdown(text)
20
+ ```
21
+
22
+ `root` is a standard mdast `Root`. Extensions enabled: GFM (tables, strikethrough,
23
+ task lists, autolinks), footnotes, math, definition lists, YAML frontmatter, and
24
+ wiki-links.
25
+
26
+ Beyond the standard extensions, `parseMarkdown` runs several post-passes that
27
+ exist to preserve information a plain mdast parse throws away:
28
+
29
+ - **Task lists in ordered lists.** GFM only supports checkboxes in unordered
30
+ lists; Foam and Obsidian use them in ordered lists too, so they are added back.
31
+ - **Emphasis marker annotation.** Whether the source wrote `*em*` or `_em_` is
32
+ recorded on the node, so a round trip does not silently rewrite one into the
33
+ other.
34
+ - **Wiki-link pipe protection.** See below.
35
+ - **Empty-alias marking.** `[[target|]]` is distinguishable from `[[target]]`.
36
+
37
+ ### Type guards
38
+
39
+ The entry exports guards for the mdast node types you will actually branch on:
40
+ `isParagraph`, `isHeading`, `isList`, `isListItem`, `isBlockquote`, `isCode`,
41
+ `isThematicBreak`, `isTable`, `isImage`, `isLink`, `isHtml`, `isText`,
42
+ `isStrong`, `isEmphasis`, `isInlineCode`, `isDelete`.
43
+
44
+ ```ts
45
+ import { parseMarkdown, isHeading, isText } from '@liminis/editor/markdown'
46
+
47
+ const { root } = parseMarkdown(text)
48
+ const titles = root.children.filter(isHeading).map((h) => h.children.filter(isText).map(t => t.value).join(''))
49
+ ```
50
+
51
+ `getFileType(path)` is also here — the editor's own file-type classifier.
52
+
53
+ ## `stringifyMarkdown`
54
+
55
+ ```ts
56
+ import { stringifyMarkdown } from '@liminis/editor'
57
+
58
+ const text = stringifyMarkdown(root, { bulletStyle: '-', fenceStyle: '`' })
59
+ ```
60
+
61
+ | Option | Type | Default |
62
+ |---|---|---|
63
+ | `wrapWidth` | `number` | no wrapping |
64
+ | `bulletStyle` | `'-' \| '*' \| '+'` | `'-'` |
65
+ | `fenceStyle` | `` '`' \| '~' `` | `` '`' `` |
66
+
67
+ Serialization is written to keep a document byte-stable across a round trip
68
+ wherever markdown permits it. The most consequential piece is **list spread**:
69
+ whether a list is "loose" (blank lines between items) is carried through the
70
+ Lexical round trip and is only ever *upgraded* to loose, never downgraded — a
71
+ downgrade silently collapses blank lines and, on re-parse, misreads item
72
+ boundaries.
73
+
74
+ ## The Lexical mappers
75
+
76
+ ```ts
77
+ import { importMarkdownToLexical, exportLexicalToMdast } from '@liminis/editor'
78
+ ```
79
+
80
+ These are what `<Editor>` uses internally. Reach for them directly only if you
81
+ are driving a Lexical editor yourself.
82
+
83
+ ### Building a headless editor
84
+
85
+ The mapper functions instantiate Lexical node classes directly, so a
86
+ `createEditor` you build yourself must be configured with the *exact* node set
87
+ `<Editor>` uses — any node type Lexical encounters that isn't registered
88
+ throws. `@liminis/editor/nodes` exports that array (`editorNodes`) alongside
89
+ the two mapper functions, without pulling in `<Editor>`/`<App>`, Mermaid, or
90
+ Prism. This is the entry point to use for a fast round-trip regression test
91
+ that exercises the real mapper without mounting a React component:
92
+
93
+ ```ts
94
+ import { createEditor } from 'lexical'
95
+ import { editorNodes, importMarkdownToLexical, exportLexicalToMdast } from '@liminis/editor/nodes'
96
+ import { parseMarkdown } from '@liminis/editor/markdown'
97
+
98
+ const editor = createEditor({
99
+ namespace: 'headless',
100
+ nodes: editorNodes,
101
+ onError: (error) => { throw error },
102
+ })
103
+
104
+ const parsed = parseMarkdown(markdownSource)
105
+
106
+ // `importMarkdownToLexical` schedules its own `editor.update()`, which by
107
+ // default reconciles on a microtask. Wrap it in a `discrete: true` update so
108
+ // the mutation is committed before `exportLexicalToMdast` reads it back.
109
+ editor.update(() => {
110
+ importMarkdownToLexical(editor, parsed.root)
111
+ }, { discrete: true })
112
+
113
+ const mdast = exportLexicalToMdast(editor)
114
+ ```
115
+
116
+ `editorNodes` is the same array `<Editor>` configures its own `LexicalComposer`
117
+ with — not a separately maintained copy — so a node type added to the
118
+ production editor is reflected here automatically. `EquationNode` imports
119
+ MathJax's lite adaptor at module scope, so loading `./nodes` carries the same
120
+ documented MathJax-lite exception as `./headless`; it does not initialize
121
+ Mermaid, C4's layout engine, or Prism, since those node classes lazy-load their
122
+ render components.
123
+
124
+ ## Wiki-links
125
+
126
+ Wiki-links are `[[target]]` or `[[target|alias]]` (Obsidian/Foam style — the
127
+ alias divider is `|`, not the upstream default `:`). They parse to a `wikiLink`
128
+ mdast node:
129
+
130
+ ```ts
131
+ { type: 'wikiLink', value: 'target', data: { alias, permalink, exists, /* hast shim fields */ } }
132
+ ```
133
+
134
+ `data.alias` is the display text — it falls back to the target when no explicit
135
+ alias is given, which is upstream behaviour and occasionally surprising.
136
+
137
+ ### The pipe-in-tables problem (#347)
138
+
139
+ A pipe inside a table cell terminates the cell. An aliased wiki-link in a table
140
+ therefore has to escape its divider (`[[target\|alias]]`) or GFM parsing splits
141
+ the link — and, worse, corrupts the shape of the whole table row.
142
+
143
+ `parseMarkdown` escapes the divider before parsing and the wiki-link extension
144
+ strips the resulting backslash off the target afterwards. The strip happens
145
+ *before* the target is resolved, so `data.permalink` and `data.exists` are
146
+ computed from the clean target.
147
+
148
+ This behaviour originally came from a `pnpm patch` on the npm package. That
149
+ patch could not travel outside the Liminis monorepo, so the extension is now
150
+ **vendored** into this package (MIT, from
151
+ [`landakram/mdast-util-wiki-link`](https://github.com/landakram/mdast-util-wiki-link);
152
+ license and modifications ship at `dist/markdown/vendor/mdast-util-wiki-link/`).
153
+
154
+ **If you are maintaining this package: do not remove the trailing-backslash
155
+ strip.** It looks like a stray special case. It is the fix for
156
+ [#347](https://github.com/verveguy/liminis/issues/347), and removing it breaks
157
+ entire markdown tables that happen to contain an aliased wiki-link, not just the
158
+ link.
159
+
160
+ ### Building your own pipeline
161
+
162
+ If you need an mdast pipeline of your own that agrees with the editor's — a
163
+ chunker, an indexer, an exporter — take the wiki-link extension from this
164
+ package rather than from npm, or your parse will differ from the editor's:
165
+
166
+ ```ts
167
+ import { fromMarkdown } from 'mdast-util-from-markdown'
168
+ import { syntax as wikiLinkSyntax } from 'micromark-extension-wiki-link'
169
+ import { wikiLinkFromMarkdown, wikiLinkToMarkdown } from '@liminis/editor/markdown'
170
+
171
+ const OPTIONS = { aliasDivider: '|' }
172
+
173
+ const tree = fromMarkdown(text, {
174
+ extensions: [wikiLinkSyntax(OPTIONS) /* , gfm(), … */],
175
+ mdastExtensions: [wikiLinkFromMarkdown(OPTIONS) /* , gfmFromMarkdown(), … */],
176
+ })
177
+ ```
178
+
179
+ `wikiLinkToMarkdown` is the matching serializer extension, built on
180
+ `mdast-util-to-markdown` v2.
181
+
182
+ > **The extensions alone are not equivalent to `parseMarkdown`.** If you enable
183
+ > GFM tables alongside them, prefer `parseMarkdown` — see below.
184
+
185
+ ### What the extensions do *not* give you
186
+
187
+ `parseMarkdown` is more than these two extensions, and the difference is not
188
+ cosmetic. Two pre/post-passes live in `parseMarkdown` and have no equivalent
189
+ inside the extension:
190
+
191
+ - **Alias-pipe escaping (the #347 fix above).** The trailing-backslash strip in
192
+ `wikiLinkFromMarkdown` only helps once the divider is *already* escaped.
193
+ `parseMarkdown` does that escaping before parsing. Without it, GFM's table
194
+ parser reaches the `|` first. Measured on `| [[target\|alias]] | x |` in a
195
+ two-column table: the extensions alone yield **5 table cells and 0 wiki-link
196
+ nodes** — the row is corrupted and the link is gone — where `parseMarkdown`
197
+ yields 4 cells and 1 wiki-link.
198
+ - **Empty-alias preservation.** `[[target|]]` is distinguishable from
199
+ `[[target]]` only because `parseMarkdown` substitutes a sentinel before parsing
200
+ and sets `data._emptyAlias` after. The extensions alone produce no wiki-link
201
+ node at all for that input.
202
+
203
+ So: **if your pipeline enables GFM tables, or you care about `[[target|]]`, call
204
+ `parseMarkdown` rather than assembling the extensions yourself.** Reach for the
205
+ raw extensions only when you control the input and neither case applies.
206
+
207
+ Note also that `<Editor>`'s own *serialization* does not go through
208
+ `wikiLinkToMarkdown`: `stringifyMarkdown` carries a hand-rolled wiki-link handler
209
+ that additionally understands `data._emptyAlias`. `wikiLinkToMarkdown` is the
210
+ faithful vendored upstream serializer, not a byte-for-byte match for what the
211
+ editor emits.
212
+
213
+ ## Wiki-link promotion on export
214
+
215
+ Everything above is about *parsing* `[[target]]` syntax the author already wrote.
216
+ Independently, `exportLexicalToMdast` (in `lexicalToMdast.ts`) also *produces*
217
+ wiki-link syntax on export, for a link the author wrote as an ordinary standard
218
+ markdown link: `convertLinkNode` promotes any **untitled** link whose URL looks
219
+ like a relative note reference — a relative `.md` path (with or without a
220
+ leading `./`/`../`), a `.md` path with an anchor, a bare `#anchor`, or a
221
+ directory-style (trailing-slash) path (see `isWikiLinkUrl`) — into `[[target]]`
222
+ / `[[target|alias]]` on export. A **titled** link (`[text](url "title")`) is
223
+ never promoted, regardless of URL shape — wiki-link syntax has no slot for a
224
+ title, so promoting one would silently drop it
225
+ ([#919](https://github.com/verveguy/liminis/issues/919)).
226
+
227
+ This is deliberate for `liminis-app`, whose documents are wiki-link-native: a
228
+ relative link to another note *is* a wiki-link, and round-tripping it as one is
229
+ correct. It is not correct for every consumer — a host whose documents are
230
+ plain markdown rendered somewhere that doesn't understand `[[...]]` syntax
231
+ (e.g. the GitHub web UI) would see every untitled relative link rewritten into
232
+ non-rendering syntax on save, which is document corruption from that
233
+ consumer's point of view, not a normalization
234
+ ([#951](https://github.com/verveguy/liminis/issues/951)).
235
+
236
+ ### The `wikiLinkPromotion` option
237
+
238
+ `exportLexicalToMdast(editor, options)` accepts an optional `ExportOptions`
239
+ object with a `wikiLinkPromotion?: 'promote' | 'off'` field, defaulting to
240
+ `'promote'` — today's only behavior, unchanged for any caller that doesn't set
241
+ it. `'off'` disables promotion of an *ordinary* standard-markdown link whose
242
+ URL merely looks wiki-link-shaped: it is emitted as a standard markdown link
243
+ instead of `[[target]]`. It does not affect a link that was genuine,
244
+ author-written `[[target]]` / `[[target|alias]]` wiki-link syntax in the
245
+ source — that always round-trips back out as a wiki-link, regardless of this
246
+ setting, so an opted-out host still never corrupts a document's existing
247
+ wiki-links, only stops creating new ones. (The two are distinguished by
248
+ provenance, tracked on the parsed link node — not by re-inspecting the URL,
249
+ which is identical either way.) This does not change which URLs
250
+ `isWikiLinkUrl` classifies as wiki-link-like, and it has no effect on
251
+ *parsing* — a genuine, author-written `[[target]]` in the source still parses
252
+ to a link node on import regardless of this setting.
253
+
254
+ Both types are exported from the root barrel (`@liminis/editor`), alongside
255
+ `exportLexicalToMdast` itself:
256
+
257
+ ```ts
258
+ import { exportLexicalToMdast, type ExportOptions, type WikiLinkPromotionMode } from '@liminis/editor'
259
+
260
+ const mdast = exportLexicalToMdast(editor, { wikiLinkPromotion: 'off' })
261
+ ```
262
+
263
+ The `<Editor>`/`<App>` component surface exposes the same setting as an
264
+ optional `wikiLinkPromotion` prop, mirroring the existing `imagePathResolution`
265
+ prop's shape (though sourced differently — see below):
266
+
267
+ ```tsx
268
+ <App wikiLinkPromotion="off" ... />
269
+ ```
270
+
271
+ Unlike `imagePathResolution` (which `<App>` derives from IPC-delivered
272
+ `SlashMDSettings`), `wikiLinkPromotion` is a direct `AppProps` field, not
273
+ settings-derived: `<App>` also supports a non-IPC "inline" mode (`content`/
274
+ `onChange` props, bypassing the host-message channel entirely), where
275
+ `settings` stays `null` forever — a settings-only path would be unreachable by
276
+ a host running that way, which is exactly the shape the option was added for.
277
+
278
+ The setting also reaches the annotation-anchor-capture export path
279
+ (`annotation-marks.ts`), not just the disk-write path: both are expected to
280
+ agree on whether a link's raw-markdown form is wiki-link or standard syntax, so
281
+ an opted-out host's annotation anchors are captured against the same link
282
+ syntax its saved document actually contains.
@@ -0,0 +1,106 @@
1
+ # Provenance
2
+
3
+ Where this repository came from, and how to read the references in it.
4
+
5
+ ## The extraction
6
+
7
+ `@liminis/editor` began as a directory inside the Liminis monorepo,
8
+ `verveguy/liminis`. It was extracted into this repository by
9
+ verveguy/liminis#995 on 2026-08-15.
10
+
11
+ The extraction used `git filter-repo` against a throwaway clone of
12
+ `verveguy/liminis` at commit `34f0d828`, scoped to the paths that made up the
13
+ package and its examples:
14
+
15
+ ```
16
+ packages/editor → (repository root)
17
+ examples/demo → examples/demo
18
+ examples/external-consumer → examples/external-consumer
19
+ scripts/verify-package.mjs → scripts/verify-package.mjs
20
+ scripts/run-demo.mjs → scripts/run-demo.mjs
21
+ ```
22
+
23
+ The result is **real history, not a squashed import**: the filtered import
24
+ contributed 140 commits, including the sequence of adoption defects that a second
25
+ consumer found in the package during August 2026 (verveguy/liminis#951 through
26
+ #954, #961 and #965). `git log` on any file under `src/` shows the commit that
27
+ introduced it.
28
+
29
+ The source repository was not rewritten. The clone was thrown away.
30
+
31
+ ### One branch arrived after the cut
32
+
33
+ verveguy/liminis#973 — a serializer defect that corrupted `**bold**` abutting an
34
+ inline-code span on every save — was still open upstream when the filter ran, so
35
+ its work sat on `preserved/liminis-issue-973-in-flight` rather than on `main`. It
36
+ merged upstream shortly afterwards and was merged here too. The branch is kept as
37
+ a record of the cut; it is an ancestor of `main` and nothing needs to be taken
38
+ from it.
39
+
40
+ ### What did not come across
41
+
42
+ - The package's CI jobs, which lived in the monorepo's `.github/workflows/test.yml`.
43
+ They are replaced by this repository's own `.github/workflows/ci.yml`.
44
+ - The monorepo's `pnpm-workspace.yaml`. This repository has **no workspace file**,
45
+ which is what makes `examples/*` external consumers by construction rather than
46
+ by convention. See the note in `scripts/verify-package.mjs`.
47
+ - History from before the package was a package. The editor previously lived at
48
+ `liminis-app/src/editor` and has roughly 287 commits of prior history there.
49
+ Those commits are interleaved with changes to an application this repository is
50
+ not about, so they were deliberately left behind. They remain in
51
+ `verveguy/liminis`.
52
+
53
+ ## Reading the references
54
+
55
+ ### `ADR-0NNN`
56
+
57
+ Resolves to `docs/decisions/adr-0NNN.md`. See that directory's `README.md`,
58
+ which also explains the handful of numbers cited *inside* those documents that
59
+ belong to the host application rather than to this package.
60
+
61
+ Where the package's own code cited an application decision — the Claude Agent SDK
62
+ integration, MCP stdio concurrency, the knowledge corrections pipeline, the ban on
63
+ native browser dialogs — the citation was removed at extraction and the reasoning
64
+ written out at the call site instead, so it stands on its own.
65
+
66
+ ### `FR-NNN`, `SC-NNN`, `NFR-NNN`
67
+
68
+ These are requirement and success-criterion identifiers from Spec Kit
69
+ specifications, and they are **scoped to the issue whose work introduced them**.
70
+ An `FR-004` in one comment and an `FR-004` in another are unrelated unless the
71
+ comments are about the same issue.
72
+
73
+ They resolve under `specs/` in `verveguy/liminis`, in the directory named for the
74
+ issue number, e.g. `specs/995-extract-liminis-editor-into/spec.md`.
75
+
76
+ They were left as-is rather than rewritten. There are several hundred across the
77
+ package's source, concentrated in its most heavily commented code, and rewriting
78
+ them all would have made `git log -p` unreadable for exactly the history this
79
+ extraction went to some trouble to preserve. Where a comment was edited for other
80
+ reasons during the extraction, its identifiers were made self-describing.
81
+
82
+ ### `#NNN`
83
+
84
+ A bare `#NNN` in a source comment refers to an issue in **`verveguy/liminis`**,
85
+ because that is where the package's backlog lived when the comment was written.
86
+ Read `#973` as `verveguy/liminis#973`.
87
+
88
+ References written *since* the extraction are fully qualified. Where you see
89
+ `verveguy/liminis#995`, that qualification is deliberate and load-bearing: issue
90
+ numbers in this repository are their own sequence, and this repository already has
91
+ issues #1, #2 and #3 that have nothing to do with the ones the comments mean.
92
+
93
+ ## Vendored documents
94
+
95
+ `src/app/mapper/__tests__/fixtures/real-documents/` holds byte-identical copies of
96
+ real ADRs from `verveguy/liminis`, used as realistic round-trip input. Its own
97
+ `README.md` records their provenance. They are vendored rather than read from a
98
+ sibling checkout precisely so that no test in this repository reads a file outside
99
+ it.
100
+
101
+ `src/app/mapper/__tests__/fixtures/roundtrip/50-reference-readme.md` and its
102
+ `.expected.md` pair contain strings that look like citations — `ADR-` prefixes, a
103
+ `docs/project_notes/` path. They are neither citations nor mistakes: they are
104
+ deliberately messy human prose being used as round-trip corpus. They were left
105
+ untouched by the extraction's citation sweep, because editing them would change
106
+ the bytes the fixture asserts on.
@@ -0,0 +1,232 @@
1
+ # Zusammen Editor-Domain Capability Map
2
+
3
+ **Date:** 2026-08-04
4
+ **Issue:** #939 (Phase 2 — unified annotation mechanism), FR-008 / US3 / SC-006
5
+ **Related:** ADR-077 (the unified annotation primitive), ADR-075 (the package boundary)
6
+
7
+ ## Why this document exists
8
+
9
+ Zusammen (`verveguy/zusammen`) maintains its own copy of this editor, plus the
10
+ anchor modules under `app/src/shared/`. Its adoption of `@liminis/editor` is a
11
+ follow-up in that repository — out of scope here. What *is* in scope is proving
12
+ the public API can serve it, because if there is a gap, adoption forces a fork
13
+ or a deep import, which is the exact failure ADR-075 exists to prevent.
14
+
15
+ This maps every editor-domain capability Zusammen has today to a symbol
16
+ reachable from one of the package's declared export subpaths. It is checkable by
17
+ reading it against `packages/editor/package.json`'s `exports` and the barrel
18
+ files, without running Zusammen.
19
+
20
+ **Result: zero unmet capabilities.** Three symbols are recorded as deliberate
21
+ app-domain exclusions rather than gaps — see the last section for why each one
22
+ is not the package's to provide.
23
+
24
+ Package subpaths referenced below:
25
+
26
+ | Subpath | Barrel |
27
+ |---|---|
28
+ | `.` | `packages/editor/src/index.ts` |
29
+ | `./annotations` | `packages/editor/src/annotations.ts` |
30
+ | `./markdown` | `packages/editor/src/markdown.ts` |
31
+ | `./headless` | `packages/editor/src/headless.ts` |
32
+ | `./contract` | `packages/editor/src/contract.ts` |
33
+ | `./styles.css` | `packages/editor/src/styles.css` |
34
+
35
+ ### One adoption requirement that is not a symbol
36
+
37
+ `./styles.css` carries the default `annotation-mark-*` rules, and **the host has
38
+ to import it** — the package does not inject it, because a CSS import inside
39
+ `<Editor>` or the root barrel would force a stylesheet on markdown-only
40
+ consumers too. Zusammen should add:
41
+
42
+ ```ts
43
+ import '@liminis/editor/styles.css'
44
+ ```
45
+
46
+ alongside its `<Editor>` mount. Skipping it is not a build error and not a
47
+ runtime error: anchors resolve, marks place, activation fires, and nothing
48
+ renders. Called out here rather than left to discovery because Liminis cannot
49
+ surface it — its only kind is `markerStyle: 'none'`, so it never places a mark
50
+ whose styling could be missing. See ADR-077, "Marker styling ships with the
51
+ package".
52
+
53
+ ---
54
+
55
+ ## 1. `app/src/shared/anchor-model.ts`
56
+
57
+ | Zusammen symbol | Package export | Subpath |
58
+ |---|---|---|
59
+ | `Anchor` | `Anchor` | `./annotations` |
60
+ | `AnchorFields` | `AnchorFields` | `./annotations` |
61
+ | `AnchorRange` | `AnchorRange` | `./annotations` |
62
+ | `AnchorOutcome` | `AnchorOutcome` | `./annotations` |
63
+ | `AnchorResolution` | `AnchorResolution` | `./annotations` |
64
+ | `ANCHOR_SCHEMA` | `ANCHOR_SCHEMA` | `./annotations` |
65
+ | `ANCHOR_RESOLUTION_SCHEMA` | `ANCHOR_RESOLUTION_SCHEMA` | `./annotations` |
66
+ | `CONTEXT_WINDOW_CHARS` | `CONTEXT_WINDOW_CHARS` | `./annotations` |
67
+ | `captureAnchor` | `captureAnchor` | `./annotations` |
68
+
69
+ Ported verbatim; `anchor-model.test.ts` carries over and passes unmodified
70
+ except for import paths.
71
+
72
+ ## 2. `app/src/shared/anchor-resolver.ts`
73
+
74
+ | Zusammen symbol | Package export | Subpath |
75
+ |---|---|---|
76
+ | `resolveAnchor` | `resolveAnchor` | `./annotations` |
77
+ | `ResolveAnchorOptions` | `ResolveAnchorOptions` | `./annotations` |
78
+ | `ProposeSemanticRelocation` | `ProposeSemanticRelocation` | `./annotations` |
79
+ | `noopProposeSemanticRelocation` | `noopProposeSemanticRelocation` | `./annotations` |
80
+ | `similarity` | `similarity` | `./annotations` |
81
+ | `REATTACH_THRESHOLD` | `REATTACH_THRESHOLD` | `./annotations` |
82
+ | `FLAG_THRESHOLD` | `FLAG_THRESHOLD` | `./annotations` |
83
+ | *(no equivalent — new)* | `resolveAnchors`, `IdentifiedAnchor`, `AnchorResolutionResult` | `./annotations` |
84
+
85
+ `resolveAnchors` is a batch wrapper added for the public surface (FR-002); it
86
+ fans out over the same `resolveAnchor` path, so classification is unchanged.
87
+ Zusammen's `main/comment-anchoring.ts` currently loops `resolveAnchor` itself
88
+ behind its `comments:resolveAnchors` IPC handler and can call the wrapper
89
+ instead.
90
+
91
+ This module is the **sole** fuzzy-matching module for anchor re-resolution
92
+ (FR-003). `anchor-resolver.test.ts` carries over unmodified.
93
+
94
+ ## 3. `app/src/shared/block-structure.ts`
95
+
96
+ | Zusammen symbol | Package export | Subpath |
97
+ |---|---|---|
98
+ | `BlockType` | `BlockType` | `./annotations` |
99
+ | `Block` | `Block` | `./annotations` |
100
+ | `SentenceChunk` | `SentenceChunk` | `./annotations` |
101
+ | `parseBlocks` | `parseBlocks` | `./annotations` |
102
+ | `findEnclosingBlock` | `findEnclosingBlock` | `./annotations` |
103
+ | `blockPlainText` | `blockPlainText` | `./annotations` |
104
+ | `sentenceChunks` | `sentenceChunks` | `./annotations` |
105
+
106
+ Ported verbatim; `block-structure.test.ts` carries over unmodified.
107
+
108
+ ## 4. `app/src/shared/comment-anchor-align.ts` → `annotations/anchor-align.ts`
109
+
110
+ | Zusammen symbol | Package export | Subpath |
111
+ |---|---|---|
112
+ | `locateInSpan` | `locateInSpan` | `./annotations` |
113
+ | `SpanRange` | `SpanRange` | `./annotations` |
114
+ | `LocateInSpanOptions` | `LocateInSpanOptions` | `./annotations` |
115
+
116
+ Renamed on port (the module is no longer comment-specific). Its test carries
117
+ over unmodified.
118
+
119
+ ## 5. `app/src/shared/comment-editor-handle.ts`
120
+
121
+ | Zusammen symbol | Package export | Subpath |
122
+ |---|---|---|
123
+ | `CommentEditorHandle` | `AnnotationEditorHandle` | `./annotations` and `.` |
124
+
125
+ Renamed, and its `removeMarksForComment` member is `removeMarksForAnnotation`.
126
+ `collectLiveAnchorSnapshots` keeps its name and signature. The handle is
127
+ populated by passing `annotationEditorHandleRef` to `<Editor>`; Zusammen's host
128
+ reads it exactly as it does today.
129
+
130
+ ## 6. `app/src/shared/comment-thread.ts` — editor-domain parts only
131
+
132
+ | Zusammen symbol | Package export | Subpath |
133
+ |---|---|---|
134
+ | `MarkerTarget` | `MarkerTarget` | `./annotations` and `.` |
135
+ | `shouldPlaceLiveMark` | `shouldPlaceLiveMark` | `./annotations` |
136
+ | `deriveMarkerTargets` | `deriveMarkerTargets` (re-expressed — see below) | `./annotations` |
137
+
138
+ `MarkerTarget`'s `threadId` field is `annotationId` in the package, and it gains
139
+ a `kind`. `shouldPlaceLiveMark` is identical, and the `shouldPlaceLiveMark`
140
+ block of `comment-thread.test.ts` carries over.
141
+
142
+ `deriveMarkerTargets` is **re-expressed, not ported**: Zusammen's takes
143
+ `readonly Thread[]` and reaches into `thread.root` (a `Comment`), both of which
144
+ are app-domain types under ADR-075. The package's takes
145
+ `(annotations, kinds)`. Zusammen keeps its own thread→annotation projection —
146
+ a few lines over types it already owns — and calls the package's version, or
147
+ skips it entirely and passes `annotations` to `<Editor>`, which derives targets
148
+ internally.
149
+
150
+ ## 7. `app/src/editor/app/editor/comment-anchor-marks.ts`
151
+
152
+ Ported to `app/editor/annotation-marks.ts`. These are package-internal: they are
153
+ Lexical-bound and are consumed by the package's own plugins, which now provide
154
+ the behaviour Zusammen wired up by hand. The capability each one provided is
155
+ still reachable, as shown:
156
+
157
+ | Zusammen symbol | How the capability is reached | Subpath |
158
+ |---|---|---|
159
+ | `wrapNativeRangeInMark` | `<Editor onCreateAnnotation>` + `OPEN_ANNOTATION_COMPOSER_COMMAND` | `.` |
160
+ | `readAnchorFields` | same — the captured anchor arrives on `AnnotationCreateEvent.anchor` | `.` |
161
+ | `placeMarkForAnchor` | `<Editor annotations>` — the surface places marks per the kind's live-mark policy | `.` |
162
+ | `collectLiveAnchorSnapshots` | `AnnotationEditorHandle.collectLiveAnchorSnapshots` | `.` |
163
+ | `removeMarksForComment` | `AnnotationEditorHandle.removeMarksForAnnotation` | `.` |
164
+ | `hasLiveMark` | not exported — see "Known gaps" | — |
165
+ | `markElementsForId` | not exported — see "Known gaps" | — |
166
+
167
+ ## 8. Editor plugins
168
+
169
+ | Zusammen module | Package equivalent | Subpath |
170
+ |---|---|---|
171
+ | `CommentSelectionPlugin.tsx` | `AnnotationPlugin` (mounted by `<Editor>`) | via `.` |
172
+ | `CommentMarkerPlugin.tsx` | `AnnotationMarkerPlugin` (mounted by `<Editor>`) | via `.` |
173
+ | `commentCommands.ts` | `OPEN_ANNOTATION_COMPOSER_COMMAND` | `.` |
174
+ | `Editor.tsx`'s `CommentMarkPlacementPlugin` | internal to `AnnotationSurface`, driven by `annotations` | via `.` |
175
+ | `Editor.tsx`'s `CommentEditorHandlePlugin` | internal, driven by `annotationEditorHandleRef` | via `.` |
176
+
177
+ The plugins are not exported individually and do not need to be: `<Editor>`
178
+ mounts them from `annotationKinds`. One behavioural difference Zusammen should
179
+ know about — `CommentSelectionPlugin` renders its own composer (a textarea with
180
+ Cancel/Comment buttons); `AnnotationPlugin` is headless and fires
181
+ `onCreateAnnotation` with the captured anchor and the selection rect instead.
182
+ The composer is host UX under ADR-075, so Zusammen brings its own. This is a
183
+ deliberate boundary choice, not a missing capability.
184
+
185
+ ## 9. `app/src/editor/app/mapper/` — mark transparency
186
+
187
+ | Zusammen capability | Package equivalent | Subpath |
188
+ |---|---|---|
189
+ | `effectiveChildren` mark-transparency | applied unconditionally inside `exportLexicalToMdast` | `.` |
190
+ | `setAnnotateTarget` | package-internal (used by the capture primitive) | — |
191
+ | `markOpenToken` / `markCloseToken` | package-internal | — |
192
+ | `OffsetSpan` + `importMarkdownToLexical*WithOffsets` | package-internal; `<Editor>` collects spans itself | — |
193
+
194
+ Mark transparency is a property of the export, not an API: a document with live
195
+ marks exports byte-identically to the same document without them, asserted
196
+ across the whole round-trip fixture corpus. Zusammen gets it by using the
197
+ package's mapper at all.
198
+
199
+ The sentinel and offset machinery is internal because its only consumer is the
200
+ capture primitive, which the package now owns. Zusammen had to expose them
201
+ because its host drove capture directly; it no longer does.
202
+
203
+ ---
204
+
205
+ ## Known gaps
206
+
207
+ None that block adoption. Two symbols are intentionally unexported, with a
208
+ rationale and a cheap escalation path:
209
+
210
+ **`hasLiveMark(editor, id)` and `markElementsForId(editor, id)`.** Both take a
211
+ `LexicalEditor`, which a host outside the composer tree does not have. In
212
+ Zusammen both are used *inside* the editor tree — by `CommentMarkerPlugin` for
213
+ decoration and scroll-to — and the package's `AnnotationMarkerPlugin` now does
214
+ that work. If Zusammen finds a host-side use during adoption, the fix is to add
215
+ them to `AnnotationEditorHandle`, which already exists for exactly this purpose
216
+ and is the same one-line-per-method change `removeMarksForAnnotation` was. That
217
+ is a considered export under ADR-075, not a fork.
218
+
219
+ ## Deliberate app-domain exclusions
220
+
221
+ These are **not gaps**. Each stays app-side because ADR-075 draws the seam at
222
+ persistence, and each of these is on the persistence side of it.
223
+
224
+ | Symbol | Why it stays in Zusammen |
225
+ |---|---|
226
+ | `deriveMarkerTargets(threads: Thread[])` (original signature) | Takes `Thread`/`Comment` — Zusammen's own persistence model. The package cannot know their shape without importing the app's domain. Re-expressed over `Annotation[]` instead. |
227
+ | `anchorOutcomeOf(comment: Comment)` | Reads a comment's last resolution event out of the WAL-reconstructed comment. That is Zusammen's event store's business; the package accepts an already-computed `outcome` on each annotation. |
228
+ | `deriveWorkspaceCommentList` | App-domain by the issue's own instruction — cross-document browse UI, not an editor capability. |
229
+ | `comment-store.ts`, `comment-anchoring.ts`, `comment-model.ts`, `renderer/comments/**`, `renderer/stores/comment*Store.ts` | Persistence, identity, lifecycle (resolve/reopen) and panel UX. FR-005 forbids all of it in the package. |
230
+
231
+ The same rule applies symmetrically to Liminis: its correction store, and the
232
+ accept/reject lifecycle, stay app-side too.