@hueest/xray 0.6.0 → 0.7.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/index.js CHANGED
@@ -4,12 +4,12 @@ import * as v from "valibot";
4
4
  /** App code imports plates from here, e.g. `import plate from 'virtual:xray/plates/my-banner'`. */
5
5
  const VIRTUAL_PREFIX = "virtual:xray/plates/";
6
6
  /**
7
- * Marks an OUTERMOST skeleton root that is a stitch continuation (ADR 0020): a
8
- * standalone `<Skeleton>` replacing a stitch a still-showing parent skeleton was
9
- * just rendering. BASE_CSS reveals such a root instantly (no `--xr-skeleton-delay`,
10
- * no fade) so the bones do not flash away and back. Never present on a nested
11
- * stitch (which inherits the parent's reveal and has no `@starting-style` of its
12
- * own) nor on an ordinary top-level skeleton (which keeps the normal delay).
7
+ * Marks an OUTERMOST skeleton root that is a stitch continuation (ADR 0020): a standalone
8
+ * `<Skeleton>` replacing a stitch a still-showing parent skeleton was just rendering. BASE_CSS
9
+ * reveals such a root instantly (no `--xr-skeleton-delay`, no fade) so the bones do not flash away
10
+ * and back. Never present on a nested stitch (which inherits the parent's reveal and has no
11
+ * `@starting-style` of its own) nor on an ordinary top-level skeleton (which keeps the normal
12
+ * delay).
13
13
  */
14
14
  const ROOT_INSTANT_ATTR = "data-xr-instant";
15
15
  /** Fixed renderer-owned classes every rendered node carries. */
@@ -17,46 +17,37 @@ const ROOT_CLASS = "xr-root";
17
17
  const NODE_CLASS = "xr-node";
18
18
  const LEAF_CLASS = "xr-leaf";
19
19
  /**
20
- * Renderer-owned base styles, emitted once by the outermost view (ADR 0007),
21
- * before a plate's node rules so a node's captured class wins by source order.
22
- * Everything visual is driven by inheritable custom properties, so consumers
23
- * theme by setting `--xr-*` in `:root` or on any ancestor and per-plate via
24
- * `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
25
- * docs/architecture.md. Class-based and unlayered `@layer` was rejected
26
- * (unlayered app CSS would beat layered skeleton rules, and it inverts
27
- * `!important`).
20
+ * Renderer-owned base styles, emitted once by the outermost view (ADR 0007), before a plate's node
21
+ * rules so a node's captured class wins by source order. Everything visual is driven by inheritable
22
+ * custom properties, so consumers theme by setting `--xr-*` in `:root` or on any ancestor — and
23
+ * per-plate via `[data-xr-root="name"]` (the root carries its plate name). See ADR 0011 and
24
+ * docs/architecture.md. Class-based and unlayered `@layer` was rejected (unlayered app CSS would
25
+ * beat layered skeleton rules, and it inverts `!important`).
28
26
  *
29
- * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use
30
- * only universally-supported CSS and are the floor. `light-dark()` is guarded
31
- * by `@supports` because an unsupported VALUE drops the whole declaration (an
32
- * invisible bone), and it is not Baseline-widely-available until 2026-11-13 —
33
- * drop the guard then. Features that merely no-op when absent (`@property`,
34
- * `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
27
+ * Progressive enhancement (ADR 0011): the plain bone fill + opacity pulse use only
28
+ * universally-supported CSS and are the floor. `light-dark()` is guarded by `@supports` because an
29
+ * unsupported VALUE drops the whole declaration (an invisible bone), and it is not
30
+ * Baseline-widely-available until 2026-11-13 — drop the guard then. Features that merely no-op when
31
+ * absent (`@property`, `prefers-contrast`, relative-color-as-fallback) carry no hard guard.
35
32
  *
36
- * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual
37
- * Bone: `--xr-bone-color` (fill), `--xr-bone-highlight-color` (sheen),
38
- * `--xr-bone-border-radius` (+ `--xr-bone-text-border-radius` /
39
- * `--xr-bone-text-block-border-radius` / `--xr-bone-media-border-radius`),
40
- * `--xr-bone-animation-duration`, the pulse
41
- * range `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`,
42
- * `--xr-bone-animation` (mode: `xr-pulse` | `none` | a custom keyframe), and
43
- * `--xr-bone-fill` (override the leaf paint, e.g. a sheen gradient built from the
44
- * two bone color tokens). The `--xr-skeleton-*` family governs whole-skeleton
45
- * display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
46
- * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once
47
- * shown; read by the `useSkeletonTiming` hook, not used in CSS), and
48
- * `--xr-skeleton-transition-duration` (the reveal/swap fade). The private
49
- * channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay undocumented
50
- * `@property` plumbing, not a theming surface.
33
+ * Tokens (ADR 0017 taxonomy). The `--xr-bone-*` family styles an individual Bone: `--xr-bone-color`
34
+ * (fill), `--xr-bone-highlight-color` (sheen), `--xr-bone-border-radius` (+
35
+ * `--xr-bone-text-line-border-radius` / `--xr-bone-text-block-border-radius` /
36
+ * `--xr-bone-media-border-radius`), `--xr-bone-animation-duration`, the pulse range
37
+ * `--xr-bone-pulse-opacity-min`/`--xr-bone-pulse-opacity-max`, `--xr-bone-animation` (mode:
38
+ * `xr-pulse` | `none` | a custom keyframe), and `--xr-bone-fill` (override the leaf paint, e.g. a
39
+ * sheen gradient built from the two bone color tokens). The `--xr-skeleton-*` family governs
40
+ * whole-skeleton display timing (ADR 0016): `--xr-skeleton-delay` (grace before the skeleton
41
+ * becomes visible), `--xr-skeleton-min-duration` (minimum visible time once shown; read by the
42
+ * `useSkeletonTiming` hook, not used in CSS), and `--xr-skeleton-transition-duration` (the
43
+ * reveal/swap fade). The private channels `--xr-o` (pulse) and `--xr-reveal` (reveal) stay
44
+ * undocumented `@property` plumbing, not a theming surface.
51
45
  *
52
- * Declared as a PLAIN template literal — no `.trim()` (or any other call) on
53
- * the initializer. A method call is not provably pure to a conservative
54
- * tree-shaker (rolldown/Rollup), and it single-handedly pinned this ~5 KB
55
- * string into every consumer chunk importing the React adapter even after the
56
- * adapter stopped referencing it (plan 005 round 2 finding). The template
57
- * therefore starts immediately after the backtick (the old leading newline
58
- * `.trim()` used to strip simply isn't there), keeping the string value
59
- * byte-identical to the trimmed original.
46
+ * Declared as a PLAIN template literal — no `.trim()` (or any other call) on the initializer. A
47
+ * method call is not provably pure to a conservative tree-shaker (rolldown/Rollup) and would pin
48
+ * this ~5 KB string into every consumer chunk that imports the React adapter, referenced or not
49
+ * (ADR 0026). The template starts immediately after the backtick, so there is no leading newline to
50
+ * strip.
60
51
  */
61
52
  const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates one inherited
62
53
  opacity property and every leaf reads it. Hundreds of independent infinite
@@ -89,10 +80,14 @@ const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates
89
80
  /* Per-kind defaults: a single text LINE reads as a rounded bar; a multi-line
90
81
  text BLOCK as a soft rect (a tall pill misreads as a button); media carries
91
82
  the author radius, defaulting to the same soft rect. Re-theme any kind via
92
- [data-xr-root] .${LEAF_CLASS}-text { ... }. */
93
- .${LEAF_CLASS}-text { border-radius: var(--xr-bone-text-border-radius, 999px) }
83
+ [data-xr-root] .${LEAF_CLASS}-text-line { ... }. */
84
+ .${LEAF_CLASS}-text-line { border-radius: var(--xr-bone-text-line-border-radius, 999px) }
94
85
  .${LEAF_CLASS}-text-block { border-radius: var(--xr-bone-text-block-border-radius, var(--xr-bone-border-radius, 4px)) }
95
86
  .${LEAF_CLASS}-media { border-radius: var(--xr-bone-media-border-radius, var(--xr-bone-border-radius, 4px)) }
87
+ /* A hidden bone holds its measured box in the flow but paints nothing: the
88
+ space-preserving complement to data-xr-ignore. visibility (not display) so
89
+ the captured size rules still lay the box out. */
90
+ .${LEAF_CLASS}-hidden { visibility: hidden }
96
91
  @keyframes xr-pulse {
97
92
  from { --xr-o: var(--xr-bone-pulse-opacity-max, 1) }
98
93
  to { --xr-o: var(--xr-bone-pulse-opacity-min, 0.5) }
@@ -148,7 +143,7 @@ const BASE_CSS = `/* One timeline per skeleton, not per leaf: the root animates
148
143
  /** The (unserialized) plate an import resolves to before anything has been captured. */
149
144
  function emptyPlate(name) {
150
145
  return {
151
- v: 2,
146
+ v: 1,
152
147
  name,
153
148
  tree: null,
154
149
  css: ""
@@ -157,22 +152,21 @@ function emptyPlate(name) {
157
152
  //#endregion
158
153
  //#region src/chunk.ts
159
154
  /**
160
- * Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR
161
- * 0008). This is the framework-neutral half of rendering: a skeleton is static,
162
- * text-free DOM, so the structure is flattened to HTML strings here, at build,
163
- * once instead of every adapter walking a tree on the latency-critical first
164
- * paint. The adapter only injects the strings and mounts a child plate at each
165
- * stitch (see the React adapters). No escaping risk: bones carry only the
166
- * content-addressed class tokens (ADR 0007), never user content.
155
+ * Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR 0008). This is the
156
+ * framework-neutral half of rendering: a skeleton is static, text-free DOM, so the structure is
157
+ * flattened to HTML strings here, at build, once — instead of every adapter walking a tree on the
158
+ * latency-critical first paint. The adapter only injects the strings and mounts a child plate at
159
+ * each stitch (see the React adapters). No escaping risk: bones carry only the content-addressed
160
+ * class tokens (ADR 0007), never user content.
167
161
  */
168
162
  function nodeClass(node) {
169
163
  return NODE_CLASS + (node.leaf ? ` ${LEAF_CLASS} ${LEAF_CLASS}-${node.leaf}` : "") + (node.cls ? ` ${node.cls}` : "");
170
164
  }
171
165
  /**
172
- * Whether this subtree holds a render-time DYNAMIC node anywhere — a stitch
173
- * (`ref`, a mount point resolved to a child plate) or a template (`count`, a cell
174
- * the adapter repeats). Either keeps the subtree from collapsing to one flat
175
- * string, since both are expanded at render rather than baked into the markup.
166
+ * Whether this subtree holds a render-time DYNAMIC node anywhere — a stitch (`ref`, a mount point
167
+ * resolved to a child plate) or a template (`count`, a cell the adapter repeats). Either keeps the
168
+ * subtree from collapsing to one flat string, since both are expanded at render rather than baked
169
+ * into the markup.
176
170
  */
177
171
  function hasDynamic(node) {
178
172
  return node.ref !== void 0 || node.count !== void 0 || (node.kids?.some(hasDynamic) ?? false);
@@ -183,13 +177,12 @@ function toHtml(node) {
183
177
  return `<div class="${nodeClass(node)}">${kids}</div>`;
184
178
  }
185
179
  /**
186
- * Serialize a node list into chunks: each maximal run with no render-time dynamic
187
- * node becomes one HTML `string`; a `ref` node becomes `{ r }`; a `count` node
188
- * becomes `{ t: html, n }` (its cell's HTML once + the repeat count); a node that
189
- * isn't itself dynamic but holds one deeper stays a real element (`{ c, k }`) and
190
- * recurses. Mirrors the element shape the runtime renderer would otherwise build
191
- * per node. A `count` cell is guaranteed stitch-free by the capture pass, so its
192
- * `toHtml` is a plain string the adapter repeats.
180
+ * Serialize a node list into chunks: each maximal run with no render-time dynamic node becomes one
181
+ * HTML `string`; a `ref` node becomes `{ r }`; a `count` node becomes `{ t: html, n }` (its cell's
182
+ * HTML once + the repeat count); a node that isn't itself dynamic but holds one deeper stays a real
183
+ * element (`{ c, k }`) and recurses. Mirrors the element shape the runtime renderer would otherwise
184
+ * build per node. A `count` cell is guaranteed stitch-free by the capture pass, so its `toHtml` is
185
+ * a plain string the adapter repeats.
193
186
  */
194
187
  function toChunks(nodes) {
195
188
  const out = [];
@@ -220,17 +213,15 @@ function toChunks(nodes) {
220
213
  return out;
221
214
  }
222
215
  /**
223
- * Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The
224
- * root's own content classes travel as `rootCls` (the adapter puts them on the
225
- * root element); its children become `chunks`. A capture-less plate has null
226
- * chunks and renders the fallback.
216
+ * Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The root's own content
217
+ * classes travel as `rootCls` (the adapter puts them on the root element); its children become
218
+ * `chunks`. A capture-less plate has null chunks and renders the fallback.
227
219
  *
228
- * Two-artifact contract (plan 005 round 2): the returned `Plate` is the TREE
229
- * artifact only — the input's `css` is deliberately NOT copied onto it. The
230
- * css string stays the producer's (`MergedPlate`/`StoredPlate`) field; each
231
- * lowering site decides independently how its css travels (the plugin's
232
- * virtual `.css` modules + `export const css`, `captureElement`'s
233
- * `{ plate, css }` pair, or the `xray:plate` dev event, which ships no css).
220
+ * Two-artifact contract (ADR 0026): the returned `Plate` is the TREE artifact only; the input's
221
+ * `css` is deliberately NOT copied onto it. The css string stays the producer's
222
+ * (`GatedPlate`/`StoredPlate`) field, and each lowering site decides independently how its css
223
+ * travels: the plugin's virtual `.css` modules + `export const css`, `captureElement`'s `{ plate,
224
+ * css }` pair, or the `xray:plate` dev event, which ships no css.
234
225
  */
235
226
  function serializePlate(merged) {
236
227
  const { v, name, tree } = merged;
@@ -252,13 +243,11 @@ function serializePlate(merged) {
252
243
  };
253
244
  }
254
245
  /**
255
- * Distinct plate names referenced by the tree's stitches, in first-seen order.
256
- * Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
257
- * gen-side stitch import graph straight off a `StoredPlate.tree`
258
- * (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
259
- * in the ADR 0022 cutover — it is the one node-side primitive that survived the
260
- * `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
261
- * in-browser `serializePlateIR`, project.ts).
246
+ * Distinct plate names referenced by the tree's stitches, in first-seen order. Walks a `RenderNode`
247
+ * tree (the stored, post-classify shape), so it drives the gen-side stitch import graph straight
248
+ * off a `StoredPlate.tree` (`renderPlateModule`, index.ts). The one node-side primitive needed at
249
+ * build time; multi-View gating happens in the in-browser `serializePlateIR` (serialize.ts, ADR
250
+ * 0022).
262
251
  */
263
252
  function collectRefs(tree) {
264
253
  const names = [];
@@ -276,11 +265,10 @@ function collectRefs(tree) {
276
265
  //#endregion
277
266
  //#region src/diagnostics.ts
278
267
  /**
279
- * Build a single diagnostic record with its canonical message, for callers
280
- * outside the walk that surface a diagnostic directly (e.g. the client mapping
281
- * `CaptureTooLargeError`, or the server reporting an invalid committed plate).
282
- * Uses the same message table as the collector so the text is identical
283
- * everywhere. `detail` must never carry user CSS or captured text.
268
+ * Build a single diagnostic record with its canonical message, for callers outside the walk that
269
+ * surface a diagnostic directly (e.g. the client mapping `CaptureTooLargeError`, or the server
270
+ * reporting an invalid committed plate). Uses the same message table as the collector so the text
271
+ * is identical everywhere. `detail` must never carry user CSS or captured text.
284
272
  */
285
273
  function makeDiagnostic(code, options) {
286
274
  return {
@@ -291,10 +279,9 @@ function makeDiagnostic(code, options) {
291
279
  };
292
280
  }
293
281
  /**
294
- * The human-readable message per code, shared verbatim by browser and server so
295
- * their text cannot drift. Messages describe the omission and its fidelity
296
- * cost; none echoes user CSS or captured text. `formatDiagnostic` appends the
297
- * aggregate count and any short detail.
282
+ * The human-readable message per code, shared verbatim by browser and server so their text cannot
283
+ * drift. Messages describe the omission and its fidelity cost; none echoes user CSS or captured
284
+ * text. `formatDiagnostic` appends the aggregate count and any short detail.
298
285
  */
299
286
  const DIAGNOSTIC_MESSAGES = {
300
287
  "unreadable-stylesheet": "could not read a stylesheet (likely cross-origin); rules from it were not captured",
@@ -309,11 +296,10 @@ const DIAGNOSTIC_MESSAGES = {
309
296
  "invalid-plate-file": "a committed plate file could not be read (corrupt JSON, wrong shape, or a version mismatch) and was ignored"
310
297
  };
311
298
  /**
312
- * The single shared formatter. Renders one diagnostic to a stable one-line
313
- * string used IDENTICALLY by the browser console and the Vite server output, so
314
- * the two can never disagree. Shape: `<message> (<count>)[: <detail>]`. Count is
315
- * omitted when 1 or absent; detail is appended only when present and is never
316
- * user content.
299
+ * The single shared formatter. Renders one diagnostic to a stable one-line string used IDENTICALLY
300
+ * by the browser console and the Vite server output, so the two can never disagree. Shape:
301
+ * `<message> (<count>)[: <detail>]`. Count is omitted when 1 or absent; detail is appended only
302
+ * when present and is never user content.
317
303
  */
318
304
  function formatDiagnostic(diagnostic) {
319
305
  const count = diagnostic.count !== void 0 && diagnostic.count > 1 ? ` (${diagnostic.count})` : "";
@@ -327,28 +313,26 @@ function diagnosticsHeader(name) {
327
313
  //#endregion
328
314
  //#region src/name.ts
329
315
  /**
330
- * Plate-name validation, shared by the Vite plugin (`index.ts`) and the
331
- * Node-only POST validator (`validate.ts`).
316
+ * Plate-name validation, shared by the Vite plugin (`index.ts`) and the Node-only POST validator
317
+ * (`validate.ts`).
332
318
  *
333
- * Lives in its own dependency-free module so `validate.ts` can reuse it without
334
- * importing the plugin entry (which would be a cycle), and so the regex stays
335
- * the single source of truth for what a plate name may be — the same rule the
336
- * loader, the file watcher, the coverage reader, the POST endpoint, and the
337
- * ref-graph emitter all gate on.
319
+ * Lives in its own dependency-free module so `validate.ts` can reuse it without importing the
320
+ * plugin entry (which would be a cycle), and so the regex stays the single source of truth for what
321
+ * a plate name may be — the same rule the loader, the file watcher, the coverage reader, the POST
322
+ * endpoint, and the ref-graph emitter all gate on.
338
323
  *
339
324
  * @module
340
325
  */
341
326
  /**
342
327
  * Names come from import specifiers and POST bodies — keep them inside the plates dir.
343
328
  *
344
- * Each path segment is `\w+(-\w+)*` — word chars with single interior hyphens,
345
- * never doubled and never at a segment edge — and segments are joined by `/`.
346
- * The class-prefix encoder (`classify.ts`) renders `/` as `--` to keep nested
347
- * names readable, and that prefix is the isolation boundary across stitched
348
- * plates (ADR 0007), so the encoding must be injective. Banning a literal `--`
349
- * and any edge hyphen guarantees no `-` ever abuts a `/`, so every `--` in the
350
- * prefix came from exactly one `/` making distinct names always map to
351
- * distinct prefixes (else e.g. `a/-b` and `a-/b` would both encode to `a---b`).
329
+ * Each path segment is `\w+(-\w+)*` — word chars with single interior hyphens, never doubled and
330
+ * never at a segment edge — and segments are joined by `/`. The class-prefix encoder
331
+ * (`classify.ts`) renders `/` as `--` to keep nested names readable, and that prefix is the
332
+ * isolation boundary across stitched plates (ADR 0007), so the encoding must be injective. Banning
333
+ * a literal `--` and any edge hyphen guarantees no `-` ever abuts a `/`, so every `--` in the
334
+ * prefix came from exactly one `/` making distinct names always map to distinct prefixes (else
335
+ * e.g. `a/-b` and `a-/b` would both encode to `a---b`).
352
336
  *
353
337
  * @internal
354
338
  */
@@ -358,71 +342,66 @@ function isValidPlateName(name) {
358
342
  //#endregion
359
343
  //#region src/validate.ts
360
344
  /**
361
- * Structural validation of a posted/committed `StoredPlate`, plus the
362
- * single-CSS-declaration / media-condition validators, for the dev `/__xray`
363
- * endpoint and disk reads.
345
+ * Structural validation of a posted/committed `StoredPlate`, plus the single-CSS-declaration /
346
+ * media-condition validators, for the dev `/__xray` endpoint and disk reads.
364
347
  *
365
- * NODE-ONLY (see the capture-input-validation plan and ADR 0008). This module
366
- * imports `valibot`, which must NEVER reach the browser bundles: the dev capture
367
- * client, the HUD, the React adapters, and the framework-neutral `core` surface
368
- * all ship to a browser, and none of them needs to validate an inbound payload.
369
- * Only the Vite plugin (`index.ts`, the package `.` entry, built with
370
- * `platform: 'node'`) imports this file, which keeps valibot out of every
371
- * browser-facing dist chunk. Do NOT import this from `plate.ts`, `css.ts`, or
372
- * anything a render/browser path also imports, and do NOT re-export these
373
- * helpers from any public subpath — they stay private until a `doctor`-style
374
- * tool creates a concrete external need (the plan's decisions).
348
+ * NODE-ONLY (ADR 0023, ADR 0008). This module imports `valibot`, which must NEVER reach the browser
349
+ * bundles: the dev capture client, the HUD, the React adapters, and the framework-neutral `core`
350
+ * surface all ship to a browser, and none of them needs to validate an inbound payload. Only the
351
+ * Vite plugin (`index.ts`, the package `.` entry, built with `platform: 'node'`) imports this file,
352
+ * which keeps valibot out of every browser-facing dist chunk. Do NOT import this from `plate.ts`,
353
+ * `css.ts`, or anything a render/browser path also imports, and do NOT re-export these helpers from
354
+ * any public subpath they stay private until a `doctor`-style tool creates a concrete external
355
+ * need (ADR 0023).
375
356
  *
376
- * The endpoint accepts ONLY the artifact shape xray itself produces. After the
377
- * ADR 0022 cutover the dev client posts a finished `StoredPlate` the whole
378
- * Plate already projected in the browser (`serializePlateIR`) so `parseStoredPlate`
379
- * is the single validator: a post-classify `RenderNode` tree plus an
380
- * already-classified css blob, guarded for `<style>`-breakout only (the per-decl
381
- * allowlist no longer applies to a finished css string; see `isValidPlateCss`).
382
- * The per-declaration / per-condition validators (`isValidDeclaration` /
383
- * `isValidCondition`) remain exported helpers the css blob already passed them
384
- * AT CAPTURE TIME in the browser. Validation is necessary but not sufficient: a
385
- * committed `StoredPlate` is also untrusted (hand-edited or from git), so the
386
- * render path independently escapes raw-text `<style>` terminators (css-escape.ts).
357
+ * The endpoint accepts ONLY the artifact shape xray itself produces. The dev client posts a
358
+ * finished `StoredPlate` the whole Plate already projected in the browser (`serializePlateIR`,
359
+ * ADR 0022) so `parseStoredPlate` is the single validator: a post-classify `RenderNode` tree plus
360
+ * an already-classified css blob, guarded for `<style>`-breakout only (a per-decl allowlist cannot
361
+ * apply to a finished css string; see `isValidPlateCss`). The per-declaration / per-condition
362
+ * validators (`isValidDeclaration` / `isValidCondition`) remain exported helpers the css blob
363
+ * already passed them AT CAPTURE TIME in the browser. Validation is necessary but not sufficient: a
364
+ * committed `StoredPlate` is also untrusted (hand-edited or from git), so the render path
365
+ * independently escapes raw-text `<style>` terminators (css-escape.ts).
387
366
  *
388
367
  * @module
389
368
  */
390
369
  /**
391
- * Bounds chosen to be far above any real capture yet small enough that a hostile
392
- * payload cannot exhaust memory or wedge the merge. A `<Skeleton>` set too high
393
- * is already caught by the per-capture node guard and the serialized-size cap;
394
- * these are the structural ceilings the schema enforces before either runs.
370
+ * Bounds chosen to be far above any real capture yet small enough that a hostile payload cannot
371
+ * exhaust memory or wedge the merge. A `<Skeleton>` set too high is already caught by the
372
+ * per-capture node guard and the serialized-size cap; these are the structural ceilings the schema
373
+ * enforces before either runs.
395
374
  */
396
375
  const MAX_TREE_NODES = 5e4;
397
376
  const MAX_BREAKPOINTS = 256;
398
377
  const MAX_TREE_DEPTH = 1e3;
399
378
  /**
400
- * A template node's repeat count (Phase 2). Far above any real feed, yet bounded
401
- * so a hostile `count` cannot drive an unbounded render-time string `.repeat`.
379
+ * A template node's repeat count (Template, ADR 0022). Far above any real feed, yet bounded so a
380
+ * hostile `count` cannot drive an unbounded render-time string `.repeat`.
402
381
  */
403
382
  const MAX_TEMPLATE_COUNT = 1e4;
404
383
  /** A single declaration is `prop: value [!important]`; real ones are short. */
405
384
  const MAX_DECL_LENGTH = 2e3;
406
385
  /**
407
- * Raw-text / markup terminators that must never appear in generated CSS, even
408
- * when no bare `<`/`>` is present: HTML comment delimiters, a CDATA opener, and
409
- * a `</style` raw-text terminator. Shared by the declaration and condition
410
- * validators (case-insensitive).
386
+ * Raw-text / markup terminators that must never appear in generated CSS, even when no bare `<`/`>`
387
+ * is present: HTML comment delimiters, a CDATA opener, and a `</style` raw-text terminator. Shared
388
+ * by the declaration and condition validators (case-insensitive).
411
389
  */
412
390
  const RAW_TEXT_TERMINATORS = /<!--|-->|<!\[cdata\[|<\/style/iu;
413
391
  /** A finite number, no NaN/Infinity (valibot's `number()` already rejects NaN). */
414
392
  const finiteNumber = v.pipe(v.number(), v.finite());
415
393
  const leafSchema = v.picklist([
416
- "text",
394
+ "text-line",
417
395
  "text-block",
418
396
  "media",
419
- "box"
397
+ "box",
398
+ "hidden"
420
399
  ]);
421
400
  /**
422
- * Depth + total-node guard shared by the recursive node schema. The recursive
423
- * `array(lazy(...))` bounds breadth per level; this guards total depth so a long
424
- * thin chain cannot recurse without limit, and the running `count` bounds the
425
- * total node budget so a fanned-out payload cannot exhaust memory.
401
+ * Depth + total-node guard shared by the recursive node schema. The recursive `array(lazy(...))`
402
+ * bounds breadth per level; this guards total depth so a long thin chain cannot recurse without
403
+ * limit, and the running `count` bounds the total node budget so a fanned-out payload cannot
404
+ * exhaust memory.
426
405
  */
427
406
  function treeWithinLimits(root) {
428
407
  let count = 0;
@@ -440,27 +419,24 @@ function treeWithinLimits(root) {
440
419
  /** A finished plate's css blob, generously bounded (post-classify, whole-plate). */
441
420
  const MAX_PLATE_CSS_LENGTH = 4e6;
442
421
  /**
443
- * A NUL byte — the one control character that must never reach the rendered
444
- * `<style>`. Unlike the per-declaration validator, the whole-plate css blob
445
- * legitimately carries newlines/tabs (`classify` joins rules with `\n`), so the
446
- * broad `CONTROL_CHARS` class does NOT apply here.
422
+ * A NUL byte — the one control character that must never reach the rendered `<style>`. Unlike the
423
+ * per-declaration validator, the whole-plate css blob legitimately carries newlines/tabs
424
+ * (`classify` joins rules with `\n`), so the broad `CONTROL_CHARS` class does NOT apply here.
447
425
  */
448
426
  const NUL = /\u0000/u;
449
427
  /**
450
- * Validate a STORED, already-classified css blob (ADR 0022 cutover). The trust
451
- * model differs from a pre-classify `decls[]` array: a `StoredPlate.css` is the
452
- * WHOLE plate's content-addressed CSS — a string of selectors, `{ ... }` rule
453
- * blocks, and `@media`/`@container` at-rules so the per-declaration
454
- * `isValidDeclaration` allowlist (which forbids `{}` and re-checks `isLayoutProp`)
455
- * cannot apply: it would reject every real plate. Braces, newlines, and bare
456
- * `<`/`>` (Media Queries Level 4 range syntax, `@media (width >= 600px)`) are all
457
- * legitimate here. The css was produced by `filterLayoutDecls`-filtered decls at
458
- * capture time in the browser; on disk we guard only the security boundary the
459
- * render path also enforces: no `<style>` raw-text breakout. Reject the raw-text /
460
- * markup terminators (`</style`, `<!--`, `-->`, `<![CDATA[`) and a NUL byte, and
461
- * bound the length. The render path independently escapes these (`escapeStyleText`,
462
- * css-escape.ts) — that stays as the belt; this is the braces. A `StoredPlate` is
463
- * untrusted exactly as a `PlateFile` was (hand-edited / arriving via git).
428
+ * Validate a STORED, already-classified css blob (ADR 0022). The trust model differs from a
429
+ * pre-classify `decls[]` array: a `StoredPlate.css` is the WHOLE plate's content-addressed CSS — a
430
+ * string of selectors, `{ ... }` rule blocks, and `@media`/`@container` at-rules — so the
431
+ * per-declaration `isValidDeclaration` allowlist (which forbids `{}` and re-checks `isLayoutProp`)
432
+ * cannot apply: it would reject every real plate. Braces, newlines, and bare `<`/`>` (Media Queries
433
+ * Level 4 range syntax, `@media (width >= 600px)`) are all legitimate here. The css was produced by
434
+ * `filterLayoutDecls`-filtered decls at capture time in the browser; the disk guard covers only the
435
+ * security boundary the render path also enforces: no `<style>` raw-text breakout. Reject the
436
+ * raw-text / markup terminators (`</style`, `<!--`, `-->`, `<![CDATA[`) and a NUL byte, and bound
437
+ * the length. The render path independently escapes these (`escapeStyleText`, css-escape.ts) that
438
+ * stays as the belt; this is the braces. A `StoredPlate` is untrusted (hand-edited / arriving via
439
+ * git).
464
440
  */
465
441
  function isValidPlateCss(css) {
466
442
  if (typeof css !== "string") return false;
@@ -470,10 +446,10 @@ function isValidPlateCss(css) {
470
446
  return true;
471
447
  }
472
448
  /**
473
- * Recursive `RenderNode` — the POST-classify tree shape (plate.ts): no `id` (ids
474
- * are stripped at classify), an optional content-class string `cls`, and the same
475
- * `leaf`/`ref`/`count`/`kids` the renderer reads. Mirrors `plateNodeSchema` minus
476
- * `id`, plus a bounded `cls`. Depth/breadth bounded by the shared `treeWithinLimits`.
449
+ * Recursive `RenderNode` — the POST-classify tree shape (plate.ts): no `id` (ids are stripped at
450
+ * classify), an optional content-class string `cls`, and the same `leaf`/`ref`/`count`/`kids` the
451
+ * renderer reads. Mirrors `plateNodeSchema` minus `id`, plus a bounded `cls`. Depth/breadth bounded
452
+ * by the shared `treeWithinLimits`.
477
453
  */
478
454
  const renderNodeSchema = v.pipe(v.object({
479
455
  leaf: v.optional(leafSchema),
@@ -483,30 +459,27 @@ const renderNodeSchema = v.pipe(v.object({
483
459
  kids: v.optional(v.pipe(v.array(v.lazy(() => renderNodeSchema)), v.maxLength(MAX_TREE_NODES)))
484
460
  }), v.check((node) => treeWithinLimits(node), "tree exceeds size or depth limits"));
485
461
  /**
486
- * The committed/posted `StoredPlate` shape AFTER the ADR 0022 cutover (plate.ts):
487
- * the whole plate already projected in the browser. Validated to the SAME trust
488
- * boundary a `PlateFile` was a `StoredPlate` on disk is untrusted (hand-edited
489
- * or arriving via git). `tree` is a `RenderNode` (post-classify, nullable for a
490
- * capture-less plate); `css` is the already-classified blob, guarded for
491
- * `<style>`-breakout only (`isValidPlateCss`, above). `breakpoints` are finite
492
- * non-negative numbers, bounded but NOT renormalized (the file is the source of
493
- * truth). `v` must be the current `PLATE_VERSION`.
462
+ * The committed/posted `StoredPlate` shape (plate.ts): the whole plate already projected in the
463
+ * browser. Validated as untrusted input a `StoredPlate` on disk is untrusted (hand-edited or
464
+ * arriving via git). `tree` is a `RenderNode` (post-classify, nullable for a capture-less plate);
465
+ * `css` is the already-classified blob, guarded for `<style>`-breakout only (`isValidPlateCss`,
466
+ * above). `breakpoints` are finite non-negative numbers, bounded but NOT renormalized (the file is
467
+ * the source of truth). `v` must be the current `PLATE_VERSION`.
494
468
  */
495
469
  const storedPlateSchema = v.object({
496
- v: v.literal(2),
470
+ v: v.literal(1),
497
471
  name: v.pipe(v.string(), v.check(isValidPlateName, "invalid plate name")),
498
472
  breakpoints: v.pipe(v.array(v.pipe(finiteNumber, v.minValue(0))), v.maxLength(MAX_BREAKPOINTS)),
499
473
  tree: v.nullable(renderNodeSchema),
500
474
  css: v.pipe(v.string(), v.check(isValidPlateCss, "invalid plate css"))
501
475
  });
502
476
  /**
503
- * Parse a committed/posted `StoredPlate` against its schema (the ADR 0022 cutover
504
- * shape). Returns the typed `StoredPlate` on success or `null` on any structural
505
- * failure (corrupt object, wrong shape, version mismatch, an injected `<style>`
506
- * terminator in the css, an oversized/over-deep tree). The disk readers and the
507
- * POST handler map `null` to an empty plate / a generic 400 and never echo the
508
- * payload a malformed or hostile stored plate degrades to empty rather than
509
- * breaking the build or smuggling a `<style>`-escaping css blob through render.
477
+ * Parse a committed/posted `StoredPlate` against its schema (ADR 0022). Returns the typed
478
+ * `StoredPlate` on success or `null` on any structural failure (corrupt object, wrong shape,
479
+ * version mismatch, an injected `<style>` terminator in the css, an oversized/over-deep tree). The
480
+ * disk readers and the POST handler map `null` to an empty plate / a generic 400 and never echo the
481
+ * payload a malformed or hostile stored plate degrades to empty rather than breaking the build or
482
+ * smuggling a `<style>`-escaping css blob through render.
510
483
  */
511
484
  function parseStoredPlate(value) {
512
485
  const result = v.safeParse(storedPlateSchema, value);
@@ -517,9 +490,9 @@ function parseStoredPlate(value) {
517
490
  /**
518
491
  * Vite plugin entry point for xray.
519
492
  *
520
- * Add `xrayVitePlugin()` to a Vite config to resolve committed Plate imports in
521
- * dev and build. The dev capture client is injected only during `vite serve`,
522
- * and only when `capture` or the HUD needs it.
493
+ * Add `xrayVitePlugin()` to a Vite config to resolve committed Plate imports in dev and build. The
494
+ * dev capture client is injected only during `vite serve`, and only when `capture` or the HUD needs
495
+ * it.
523
496
  *
524
497
  * @module @hueest/xray
525
498
  */
@@ -528,19 +501,20 @@ const BOOT_ID = "virtual:xray/client";
528
501
  const RESOLVED_BOOT_ID = `\0${BOOT_ID}`;
529
502
  const ENDPOINT = "/__xray";
530
503
  /**
531
- * The shared base-css virtual module every plate's `.css` module imports, so
532
- * Vite dedupes BASE_CSS across plates instead of repeating it per plate.
533
- * Deliberately NOT `\0`-prefixed: an unprefixed virtual id is tried first (the
534
- * vanilla-extract pattern) because Vite's CSS plugins match on the module id,
535
- * and this id must be routed through that pipeline (it ends in `.css`).
504
+ * The shared base-css virtual module every plate's `.css` module imports, so Vite dedupes BASE_CSS
505
+ * across plates instead of repeating it per plate. Deliberately NOT `\0`-prefixed: an unprefixed
506
+ * virtual id is tried first (the vanilla-extract pattern) because Vite's CSS plugins match on the
507
+ * module id, and this id must be routed through that pipeline (it ends in `.css`).
536
508
  */
537
509
  const BASE_CSS_ID = "virtual:xray/base.css";
538
- /** A plate's per-name css virtual module id. Also un-`\0`-prefixed for the
539
- * same reason as `BASE_CSS_ID` see above. */
510
+ /**
511
+ * A plate's per-name css virtual module id. Also un-`\0`-prefixed for the same reason as
512
+ * `BASE_CSS_ID` — see above.
513
+ */
540
514
  const plateCssId = (name) => `${VIRTUAL_PREFIX}${name}.css`;
541
515
  /**
542
- * Create the Vite plugins that resolve committed Plates and, during dev,
543
- * capture new Plates from rendered `<Skeleton>` boundaries.
516
+ * Create the Vite plugins that resolve committed Plates and, during dev, capture new Plates from
517
+ * rendered `<Skeleton>` boundaries.
544
518
  *
545
519
  * Typical setup:
546
520
  *
@@ -553,8 +527,10 @@ const plateCssId = (name) => `${VIRTUAL_PREFIX}${name}.css`;
553
527
  * ```
554
528
  *
555
529
  * Returns two plugins:
530
+ *
556
531
  * - `xray:data` resolves `virtual:xray/plates/<name>` from committed Plate files in dev and build.
557
- * - `xray:dev` runs only during `vite serve`; it injects the capture client, receives Captures, writes Plate files, and hot-updates the page.
532
+ * - `xray:dev` runs only during `vite serve`; it injects the capture client, receives Captures,
533
+ * writes Plate files, and hot-updates the page.
558
534
  */
559
535
  function xrayVitePlugin(options = {}) {
560
536
  const platesDir = options.platesDir ?? "plates";
@@ -659,8 +635,8 @@ function xrayVitePlugin(options = {}) {
659
635
  }];
660
636
  }
661
637
  /**
662
- * The injected dev module: boot the client, POST captures back, optionally
663
- * mount the HUD. The client installs first so the HUD finds the light box store.
638
+ * The injected dev module: boot the client, POST captures back, optionally mount the HUD. The
639
+ * client installs first so the HUD finds the light box store.
664
640
  */
665
641
  function bootstrapCode(delay, settleCap, collect, hud, capture, recordMode, replayMode) {
666
642
  return [
@@ -720,39 +696,33 @@ function bootstrapCode(delay, settleCap, collect, hud, capture, recordMode, repl
720
696
  ].join("\n");
721
697
  }
722
698
  /**
723
- * Drop both of a plate's virtual modules from Vite's dev transform cache after
724
- * its file changes, each through the channel appropriate to it:
699
+ * Drop both of a plate's virtual modules from Vite's dev transform cache after its file changes,
700
+ * each through the channel appropriate to it:
725
701
  *
726
- * - The `.css` module (UN-prefixed id — see `plateCssId`) is invalidated AND
727
- * reloaded: standard Vite CSS HMR (`reloadModule` re-transforms and pushes
728
- * the update over the ws), so fresh styles land in the open page with no
729
- * reload. The css module has no capture boundary, so a live reload of it
702
+ * - The `.css` module (UN-prefixed id — see `plateCssId`) is invalidated AND reloaded: standard Vite
703
+ * CSS HMR (`reloadModule` re-transforms and pushes the update over the ws), so fresh styles land
704
+ * in the open page with no reload. The css module has no capture boundary, so a live reload of it
730
705
  * carries no remount-loop risk.
731
- * - The JS (tree) module (`\0`-prefixed id) is invalidated ONLY — deliberately
732
- * NO `reloadModule`/HMR push. A live re-import would remount the capture
733
- * boundary and re-fire the capture (the exact infinite loop the "no
734
- * addWatchFile" comment in `load` guards against; live tree updates keep
735
- * flowing over the `xray:plate` announce into the dev store instead). Bare
736
- * `invalidateModule` is server-side cache marking only nothing reaches
737
- * the client until the NEXT full page load, which then re-transforms from
738
- * disk and serves a tree consistent with the css.
706
+ * - The JS (tree) module (`\0`-prefixed id) is invalidated ONLY — deliberately NO `reloadModule`/HMR
707
+ * push. A live re-import would remount the capture boundary and re-fire the capture (the exact
708
+ * infinite loop the "no addWatchFile" comment in `load` guards against; live tree updates keep
709
+ * flowing over the `xray:plate` announce into the dev store instead). Bare `invalidateModule` is
710
+ * server-side cache marking only nothing reaches the client until the NEXT full page load,
711
+ * which then re-transforms from disk and serves a tree consistent with the css.
739
712
  *
740
- * WITHOUT the JS half (the round-3 QA bug, found dogfooding the masterworks
741
- * frontend): after a capture, a page reload paired Vite's CACHED old tree
742
- * transform (stale content-addressed class names) with the freshly reloaded
743
- * css class names no longer matched rules and skeletons rendered garbled
744
- * until a dev-server restart cleared the transform cache. The tree/css split
745
- * de-synchronized the two modules' invalidation policies; this helper is the
746
- * single place that keeps them in lockstep.
713
+ * WITHOUT the JS half, a page reload after a capture pairs Vite's CACHED stale tree transform
714
+ * (stale content-addressed class names) with the freshly reloaded css class names no longer match
715
+ * rules and skeletons render garbled until a dev-server restart clears the transform cache. The two
716
+ * modules need distinct invalidation policies; this helper is the single place that keeps them in
717
+ * lockstep.
747
718
  *
748
- * Scope note (stitched plates): a parent's module embeds ONLY its own stored
749
- * tree; a child's data arrives at runtime through the child module import
750
- * (`refs: { "<child>": __xr0 }`), never inlined into the parent's transform.
751
- * So invalidating the re-captured plate's OWN two modules is sufficient
752
- * parent modules hold nothing child-derived that could go stale.
719
+ * Scope note (stitched plates): a parent's module embeds ONLY its own stored tree; a child's data
720
+ * arrives at runtime through the child module import (`refs: { "<child>": __xr0 }`), never inlined
721
+ * into the parent's transform. So invalidating the re-captured plate's OWN two modules is
722
+ * sufficient parent modules hold nothing child-derived that could go stale.
753
723
  *
754
- * No-op per module when it was never resolved/imported yet (a fresh plate no
755
- * page has loaded, or a server stub without the module-graph subset).
724
+ * No-op per module when it was never resolved/imported yet (a fresh plate no page has loaded, or a
725
+ * server stub without the module-graph subset).
756
726
  */
757
727
  async function invalidatePlateModules(server, name) {
758
728
  const jsMod = server.moduleGraph?.getModuleById(RESOLVED_PREFIX + name);
@@ -761,10 +731,10 @@ async function invalidatePlateModules(server, name) {
761
731
  if (cssMod) await server.reloadModule?.(cssMod);
762
732
  }
763
733
  /**
764
- * Print a plate's diagnostics to the Vite server output using the SAME grouped
765
- * header + shared formatter the browser console uses (diagnostics.ts), so the
766
- * two can never drift. Prefers Vite's logger (it preserves the formatting);
767
- * falls back to `console.warn`. No-op when there are no diagnostics.
734
+ * Print a plate's diagnostics to the Vite server output using the SAME grouped header + shared
735
+ * formatter the browser console uses (diagnostics.ts), so the two can never drift. Prefers Vite's
736
+ * logger (it preserves the formatting); falls back to `console.warn`. No-op when there are no
737
+ * diagnostics.
768
738
  */
769
739
  function reportServerDiagnostics(server, name, diagnostics) {
770
740
  if (!diagnostics || diagnostics.length === 0) return;
@@ -776,14 +746,12 @@ function reportServerDiagnostics(server, name, diagnostics) {
776
746
  /** A committed plate file past this is almost always a `<Skeleton>` set too high. */
777
747
  const MAX_PLATE_BYTES = 2e6;
778
748
  /**
779
- * Hard ceiling on the POST body we will buffer. The posted StoredPlate is the
780
- * whole projected Plate the `MAX_PLATE_BYTES` cap guards, so this sits above the
781
- * plate cap with headroom (the JSON envelope + diagnostics add a little) and
782
- * exists only to stop an oversized or runaway body from being read into memory.
783
- * Enforced both
784
- * up front from `content-length` and while streaming, so a lying or absent
785
- * `content-length` cannot get past it. The dev endpoint is local and
786
- * unauthenticated, so this is the real backstop, not the header.
749
+ * Hard ceiling on the POST body the endpoint will buffer. The posted StoredPlate is the whole
750
+ * projected Plate the `MAX_PLATE_BYTES` cap guards, so this sits above the plate cap with headroom
751
+ * (the JSON envelope + diagnostics add a little) and exists only to stop an oversized or runaway
752
+ * body from being read into memory. Enforced both up front from `content-length` and while
753
+ * streaming, so a lying or absent `content-length` cannot get past it. The dev endpoint is local
754
+ * and unauthenticated, so this is the real backstop, not the header.
787
755
  */
788
756
  const MAX_BODY_BYTES = 4e6;
789
757
  /** The only content type the capture client posts; anything else is rejected up front. */
@@ -793,12 +761,11 @@ function isJsonContentType(value) {
793
761
  return /^application\/json\b/i.test(header.trim());
794
762
  }
795
763
  /**
796
- * Reject cross-origin browser writes. Browsers always attach `Origin` to
797
- * POSTs; a request from the app's own page carries the dev server's origin,
798
- * so its host must equal the `Host` the request arrived on. A request with
799
- * NO Origin header is not from a browser page (curl, a node script) and is
800
- * allowed — the gate targets drive-by browser writes to Plate and Fixture
801
- * files, not local tooling.
764
+ * Reject cross-origin browser writes. Browsers always attach `Origin` to POSTs; a request from the
765
+ * app's own page carries the dev server's origin, so its host must equal the `Host` the request
766
+ * arrived on. A request with NO Origin header is not from a browser page (curl, a node script) and
767
+ * is allowed the gate targets drive-by browser writes to Plate and Fixture files, not local
768
+ * tooling.
802
769
  */
803
770
  function isSameOriginWrite(req) {
804
771
  const origin = Array.isArray(req.headers?.origin) ? req.headers.origin[0] : req.headers?.origin;
@@ -814,14 +781,12 @@ function isRecord(value) {
814
781
  return typeof value === "object" && value !== null;
815
782
  }
816
783
  /**
817
- * Derive the HUD coverage bands from a StoredPlate's `breakpoints` (ADR 0022
818
- * cutover). Per-View spans no longer exist on disk a post-cutover StoredPlate
819
- * sweeps ALL widths in one session, so coverage is complete per-Plate and the
820
- * bands are just the breakpoint partition: each adjacent breakpoint pair is a
821
- * covered band, the first band open-min, the last open-max. `breakpoints=[b1,b2]`
822
- * `[{max:b1},{min:b1,max:b2},{min:b2}]`; `breakpoints=[]` one open band `[{}]`.
823
- * Keeps the `xray:plate`/coverage `spans` shape (`{min?, max?}[]`) identical — only
824
- * the SOURCE changes (derived from breakpoints, not read off per-View captures).
784
+ * Derive the HUD coverage bands from a StoredPlate's `breakpoints` (ADR 0022). Per-View spans do
785
+ * not exist on disk: a StoredPlate sweeps ALL widths in one session, so coverage is complete
786
+ * per-Plate and the bands are the breakpoint partition each adjacent breakpoint pair is a covered
787
+ * band, the first band open-min, the last open-max. `breakpoints=[b1,b2]`
788
+ * `[{max:b1},{min:b1,max:b2},{min:b2}]`; `breakpoints=[]` one open band `[{}]`. The
789
+ * `xray:plate`/coverage `spans` shape (`{min?, max?}[]`) is unchanged; only the SOURCE is derived.
825
790
  */
826
791
  function spansFromBreakpoints(breakpoints) {
827
792
  const sorted = [...breakpoints].toSorted((a, b) => a - b);
@@ -834,20 +799,18 @@ function spansFromBreakpoints(breakpoints) {
834
799
  spans.push({ min: sorted.at(-1) });
835
800
  return spans;
836
801
  }
837
- /** Send the stored plate (lowered to chunks) and its derived coverage bands over
838
- * the HMR event so the client hot-swaps it in place and the HUD updates its band
839
- * display. The gen-side runs `serializePlate(stored)` (the Bundle seam) here; the
840
- * `spans` derive from the stored breakpoints (the per-View list is gone).
802
+ /**
803
+ * Send the stored plate (lowered to chunks) and its derived coverage bands over the HMR event so
804
+ * the client hot-swaps it in place and the HUD updates its band display. The gen-side runs
805
+ * `serializePlate(stored)` (the Bundle seam) here; the `spans` derive from the stored breakpoints.
841
806
  *
842
- * Two-artifact contract (plan 005 round 2): the event carries the TREE only —
843
- * `serializePlate` now returns a css-less `Plate`, so the old ws css
844
- * side-channel is gone by construction. The css half of a capture travels
845
- * exclusively over Vite's CSS-HMR (`invalidatePlateModules` re-pushes the
846
- * `.css` virtual module after the write). This is also what killed the
847
- * round-1 duplication bug class for good: the live-store plate that outranks
848
- * the statically-imported module in `useLivePlate` CANNOT carry inline css
849
- * anymore, so no push can revert a mounted `<Skeleton>` to inline-`<style>`
850
- * rendering. */
807
+ * Two-artifact contract (ADR 0026): the event carries the TREE only — `serializePlate` returns a
808
+ * css-less `Plate`, so there is no ws css side-channel by construction. The css half of a capture
809
+ * travels exclusively over Vite's CSS-HMR (`invalidatePlateModules` re-pushes the `.css` virtual
810
+ * module after the write). The live-store plate that outranks the statically-imported module in
811
+ * `useLivePlate` cannot carry inline css, so no push can revert a mounted `<Skeleton>` to
812
+ * inline-`<style>` rendering.
813
+ */
851
814
  function announcePlate(ws, name, stored) {
852
815
  const breakpoints = stored?.breakpoints ?? [];
853
816
  const merged = stored ? {
@@ -868,10 +831,9 @@ function announcePlate(ws, name, stored) {
868
831
  });
869
832
  }
870
833
  /**
871
- * Per-plate breakpoints + derived coverage bands, read from the committed
872
- * StoredPlate files. The `spans` derive from each plate's `breakpoints` (ADR 0022
873
- * cutover per-View spans no longer exist on disk; a StoredPlate sweeps all
874
- * widths in one session, so its coverage is complete).
834
+ * Per-plate breakpoints + derived coverage bands, read from the committed StoredPlate files. The
835
+ * `spans` derive from each plate's `breakpoints` (ADR 0022: a StoredPlate sweeps all widths in one
836
+ * session, so its coverage is complete).
875
837
  *
876
838
  * @internal
877
839
  */
@@ -901,22 +863,25 @@ function readCoverage(dir) {
901
863
  }
902
864
  return out;
903
865
  }
904
- /** The on-disk Fixture sidecar shape (ADR 0015): version, plate name, and the
905
- * OPAQUE devalue string. The node side never parses `data` it is the browser
906
- * dev client's job (it has the live runtime). */
866
+ /**
867
+ * The on-disk Fixture sidecar shape (ADR 0015): version, plate name, and the OPAQUE devalue string.
868
+ * The node side never parses `data` — it is the browser dev client's job (it has the live
869
+ * runtime).
870
+ */
907
871
  const FIXTURE_VERSION = 1;
908
872
  const FIXTURE_SUFFIX = ".fixture.json";
909
- /** A recorded Fixture sidecar — `data` is the verbatim devalue string, treated
910
- * as opaque text server-side. */
873
+ /**
874
+ * A recorded Fixture sidecar — `data` is the verbatim devalue string, treated as opaque text
875
+ * server-side.
876
+ */
911
877
  function isFixtureFile(value) {
912
878
  return isRecord(value) && typeof value.v === "number" && typeof value.name === "string" && typeof value.data === "string";
913
879
  }
914
880
  /**
915
- * All recorded Fixtures as `{ <name>: <devalueString> }`, read from the
916
- * `<name>.fixture.json` sidecars (ADR 0015). The map values are the OPAQUE
917
- * devalue strings exactly as written the node side never parses them; the
918
- * browser dev client owns the devalue decode. The `.fixture.json` suffix keeps
919
- * these distinct from Plate files (`isValidPlateName` rejects the dotted name, so
881
+ * All recorded Fixtures as `{ <name>: <devalueString> }`, read from the `<name>.fixture.json`
882
+ * sidecars (ADR 0015). The map values are the OPAQUE devalue strings exactly as written — the node
883
+ * side never parses them; the browser dev client owns the devalue decode. The `.fixture.json`
884
+ * suffix keeps these distinct from Plate files (`isValidPlateName` rejects the dotted name, so
920
885
  * `readCoverage` never picks them up — Fixtures never leak into Plate coverage).
921
886
  *
922
887
  * @internal
@@ -951,12 +916,11 @@ function readFixtures(dir) {
951
916
  return out;
952
917
  }
953
918
  /**
954
- * Write a recorded Fixture sidecar (ADR 0015). Reuses the capture endpoint's
955
- * front-door checks (method, content type, declared/streamed size cap) and the
956
- * `isValidPlateName` gate, but treats the devalue `data` payload as OPAQUE TEXT:
957
- * it is validated only as a string and written verbatim, never eval'd or parsed
958
- * server-side. The sidecar lives NEXT TO the Plate but is never merged into it,
959
- * so the persisted Plate stays purely structural.
919
+ * Write a recorded Fixture sidecar (ADR 0015). Reuses the capture endpoint's front-door checks
920
+ * (method, content type, declared/streamed size cap) and the `isValidPlateName` gate, but treats
921
+ * the devalue `data` payload as OPAQUE TEXT: it is validated only as a string and written verbatim,
922
+ * never eval'd or parsed server-side. The sidecar lives NEXT TO the Plate but is never merged into
923
+ * it, so the persisted Plate stays purely structural.
960
924
  *
961
925
  * @internal
962
926
  */
@@ -1019,12 +983,11 @@ function handleFixturePost(req, res, fixturePath) {
1019
983
  });
1020
984
  }
1021
985
  /**
1022
- * Validate a posted `StoredPlate` envelope, write it AS-IS, and hot-swap the
1023
- * plate in place. After the ADR 0022 cutover the dev client posts a finished
1024
- * StoredPlate (the whole Plate already projected in the browser) inside a
1025
- * `{ plate, diagnostics? }` envelope there is NO server-side accumulation/merge
1026
- * anymore. `diagnostics` is a SIBLING field the server logs and then drops; it
1027
- * never reaches the stored artifact (ADR 0008).
986
+ * Validate a posted `StoredPlate` envelope, write it AS-IS, and hot-swap the plate in place. The
987
+ * dev client posts a finished StoredPlate (the whole Plate already projected in the browser, ADR
988
+ * 0022) inside a `{ plate, diagnostics? }` envelope; there is NO server-side accumulation/merge.
989
+ * `diagnostics` is a SIBLING field the server logs and then drops; it never reaches the stored
990
+ * artifact (ADR 0008).
1028
991
  *
1029
992
  * @internal
1030
993
  */
@@ -1101,12 +1064,11 @@ function handleCapturePost(req, res, server, platePath) {
1101
1064
  });
1102
1065
  }
1103
1066
  /**
1104
- * Loosely validate the envelope's SIBLING `diagnostics` (never stored, just
1105
- * logged). The StoredPlate validator does not carry diagnostics — they are a
1106
- * transient dev-only field on the POST envelope — so this filters the array to
1107
- * the known-code shape `reportServerDiagnostics` formats, dropping anything that
1108
- * is not a `{ code, message, ... }` record. A malformed diagnostics field never
1109
- * fails the post (the plate already validated); it just logs nothing.
1067
+ * Loosely validate the envelope's SIBLING `diagnostics` (never stored, just logged). The
1068
+ * StoredPlate validator does not carry diagnostics — they are a transient dev-only field on the
1069
+ * POST envelope — so this filters the array to the known-code shape `reportServerDiagnostics`
1070
+ * formats, dropping anything that is not a `{ code, message, ... }` record. A malformed diagnostics
1071
+ * field never fails the post (the plate already validated); it just logs nothing.
1110
1072
  */
1111
1073
  function validEnvelopeDiagnostics(value) {
1112
1074
  if (!Array.isArray(value)) return void 0;
@@ -1123,11 +1085,10 @@ function validEnvelopeDiagnostics(value) {
1123
1085
  return out.length > 0 ? out : void 0;
1124
1086
  }
1125
1087
  /**
1126
- * Read and validate a committed StoredPlate, distinguishing "no file" from "file
1127
- * present but unreadable". `invalid` is true ONLY when the file exists but is
1128
- * corrupt JSON, the wrong shape, or a version mismatch — a missing file is the
1129
- * normal pre-capture state and is not flagged. Callers use `invalid` to warn
1130
- * (same diagnostics vocabulary) that an existing plate was ignored.
1088
+ * Read and validate a committed StoredPlate, distinguishing "no file" from "file present but
1089
+ * unreadable". `invalid` is true ONLY when the file exists but is corrupt JSON, the wrong shape, or
1090
+ * a version mismatch — a missing file is the normal pre-capture state and is not flagged. Callers
1091
+ * use `invalid` to warn (same diagnostics vocabulary) that an existing plate was ignored.
1131
1092
  */
1132
1093
  function loadStoredPlate(file) {
1133
1094
  let raw;
@@ -1160,20 +1121,17 @@ function readStoredPlate(file) {
1160
1121
  return loadStoredPlate(file).stored;
1161
1122
  }
1162
1123
  /**
1163
- * Render a plate as the virtual module's source. Each stitch (`ref` node) becomes
1164
- * a real import of that child's virtual module, wired into a `refs` map on the
1165
- * default export so the bundler builds the actual reference graph and chunks
1166
- * the transitive closure a page uses, with no runtime registry (ADR 0006). A
1167
- * stitchless plate is just its JSON. Self-references are dropped (a module can't
1168
- * import itself); an unknown child resolves to its own empty plate, never a
1169
- * build break.
1124
+ * Render a plate as the virtual module's source. Each stitch (`ref` node) becomes a real import of
1125
+ * that child's virtual module, wired into a `refs` map on the default export — so the bundler
1126
+ * builds the actual reference graph and chunks the transitive closure a page uses, with no runtime
1127
+ * registry (ADR 0006). A stitchless plate is just its JSON. Self-references are dropped (a module
1128
+ * can't import itself); an unknown child resolves to its own empty plate, never a build break.
1170
1129
  *
1171
- * `name` defaults to the plate's own `name`, but the loader passes the
1172
- * PATH-derived name so a hand-edited StoredPlate whose `name` field drifted from
1173
- * its filename still serializes and self-references under the canonical name
1174
- * (`collectRefs` reads `stored.tree`, a RenderNode, unchanged by the cutover).
1130
+ * `name` defaults to the plate's own `name`, but the loader passes the PATH-derived name so a
1131
+ * hand-edited StoredPlate whose `name` field drifted from its filename still serializes and
1132
+ * self-references under the canonical name (`collectRefs` reads `stored.tree`, a RenderNode).
1175
1133
  *
1176
- * Two-artifact module shape (plan 005 round 2):
1134
+ * Two-artifact module shape (ADR 0026):
1177
1135
  *
1178
1136
  * ```js
1179
1137
  * import "virtual:xray/base.css"
@@ -1182,26 +1140,22 @@ function readStoredPlate(file) {
1182
1140
  * export const css = "<the plate's scoped css string>"
1183
1141
  * ```
1184
1142
  *
1185
- * The default export is the TREE artifact (`Plate` carries no css field
1186
- * anymore); the page's styling flows through Vite's own CSS pipeline via the
1187
- * two side-effect imports (dev inject + HMR, build extraction into the
1188
- * chunk's `.css` asset). `export const css` is the plate's css as data, for
1189
- * dev tooling / programmatic consumers that want the string itself
1190
- * production adapters import only the default, so Rollup TREE-SHAKES the
1191
- * unused named export out of built chunks (verified empirically in the plan's
1192
- * scratch app; the side-effect css imports do not pin it).
1143
+ * The default export is the TREE artifact (`Plate` carries no css field); the page's styling flows
1144
+ * through Vite's own CSS pipeline via the two side-effect imports (dev inject + HMR, build
1145
+ * extraction into the chunk's `.css` asset). `export const css` is the plate's css as data, for dev
1146
+ * tooling / programmatic consumers that want the string itself production adapters import only
1147
+ * the default, so Rollup TREE-SHAKES the unused named export out of built chunks (verified
1148
+ * empirically; the side-effect css imports do not pin it).
1193
1149
  *
1194
- * BASE_CSS rides as a JS-level import of the SHARED `virtual:xray/base.css`
1195
- * module (not an `@import` inside each plate's css): Vite dedupes JS module
1196
- * imports by id, so the base rules reach the page exactly once no matter how
1197
- * many plates (nested/stitched included every plate module emits this
1198
- * import, module identity collapses them) a page mounts, and at build a
1199
- * multi-chunk app gets it hoisted/shared rather than copied per chunk. An
1200
- * `@import` inside each plate's `.css` would instead be INLINED per importer
1201
- * by Vite's postcss-import at build, duplicating BASE_CSS into every plate's
1202
- * asset — the opposite of the dedupe this design locks in. Base rides before
1203
- * the plate's css module so a plate rule still beats a base rule at equal
1204
- * specificity by source order (the old inline `${BASE_CSS}\n${css}` order).
1150
+ * BASE_CSS rides as a JS-level import of the SHARED `virtual:xray/base.css` module (not an
1151
+ * `@import` inside each plate's css): Vite dedupes JS module imports by id, so the base rules reach
1152
+ * the page exactly once no matter how many plates (nested/stitched included every plate module
1153
+ * emits this import, module identity collapses them) a page mounts, and at build a multi-chunk app
1154
+ * gets it hoisted/shared rather than copied per chunk. An `@import` inside each plate's `.css`
1155
+ * would instead be INLINED per importer by Vite's postcss-import at build, duplicating BASE_CSS
1156
+ * into every plate's asset the opposite of the dedupe this design locks in. Base rides before the
1157
+ * plate's css module so a plate rule still beats a base rule at equal specificity by source order
1158
+ * (the `${BASE_CSS}\n${css}` composition order).
1205
1159
  *
1206
1160
  * @internal
1207
1161
  */