@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.
- package/LICENSE +34 -0
- package/README.md +376 -0
- package/dist/annotations/anchor-align.d.ts +50 -0
- package/dist/annotations/anchor-align.js +74 -0
- package/dist/annotations/anchor-model.d.ts +81 -0
- package/dist/annotations/anchor-model.js +80 -0
- package/dist/annotations/anchor-resolver.d.ts +92 -0
- package/dist/annotations/anchor-resolver.js +231 -0
- package/dist/annotations/block-structure.d.ts +65 -0
- package/dist/annotations/block-structure.js +113 -0
- package/dist/annotations/types.d.ts +159 -0
- package/dist/annotations/types.js +50 -0
- package/dist/annotations.d.ts +25 -0
- package/dist/annotations.js +27 -0
- package/dist/app/App.d.ts +64 -0
- package/dist/app/App.js +180 -0
- package/dist/app/editor/AmbientCorrectionPlugin.d.ts +18 -0
- package/dist/app/editor/AmbientCorrectionPlugin.js +185 -0
- package/dist/app/editor/AnchorScrollPlugin.d.ts +16 -0
- package/dist/app/editor/AnchorScrollPlugin.js +134 -0
- package/dist/app/editor/AnnotationMarkerPlugin.d.ts +24 -0
- package/dist/app/editor/AnnotationMarkerPlugin.js +224 -0
- package/dist/app/editor/AnnotationPlugin.d.ts +42 -0
- package/dist/app/editor/AnnotationPlugin.js +135 -0
- package/dist/app/editor/AnnotationSurface.d.ts +34 -0
- package/dist/app/editor/AnnotationSurface.js +115 -0
- package/dist/app/editor/AssetContext.d.ts +20 -0
- package/dist/app/editor/AssetContext.js +81 -0
- package/dist/app/editor/BlockClickPlugin.d.ts +8 -0
- package/dist/app/editor/BlockClickPlugin.js +126 -0
- package/dist/app/editor/CodeBlockPlugin.d.ts +1 -0
- package/dist/app/editor/CodeBlockPlugin.js +219 -0
- package/dist/app/editor/CorrectionPanelPlugin.d.ts +14 -0
- package/dist/app/editor/CorrectionPanelPlugin.js +333 -0
- package/dist/app/editor/DragHandlePlugin.d.ts +1 -0
- package/dist/app/editor/DragHandlePlugin.js +300 -0
- package/dist/app/editor/Editor.d.ts +84 -0
- package/dist/app/editor/Editor.js +539 -0
- package/dist/app/editor/FrontmatterPlugin.d.ts +19 -0
- package/dist/app/editor/FrontmatterPlugin.js +301 -0
- package/dist/app/editor/ImageModal.d.ts +13 -0
- package/dist/app/editor/ImageModal.js +267 -0
- package/dist/app/editor/ImagePlugin.d.ts +3 -0
- package/dist/app/editor/ImagePlugin.js +187 -0
- package/dist/app/editor/LinkClickPlugin.d.ts +23 -0
- package/dist/app/editor/LinkClickPlugin.js +90 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.d.ts +49 -0
- package/dist/app/editor/MarkdownShortcutsPlugin.js +373 -0
- package/dist/app/editor/OrderedTaskListPlugin.d.ts +21 -0
- package/dist/app/editor/OrderedTaskListPlugin.js +78 -0
- package/dist/app/editor/SearchPlugin.d.ts +1 -0
- package/dist/app/editor/SearchPlugin.js +379 -0
- package/dist/app/editor/SelectionContextMenuPlugin.d.ts +29 -0
- package/dist/app/editor/SelectionContextMenuPlugin.js +160 -0
- package/dist/app/editor/SlashMenu.d.ts +11 -0
- package/dist/app/editor/SlashMenu.js +494 -0
- package/dist/app/editor/SlashMenuPlugin.d.ts +1 -0
- package/dist/app/editor/SlashMenuPlugin.js +168 -0
- package/dist/app/editor/TableActionsPlugin.d.ts +1 -0
- package/dist/app/editor/TableActionsPlugin.js +398 -0
- package/dist/app/editor/TogglePlugin.d.ts +1 -0
- package/dist/app/editor/TogglePlugin.js +159 -0
- package/dist/app/editor/Toolbar.d.ts +10 -0
- package/dist/app/editor/Toolbar.js +263 -0
- package/dist/app/editor/WikiLinkExistencePlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkExistencePlugin.js +104 -0
- package/dist/app/editor/WikiLinkFormatPlugin.d.ts +17 -0
- package/dist/app/editor/WikiLinkFormatPlugin.js +203 -0
- package/dist/app/editor/annotation-marks.d.ts +177 -0
- package/dist/app/editor/annotation-marks.js +750 -0
- package/dist/app/editor/annotationCommands.d.ts +13 -0
- package/dist/app/editor/annotationCommands.js +11 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.d.ts +35 -0
- package/dist/app/editor/c4/C4InteractiveRenderer.js +299 -0
- package/dist/app/editor/c4/edge-clipping.d.ts +24 -0
- package/dist/app/editor/c4/edge-clipping.js +139 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.d.ts +38 -0
- package/dist/app/editor/c4/hooks/useC4DiagramDrag.js +112 -0
- package/dist/app/editor/c4/layout.d.ts +25 -0
- package/dist/app/editor/c4/layout.js +839 -0
- package/dist/app/editor/c4/parser.d.ts +19 -0
- package/dist/app/editor/c4/parser.js +410 -0
- package/dist/app/editor/c4/render-to-string.d.ts +24 -0
- package/dist/app/editor/c4/render-to-string.js +34 -0
- package/dist/app/editor/c4/renderer.d.ts +64 -0
- package/dist/app/editor/c4/renderer.js +569 -0
- package/dist/app/editor/c4/types.d.ts +203 -0
- package/dist/app/editor/c4/types.js +43 -0
- package/dist/app/editor/correction-yaml.d.ts +10 -0
- package/dist/app/editor/correction-yaml.js +40 -0
- package/dist/app/editor/editorNodes.d.ts +9 -0
- package/dist/app/editor/editorNodes.js +36 -0
- package/dist/app/editor/index.d.ts +1 -0
- package/dist/app/editor/index.js +1 -0
- package/dist/app/editor/mark-overlap-resolver.d.ts +6 -0
- package/dist/app/editor/mark-overlap-resolver.js +45 -0
- package/dist/app/editor/nodes/C4Component.d.ts +14 -0
- package/dist/app/editor/nodes/C4Component.js +284 -0
- package/dist/app/editor/nodes/C4Node.d.ts +32 -0
- package/dist/app/editor/nodes/C4Node.js +112 -0
- package/dist/app/editor/nodes/CalloutNode.d.ts +23 -0
- package/dist/app/editor/nodes/CalloutNode.js +94 -0
- package/dist/app/editor/nodes/CustomLinkNode.d.ts +57 -0
- package/dist/app/editor/nodes/CustomLinkNode.js +209 -0
- package/dist/app/editor/nodes/CustomListItemNode.d.ts +46 -0
- package/dist/app/editor/nodes/CustomListItemNode.js +120 -0
- package/dist/app/editor/nodes/CustomListNode.d.ts +27 -0
- package/dist/app/editor/nodes/CustomListNode.js +54 -0
- package/dist/app/editor/nodes/DefinitionListNode.d.ts +44 -0
- package/dist/app/editor/nodes/DefinitionListNode.js +151 -0
- package/dist/app/editor/nodes/DiagramContextMenu.d.ts +45 -0
- package/dist/app/editor/nodes/DiagramContextMenu.js +116 -0
- package/dist/app/editor/nodes/EquationComponent.d.ts +17 -0
- package/dist/app/editor/nodes/EquationComponent.js +130 -0
- package/dist/app/editor/nodes/EquationNode.d.ts +43 -0
- package/dist/app/editor/nodes/EquationNode.js +179 -0
- package/dist/app/editor/nodes/FootnoteNode.d.ts +40 -0
- package/dist/app/editor/nodes/FootnoteNode.js +151 -0
- package/dist/app/editor/nodes/FrontmatterNode.d.ts +24 -0
- package/dist/app/editor/nodes/FrontmatterNode.js +88 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.d.ts +17 -0
- package/dist/app/editor/nodes/HorizontalRuleNode.js +57 -0
- package/dist/app/editor/nodes/HtmlNode.d.ts +32 -0
- package/dist/app/editor/nodes/HtmlNode.js +121 -0
- package/dist/app/editor/nodes/ImageComponent.d.ts +11 -0
- package/dist/app/editor/nodes/ImageComponent.js +228 -0
- package/dist/app/editor/nodes/ImageNode.d.ts +53 -0
- package/dist/app/editor/nodes/ImageNode.js +183 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.d.ts +42 -0
- package/dist/app/editor/nodes/ListItemParagraphBreakNode.js +95 -0
- package/dist/app/editor/nodes/MermaidComponent.d.ts +11 -0
- package/dist/app/editor/nodes/MermaidComponent.js +216 -0
- package/dist/app/editor/nodes/MermaidNode.d.ts +27 -0
- package/dist/app/editor/nodes/MermaidNode.js +96 -0
- package/dist/app/editor/nodes/ToggleNode.d.ts +56 -0
- package/dist/app/editor/nodes/ToggleNode.js +237 -0
- package/dist/app/editor/nodes/diagram-context-menu.d.ts +11 -0
- package/dist/app/editor/nodes/diagram-context-menu.js +70 -0
- package/dist/app/editor/nodes/diagram-utils.d.ts +13 -0
- package/dist/app/editor/nodes/diagram-utils.js +29 -0
- package/dist/app/editor/nodes/index.d.ts +30 -0
- package/dist/app/editor/nodes/index.js +17 -0
- package/dist/app/editor/utils.d.ts +2 -0
- package/dist/app/editor/utils.js +17 -0
- package/dist/app/mapper/index.d.ts +3 -0
- package/dist/app/mapper/index.js +2 -0
- package/dist/app/mapper/lexicalToMdast.d.ts +62 -0
- package/dist/app/mapper/lexicalToMdast.js +1937 -0
- package/dist/app/mapper/mdastToLexical.d.ts +31 -0
- package/dist/app/mapper/mdastToLexical.js +1075 -0
- package/dist/contract.d.ts +8 -0
- package/dist/contract.js +8 -0
- package/dist/headless.d.ts +40 -0
- package/dist/headless.js +45 -0
- package/dist/host/context.d.ts +20 -0
- package/dist/host/context.js +24 -0
- package/dist/host/defaults.d.ts +21 -0
- package/dist/host/defaults.js +47 -0
- package/dist/host/messages.d.ts +26 -0
- package/dist/host/messages.js +47 -0
- package/dist/host/types.d.ts +75 -0
- package/dist/host/types.js +13 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +51 -0
- package/dist/markdown/annotate-sentinels.d.ts +42 -0
- package/dist/markdown/annotate-sentinels.js +86 -0
- package/dist/markdown/parse.d.ts +56 -0
- package/dist/markdown/parse.js +516 -0
- package/dist/markdown/stringify.d.ts +7 -0
- package/dist/markdown/stringify.js +439 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/LICENSE +21 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/README.md +41 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.d.ts +72 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/from-markdown.js +127 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.d.ts +8 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/index.js +6 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.d.ts +17 -0
- package/dist/markdown/vendor/mdast-util-wiki-link/to-markdown.js +33 -0
- package/dist/markdown.d.ts +28 -0
- package/dist/markdown.js +44 -0
- package/dist/mathjax-config.d.ts +89 -0
- package/dist/mathjax-config.js +133 -0
- package/dist/nodes.d.ts +34 -0
- package/dist/nodes.js +34 -0
- package/dist/stores/correctionStore.d.ts +15 -0
- package/dist/stores/correctionStore.js +12 -0
- package/dist/stores/editorStore.d.ts +16 -0
- package/dist/stores/editorStore.js +27 -0
- package/dist/styles.css +2477 -0
- package/dist/types.d.ts +296 -0
- package/dist/types.js +91 -0
- package/dist/utils/file-types.d.ts +10 -0
- package/dist/utils/file-types.js +46 -0
- package/docs/annotations.md +134 -0
- package/docs/decisions/README.md +72 -0
- package/docs/decisions/adr-002.md +27 -0
- package/docs/decisions/adr-007.md +27 -0
- package/docs/decisions/adr-010.md +26 -0
- package/docs/decisions/adr-024.md +124 -0
- package/docs/decisions/adr-024b.md +0 -0
- package/docs/decisions/adr-025.md +103 -0
- package/docs/decisions/adr-027.md +66 -0
- package/docs/decisions/adr-057.md +67 -0
- package/docs/decisions/adr-070.md +131 -0
- package/docs/decisions/adr-075.md +410 -0
- package/docs/decisions/adr-076.md +183 -0
- package/docs/decisions/adr-077.md +410 -0
- package/docs/decisions/adr-078.md +243 -0
- package/docs/decisions/adr-079.md +167 -0
- package/docs/decisions/adr-080.md +325 -0
- package/docs/decisions/adr-081.md +108 -0
- package/docs/decisions/adr-082.md +123 -0
- package/docs/decisions/adr-083.md +204 -0
- package/docs/decisions/adr-084.md +179 -0
- package/docs/editor-api.md +106 -0
- package/docs/markdown-pipeline.md +282 -0
- package/docs/provenance.md +106 -0
- package/docs/zusammen-editor-capability-map.md +232 -0
- package/package.json +175 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# ADR-077: Comments and Corrections Are Two Kinds of One Annotation Mechanism
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-04
|
|
4
|
+
**Status:** Accepted (amended by ADR-080 — see the two notes marked *Amended by ADR-080* below)
|
|
5
|
+
**Supersedes:** none
|
|
6
|
+
**Amends:** ADR-075 (adds a sixth export subpath to the surface it fixed at five)
|
|
7
|
+
**Amended by:** ADR-080 (#964 supplies the Liminis consumer this ADR said did not exist, and wires `onCreateAnnotation`)
|
|
8
|
+
**Issue:** #939
|
|
9
|
+
|
|
10
|
+
> Authored as ADR-076. Renumbered to 077 when this branch rebased onto `main`,
|
|
11
|
+
> because #943 landed its own ADR-076 first. No numbering collision is left
|
|
12
|
+
> behind — unlike ADR-024/024b, which the index still records as historical.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
|
|
16
|
+
Two near-identical editor trees had been diverging. Zusammen accreted a review
|
|
17
|
+
**commenting** feature — durable, text-anchored comments rendered as live
|
|
18
|
+
`@lexical/mark` MarkNodes, transparent on export. Liminis accreted an AI
|
|
19
|
+
**correction** feature — a context-menu entry, a suggestion panel, and a
|
|
20
|
+
term-substitution scan.
|
|
21
|
+
|
|
22
|
+
ADR-075 (#938) stopped the divergence for the *core* by making the editor one
|
|
23
|
+
package with a curated public surface and a persistence-drawn seam. It
|
|
24
|
+
deliberately left the two annotation-shaped features unreconciled. That left a
|
|
25
|
+
live problem: the package shipped a correction-only code path, and Zusammen
|
|
26
|
+
could not adopt the package without forking it back — the exact failure ADR-075
|
|
27
|
+
set out to end.
|
|
28
|
+
|
|
29
|
+
The two features are the same shape. Both are a range-anchored marker over
|
|
30
|
+
document text, with data flowing in from a host and events flowing out to it.
|
|
31
|
+
Only their storage, lifecycle and presentation differ, and all three of those
|
|
32
|
+
are already on the host's side of ADR-075's seam.
|
|
33
|
+
|
|
34
|
+
A complicating fact, found while implementing: **Liminis's correction feature
|
|
35
|
+
has no marks, no anchors and no resolution.** The issue's premise — that
|
|
36
|
+
corrections are host-injected annotations with inline accept/reject — described
|
|
37
|
+
a feature that does not exist. A literal reading of "corrections must use the
|
|
38
|
+
one marker renderer" and "no user-visible correction change" cannot both hold,
|
|
39
|
+
because giving corrections in-document markers *is* a visible change.
|
|
40
|
+
|
|
41
|
+
## Decision
|
|
42
|
+
|
|
43
|
+
**One annotation mechanism, baked into the package, with comments and
|
|
44
|
+
corrections as two configured usages of it.**
|
|
45
|
+
|
|
46
|
+
An annotation is `{ id, kind, anchor, outcome?, presentation?, payload? }`. The
|
|
47
|
+
package owns the mechanics: the durable anchor model, selection→anchor capture,
|
|
48
|
+
the fuzzy anchor resolver, MarkNode marker rendering, mark-transparency on
|
|
49
|
+
export, the live-mark placement policy, and the in-document affordances. A host
|
|
50
|
+
owns everything else.
|
|
51
|
+
|
|
52
|
+
### The kind configuration is the only difference
|
|
53
|
+
|
|
54
|
+
`AnnotationKindConfig` — marker style, create affordance, live-mark policy, and
|
|
55
|
+
whether capture retains its mark — is the sole thing distinguishing a comment
|
|
56
|
+
from a correction. There is exactly one anchor model, one resolver, and one
|
|
57
|
+
marker renderer, with no kind-specific variant of any of the three.
|
|
58
|
+
|
|
59
|
+
### Corrections render no live mark
|
|
60
|
+
|
|
61
|
+
The `correction` kind is configured `markerStyle: 'none'`, `livemarkPolicy:
|
|
62
|
+
() => false`, `retainMarkOnCreate: false`. Correction authoring is *wired* to
|
|
63
|
+
the same capture primitive comments use: `SelectionContextMenuPlugin`'s
|
|
64
|
+
"Correction…" entry dispatches `OPEN_ANNOTATION_COMPOSER_COMMAND`, whose
|
|
65
|
+
handler wraps the selection in a transient MarkNode, reads the anchor back
|
|
66
|
+
from it, and — because `retainMarkOnCreate` is false — discards the mark
|
|
67
|
+
again, so nothing ever paints. That handler is not mounted in Liminis today;
|
|
68
|
+
see "What Liminis actually runs today" below, which is the authoritative
|
|
69
|
+
statement and takes precedence over any looser reading of this paragraph.
|
|
70
|
+
|
|
71
|
+
This is what resolves the contradiction above. Corrections are expressed
|
|
72
|
+
through the mechanism — their authoring entry point dispatches its command,
|
|
73
|
+
their behaviour is kind configuration — while rendering exactly what they
|
|
74
|
+
rendered before: nothing.
|
|
75
|
+
Parity is preserved, and the reviewer's test for "is this really unified?" —
|
|
76
|
+
point at the kind-config objects as the only difference — holds.
|
|
77
|
+
|
|
78
|
+
`markerStyle: 'none'` is enforced structurally rather than by convention:
|
|
79
|
+
`deriveMarkerTargets` drops such annotations outright, ahead of and in
|
|
80
|
+
precedence over `livemarkPolicy`. A marker target is not a styling hint — it
|
|
81
|
+
is what causes a real `MarkNode`, and therefore a `<mark>` element, to be
|
|
82
|
+
placed. The marker plugin skips decoration for `none`, so that element would
|
|
83
|
+
carry no `annotation-mark-*` class, and the package's stylesheet neutralizes
|
|
84
|
+
the UA's default yellow `<mark>` background only under those classes. Without
|
|
85
|
+
the gate, a host that configured `none` and left `livemarkPolicy` at its
|
|
86
|
+
default would get a persistent uncontrolled highlight — the opposite of what
|
|
87
|
+
`none` promises. Liminis's explicit `livemarkPolicy: () => false` is now
|
|
88
|
+
belt-and-braces rather than load-bearing.
|
|
89
|
+
|
|
90
|
+
**What Liminis actually runs today.** The create handler lives in
|
|
91
|
+
`AnnotationPlugin`, which the annotation surface mounts only when a host
|
|
92
|
+
supplies `onCreateAnnotation`. `liminis-app` supplies `annotationKinds` but
|
|
93
|
+
*not* `onCreateAnnotation` — it has nothing to do with a correction anchor,
|
|
94
|
+
since corrections have no durable anchors (below). So in Liminis the
|
|
95
|
+
"Correction…" dispatch currently finds no listener and is inert: the correction
|
|
96
|
+
panel opens exactly as it always has, and no capture runs. This is deliberate
|
|
97
|
+
— running a wrap/read/unwrap cycle purely to discard the result would be cost
|
|
98
|
+
and FR-006 risk on a user-visible path for no product value — but it means the
|
|
99
|
+
claim "corrections exercise the capture primitive" is true of the *wiring*, not
|
|
100
|
+
of a running Liminis session.
|
|
101
|
+
|
|
102
|
+
> **Amended by ADR-080 (#964).** No longer true. `EditorColumn` now supplies
|
|
103
|
+
> `onCreateAnnotation` for the `comment` kind, and `AnnotationPlugin` registers
|
|
104
|
+
> one handler for *every* configured kind — so the "Correction…" dispatch does
|
|
105
|
+
> find a listener, and the wrap/read/unwrap cycle this paragraph declined to
|
|
106
|
+
> pay for now runs on the correction path, with its result discarded by the
|
|
107
|
+
> host handler. The cost was accepted rather than chosen: removing the
|
|
108
|
+
> correction affordance would change correction behaviour, and suppressing the
|
|
109
|
+
> capture would need a package change. User-visible behaviour is unchanged and
|
|
110
|
+
> guarded. See ADR-080 §6. FR-001's substantive guarantee is unaffected: the
|
|
111
|
+
package contains exactly one anchor model, one resolver and one marker
|
|
112
|
+
renderer, and no correction-specific variant of any of them. A host that does
|
|
113
|
+
want correction anchors turns them on by passing `onCreateAnnotation`, with no
|
|
114
|
+
package change.
|
|
115
|
+
|
|
116
|
+
**What that costs.** `annotationsEnabled` is true for any non-empty
|
|
117
|
+
`annotationKinds`, so configuring the `correction` kind makes the editor load
|
|
118
|
+
the lazy `AnnotationSurface` chunk (~49 KB) and its annotation modules at
|
|
119
|
+
mount — machinery Liminis gains no behaviour from today. Measured live on a
|
|
120
|
+
running app during Validate: the annotation code stays in its own chunk and
|
|
121
|
+
never reaches windows that host no editor, so #938's bundle win survives
|
|
122
|
+
(graph window +417 bytes against `main`). We accept the editor-window cost for
|
|
123
|
+
this phase: registering the kind keeps the host-side wiring on the real path
|
|
124
|
+
rather than dead, and dropping the config to reclaim the chunk would trade that
|
|
125
|
+
away for a saving that only matters until the callback is supplied.
|
|
126
|
+
|
|
127
|
+
Corrections do **not** exercise the resolver. They have no cross-session
|
|
128
|
+
anchors; their lifetime and storage remain the app-side correction store. This
|
|
129
|
+
is a refactor, not a data-model change for corrections.
|
|
130
|
+
|
|
131
|
+
### A sixth export subpath: `./annotations`
|
|
132
|
+
|
|
133
|
+
ADR-075 fixed the surface at five subpaths and permitted adding a *considered*
|
|
134
|
+
export. This is one.
|
|
135
|
+
|
|
136
|
+
The headless annotation surface — anchor model, resolver, block structure, span
|
|
137
|
+
alignment, and the annotation types — must be callable outside a rendered
|
|
138
|
+
editor (Zusammen resolves anchors in its main process). It therefore cannot
|
|
139
|
+
live on the root barrel. It does not belong on `./headless` either, whose
|
|
140
|
+
`mathjax-config` re-export drags ~1.9 MB of side-effectful imports; and
|
|
141
|
+
`./markdown` is contractually pure-mdast, while the anchor schema needs `zod`.
|
|
142
|
+
|
|
143
|
+
A dedicated subpath also gives the "annotations are off by default" requirement
|
|
144
|
+
a crisp assertion target. Its import graph is tested to reach no Lexical, React,
|
|
145
|
+
DOM, MathJax, Mermaid, C4 or Prism module, and nothing outside `src/annotations/`.
|
|
146
|
+
|
|
147
|
+
### The mechanism is opt-in, behind a lazy boundary
|
|
148
|
+
|
|
149
|
+
A consumer that configures no kinds gets nothing: no marker, no affordance, no
|
|
150
|
+
registered command — and, more importantly, no annotation module in its import
|
|
151
|
+
graph. `Editor.tsx` reaches the annotation surface only through
|
|
152
|
+
`lazy(() => import('./AnnotationSurface'))`, and imports the annotation prop
|
|
153
|
+
*types* only as `import type`, which the compiler erases.
|
|
154
|
+
|
|
155
|
+
Two exceptions are deliberate and scoped:
|
|
156
|
+
|
|
157
|
+
- **`MarkNode` registration** and **mapper mark-transparency** are
|
|
158
|
+
unconditional. The mapper is shared and cannot be kind-aware; making it so
|
|
159
|
+
would fork the round-trip path, which is the exact divergence #938 ended.
|
|
160
|
+
`MarkNode` is an inert node when nothing places one.
|
|
161
|
+
- **`annotationCommands.ts`** stays statically importable. It declares one
|
|
162
|
+
Lexical command and imports nothing but `lexical`, carrying no anchor, mark or
|
|
163
|
+
resolver machinery. That is what lets the selection context menu enter the
|
|
164
|
+
mechanism without dragging any of it into the disabled graph. A test pins the
|
|
165
|
+
module to that single import.
|
|
166
|
+
|
|
167
|
+
### Marker styling ships with the package
|
|
168
|
+
|
|
169
|
+
`AnnotationMarkerPlugin` decorates a live mark's element with
|
|
170
|
+
`annotation-mark-<markerStyle>` (`highlight` / `squiggle`), plus
|
|
171
|
+
`annotation-mark-active` and `annotation-mark-pulse` for state. Default rules
|
|
172
|
+
for all of them are in `packages/editor/src/styles.css`.
|
|
173
|
+
|
|
174
|
+
They ship in the package rather than being left to the host because the
|
|
175
|
+
mechanism is a baked-in feature, not an extension point (see the Context above):
|
|
176
|
+
a host that enables the `comment` kind should get a visible, working marker
|
|
177
|
+
without first having to discover and reimplement those exact class names. The
|
|
178
|
+
failure mode is otherwise silent and invisible from inside Liminis — anchors
|
|
179
|
+
place, activation fires, and nothing renders.
|
|
180
|
+
|
|
181
|
+
Hosts that want a different look override the rules, or supply a per-annotation
|
|
182
|
+
class through `presentation.className`, which the plugin applies alongside the
|
|
183
|
+
kind's own style. A test asserts every `AnnotationMarkerStyle` other than `none`
|
|
184
|
+
has a rule, cross-checked against the union declaration so a new style cannot be
|
|
185
|
+
added without one.
|
|
186
|
+
|
|
187
|
+
**Shipping the rules is not the same as delivering them.** Nothing in the
|
|
188
|
+
package injects the stylesheet: neither the root barrel nor `<Editor>` imports
|
|
189
|
+
`styles.css`, deliberately — a component that imports CSS forces it on every
|
|
190
|
+
consumer, including the markdown-only ones that take `./markdown` precisely to
|
|
191
|
+
stay clear of the React surface. So a host that enables a marker-visible kind
|
|
192
|
+
**must** import the stylesheet itself:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import '@liminis/editor/styles.css'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Miss it and the failure is the silent one described above, one step earlier:
|
|
199
|
+
anchors place, activation fires, and nothing renders. `liminis-app` does this in
|
|
200
|
+
`src/renderer/App.tsx`, and `editor-package-wiring.test.ts` pins all three
|
|
201
|
+
halves of the contract — the subpath is declared, the app performs the import,
|
|
202
|
+
and the package still does not perform it on the host's behalf.
|
|
203
|
+
|
|
204
|
+
### The seam stays at persistence
|
|
205
|
+
|
|
206
|
+
The package contains no persistence, actor/identity, or annotation-lifecycle
|
|
207
|
+
logic. Comment storage, correction storage, resolve/reopen, accept/reject, and
|
|
208
|
+
the semantic-relocation strategy stay in the consuming apps, reached only via
|
|
209
|
+
injected services or callbacks. `resolveAnchors` is async precisely because the
|
|
210
|
+
relocation seam may be host-supplied and slow; with no strategy supplied, an
|
|
211
|
+
unmatchable anchor yields `orphaned` rather than erroring.
|
|
212
|
+
|
|
213
|
+
Two of Zusammen's helpers are deliberately **not** ported:
|
|
214
|
+
`deriveMarkerTargets` and `anchorOutcomeOf` take Zusammen's `Comment`/`Thread`
|
|
215
|
+
types, which are app-domain. The package accepts already-derived annotations
|
|
216
|
+
instead and re-expresses `deriveMarkerTargets` over them.
|
|
217
|
+
|
|
218
|
+
## Consequences
|
|
219
|
+
|
|
220
|
+
**Good:**
|
|
221
|
+
|
|
222
|
+
- Zusammen can adopt the package without forking it. Every editor-domain
|
|
223
|
+
capability it has today maps to a package export (see
|
|
224
|
+
`docs/project_notes/zusammen-editor-capability-map.md`).
|
|
225
|
+
- One anchor model, one resolver, one marker renderer. A third annotation-shaped
|
|
226
|
+
feature is a configuration entry, not a new code path.
|
|
227
|
+
- Zusammen's anchor test suites carry over and pass unmodified, so the
|
|
228
|
+
classification outcomes and thresholds are preserved case-for-case rather
|
|
229
|
+
than reimplemented and hoped-for.
|
|
230
|
+
- Mark transparency is now asserted across the whole round-trip fixture corpus,
|
|
231
|
+
not just hand-built cases.
|
|
232
|
+
|
|
233
|
+
**Bad / accepted:**
|
|
234
|
+
|
|
235
|
+
- The resolver has **no Liminis consumer**. It exists for Zusammen and for the
|
|
236
|
+
ported tests. This is deliberate, and stated here so it isn't later mistaken
|
|
237
|
+
for dead code and deleted.
|
|
238
|
+
> **Amended by ADR-080 (#964).** It has one now:
|
|
239
|
+
> `liminis-app/src/main/comments/comment-anchoring.ts` calls `resolveAnchors`
|
|
240
|
+
> on every document open.
|
|
241
|
+
- Liminis now configures an annotation kind, which turns the mechanism on and
|
|
242
|
+
switches its import path to the offset-collecting variant. Invisible to users,
|
|
243
|
+
but it is no longer true that Liminis takes the annotations-disabled path.
|
|
244
|
+
- The sixth subpath is a small widening of a surface ADR-075 deliberately kept
|
|
245
|
+
narrow.
|
|
246
|
+
|
|
247
|
+
**Neutral:**
|
|
248
|
+
|
|
249
|
+
- The correction term-substitution scan remains separate from the resolver.
|
|
250
|
+
They answer different questions: the scan is exact word-boundary *candidate
|
|
251
|
+
discovery*; the resolver re-locates an anchor captured against an older
|
|
252
|
+
document version. "Fuzzy matching in exactly one module" constrains the
|
|
253
|
+
resolver, not the scan.
|
|
254
|
+
|
|
255
|
+
## Implementation Notes
|
|
256
|
+
|
|
257
|
+
Two constraints worth recording, both found by tests rather than by reading:
|
|
258
|
+
|
|
259
|
+
**A Lexical command handler runs inside an active update.** The nested update
|
|
260
|
+
that places a mark is queued, not applied, so reading the anchor back inline
|
|
261
|
+
always yields null. The anchor read is deferred to a microtask, which runs after
|
|
262
|
+
the outer update flushes. Zusammen reaches the same split by reading the anchor
|
|
263
|
+
at composer-submit time.
|
|
264
|
+
|
|
265
|
+
**`convertLinkNode`'s child loop silently drops unrecognized node types.** An
|
|
266
|
+
unflattened MarkNode over link text therefore deleted that text from the export
|
|
267
|
+
entirely (`[note](#x)` → `[](#x)`). Fourteen corpus fixtures caught this; it had
|
|
268
|
+
no equivalent in Zusammen's copy.
|
|
269
|
+
|
|
270
|
+
**Mid-resolution staleness needs no new guard.** `placeMarkForAnchor` already
|
|
271
|
+
re-verifies the anchor's target text against the current offset spans and
|
|
272
|
+
refuses to place on a mismatch, so a stale anchor no-ops and retries when the
|
|
273
|
+
next parse produces fresh spans. This is now a tested property rather than an
|
|
274
|
+
assumption.
|
|
275
|
+
|
|
276
|
+
### Overlapping annotations (#970)
|
|
277
|
+
|
|
278
|
+
**Placement resolves against the pristine tree, in absolute text offsets.**
|
|
279
|
+
`OffsetSpan[]` describes the parse that produced it, and placing a mark splits
|
|
280
|
+
the `TextNode` it lands in — Lexical's `splitText` keeps the original key on the
|
|
281
|
+
first segment, so after one placement the table can name offsets past the end of
|
|
282
|
+
what that key now holds. An earlier fix ordered a batch back to front, which
|
|
283
|
+
keeps *disjoint* entries valid but silently declined any entry whose range
|
|
284
|
+
overlapped, nested inside, or coincided with another's. `placeMarksForAnchors`
|
|
285
|
+
now runs two phases inside one `editor.update()`: resolve every entry against the
|
|
286
|
+
untouched tree into a character offset across all text nodes, then apply each
|
|
287
|
+
against the live tree. Marking changes no text, so those offsets are invariant
|
|
288
|
+
under every placement — the same invariant `$withPreservedSelection` relies on to
|
|
289
|
+
restore the caret. Placement order therefore no longer affects the outcome.
|
|
290
|
+
|
|
291
|
+
**Overlap is represented as id-unioned sibling marks, not nested ones.**
|
|
292
|
+
`$wrapSelectionInMarkNode` creates a nested `MarkNode` when a selection lands
|
|
293
|
+
inside an existing mark and never merges ids; its own source defers un-nesting to
|
|
294
|
+
`registerNestedElementResolver<MarkNode>`. Nothing registered one, so the shared
|
|
295
|
+
region of two overlapping annotations carried only the inner id and a click there
|
|
296
|
+
fired twice as it bubbled — while `AnnotationMarkerPlugin` was already written
|
|
297
|
+
for the opposite shape. `mark-overlap-resolver.ts` registers that resolver, so an
|
|
298
|
+
overlap becomes a run of siblings whose shared part carries both ids. It is a
|
|
299
|
+
node transform, so it runs inside the update that created the nesting and the
|
|
300
|
+
single-reconciliation guarantee is unaffected.
|
|
301
|
+
|
|
302
|
+
**A range over a whole inline construct recovers the whole construct.** The
|
|
303
|
+
guarantee is deliberately scoped that way — a boundary never *splits* a run of
|
|
304
|
+
inline syntax, whatever the mark covers, but only whole coverage moves it
|
|
305
|
+
outside the construct (see the paragraph after next). The annotated-serialize
|
|
306
|
+
sentinel used to be spliced into a mark's first/last *text leaf*, and that walk
|
|
307
|
+
recursed into inline element children — so a mark over
|
|
308
|
+
`it [rests](https://example.com)` recovered as `it [rests`. `lexicalToMdast.ts`
|
|
309
|
+
now hoists a boundary token outside every inline construct whose whole rendered
|
|
310
|
+
content the mark covers, emitting it as its own mdast text node beside the
|
|
311
|
+
construct; nested constructs resolve to the outermost.
|
|
312
|
+
|
|
313
|
+
The rule is *whole* coverage, not any coverage. A mark covering only part of a
|
|
314
|
+
construct's text keeps today's splice, because widening it would change what the
|
|
315
|
+
annotation means: `big` inside `**big world**` would recover as `**big world**`
|
|
316
|
+
and re-place onto `big world`. Whole coverage has no such problem — re-placing
|
|
317
|
+
`[project docs](…)` snaps back inside the syntax and highlights `project docs`
|
|
318
|
+
again. This is a deliberate deviation from #970's FR-014, whose second clause
|
|
319
|
+
would widen partial coverage too; the first clause (no boundary strictly inside a
|
|
320
|
+
delimiter run) holds either way, since boundaries always sit at text-leaf edges.
|
|
321
|
+
|
|
322
|
+
**Annotate mode must not be visible to any *decision* the export makes.** The
|
|
323
|
+
whole range calculation is offset arithmetic against a plain export of the same
|
|
324
|
+
state, so the annotated form has to differ by its tokens and nothing else. Two
|
|
325
|
+
pre-existing violations surfaced when that property was given a corpus-wide test
|
|
326
|
+
(`annotated-serialize-corpus.test.ts`): the list-item explicit-task-marker check
|
|
327
|
+
and stringify's paragraph-ends-with-colon join rule both matched raw text values
|
|
328
|
+
that a token had been spliced into. Both now read the sentinel-free form via
|
|
329
|
+
`markdown/annotate-sentinels.ts`. Any new decision made from a text value has the
|
|
330
|
+
same obligation.
|
|
331
|
+
|
|
332
|
+
**Host-side mitigations in `liminis-app` became dead on merge, and were
|
|
333
|
+
removed.** *(Written before either PR merged, and kept as the reasoning behind
|
|
334
|
+
the checklist — see the authoritative status note below for what actually
|
|
335
|
+
happened.)* Both defects were live long enough for #964 (PR #967) to ship around
|
|
336
|
+
them, and neither mitigation was visible from this worktree at the time — PR #967
|
|
337
|
+
was open against the same `main` this branch forked from, so the files named
|
|
338
|
+
below did not exist here and could not be edited as part of this change.
|
|
339
|
+
Whichever of the two PRs merged **second** had to carry the removals; if that
|
|
340
|
+
were #967, this list was the checklist.
|
|
341
|
+
|
|
342
|
+
**Tracked as issue #972**, because an ADR paragraph is not a reminder — if the
|
|
343
|
+
second PR had merged without the removals, the write guard would have kept
|
|
344
|
+
declining legitimate suffix deletions and the release notes would have stayed
|
|
345
|
+
wrong.
|
|
346
|
+
|
|
347
|
+
> **Status, 2026-08-13.** PR #967 (issue #964) merged first, so PR #971 is the
|
|
348
|
+
> second and carries the removals. Done here: the `comment-markdown-integrity`
|
|
349
|
+
> pins now assert all seven ids and the full `it [rests](https://example.com)`
|
|
350
|
+
> span; `canRefreshAnchorText`, both call sites and its tests are deleted, with
|
|
351
|
+
> the two call sites reduced to a plain "nothing moved" equality check so a
|
|
352
|
+
> genuine edit — including a suffix deletion — refreshes again.
|
|
353
|
+
>
|
|
354
|
+
> **Not done, and deliberately:** `findOverlappingThreadIds` and its comment-list
|
|
355
|
+
> notice. The notice is untrue now, but it is app-side presentation, which
|
|
356
|
+
> #970's spec places out of its own scope, and removing it means unpicking a
|
|
357
|
+
> prop threaded through `EditorColumn` into `CommentList` plus ~90 lines of
|
|
358
|
+
> tests. #972 must not be closed as fully absorbed until that and the
|
|
359
|
+
> `RELEASE_NOTES.md` / ADR-080 known-limitations wording are dealt with.
|
|
360
|
+
|
|
361
|
+
Part 1 (overlapping marks) shipped as a documented limitation rather than a
|
|
362
|
+
guard, so only descriptions go:
|
|
363
|
+
|
|
364
|
+
- `renderer/components/comments/useDocumentComments.ts` — `findOverlappingThreadIds`
|
|
365
|
+
and the comment-list notice *"Shares a passage with another comment — only one
|
|
366
|
+
of them shows a highlight."* The notice is now untrue; drop the helper and its
|
|
367
|
+
tests.
|
|
368
|
+
- `renderer/components/__tests__/comment-markdown-integrity.test.tsx` — pins
|
|
369
|
+
5 of 7 annotations placing, and the exact spans they cover. It is written to
|
|
370
|
+
fail when this lands. Update it to the full set and to
|
|
371
|
+
`covered['over-link'] === 'it [rests](https://example.com)'` — that failure is
|
|
372
|
+
the signal, not a regression.
|
|
373
|
+
- The corresponding *Known limitations at first release* entries in
|
|
374
|
+
`RELEASE_NOTES.md` and ADR-080.
|
|
375
|
+
|
|
376
|
+
Part 2 (truncated span over an inline link) shipped a real write guard, because
|
|
377
|
+
FR-019's refresh would otherwise have persisted an anchor ending mid-syntax.
|
|
378
|
+
That guard is `canRefreshAnchorText` in `shared/comment-types.ts` — *refuse to
|
|
379
|
+
replace a stored `targetText` with text contained within it* — applied in
|
|
380
|
+
`main/ipc/comments-handlers.ts` (`comments:refreshAnchors`, the authoritative
|
|
381
|
+
check) and pre-filtered in `useDocumentComments.ts`. It is deliberately blunt
|
|
382
|
+
and has a standing cost: a user who genuinely deletes the tail of an anchored
|
|
383
|
+
passage gets no re-capture either, because the host cannot tell that apart from
|
|
384
|
+
the truncation without re-implementing the mapper's knowledge of inline syntax —
|
|
385
|
+
which is what now lives here instead. Delete the helper, both call sites, and
|
|
386
|
+
the tests that pin it:
|
|
387
|
+
|
|
388
|
+
- `shared/__tests__/comment-types.test.ts` → `describe('canRefreshAnchorText — the FR-019 write guard (#970)')`
|
|
389
|
+
- `main/ipc/__tests__/comments-handlers.test.ts` → *"refuses to truncate a stored
|
|
390
|
+
anchor that spans an inline link"* and *"declines to shrink a stored anchor,
|
|
391
|
+
which is the guard's accepted cost"*
|
|
392
|
+
- `renderer/components/comments/__tests__/useDocumentComments.test.ts` → *"sends
|
|
393
|
+
nothing when a mark over an inline link reports a truncated span"*
|
|
394
|
+
|
|
395
|
+
Leaving the guard in after the fix keeps declining legitimate suffix deletions
|
|
396
|
+
for no reason. What makes the removal safe is
|
|
397
|
+
`annotation-inline-construct-ranges.test.ts`: for an unedited document the
|
|
398
|
+
recovered slice now equals the captured `targetText` across every inline
|
|
399
|
+
construct the editor round-trips, so the refresh has no spurious difference left
|
|
400
|
+
to act on.
|
|
401
|
+
|
|
402
|
+
## References
|
|
403
|
+
|
|
404
|
+
- Issue #939 (this phase), #938 (Phase 1 — package carve-out), #940 (Phase 3 — OSS hardening)
|
|
405
|
+
- ADR-075 (the `@liminis/editor` package boundary), ADR-057 (knowledge
|
|
406
|
+
corrections pipeline), ADR-070 (dedicated Lexical node types), ADR-042 (MCP
|
|
407
|
+
stdio serialization)
|
|
408
|
+
- Zusammen ADR-008 (comment anchor resolution), ADR-012 and its #47 amendment
|
|
409
|
+
(comment anchor as live mark) — the design sources for the ported machinery
|
|
410
|
+
- `docs/project_notes/zusammen-editor-capability-map.md`
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# ADR-078: `@liminis/editor` Is Delivered by a Non-Bundling `tsc` Emit Behind `publishConfig`
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-05
|
|
4
|
+
**Status:** Accepted
|
|
5
|
+
**Supersedes:** none
|
|
6
|
+
**Amends:** ADR-075 (closes three of its deferrals; re-points the other two)
|
|
7
|
+
**Issue:** #940
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
ADR-075 carved `@liminis/editor` out of `liminis-app` and fixed its public
|
|
12
|
+
surface; ADR-077 added the unified annotation mechanism and a sixth export
|
|
13
|
+
subpath. Both left the package consumed **in-workspace as raw TypeScript**:
|
|
14
|
+
`main`/`types` point at `src/index.ts`, there is no build, `react`/`react-dom`/
|
|
15
|
+
`lexical` are plain dependencies, and wiki-link behaviour depends on a pnpm
|
|
16
|
+
`patchedDependencies` entry that exists only in this repository.
|
|
17
|
+
|
|
18
|
+
That is enough for `liminis-app` and not enough for anyone else. #940's job was
|
|
19
|
+
to make the package genuinely installable outside the monorepo — for Zusammen
|
|
20
|
+
first, and for open-sourcing generally — without changing what it contains.
|
|
21
|
+
|
|
22
|
+
Four things had to be decided, and each has a failure mode that is silent rather
|
|
23
|
+
than loud.
|
|
24
|
+
|
|
25
|
+
## Decision
|
|
26
|
+
|
|
27
|
+
### 1. The build is a structure-preserving `tsc` emit. No bundler.
|
|
28
|
+
|
|
29
|
+
`tsc -p tsconfig.build.json` emits ESM plus `.d.ts` module-for-module into
|
|
30
|
+
`dist/`, followed by `tsc-alias --resolve-full-paths` to turn `tsc`'s
|
|
31
|
+
extensionless relative specifiers into Node-ESM-resolvable `./x.js` ones. A
|
|
32
|
+
small `scripts/copy-assets.mjs` places `styles.css` and the vendored module's
|
|
33
|
+
license, which `tsc` does not copy.
|
|
34
|
+
|
|
35
|
+
**Why not a bundler.** ADR-075 §4's `ReferenceError: Prism is not defined`
|
|
36
|
+
reproduction — 138 e2e failures, blank renderer — was a *rolldown whole-graph
|
|
37
|
+
chunking* artifact. Introducing a second rolldown here (via `tsdown`, or `vite
|
|
38
|
+
build` in library mode) would recreate exactly that risk surface, and would make
|
|
39
|
+
the six subpaths' isolation depend on chunking configuration whose failure mode
|
|
40
|
+
is a silently heavier bundle rather than an error.
|
|
41
|
+
|
|
42
|
+
With a non-bundling emit the isolation holds **by construction**:
|
|
43
|
+
|
|
44
|
+
- `./markdown` cannot acquire `./headless`'s ~1.9 MB MathJax configuration
|
|
45
|
+
graph, because no chunker exists to hoist it there.
|
|
46
|
+
- `./contract` cannot acquire *renderer* code, because the module boundary is
|
|
47
|
+
preserved verbatim. Note it does **not** make the entry zod-free: `contract.js`
|
|
48
|
+
re-exports `./types.js`, which does `import { z } from 'zod'`, so a *value*
|
|
49
|
+
import of `./contract` pulls zod in. What keeps zod out of the preload
|
|
50
|
+
boundary is `import type` erasure, not the subpath — as ADR-075 §1 says
|
|
51
|
+
plainly, and as `editor-package-wiring.test.ts` is the only thing pinning.
|
|
52
|
+
Emitting module-for-module preserves that property exactly as it was; it does
|
|
53
|
+
not strengthen it.
|
|
54
|
+
- `lazy(() => import('./AnnotationSurface'))` survives as a real dynamic import
|
|
55
|
+
to a real file, so the annotation boundary is still a boundary.
|
|
56
|
+
|
|
57
|
+
It also adds no bundler dependency: `typescript` was already a devDependency,
|
|
58
|
+
and `tsc-alias` is a small, mature post-pass.
|
|
59
|
+
|
|
60
|
+
Rejected alternatives: `tsdown` (0.x, and it pulls in the exact rolldown hazard);
|
|
61
|
+
`vite build` lib mode (chunking config that fails silently on the
|
|
62
|
+
`./markdown` ↔ `./headless` boundary).
|
|
63
|
+
|
|
64
|
+
### 2. `publishConfig` swaps the manifest at pack time. `liminis-app` keeps consuming source.
|
|
65
|
+
|
|
66
|
+
The checked-in `package.json` still has `main`/`types`/`exports` pointing at
|
|
67
|
+
`src/`. A `publishConfig` block carries the `dist/` versions, and pnpm applies it
|
|
68
|
+
at `pnpm pack` / `pnpm publish` — verified, including for a `private: true`
|
|
69
|
+
package, so no `prepack` manifest-swap fallback was needed.
|
|
70
|
+
|
|
71
|
+
**Why this rather than switching the app to built output.** `liminis-app`
|
|
72
|
+
carries two pieces of configuration that exist *solely* because the package is
|
|
73
|
+
raw TypeScript, both asserted by `editor-package-wiring.test.ts`:
|
|
74
|
+
|
|
75
|
+
- `electron.vite.config.ts` — `externalizeDepsPlugin({ exclude: ['@liminis/editor'] })`
|
|
76
|
+
on `main` and `preload`.
|
|
77
|
+
- `renderer/styles/main.css` — `@source "../../../../packages/editor/src"`,
|
|
78
|
+
because Tailwind v4's automatic source detection skips `node_modules`.
|
|
79
|
+
|
|
80
|
+
Switching the app to built output invalidates **both at the same moment**, and
|
|
81
|
+
they must then change in lockstep with the tests asserting them. The Tailwind
|
|
82
|
+
half is the dangerous one: its failure mode is unstyled output, not an error, and
|
|
83
|
+
the existing guard ("the `@source` path exists") would still pass against a path
|
|
84
|
+
that exists but contains no class-bearing source.
|
|
85
|
+
|
|
86
|
+
`publishConfig` makes that whole class of problem not arise. The cost is that
|
|
87
|
+
the built path is exercised only in CI — which is what SC-005 mandates anyway.
|
|
88
|
+
|
|
89
|
+
`private: true` is kept: publishing is out of scope for #940, and `private`
|
|
90
|
+
blocks `publish` without blocking `pack`.
|
|
91
|
+
|
|
92
|
+
### 3. `mdast-util-wiki-link` is vendored, not merely un-patched.
|
|
93
|
+
|
|
94
|
+
The upstream package is vendored under
|
|
95
|
+
`src/markdown/vendor/mdast-util-wiki-link/` (MIT, Mark Hudnall, attribution and
|
|
96
|
+
license preserved and shipped in `dist/`), with the trailing-backslash fix from
|
|
97
|
+
[#347](https://github.com/verveguy/liminis/issues/347) applied inline.
|
|
98
|
+
|
|
99
|
+
**Why vendor rather than drop the patch.** Three reasons, in increasing order of
|
|
100
|
+
how easy they are to miss:
|
|
101
|
+
|
|
102
|
+
1. The strip must happen **before** `pageResolver` runs, so `data.permalink` and
|
|
103
|
+
`data.exists` derive from the cleaned target. `parse.ts`'s existing
|
|
104
|
+
`stripEscapedPipeFromWikiLinks` post-pass fixes only `node.value` and cannot
|
|
105
|
+
reproduce that. (It is kept, as defense-in-depth.)
|
|
106
|
+
2. `liminis-app/src/main/canonical-chunker.ts` is a **second, independent
|
|
107
|
+
consumer** with no post-pass at all. It feeds the knowledge graph, and a
|
|
108
|
+
wiki-link parse change there is invisible to the editor suite *and* to e2e —
|
|
109
|
+
it surfaces only as subtly different indexed chunk text. It now imports
|
|
110
|
+
`wikiLinkFromMarkdown`/`wikiLinkToMarkdown` from `@liminis/editor/markdown`,
|
|
111
|
+
so the two consumers cannot drift onto *different wiki-link extensions*.
|
|
112
|
+
|
|
113
|
+
That is narrower than "the two parses cannot diverge", and the difference
|
|
114
|
+
matters. Sharing the extension closes the vendoring gap only. It does not
|
|
115
|
+
close a **pre-existing** divergence that predates this ADR: `parseMarkdown`
|
|
116
|
+
escapes the alias divider before parsing and carries an `_emptyAlias`
|
|
117
|
+
sentinel, while the chunker calls the extension directly. Without the escape,
|
|
118
|
+
GFM's table parser reaches the `|` in `[[target|alias]]` first and splits the
|
|
119
|
+
row — measured at 5 cells / 0 wiki-links where `parseMarkdown` gives 4 / 1.
|
|
120
|
+
So an aliased wiki-link inside a table is still chunked differently from how
|
|
121
|
+
the editor parses it. Closing that changes the text fed to the knowledge
|
|
122
|
+
graph, so it is deliberately left for a follow-up rather than folded into
|
|
123
|
+
#940's delivery work. `canonical-chunker.ts` carries the same caveat at the
|
|
124
|
+
import site.
|
|
125
|
+
3. Upstream's `to-markdown` deep-imports `mdast-util-to-markdown/lib/util/safe`
|
|
126
|
+
from **v0.6.5** — a v0 duplicate of the v2 the editor already uses, plus
|
|
127
|
+
`@babel/runtime`, both pulled in to serve one function. The vendored copy uses
|
|
128
|
+
v2's `state.safe()` and drops both.
|
|
129
|
+
|
|
130
|
+
The root `patchedDependencies` entry, the patch file, and the dependency are
|
|
131
|
+
gone. `liminis-app/pnpm-workspace.yaml` carried a second, vestigial
|
|
132
|
+
`patchedDependencies` block that pnpm never read; it is removed and the file now
|
|
133
|
+
says plainly that nothing in it takes effect.
|
|
134
|
+
|
|
135
|
+
### 4. The whole Lexical surface is peered, not just bare `lexical`.
|
|
136
|
+
|
|
137
|
+
`react`, `react-dom`, `lexical` and all eleven `@lexical/*` packages move to
|
|
138
|
+
`peerDependencies`, mirrored into `devDependencies` so the workspace install is
|
|
139
|
+
unchanged.
|
|
140
|
+
|
|
141
|
+
Peering bare `lexical` while shipping `@lexical/react` as a hard dependency still
|
|
142
|
+
permits two Lexical registries in a consumer's tree — the precise failure peering
|
|
143
|
+
exists to prevent. React is the same story with `Invalid hook call`. pnpm v8+ and
|
|
144
|
+
npm v7+ auto-install peers, so the consumer burden is small.
|
|
145
|
+
|
|
146
|
+
### 5. `sideEffects` stays undeclared. The question is closed, not deferred.
|
|
147
|
+
|
|
148
|
+
ADR-075 §4 prohibits the field and deferred a re-decision to #940, on the theory
|
|
149
|
+
that an external consumer of *built* output would have a different module graph
|
|
150
|
+
and might legitimately need it.
|
|
151
|
+
|
|
152
|
+
That theory does not survive decision 1. Because the build performs no chunking
|
|
153
|
+
of its own, an external consumer's bundler sees the **same module graph**
|
|
154
|
+
`liminis-app`'s bundler sees today. The prismjs hazard is therefore identical,
|
|
155
|
+
not different, and the narrowing FR-011 would have permitted is not available.
|
|
156
|
+
|
|
157
|
+
The measurement the field was wanted for does not need it either: FR-006 points
|
|
158
|
+
at the `React.lazy()` boundary on `AnnotationSurface`, which is structural.
|
|
159
|
+
|
|
160
|
+
ADR-075 §4's prohibition stands unchanged, and its guard in
|
|
161
|
+
`editor-package-wiring.test.ts` stays. It is now closed rather than open.
|
|
162
|
+
|
|
163
|
+
## Consequences
|
|
164
|
+
|
|
165
|
+
- **`pnpm build:editor` and `pnpm verify:package`** exist at the repository root.
|
|
166
|
+
`verify:package` builds, packs, installs the tarball into
|
|
167
|
+
`examples/external-consumer` with `--ignore-workspace`, type-checks it under
|
|
168
|
+
both `moduleResolution: "bundler"` and `"nodenext"`, builds three measurement
|
|
169
|
+
arms, and asserts the entry-graph boundaries. A new `package-build` CI job runs
|
|
170
|
+
`verify:package` — and only that, since it builds the package itself as its
|
|
171
|
+
first step. `build:editor` is the standalone convenience alias for a local
|
|
172
|
+
build without the whole verification cycle.
|
|
173
|
+
|
|
174
|
+
`--ignore-workspace` is load-bearing: without it pnpm links the package by
|
|
175
|
+
symlink and the tarball — the thing under test — is never resolved.
|
|
176
|
+
|
|
177
|
+
- **The measurements read a per-chunk `modules.json`, not the emitted
|
|
178
|
+
JavaScript.** Grepping bundled output for identifier substrings does not work:
|
|
179
|
+
minification loses the names, and a chunk *containing* the string `annotation`
|
|
180
|
+
says nothing about whether that chunk is ever loaded. What is asserted is the
|
|
181
|
+
**eager closure** — the entry chunk plus static imports, excluding dynamic
|
|
182
|
+
ones, because a `React.lazy()` chunk being separate is the mechanism under
|
|
183
|
+
test.
|
|
184
|
+
|
|
185
|
+
As measured when this landed: the `./markdown`-only arm is 91 modules with no
|
|
186
|
+
MathJax, no Lexical and no `react-dom`. The annotations-disabled arm loads no
|
|
187
|
+
annotation module eagerly across 655; `AnnotationSurface` appears only as an
|
|
188
|
+
async chunk. The enabled arm's identical probe finds four — the control that
|
|
189
|
+
keeps the first result from being a probe pointed in the wrong place.
|
|
190
|
+
|
|
191
|
+
Per ADR-077 the scope is `AnnotationSurface` and `annotations/` specifically,
|
|
192
|
+
*not* "the string `annotation` does not appear": unconditional `MarkNode`
|
|
193
|
+
registration and a statically-importable `annotationCommands.ts` are deliberate
|
|
194
|
+
exceptions and are expected in both arms.
|
|
195
|
+
|
|
196
|
+
- **Two manifests now exist for one package**, and they can drift. The wiring
|
|
197
|
+
test asserts that every subpath in `exports` is mirrored into
|
|
198
|
+
`publishConfig.exports` pointing at `dist/`; without that, adding a seventh
|
|
199
|
+
subpath would leave external consumers silently unable to import it while every
|
|
200
|
+
in-repo check stayed green.
|
|
201
|
+
|
|
202
|
+
- **`examples/` is outside the `packages/*` workspace glob** on purpose. A
|
|
203
|
+
fixture placed under `packages/` becomes a workspace member — pnpm links the
|
|
204
|
+
editor by symlink, and `pnpm -r typecheck`/`lint` pick it up — at which point
|
|
205
|
+
"external consumer" stops being true. `examples/demo/` and
|
|
206
|
+
`examples/external-consumer/` install the packed tarball instead.
|
|
207
|
+
|
|
208
|
+
- **The package ships a LICENSE (MIT) and consumer documentation** —
|
|
209
|
+
`README.md` plus `docs/{editor-api,markdown-pipeline,annotations}.md`, inside
|
|
210
|
+
the tarball. The MIT declaration is scoped to `packages/editor` alone; the rest
|
|
211
|
+
of the repository remains unlicensed and `liminis-app` remains `UNLICENSED`.
|
|
212
|
+
|
|
213
|
+
Two silent failure modes are documented explicitly because nothing in the
|
|
214
|
+
package can catch them: not importing `@liminis/editor/styles.css` (unstyled
|
|
215
|
+
editor, invisible annotation markers), and Tailwind v4 skipping `node_modules`
|
|
216
|
+
in source detection (the editor's utility classes simply absent). An external
|
|
217
|
+
consumer has no `@source` directive handed to them the way `liminis-app` does,
|
|
218
|
+
so the README tells them what to write.
|
|
219
|
+
|
|
220
|
+
- **An emitted-declaration global-leak probe** runs in `verify:package`.
|
|
221
|
+
`src/ambient/jsx.d.ts` performs a `declare global { namespace JSX }`
|
|
222
|
+
augmentation; had anything of that class reached `dist/`, every consumer would
|
|
223
|
+
inherit it. ADR-075 records the same class of defect caught once before, when
|
|
224
|
+
an `acquireVsCodeApi` global leaked through `.` and `./contract`.
|
|
225
|
+
|
|
226
|
+
- **Two of ADR-075's #940 deferrals are not addressed here** and are re-pointed
|
|
227
|
+
at a follow-up: a DOM-free tsconfig project enforcing `./headless`'s contract,
|
|
228
|
+
and splitting `mathjax-config.ts` into lite and browser halves. Neither appears
|
|
229
|
+
in #940's requirements, and both are independent of delivery. Leaving ADR-075's
|
|
230
|
+
text claiming #940 covers them would be wrong, so it no longer does.
|
|
231
|
+
|
|
232
|
+
- **Not done: publishing.** #940 proves publishability. The registry, versioning
|
|
233
|
+
and release-automation decisions are separate, and `private: true` remains as
|
|
234
|
+
the guard against doing it by accident.
|
|
235
|
+
|
|
236
|
+
## References
|
|
237
|
+
|
|
238
|
+
- ADR-075 — the package boundary, the six-subpath inventory, the `sideEffects`
|
|
239
|
+
landmine and its reproduction
|
|
240
|
+
- ADR-077 — the annotation mechanism and its lazy boundary; the
|
|
241
|
+
host-must-import-`styles.css` contract
|
|
242
|
+
- Issue #940 (this ADR), #938 (carve the package), #939 (annotation mechanism),
|
|
243
|
+
#347 (wiki links inside markdown tables — the origin of the vendored fix)
|