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