@hueest/xray 0.2.0 → 0.4.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/core.js CHANGED
@@ -1,212 +1,14 @@
1
- import { a as ROOT_ATTR, c as SPEC_GATE, d as emptyPlate, i as NODE_CLASS, o as ROOT_CLASS, r as LEAF_CLASS, t as BASE_CSS } from "./plate-DoE1HEXp.js";
2
- import { a as captureRegime, i as capture, n as regimeFor, o as defineXrayCaptureWalker, r as CaptureTooLargeError, u as classify } from "./breakpoints-CMoFUUOv.js";
1
+ import { a as ROOT_ATTR, d as emptyPlate, o as ROOT_CLASS, t as BASE_CSS } from "./plate-BRR6d8Se.js";
2
+ import { a as serializePlate, c as defineXrayCaptureWalker, i as collectRefs, l as runPlatePasses, o as CaptureTooLargeError, s as captureView, t as serializePlateIR } from "./project-BZosujs9.js";
3
3
  import { t as escapeStyleText } from "./css-escape-N7bOusGW.js";
4
- //#region src/chunk.ts
5
- /**
6
- * Serialize a merged plate's RenderNode tree into the shipped `Plate` (ADR
7
- * 0008). This is the framework-neutral half of rendering: a skeleton is static,
8
- * text-free DOM, so the structure is flattened to HTML strings here, at build,
9
- * once — instead of every adapter walking a tree on the latency-critical first
10
- * paint. The adapter only injects the strings and mounts a child plate at each
11
- * stitch (see the React adapters). No escaping risk: bones carry only the
12
- * content-addressed class tokens (ADR 0007), never user content.
13
- */
14
- function nodeClass(node) {
15
- return NODE_CLASS + (node.leaf ? ` ${LEAF_CLASS} ${LEAF_CLASS}-${node.leaf}` : "") + (node.cls ? ` ${node.cls}` : "");
16
- }
17
- /**
18
- * Whether this subtree holds a stitch (`ref`) anywhere — a stitched subtree
19
- * can't collapse to a flat string, since a `ref` resolves to a child plate at
20
- * render time and must stay a mount point.
21
- */
22
- function hasStitch(node) {
23
- return node.ref !== void 0 || (node.kids?.some(hasStitch) ?? false);
24
- }
25
- /** Serialize a stitch-free subtree to one HTML string (caller guarantees no `ref` below). */
26
- function toHtml(node) {
27
- const kids = node.kids ? node.kids.map(toHtml).join("") : "";
28
- return `<div class="${nodeClass(node)}">${kids}</div>`;
29
- }
30
- /**
31
- * Serialize a node list into chunks: each maximal stitch-free run becomes one
32
- * HTML `string`; a `ref` node becomes `{ r }`; a node that isn't itself a stitch
33
- * but holds one deeper stays a real element (`{ c, k }`) and recurses. Mirrors
34
- * the element shape the runtime renderer would otherwise build per node.
35
- */
36
- function toChunks(nodes) {
37
- const out = [];
38
- let buf = "";
39
- const flush = () => {
40
- if (buf) {
41
- out.push(buf);
42
- buf = "";
43
- }
44
- };
45
- for (const node of nodes) {
46
- if (!hasStitch(node)) {
47
- buf += toHtml(node);
48
- continue;
49
- }
50
- flush();
51
- if (node.ref !== void 0) out.push({ r: node.ref });
52
- else out.push({
53
- c: nodeClass(node),
54
- k: toChunks(node.kids ?? [])
55
- });
56
- }
57
- flush();
58
- return out;
59
- }
60
- /**
61
- * Turn a merged plate (RenderNode tree) into the shipped plate (chunks). The
62
- * root's own content classes travel as `rootCls` (the adapter puts them on the
63
- * root element); its children become `chunks`. A capture-less plate has null
64
- * chunks and renders the fallback.
65
- */
66
- function serializePlate(merged) {
67
- const { v, name, tree, css } = merged;
68
- if (!tree) return {
69
- v,
70
- name,
71
- chunks: null,
72
- css
73
- };
74
- const chunks = toChunks(tree.kids ?? []);
75
- return tree.cls ? {
76
- v,
77
- name,
78
- chunks,
79
- rootCls: tree.cls,
80
- css
81
- } : {
82
- v,
83
- name,
84
- chunks,
85
- css
86
- };
87
- }
88
- //#endregion
89
- //#region src/merge.ts
90
- /**
91
- * Turn the per-view captures in a plate file into the one plate a Skeleton
92
- * renders.
93
- *
94
- * Each view is kept whole — its own subtree, its own ids, its own rules —
95
- * and shown only across the viewport range it owns, via `@media display:none`
96
- * gates. We do NOT merge the views' trees into one. An earlier design (ADR
97
- * 0004) did, sharing common structure and forking the rest, but aligning two
98
- * genuinely-different DOM trees (mobile stacked vs desktop two-column) without
99
- * stable keys proved unreliable: every heuristic mis-paired some nodes and
100
- * remapped one view's rules onto another's elements, corrupting both. A lone
101
- * capture is pixel-faithful; keeping each view lone preserves that. The cost
102
- * is a larger plate (roughly the sum of the views) — bounded by the plate
103
- * size cap — in exchange for correctness.
104
- *
105
- * Pure data-in/data-out — runs in node (plugin load) and in tests.
106
- */
107
- function mergeViews(file) {
108
- const views = file.views;
109
- if (views.length === 0) return emptyPlate(file.name);
110
- if (views.length === 1) {
111
- const [only] = views;
112
- if (!only) return emptyPlate(file.name);
113
- const { tree, css } = classify(only.rules, only.tree, file.name);
114
- return {
115
- v: 1,
116
- name: file.name,
117
- tree,
118
- css
119
- };
120
- }
121
- const starts = views.map((r) => regimeFor(r.width, file.breakpoints).min ?? 0);
122
- let nextId = 1;
123
- const rootKids = [];
124
- const rules = [];
125
- views.forEach((view, i) => {
126
- const idMap = /* @__PURE__ */ new Map();
127
- const kids = (view.tree.kids ?? []).map((kid) => cloneWithIds(kid, idMap, () => nextId++));
128
- rootKids.push(...kids);
129
- rules.push(...remapRules(view.rules, idMap));
130
- const lo = i === 0 ? 0 : starts[i] ?? 0;
131
- const hi = i === views.length - 1 ? Number.POSITIVE_INFINITY : starts[i + 1] ?? 0;
132
- for (const kid of kids) {
133
- if (lo > 0) rules.push(gate(kid.id, `(max-width: ${lo - .02}px)`));
134
- if (hi !== Number.POSITIVE_INFINITY) rules.push(gate(kid.id, `(min-width: ${hi}px)`));
135
- }
136
- });
137
- const { tree, css } = classify(rules, {
138
- id: 0,
139
- kids: rootKids
140
- }, file.name);
141
- return {
142
- v: 1,
143
- name: file.name,
144
- tree,
145
- css
146
- };
147
- }
148
- function gate(id, condition) {
149
- return {
150
- ids: [id],
151
- decls: ["display: none !important"],
152
- media: [condition],
153
- spec: SPEC_GATE,
154
- order: id
155
- };
156
- }
157
- /** Deep-clone a captured subtree into fresh ids, recording the mapping. */
158
- function cloneWithIds(node, idMap, allocate) {
159
- const id = allocate();
160
- idMap.set(node.id, id);
161
- const kids = node.kids?.map((kid) => cloneWithIds(kid, idMap, allocate));
162
- return {
163
- id,
164
- ...node.leaf ? { leaf: node.leaf } : {},
165
- ...node.ref !== void 0 ? { ref: node.ref } : {},
166
- ...kids ? { kids } : {}
167
- };
168
- }
169
- /** Distinct plate names referenced by the tree's stitches, in first-seen order. */
170
- function collectRefs(tree) {
171
- const names = [];
172
- const seen = /* @__PURE__ */ new Set();
173
- const visit = (node) => {
174
- if (node.ref !== void 0 && !seen.has(node.ref)) {
175
- seen.add(node.ref);
176
- names.push(node.ref);
177
- }
178
- for (const kid of node.kids ?? []) visit(kid);
179
- };
180
- if (tree) visit(tree);
181
- return names;
182
- }
183
- function remapRules(rules, idMap) {
184
- return rules.map((rule) => ({
185
- ...rule,
186
- ids: rule.ids.map((id) => idMap.get(id) ?? id)
187
- }));
188
- }
189
- /** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
190
- function addCapture(file, name, capture, breakpoints) {
191
- const allBreakpoints = [...new Set([...file?.breakpoints ?? [], ...breakpoints])].toSorted((a, b) => a - b);
192
- const spanKey = (width) => JSON.stringify(regimeFor(width, allBreakpoints));
193
- const { diagnostics: _diagnostics, ...persisted } = capture;
194
- const views = [...file?.views ?? [], persisted];
195
- const byView = /* @__PURE__ */ new Map();
196
- for (const view of views) byView.set(spanKey(view.width), view);
197
- return {
198
- v: 1,
199
- name,
200
- breakpoints: allBreakpoints,
201
- views: [...byView.values()].toSorted((a, b) => a.width - b.width)
202
- };
203
- }
204
- //#endregion
205
4
  //#region src/core.ts
206
5
  /**
207
- * Capture a live DOM subtree straight to a renderable Plate `capture` followed
208
- * by `serializePlate` in one call. `roots` are the top-level elements to capture
209
- * (a component's rendered roots). Pass the result to a `<Skeleton plate>`.
6
+ * Capture a live DOM subtree straight to a renderable Plate. Runs the same
7
+ * uniform pipeline the dev client does for a single View measure (`captureView`),
8
+ * reduce (`runPlatePasses`), project (`serializePlateIR` the structured
9
+ * `StoredPlate`), then lower to chunks (`serializePlate`) — in one call. `roots`
10
+ * are the top-level elements to capture (a component's rendered roots). Pass the
11
+ * result to a `<Skeleton plate>`.
210
12
  *
211
13
  * `options.walker` is the public programmable capture walker (ADR 0018, Q2 —
212
14
  * yes, public): the same `XrayCaptureWalker` the `<Skeleton captureWalker>` prop
@@ -214,7 +16,14 @@ function addCapture(file, name, capture, breakpoints) {
214
16
  * capture-only and never persisted into the returned Plate.
215
17
  */
216
18
  function captureElement(roots, options) {
217
- return serializePlate(capture(roots, options));
19
+ const plate = captureView(roots, {
20
+ collect: options.collect,
21
+ walker: options.walker,
22
+ captureRootIsBoundary: options.captureRootIsBoundary
23
+ });
24
+ plate.name = options.name;
25
+ runPlatePasses(plate);
26
+ return serializePlate(serializePlateIR(plate));
218
27
  }
219
28
  /**
220
29
  * Render a stitch-free Plate to a self-contained HTML string — the
@@ -228,9 +37,24 @@ function captureElement(roots, options) {
228
37
  function renderPlateHtml(plate) {
229
38
  const chunks = plate.chunks;
230
39
  if (!chunks) return "";
231
- if (chunks.length !== 1 || typeof chunks[0] !== "string") throw new Error("xray: renderPlateHtml renders stitch-free plates only — use a framework adapter for stitched plates");
232
40
  const className = plate.rootCls ? `${ROOT_CLASS} ${plate.rootCls}` : ROOT_CLASS;
233
- return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${escapeStyleText(plate.css)}</style>${chunks[0]}</div>`;
41
+ return `<div ${ROOT_ATTR}="${plate.name}" class="${className}" aria-hidden="true" aria-busy="true"><style>${BASE_CSS}\n${escapeStyleText(plate.css)}</style>${chunksToHtml(chunks)}</div>`;
42
+ }
43
+ /**
44
+ * Flatten chunks to one HTML string. A `string` concatenates; a template chunk
45
+ * (`{ t, n }`) expands to its cell repeated `n` times (the cell is stitch-free by
46
+ * construction); a stitch-free wrapper (`{ c, k }`) recurses inside a real `<div>`.
47
+ * A stitch (`{ r }`) resolves to a child plate at render and needs a framework
48
+ * adapter to mount, so a stitched plate throws here. Chunk HTML is xray-generated
49
+ * and interpolated raw, exactly as the single-string path was before.
50
+ */
51
+ function chunksToHtml(chunks) {
52
+ let html = "";
53
+ for (const chunk of chunks) if (typeof chunk === "string") html += chunk;
54
+ else if ("t" in chunk) html += chunk.t.repeat(chunk.n);
55
+ else if ("r" in chunk) throw new Error("xray: renderPlateHtml renders stitch-free plates only — use a framework adapter for stitched plates");
56
+ else html += `<div class="${chunk.c}">${chunksToHtml(chunk.k)}</div>`;
57
+ return html;
234
58
  }
235
59
  //#endregion
236
- export { CaptureTooLargeError, addCapture, capture, captureElement, captureRegime, collectRefs, defineXrayCaptureWalker, emptyPlate, mergeViews, renderPlateHtml, serializePlate };
60
+ export { CaptureTooLargeError, captureElement, collectRefs, defineXrayCaptureWalker, emptyPlate, renderPlateHtml, serializePlate };
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ import { Plugin } from "vite";
7
7
  * browser console (`client.ts`) and the Vite server output (`index.ts`) print.
8
8
  *
9
9
  * Motivation: the serializer silently swallows a handful of capture omissions
10
- * (an unreadable cross-origin stylesheet, skipped pseudo-elements, a collapsed
11
- * list run, an over-the-limit subtree). Early adopters asking "why does this
10
+ * (an unreadable cross-origin stylesheet, skipped pseudo-elements, off-screen
11
+ * bones dropped at the fold, an over-the-limit subtree). Early adopters asking "why does this
12
12
  * skeleton look wrong?" had to read the serializer source to find out. These
13
13
  * diagnostics surface those omissions as data the capture process returns, so a
14
14
  * caller can log them — without the core performing any console side effects.
@@ -16,7 +16,7 @@ import { Plugin } from "vite";
16
16
  * Strictly DEV-ONLY (see the plan's decisions and ADR 0008): this type is NOT a
17
17
  * public package export and MUST NEVER reach the persisted Plate JSON, the
18
18
  * `MergedPlate`, or the production adapter. It rides on the TRANSIENT
19
- * `ViewCapture.diagnostics` field only, which `captureRegime` populates and the
19
+ * `ViewIR.diagnostics` field only, which `captureView` populates and the
20
20
  * dev client strips before posting (so committed `plates/<name>.json` stays
21
21
  * byte-identical to a capture that produced no diagnostics).
22
22
  *
@@ -46,10 +46,10 @@ interface CaptureDiagnostic {
46
46
  * serializer (or an existing oversized-capture warning folded into the same
47
47
  * vocabulary). Kept as a const union so the message table is exhaustive.
48
48
  */
49
- type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-dynamic-pseudo' | 'skipped-pseudo-element' | 'unsupported-rule' | 'dropped-tag' | 'pruned-run' | 'too-large' | 'invalid-plate-file';
49
+ type DiagnosticCode = 'unreadable-stylesheet' | 'unreadable-import' | 'skipped-dynamic-pseudo' | 'skipped-pseudo-element' | 'unsupported-rule' | 'dropped-tag' | 'dropped-subset' | 'dropped-cropped' | 'too-large' | 'invalid-plate-file';
50
50
  //#endregion
51
51
  //#region src/plate.d.ts
52
- type LeafKind = 'text' | 'media' | 'box';
52
+ type LeafKind = 'text' | 'text-block' | 'media' | 'box';
53
53
  /**
54
54
  * The shipped structural capture of a component's rendered DOM — the data a
55
55
  * Skeleton renders from (see CONTEXT.md). Self-contained: pre-serialized chunks
@@ -83,21 +83,27 @@ interface Plate {
83
83
  /**
84
84
  * One serialized child of a plate root (or of a stitch-bearing node): a
85
85
  * stitch-free HTML run (`string`), a stitch to a child plate (`{ r: name }`),
86
- * or a stitch-bearing element kept real so its inner stitches can mount
87
- * (`{ c: className, k: chunk[] }`). The framework-neutral render contract: an
88
- * adapter injects the strings and mounts a child plate at each stitch (ADR 0008).
86
+ * a stitch-bearing element kept real so its inner stitches can mount
87
+ * (`{ c: className, k: chunk[] }`), or a templated uniform run (`{ t: html, n }`):
88
+ * the cell's HTML once plus a repeat count the adapter expands to N copies (Phase
89
+ * 2). The framework-neutral render contract: an adapter injects the strings,
90
+ * mounts a child plate at each stitch, and repeats each template (ADR 0008).
89
91
  */
90
92
  type Chunk = string | {
91
93
  r: string;
92
94
  } | {
93
95
  c: string;
94
96
  k: Chunk[];
97
+ } | {
98
+ t: string;
99
+ n: number;
95
100
  };
96
101
  /**
97
- * A plate after view-merge + classify but before chunk serialization: the
98
- * build-time RenderNode tree plus its scoped CSS. Produced by `mergeViews` and
99
- * `capture`, then serialized into the shipped `Plate` (chunks) at load
100
- * (`serializePlate`). Never shipped to the client.
102
+ * A plate after the in-browser Serialize (emit + merge/gate + classify) but
103
+ * before chunk serialization: the build-time RenderNode tree plus its scoped
104
+ * CSS. Produced by `serializePlateIR` (the structural part of `StoredPlate`) and
105
+ * the single-View `capture`, then serialized into the shipped `Plate` (chunks) at
106
+ * load (`serializePlate`). Never shipped to the client.
101
107
  */
102
108
  interface MergedPlate {
103
109
  v: number;
@@ -116,6 +122,12 @@ interface PlateNode {
116
122
  * against `Plate.refs` (prod) or the live store (dev).
117
123
  */
118
124
  ref?: string;
125
+ /**
126
+ * A templated uniform run (Phase 2): render this node's subtree `count` times.
127
+ * The serializer emits the subtree's HTML once plus the count; the adapter
128
+ * expands it to N copies at render. Absent on ordinary nodes.
129
+ */
130
+ count?: number;
119
131
  kids?: PlateNode[];
120
132
  }
121
133
  /**
@@ -128,6 +140,8 @@ interface RenderNode {
128
140
  leaf?: LeafKind;
129
141
  ref?: string;
130
142
  cls?: string;
143
+ /** A templated uniform run (Phase 2): serialize this subtree once + a count the adapter repeats. */
144
+ count?: number;
131
145
  kids?: RenderNode[];
132
146
  }
133
147
  /**
@@ -148,8 +162,11 @@ interface PlateRule {
148
162
  }
149
163
  /**
150
164
  * One capture of a component within one viewport view — `[min, max)` in px,
151
- * either side open-ended when undefined. What the dev client ships to the
152
- * plugin, and what the plate file accumulates (ADR 0004).
165
+ * either side open-ended when undefined. The id-form `{ tree, rules }` shape
166
+ * `emit` projects a measured View into (ADR 0004). NO LONGER the disk/wire shape
167
+ * after the ADR 0022 cutover — `StoredPlate` (below) is. Kept as the
168
+ * framework-neutral per-View capture shape, exported for consumers reading the
169
+ * id-form projection directly.
153
170
  */
154
171
  interface ViewCapture {
155
172
  /** Viewport width at capture time. */
@@ -160,23 +177,31 @@ interface ViewCapture {
160
177
  rules: PlateRule[];
161
178
  /**
162
179
  * TRANSIENT, dev-only capture diagnostics (why this view may be lower
163
- * fidelity). Populated by `captureRegime` in the browser; the dev client logs
164
- * it and posts it (so the Vite server can print the same text), then it is
165
- * STRIPPED at the merge layer (`addCapture`) before persisting. It MUST NEVER
166
- * be persisted: it is not part of the committed `plates/<name>.json` shape,
167
- * the `isViewCapture` validator does not require it, and `addCapture` drops it
168
- * so neither it nor `mergeViews` ever writes it. Absent in production (the
169
- * prod adapter ships no capture). See diagnostics.ts and ADR 0008.
180
+ * fidelity). Captured in the browser (mirrored onto `ViewIR.diagnostics`); the
181
+ * dev client logs it and posts it alongside the `StoredPlate` (so the Vite
182
+ * server can print the same text). It MUST NEVER be persisted: it is not part
183
+ * of the committed
184
+ * `StoredPlate` disk shape, the validator does not carry it, and the POST
185
+ * envelope keeps it in a SIBLING field that never reaches the stored artifact
186
+ * (ADR 0008). Absent in production (the prod adapter ships no capture). See
187
+ * diagnostics.ts and ADR 0008.
170
188
  */
171
189
  diagnostics?: CaptureDiagnostic[];
172
190
  }
173
- /** The committed `plates/<name>.json`: per-view captures, merged at load. */
174
- interface PlateFile {
175
- v: number;
176
- name: string;
177
- /** Width thresholds (view boundaries) derived so far. */
191
+ /**
192
+ * The committed `plates/<name>.json` shape AFTER the ADR 0022 cutover: the whole
193
+ * Plate already projected in the browser (the in-browser Serialize folds emit +
194
+ * the multi-View merge/gate + classify in one session), so disk holds ONE gated,
195
+ * content-addressed tree + css instead of per-View captures merged at load. It is
196
+ * a `MergedPlate` (the structural `{tree, css}` `serializePlate` consumes) PLUS the
197
+ * `breakpoints` the HUD coverage reads — the analysis-only geometry never crosses
198
+ * the wire (ADR 0021). The gen-side runs `serializePlate(stored) → Plate` (chunks)
199
+ * at module-load, leaving the Tailwind/Bundle passthrough a clean gen-side seam.
200
+ * Replaces `PlateFile` (+ `ViewCapture`) on disk and on the `/__xray` wire.
201
+ */
202
+ interface StoredPlate extends MergedPlate {
203
+ /** Width thresholds (view boundaries); the HUD coverage derives its bands from these. */
178
204
  breakpoints: number[];
179
- views: ViewCapture[];
180
205
  }
181
206
  /**
182
207
  * A dev-only sticky boolean toggle (get/set/subscribe), persisted per tab.
@@ -298,6 +323,11 @@ interface CaptureHook {
298
323
  delay?: number;
299
324
  walker?: unknown;
300
325
  mode?: 'auto' | 'manual';
326
+ collect?: {
327
+ maxNodes?: number;
328
+ maxBreadth?: number;
329
+ maxDepth?: number;
330
+ };
301
331
  }) => (() => void) | undefined;
302
332
  /** Light Box toggle: force every `<Skeleton>` to its skeleton, read by `<Skeleton>`. */
303
333
  lightbox?: ToggleStore;
@@ -305,8 +335,8 @@ interface CaptureHook {
305
335
  capture?: ToggleStore;
306
336
  /** Read side for `<Skeleton>`. */
307
337
  plates?: PlateStore;
308
- /** Write side for the dev client's HMR-event handler. */
309
- updatePlate?: (name: string, plate: Plate) => void;
338
+ /** Write side for the dev client's HMR-event handler. Returns whether the plate changed. */
339
+ updatePlate?: (name: string, plate: Plate) => boolean;
310
340
  /** Dev-only: run the HUD-triggered capture-all-views sweep via the extension (ADR 0010). */
311
341
  captureAllViews?: () => Promise<void>;
312
342
  /** Dev-only per-plate breakpoints + captured spans, from disk; fed by the bootstrap. */
@@ -335,29 +365,17 @@ declare global {
335
365
  var __XRAY__: CaptureHook | undefined;
336
366
  }
337
367
  //#endregion
338
- //#region src/merge.d.ts
368
+ //#region src/chunk.d.ts
339
369
  /**
340
- * Turn the per-view captures in a plate file into the one plate a Skeleton
341
- * renders.
342
- *
343
- * Each view is kept whole its own subtree, its own ids, its own rules —
344
- * and shown only across the viewport range it owns, via `@media display:none`
345
- * gates. We do NOT merge the views' trees into one. An earlier design (ADR
346
- * 0004) did, sharing common structure and forking the rest, but aligning two
347
- * genuinely-different DOM trees (mobile stacked vs desktop two-column) without
348
- * stable keys proved unreliable: every heuristic mis-paired some nodes and
349
- * remapped one view's rules onto another's elements, corrupting both. A lone
350
- * capture is pixel-faithful; keeping each view lone preserves that. The cost
351
- * is a larger plate (roughly the sum of the views) — bounded by the plate
352
- * size cap — in exchange for correctness.
353
- *
354
- * Pure data-in/data-out — runs in node (plugin load) and in tests.
370
+ * Distinct plate names referenced by the tree's stitches, in first-seen order.
371
+ * Walks a `RenderNode` tree (the stored, post-classify shape), so it drives the
372
+ * gen-side stitch import graph straight off a `StoredPlate.tree`
373
+ * (`renderPlateModule`, index.ts). Relocated here from the now-deleted `merge.ts`
374
+ * in the ADR 0022 cutover — it is the one node-side primitive that survived the
375
+ * `mergeViews`/`addCapture` removal (the multi-View merge/gate now lives in the
376
+ * in-browser `serializePlateIR`, project.ts).
355
377
  */
356
- declare function mergeViews(file: PlateFile): MergedPlate;
357
- /** Distinct plate names referenced by the tree's stitches, in first-seen order. */
358
378
  declare function collectRefs(tree: RenderNode | null): string[];
359
- /** Fold a fresh capture into the plate file: union breakpoints, one capture per view. */
360
- declare function addCapture(file: PlateFile | null, name: string, capture: ViewCapture, breakpoints: readonly number[]): PlateFile;
361
379
  //#endregion
362
380
  //#region src/name.d.ts
363
381
  /**
@@ -412,8 +430,19 @@ interface XrayOptions {
412
430
  delay?: number;
413
431
  /** Upper bound in milliseconds on waiting for a busy subtree to go quiet before capturing anyway. Default: `5000`. */
414
432
  settleCap?: number;
415
- /** Refuse captures larger than this many nodes (Skeleton too high in the tree). Default: `4000`. */
416
- maxNodes?: number;
433
+ /**
434
+ * Collect-stage limits (ADR 0022): refuse to capture a boundary whose measured
435
+ * tree exceeds any axis — too big means the Skeleton sits too high in the tree.
436
+ * `maxNodes` caps total surviving nodes (default `4096`), `maxBreadth` the widest
437
+ * level (default `512`), `maxDepth` the nesting depth (default `32`). Omit
438
+ * `maxNodes` and it derives as `round(maxBreadth × maxDepth / 4)`. Override one
439
+ * boundary with the flat `maxNodes`/`maxBreadth`/`maxDepth` `<Skeleton>` props.
440
+ */
441
+ collect?: {
442
+ maxNodes?: number;
443
+ maxBreadth?: number;
444
+ maxDepth?: number;
445
+ };
417
446
  /**
418
447
  * Dev-only Fixtures (ADR 0015): record a `<Skeleton>`'s render-input `data` to a
419
448
  * `plates/<name>.fixture.json` sidecar and replay it in place of live data so
@@ -488,16 +517,16 @@ interface CaptureServer {
488
517
  };
489
518
  }
490
519
  /**
491
- * Per-plate breakpoints + captured spans, read from the committed plate files.
520
+ * Per-plate breakpoints + derived coverage bands, read from the committed
521
+ * StoredPlate files. The `spans` derive from each plate's `breakpoints` (ADR 0022
522
+ * cutover — per-View spans no longer exist on disk; a StoredPlate sweeps all
523
+ * widths in one session, so its coverage is complete).
492
524
  *
493
525
  * @internal
494
526
  */
495
527
  declare function readCoverage(dir: string): Record<string, {
496
528
  breakpoints: number[];
497
- spans: {
498
- min?: number;
499
- max?: number;
500
- }[];
529
+ spans: Span[];
501
530
  }>;
502
531
  /**
503
532
  * All recorded Fixtures as `{ <name>: <devalueString> }`, read from the
@@ -522,7 +551,12 @@ declare function readFixtures(dir: string): Record<string, string>;
522
551
  */
523
552
  declare function handleFixturePost(req: CaptureRequest, res: CaptureResponse, fixturePath: (name: string) => string): void;
524
553
  /**
525
- * Fold a posted capture into its plate file and hot-swap the plate in place.
554
+ * Validate a posted `StoredPlate` envelope, write it AS-IS, and hot-swap the
555
+ * plate in place. After the ADR 0022 cutover the dev client posts a finished
556
+ * StoredPlate (the whole Plate already projected in the browser) inside a
557
+ * `{ plate, diagnostics? }` envelope — there is NO server-side accumulation/merge
558
+ * anymore. `diagnostics` is a SIBLING field the server logs and then drops; it
559
+ * never reaches the stored artifact (ADR 0008).
526
560
  *
527
561
  * @internal
528
562
  */
@@ -536,8 +570,13 @@ declare function handleCapturePost(req: CaptureRequest, res: CaptureResponse, se
536
570
  * import itself); an unknown child resolves to its own empty plate, never a
537
571
  * build break.
538
572
  *
573
+ * `name` defaults to the plate's own `name`, but the loader passes the
574
+ * PATH-derived name so a hand-edited StoredPlate whose `name` field drifted from
575
+ * its filename still serializes and self-references under the canonical name
576
+ * (`collectRefs` reads `stored.tree`, a RenderNode, unchanged by the cutover).
577
+ *
539
578
  * @internal
540
579
  */
541
- declare function renderPlateModule(merged: MergedPlate): string;
580
+ declare function renderPlateModule(merged: MergedPlate, name?: string): string;
542
581
  //#endregion
543
- export { type LeafKind, type Plate, type PlateFile, type PlateNode, type PlateRule, type ViewCapture, XrayOptions, addCapture, collectRefs, handleCapturePost, handleFixturePost, isValidPlateName, mergeViews, readCoverage, readFixtures, renderPlateModule, xrayVitePlugin };
582
+ export { type LeafKind, type Plate, type PlateNode, type PlateRule, type StoredPlate, type ViewCapture, XrayOptions, collectRefs, handleCapturePost, handleFixturePost, isValidPlateName, readCoverage, readFixtures, renderPlateModule, xrayVitePlugin };