@liminis/editor 0.2.1 → 0.3.0

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/dist/styles.css CHANGED
@@ -107,6 +107,135 @@
107
107
  --slashmd-callout-caution-border: oklch(0.63 0.24 27);
108
108
  }
109
109
 
110
+ /* ========================================
111
+ PUBLIC ALIASES — --liminis-editor-* names (ADR-93)
112
+
113
+ The package's own :root/.dark declarations above keep carrying the real
114
+ defaults, under the legacy names, exactly as before. Every alias below
115
+ only reads a legacy name via var() — it never declares an independent
116
+ value — so a host overriding only the legacy name still wins: the alias
117
+ re-resolves through .dark and the @media print block's overrides of that
118
+ legacy name, since a var() reference resolves per-element at the point of
119
+ use, not at the point this declaration is parsed. This lets a host read
120
+ --liminis-editor-* directly (verveguy/zusammen#129) without disabling the
121
+ ADR-087 fallback layer that a host supplying only the legacy name relies
122
+ on.
123
+
124
+ Scope of the "legacy name still wins" guarantee: this re-resolution is
125
+ correct for a legacy override applied at (or that plainly inherits down
126
+ to) :root/document.documentElement — the standard integration point, and
127
+ the only one covered by this package's own e2e coverage. It does NOT
128
+ extend to a legacy override scoped to an arbitrary DESCENDANT element
129
+ between :root and the editor's own DOM (e.g. a per-widget wrapper div a
130
+ host might apply its own theme vars to): a custom property inherits its
131
+ parent's already-*computed* value, not the var() expression, so this
132
+ :root-declared alias is fixed at parse time using :root's own value and
133
+ won't pick up a change made lower in the tree. Re-declaring the alias on
134
+ every element (`*`) was tried and rejected: it fixes the descendant case
135
+ but then shadows a --liminis-editor-* override a host sets on an
136
+ ancestor of the element being read, breaking the higher-priority
137
+ guarantee (User Story 3) that a directly-set --liminis-editor-* name
138
+ always wins — confirmed by test, not assumed. Scoping the redeclaration
139
+ to a known package wrapper class (e.g. .editor-app-root) was also tried
140
+ and rejected: not every integration renders that wrapper (this repo's
141
+ own examples/electron shell doesn't), so the alias would go undefined
142
+ there. See ADR-93 for the full trade-off and verveguy/liminis-editor#93's
143
+ PR review thread for the CSS-inheritance mechanics that rule out a
144
+ `:root`-only or `*`-only fix satisfying both scenarios at once.
145
+ ======================================== */
146
+ :root {
147
+ /* Aliases for tokens with an existing --vscode-, --slashmd- or --checkbox- default */
148
+ --liminis-editor-background: var(--vscode-background);
149
+ --liminis-editor-bold-color: var(--slashmd-bold-color);
150
+ --liminis-editor-border: var(--vscode-border);
151
+ --liminis-editor-callout-caution-bg: var(--slashmd-callout-caution-bg);
152
+ --liminis-editor-callout-caution-border: var(--slashmd-callout-caution-border);
153
+ --liminis-editor-callout-important-bg: var(--slashmd-callout-important-bg);
154
+ --liminis-editor-callout-important-border: var(--slashmd-callout-important-border);
155
+ --liminis-editor-callout-note-bg: var(--slashmd-callout-note-bg);
156
+ --liminis-editor-callout-note-border: var(--slashmd-callout-note-border);
157
+ --liminis-editor-callout-tip-bg: var(--slashmd-callout-tip-bg);
158
+ --liminis-editor-callout-tip-border: var(--slashmd-callout-tip-border);
159
+ --liminis-editor-callout-warning-bg: var(--slashmd-callout-warning-bg);
160
+ --liminis-editor-callout-warning-border: var(--slashmd-callout-warning-border);
161
+ --liminis-editor-checkbox-border: var(--checkbox-border);
162
+ --liminis-editor-code-bg: var(--vscode-code-bg);
163
+ --liminis-editor-external-link: var(--vscode-external-link);
164
+ --liminis-editor-font-family: var(--vscode-font-family);
165
+ --liminis-editor-font-size: var(--vscode-font-size);
166
+ --liminis-editor-foreground: var(--vscode-foreground);
167
+ --liminis-editor-h1-color: var(--slashmd-h1-color);
168
+ --liminis-editor-h1-indent: var(--slashmd-h1-indent);
169
+ --liminis-editor-h2-color: var(--slashmd-h2-color);
170
+ --liminis-editor-h2-indent: var(--slashmd-h2-indent);
171
+ --liminis-editor-h3-color: var(--slashmd-h3-color);
172
+ --liminis-editor-h3-indent: var(--slashmd-h3-indent);
173
+ --liminis-editor-h4-color: var(--slashmd-h4-color);
174
+ --liminis-editor-h4-indent: var(--slashmd-h4-indent);
175
+ --liminis-editor-h5-color: var(--slashmd-h5-color);
176
+ --liminis-editor-h5-indent: var(--slashmd-h5-indent);
177
+ --liminis-editor-italic-color: var(--slashmd-italic-color);
178
+ --liminis-editor-link: var(--vscode-link);
179
+ --liminis-editor-selection: var(--vscode-selection);
180
+ --liminis-editor-token-comment: var(--slashmd-token-comment);
181
+ --liminis-editor-token-function: var(--slashmd-token-function);
182
+ --liminis-editor-token-keyword: var(--slashmd-token-keyword);
183
+ --liminis-editor-token-operator: var(--slashmd-token-operator);
184
+ --liminis-editor-token-property: var(--slashmd-token-property);
185
+ --liminis-editor-token-punctuation: var(--slashmd-token-punctuation);
186
+ --liminis-editor-token-selector: var(--slashmd-token-selector);
187
+ --liminis-editor-token-variable: var(--slashmd-token-variable);
188
+
189
+ /* Aliases preserving an existing nested consumption-site fallback chain —
190
+ these two legacy names have no --vscode-* declaration of their own, only
191
+ an existing fallback to a second legacy name at their consumption site. */
192
+ --liminis-editor-input-bg: var(--vscode-input-bg, var(--vscode-code-bg));
193
+ --liminis-editor-foreground-muted: var(--vscode-foreground-muted, var(--vscode-border));
194
+
195
+ /* Aliases for tokens with no CSS custom property backing anywhere — only an
196
+ inline literal fallback at their consumption site(s). The literal below
197
+ reproduces that existing light-mode fallback so a host supplying nothing
198
+ sees no change (FR-002); a host setting the legacy name still wins
199
+ (FR-003). Where the same token's literal differs by site (errorForeground,
200
+ toolbar-hoverBackground), a single canonical value is chosen here — see
201
+ ADR-93 for the two accepted visual deltas this causes at three sites. */
202
+ --liminis-editor-button-background: var(--vscode-button-background, #007acc);
203
+ --liminis-editor-button-foreground: var(--vscode-button-foreground, #ffffff);
204
+ --liminis-editor-menu-background: var(--vscode-menu-background, #ffffff);
205
+ --liminis-editor-menu-border: var(--vscode-menu-border, #d4d4d4);
206
+ --liminis-editor-menu-foreground: var(--vscode-menu-foreground, #333333);
207
+ --liminis-editor-menu-selectionBackground: var(--vscode-menu-selectionBackground, #e8e8e8);
208
+ --liminis-editor-menu-separatorBackground: var(--vscode-menu-separatorBackground, #d4d4d4);
209
+ --liminis-editor-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.15));
210
+ --liminis-editor-notificationsInfoIcon-foreground: var(--vscode-notificationsInfoIcon-foreground, #3794ff);
211
+ --liminis-editor-inputValidation-errorBackground: var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.15));
212
+ --liminis-editor-errorForeground: var(--vscode-errorForeground, #f14c4c);
213
+ --liminis-editor-focus-border: var(--vscode-focus-border, #007acc);
214
+ --liminis-editor-focusBorder: var(--vscode-focusBorder, #007acc);
215
+
216
+ /* Aliases for liminis-app's own Tailwind @theme brand tokens — the package
217
+ only ever supplied a literal final fallback for these, never asserted
218
+ ownership; this alias makes that existing fallback reachable under the
219
+ new name too, without changing what it resolves to. */
220
+ --liminis-editor-primary: var(--color-primary, #3b82f6);
221
+ --liminis-editor-primary-100: var(--color-primary-100, rgba(59, 130, 246, 0.1));
222
+ --liminis-editor-muted-foreground: var(--color-muted-foreground, #6b7280);
223
+ --liminis-editor-muted-100: var(--color-muted-100, rgba(128, 128, 128, 0.1));
224
+ }
225
+
226
+ .dark {
227
+ /* Dark-mode literal overrides for the subset of the no-CSS-backing group
228
+ above whose consumption sites select a different literal in dark mode
229
+ (via a JS `isDark`/`dark` check reading the same .dark class this
230
+ selector keys off) rather than a single literal used in both themes. */
231
+ --liminis-editor-button-background: var(--vscode-button-background, #0e639c);
232
+ --liminis-editor-menu-background: var(--vscode-menu-background, #252526);
233
+ --liminis-editor-menu-border: var(--vscode-menu-border, #454545);
234
+ --liminis-editor-menu-foreground: var(--vscode-menu-foreground, #cccccc);
235
+ --liminis-editor-menu-selectionBackground: var(--vscode-menu-selectionBackground, #094771);
236
+ --liminis-editor-menu-separatorBackground: var(--vscode-menu-separatorBackground, #454545);
237
+ }
238
+
110
239
  * {
111
240
  box-sizing: border-box;
112
241
  }
@@ -1038,6 +1167,7 @@ body {
1038
1167
  }
1039
1168
 
1040
1169
  .drag-handle:hover .drag-handle-icon {
1170
+ /* Superseded by --liminis-editor-toolbar-hoverBackground's canonical 0.15-alpha default (ADR-93) */
1041
1171
  background: var(--liminis-editor-toolbar-hoverBackground, var(--vscode-toolbar-hoverBackground, rgba(128, 128, 128, 0.1)));
1042
1172
  }
1043
1173
 
@@ -1077,6 +1207,7 @@ body {
1077
1207
 
1078
1208
  .block-delete-button:hover .block-delete-icon {
1079
1209
  background: var(--liminis-editor-inputValidation-errorBackground, var(--vscode-inputValidation-errorBackground, rgba(255, 0, 0, 0.15)));
1210
+ /* Superseded by --liminis-editor-errorForeground's canonical #f14c4c default (ADR-93) */
1080
1211
  color: var(--liminis-editor-errorForeground, var(--vscode-errorForeground, #f44336));
1081
1212
  }
1082
1213
 
@@ -1592,6 +1723,7 @@ body {
1592
1723
  }
1593
1724
 
1594
1725
  .search-close-button:hover {
1726
+ /* Superseded by --liminis-editor-errorForeground's canonical #f14c4c default (ADR-93) */
1595
1727
  color: var(--liminis-editor-errorForeground, var(--vscode-errorForeground, #f44336));
1596
1728
  }
1597
1729
 
@@ -0,0 +1,159 @@
1
+ # ADR-091: `DocumentOutlineHandle` Gets Two Independent, Non-Reconciled Entry Sources
2
+
3
+ **Date:** 2026-08-19
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #84 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ Issue #69 extracted `DocumentOutline` from liminis-app's `TableOfContents.tsx`
12
+ and deliberately rebuilt it on **live Lexical state**: `OutlinePlugin` walks
13
+ `HeadingNode`s on every editor update and feeds the shared
14
+ `DocumentOutlineHandle`. That decision avoided a second markdown parse and got
15
+ #69's "the outline updates as headings change" requirement for free — but it
16
+ also meant an outline is only possible where a Lexical `<Editor>` is mounted.
17
+
18
+ The pre-extraction `TableOfContents.tsx` had no such limitation: it derived
19
+ entries from **markdown**, using this package's own `parseMarkdown`/
20
+ `isHeading`/`isText`/`isInlineCode`, and carried each heading's mdast
21
+ `position.start.line` — a 1-based source line its raw-mode variant used to
22
+ scroll a non-Lexical editor directly. That capability did not survive
23
+ extraction, which is what stalled liminis-app's raw markdown mode
24
+ (verveguy/liminis#1022) from adopting `DocumentOutline` at all.
25
+
26
+ Three questions needed a considered answer:
27
+
28
+ 1. **Does markdown replace the Lexical derivation, or sit alongside it?**
29
+ Unifying on markdown for both paths would give up #69's "no second parse"
30
+ property for the live-Lexical/WYSIWYG path, and risked introducing subtle
31
+ Lexical-state/markdown divergence bugs in a path that already works.
32
+ 2. **How does markdown reach the handle?** `OutlineHandleImpl`'s `publish`/
33
+ `connect`/`disconnect` are reachable today only via a brand-gated cast
34
+ (`handle as OutlineHandleImpl`) that only `OutlinePlugin` — always
35
+ constructed from `createDocumentOutlineHandle()` — can perform. A raw-mode
36
+ host holds a plain `DocumentOutlineHandle` and cannot replicate that cast.
37
+ 3. **Should the two paths be reconciled when both are somehow available on
38
+ one handle at once?** (E.g., a host that mounts both a Lexical editor and
39
+ also calls the markdown methods on the same handle.)
40
+
41
+ ## Decision
42
+
43
+ **`DocumentOutlineHandle` gains two new public methods —
44
+ `publishFromMarkdown(markdown)` and `setActiveLine(line)` — that are a second,
45
+ independent entry source feeding the same `publish`/`subscribe`/`getSnapshot`
46
+ machinery `OutlinePlugin` already uses.** `DocumentOutline.tsx` renders
47
+ correctly regardless of which path produced its snapshot, with no branching
48
+ on the source — its only change is an optional `onEntrySelect` prop, fired
49
+ alongside the existing `scrollToHeading` call, since that call is a no-op on
50
+ the markdown path (no Lexical editor to scroll) and a raw-mode host needs
51
+ some way to react to a click without re-implementing the entry list itself
52
+ (see "Bad / accepted" below). `OutlinePlugin.tsx` needs zero changes: the
53
+ Lexical path keeps working exactly as it did in 0.2.1.
54
+
55
+ **The two paths are parallel and deliberately not reconciled.** A handle is
56
+ fed by one path or the other — `<Editor documentOutlineHandle>`/
57
+ `OutlinePlugin`, or `publishFromMarkdown`/`setActiveLine` — matching how the
58
+ pre-extraction app never ran both on the same instance either. Mounting both
59
+ on one handle at once is unsupported and produces whichever `publish()` call
60
+ happened most recently, the same "last write wins" behavior `publish()`
61
+ already has for any two callers.
62
+
63
+ **`publishFromMarkdown` and `setActiveLine` are split into two methods, not
64
+ one combined call**, because content changes and scroll events fire
65
+ independently and at different rates in a raw-mode host, which owns its own
66
+ scroll tracking (there is no package-side equivalent of `OutlinePlugin`'s
67
+ rAF-throttled viewport observer, since the package does not own a raw-mode
68
+ host's editor or its scroll container). `OutlineHandleImpl` stores the
69
+ last-derived markdown entries and last active line internally so either
70
+ method can be called alone.
71
+
72
+ **The pure derivation functions (`deriveOutlineFromMarkdown`,
73
+ `resolveActiveOutlineIndex`) live in a sibling file
74
+ (`documentOutlineMarkdown.ts`), not inlined into `documentOutlineHandle.ts`,
75
+ and are exported standalone from the package root** alongside the handle
76
+ methods that compose them — independently unit-testable with no handle/
77
+ publish machinery involved, and usable by a host that wants the derivation
78
+ without the handle's mutable-state class at all.
79
+
80
+ **`OutlineEntry` gains an optional `line?: number`**, populated only on
81
+ markdown-derived entries; the Lexical path leaves it unset since live Lexical
82
+ state has no source-line mapping to supply. `snapshotsEqual` compares it, so
83
+ a line-only change (same text/level, new line) still notifies subscribers.
84
+
85
+ ## Consequences
86
+
87
+ **Good:**
88
+
89
+ - A raw-mode host with zero Lexical editors mounted can produce a populated,
90
+ navigable outline by calling two methods on a handle it already holds, and
91
+ render it with the shared `<DocumentOutline>` component — including
92
+ click-to-navigate via `onEntrySelect`, since `scrollToHeading` alone
93
+ no-ops on this path — satisfying #84's FR-008/SC-004 ("adopt without
94
+ reimplementing... logic") without liminis#1022 forking outline/
95
+ heading-derivation *or* list-rendering code.
96
+ - The existing Lexical/WYSIWYG path is untouched at the source level:
97
+ `OutlinePlugin.tsx` has no diff, and #69's existing test suites pass
98
+ unmodified, directly verifying FR-005/US3 ("WYSIWYG behavior is
99
+ unaffected").
100
+ - `DocumentOutline.tsx` has no branching on which path produced its
101
+ snapshot — the two entry sources are invisible to it — and its one
102
+ addition (`onEntrySelect`) is additive and optional, so existing WYSIWYG
103
+ consumers that don't pass it see no behavior change.
104
+ - The standalone-exported pure functions give a host an escape hatch if it
105
+ ever wants derivation without the handle/publish machinery (e.g. a
106
+ server-side outline preview), without that being the primary API shape.
107
+
108
+ **Bad / accepted:**
109
+
110
+ - **Text extraction is now implemented twice**: Lexical's built-in
111
+ `getTextContent()` on the WYSIWYG path, and a hand-written recursive mdast
112
+ walk (`extractText` in `documentOutlineMarkdown.ts`) on the markdown path.
113
+ A future inline mdast node type could silently produce different text on
114
+ the two paths with nothing to catch it beyond the shared fixture the two
115
+ paths' test suites both happen to exercise (`OutlinePlugin.test.tsx`'s and
116
+ `documentOutlineMarkdown.test.ts`'s equivalent inline-code case). No shared
117
+ test harness enforces parity mechanically; considered out of scope for this
118
+ issue.
119
+ - **The two paths are never reconciled**, so a host that (incorrectly) drives
120
+ both at once on the same handle gets undefined-in-practice "whichever
121
+ `publish()` ran last" behavior rather than an explicit error. Accepted
122
+ because detecting and rejecting that misuse would add complexity for a
123
+ configuration nothing in the spec calls for and the pre-extraction app
124
+ never did either.
125
+ - **Empty-title headings are included on the markdown path**, diverging from
126
+ the pre-extraction `TableOfContents.tsx`'s `if (!title) continue`. This
127
+ matches FR-002's literal requirement ("same rules as the existing Lexical
128
+ path", which does not skip them) over parity with the old app behavior —
129
+ liminis#1022 needs to be aware of this when it adopts the markdown path.
130
+
131
+ **Neutral:**
132
+
133
+ - No new export subpath was added; `deriveOutlineFromMarkdown`/
134
+ `resolveActiveOutlineIndex` are exported from the same root `.` entry as
135
+ the rest of the outline surface, not from `./markdown`'s Lexical-free
136
+ barrel. ADR-075 draws that barrel's isolation boundary around avoiding
137
+ Lexical/MathJax/Mermaid/C4 for a consumer that only parses markdown; this
138
+ issue's FR-008 requires only that liminis#1022 can adopt the capability
139
+ without forking logic, not that it be reachable without the root barrel's
140
+ full import graph. Revisit only if that assumption proves wrong during
141
+ #1022's own implementation.
142
+
143
+ ## References
144
+
145
+ - Issue #84 (this decision)
146
+ - `src/app/editor/documentOutlineHandle.ts` — `OutlineEntry.line`,
147
+ `publishFromMarkdown`, `setActiveLine`, updated `snapshotsEqual`
148
+ - `src/app/editor/documentOutlineMarkdown.ts` — `deriveOutlineFromMarkdown`,
149
+ `resolveActiveOutlineIndex`
150
+ - `src/app/editor/OutlinePlugin.tsx` — the unchanged Lexical/WYSIWYG path
151
+ - `src/app/editor/DocumentOutline.tsx` — the `onEntrySelect` prop, its one
152
+ change, added so a raw-mode host can react to clicks
153
+ - `docs/editor-api.md` — "Raw-mode / markdown-derived entries" section
154
+ - `docs/decisions/adr-075.md` — the package-boundary/export-subpath decision
155
+ this ADR's "Neutral" section revisits
156
+ - `specs/69-extract-the-table-of/spec.md`, `docs/decisions/adr-088.md` —
157
+ issue #69's own decision to derive from live Lexical state, whose
158
+ WYSIWYG-only assumption this issue lifts without replacing it
159
+ - verveguy/liminis#1022 (external repo) — the consumer this decision unblocks
@@ -0,0 +1,202 @@
1
+ # ADR-092: A Checked-In Baseline Guards `styles.css`'s Defined Tokens Against Silent Rename or Removal
2
+
3
+ **Date:** 2026-08-20
4
+ **Status:** Accepted
5
+ **Supersedes:** none
6
+ **Amends:** none
7
+ **Issue:** #79 (verveguy/liminis-editor)
8
+
9
+ ## Context
10
+
11
+ `tests/theming-contract.test.ts` (ADR-085) already guards the *consumed*
12
+ token set — every `var(--x)` reference under `src/` must be documented in
13
+ the README's generated table, resolve without a host, carry a description,
14
+ and (per ADR-087) preserve a fallback to its pre-`0.2.0` name where renamed.
15
+ None of those four checks say anything about a token that is *defined*
16
+ (declared with a value in `styles.css`) but never itself consumed by
17
+ `src/` — the `--vscode-*`, `--slashmd-*`, `--color-*` and `--checkbox-*`
18
+ fallback targets `0.2.0` (#51) left in place specifically so a host still
19
+ supplying only the old name keeps working.
20
+
21
+ Zusammen, a downstream host, reads several of those definitions directly —
22
+ `var(--vscode-background)`, `var(--vscode-border)`, etc. — mapping its own
23
+ design-system tokens onto them, not overriding them. Nothing about that is
24
+ visible from inside this package: `src/` never references those names via
25
+ `var()`, so the existing consumed-token guard has nothing to check. `0.2.0`
26
+ happened to leave every defined token's name untouched (0 removed, 0 added,
27
+ comparing the published `0.1.1` and `0.2.0` packages) — but that was a
28
+ property of what `0.2.0` chose to do, not a guarantee enforced anywhere. A
29
+ future rename that touched definition sites as well as consumption sites
30
+ would drop Zusammen's palette silently: an unresolved CSS custom property
31
+ does not error, the declaration is just gone.
32
+
33
+ This is a genuinely different axis from every existing guard in this repo.
34
+ Two questions needed a considered answer:
35
+
36
+ 1. **What should CI compare the live defined set against?** Every existing
37
+ contract test (`theming-contract`, `adr-citations`,
38
+ `package-manifest-contract`) computes both sides of its comparison
39
+ dynamically from current source — there is no prior example in this repo
40
+ of a checked-in snapshot a maintainer edits by hand as a deliberate,
41
+ reviewable act. A dynamic self-comparison can't work here: the whole
42
+ point is to catch names *disappearing* between one state and the next,
43
+ which requires a reference point independent of "whatever `styles.css`
44
+ currently says."
45
+ 2. **Should an addition to the defined set ever fail CI?** The issue's
46
+ FR-006 and SC-002 are explicit that it must not — only a *disappearance*
47
+ of a previously-baselined name is a failure. (Acceptance Scenario 4's
48
+ prose reads, in isolation, as if new tokens need the baseline updated
49
+ before CI passes at all; FR-006/SC-002 are the more explicit,
50
+ cross-referencing pair on this exact point, so this decision follows
51
+ them: the guard is one-directional.)
52
+
53
+ ## Decision
54
+
55
+ **A new checked-in baseline, `scripts/lib/theming-defined-tokens-baseline.json`,
56
+ records the flat, sorted array of every token name `defaultedTokens()`
57
+ (already existing, unchanged) finds declared in `src/styles.css`.**
58
+ `diffDefinedTokenBaseline(current, baseline)`, added to
59
+ `scripts/lib/theming-tokens.mjs` alongside the other pure extraction/compare
60
+ functions, returns `{ missing, added }` — both sorted, both computed by
61
+ name, never by count. `tests/theming-contract.test.ts` fails only when
62
+ `missing` is non-empty, and names the specific token(s) in the failure
63
+ message.
64
+
65
+ **The comparison is one-directional: `missing` fails CI, `added` never
66
+ does.** A token added to `styles.css` without a baseline update is not
67
+ itself an error — it's simply not yet protected by this guard until someone
68
+ runs the updater. This is deliberate (FR-006), not an oversight: the
69
+ baseline's job is to catch *disappearance*, not to force every addition
70
+ through an extra step before it can ship.
71
+
72
+ **A same-count rename still fails.** Because the diff is by name, a
73
+ declaration renamed in the same change (old name deleted, new name added,
74
+ total count unchanged) produces one entry in `missing` and one in `added` —
75
+ never a silent no-op. This reproduces, and would have caught, the exact
76
+ scenario the issue's Background describes: `0.2.0` was 42 defined tokens
77
+ before and 42 after, which a count-only check would have called clean.
78
+
79
+ **`scripts/update-theming-baseline.mjs`, a new script mirroring
80
+ `generate-theming-docs.mjs`'s shape, is the update mechanism**, exposed as
81
+ `pnpm docs:theming-baseline`. It reads `defaultedTokens('src/styles.css')`
82
+ and writes the sorted JSON array. A maintainer making a deliberate rename or
83
+ removal runs it in the same change that edits `styles.css`; that diff is
84
+ then a normal part of PR review, distinguishing "I meant to do this" from
85
+ "this disappeared by accident." This matches the repo's existing
86
+ `docs:theming` precedent rather than asking for a bare hand-edit of JSON.
87
+
88
+ **The guard evaluates `src/styles.css`, not a built `dist/`.** `pnpm test`
89
+ alone (no prior build) is what a local run and this guard's own CI job
90
+ invoke; `dist/` is gitignored and does not exist without a build step
91
+ first. `scripts/copy-assets.mjs` makes `dist/styles.css` a byte-for-byte
92
+ copy of `src/styles.css` with no transform, so evaluating the guard against
93
+ `src/` is representative of what ships (FR-008) without requiring a build
94
+ at test time — the same choice the pre-existing consumed-token guard
95
+ already made.
96
+
97
+ **The baseline lives in `scripts/lib/`, not a fixtures directory.** This
98
+ repo has no checked-in test-fixtures directory; the existing mutation tests
99
+ build throwaway fixtures under `os.tmpdir()`. The baseline is not test
100
+ scaffolding — it's permanent project data describing the current package
101
+ surface, the same category as `PREVIOUS_NAME` and `TOKEN_DESCRIPTIONS` in
102
+ the same module, so it's colocated with them.
103
+
104
+ ## Consequences
105
+
106
+ **Good:**
107
+
108
+ - Removing or renaming a defined token without updating the baseline now
109
+ fails CI, naming the specific token(s) — the exact defence the issue's
110
+ Background describes as missing, closing the gap that let `0.2.0`'s
111
+ rename through by coincidence rather than by guarantee.
112
+ - The name-vs-count distinction is exercised directly by a mutation test
113
+ (`flags a same-count rename, not masked by an unchanged total`), so the
114
+ guard's core guarantee is demonstrated, not merely asserted.
115
+ - Adding a new token is a one-line, low-friction baseline update via
116
+ `pnpm docs:theming-baseline` — never a blocker on its own, per FR-006.
117
+ - The README now states, in the paragraph a first-time reader reaches
118
+ before the token table, that defined tokens (including pre-`0.2.0`
119
+ fallback names) are public API a host may read directly, and that
120
+ renaming or removing one is a breaking change under the existing
121
+ `0.x` versioning policy — answering SC-004 without requiring this ADR or
122
+ the issue as prerequisite reading.
123
+
124
+ **Bad / accepted:**
125
+
126
+ - The baseline is a plain hand-maintained (script-updated) JSON array with
127
+ no schema enforcement beyond `diffDefinedTokenBaseline`'s runtime
128
+ comparison — a malformed or hand-edited-incorrectly baseline would only
129
+ surface as an unexpected `missing`/`added` result at test time, not a
130
+ separate validation error. Accepted: the array is trivial enough (one
131
+ string per line) that this class of mistake is unlikely and, if it
132
+ happens, self-corrects on the next `pnpm docs:theming-baseline` run.
133
+ - Because the guard is one-directional by design, a token added to
134
+ `styles.css` and never baselined is not protected — if it is later
135
+ removed, that removal also goes uncaught (it was never in `missing`'s
136
+ input set to begin with). This is an accepted consequence of FR-006's
137
+ explicit choice, not a bug to "fix" by making the guard bidirectional;
138
+ a maintainer who wants a newly-added token protected needs to run the
139
+ updater, same as documenting it in the README table already requires a
140
+ separate `pnpm docs:theming` run today.
141
+ - `theming-tokens.d.mts` is hand-maintained with no automated sync check
142
+ against `theming-tokens.mjs`'s actual exports; forgetting to add
143
+ `diffDefinedTokenBaseline`'s declaration there would only surface as a
144
+ `pnpm typecheck` failure. Accepted as the same pre-existing tradeoff
145
+ ADR-085 already lives with for every other exported function in this
146
+ module.
147
+
148
+ **Neutral:**
149
+
150
+ - This ADR does not change which tokens are currently defined, consumed,
151
+ or documented, and does not decide whether `--liminis-editor-*` names
152
+ should eventually be defined natively with the legacy prefixes as an
153
+ alias layer (issue #79's Item 3) — that remains a follow-up candidate
154
+ issue, independent of this guard. See "Not decided here" below.
155
+
156
+ ## Not decided here
157
+
158
+ Issue #79 named three things to do and ranked "Item 2 is the substance."
159
+ This decision delivers Item 1 (the README states the contract) and Item 2
160
+ (this baseline guard). **Item 3 — whether the package should eventually
161
+ *define* `--liminis-editor-*` names directly, with `--vscode-*`/
162
+ `--slashmd-*`/`--color-*`/`--checkbox-*` kept as an alias layer — is left
163
+ open.** It is a design decision (how an alias layer would interact with the
164
+ existing fallback chain from ADR-087), not a guard, and resolving it well is
165
+ independent of stating today's contract and protecting it against
166
+ shrinkage.
167
+
168
+ This gap is not just theoretical follow-up work: **a downstream consumer is
169
+ already blocked on it.** Zusammen currently reads this package's legacy
170
+ definitions directly (`var(--vscode-background)`, `var(--vscode-border)`,
171
+ etc., per this issue's Background) and wants to migrate those reads onto the
172
+ `--liminis-editor-*` vocabulary instead — but there is nothing to migrate
173
+ to, because this package defines zero `--liminis-editor-*` names today (see
174
+ "Has a default: No" for every row in the README's token table). That
175
+ migration is tracked at
176
+ [verveguy/zusammen#129](https://github.com/verveguy/zusammen/issues/129) and
177
+ cannot proceed until Item 3 is decided. Whoever picks up Item 3 should treat
178
+ that issue as a concrete acceptance case, not just this ADR's own
179
+ description of the gap.
180
+
181
+ ## References
182
+
183
+ - Issue #79 (this decision)
184
+ - `scripts/lib/theming-tokens.mjs` — `defaultedTokens()` (unchanged, already
185
+ computed the "defined" set) and the new `diffDefinedTokenBaseline()`
186
+ - `scripts/lib/theming-defined-tokens-baseline.json` — the checked-in
187
+ baseline
188
+ - `scripts/update-theming-baseline.mjs` — the update mechanism
189
+ (`pnpm docs:theming-baseline`)
190
+ - `tests/theming-contract.test.ts` — the new "defined tokens are a public
191
+ API surface" `describe` block and its mutation tests
192
+ - `scripts/copy-assets.mjs` — establishes `dist/styles.css` as a
193
+ byte-for-byte copy of `src/styles.css`, the basis for FR-008
194
+ - `docs/decisions/adr-085.md` — the consumed/documented guard and the
195
+ shared-module, mutation-testing conventions this decision follows
196
+ - `docs/decisions/adr-087.md` — the `--liminis-editor-*` rename this
197
+ decision's guard would have caught had it also touched definition sites
198
+ - Issue #52 — the prior silent-drop incident that motivated the
199
+ resolves-without-host guard this new guard sits alongside
200
+ - [verveguy/zusammen#129](https://github.com/verveguy/zusammen/issues/129) —
201
+ the downstream migration blocked on issue #79's Item 3, deferred by this
202
+ decision (see "Not decided here")