@changfenhuang/dsh-genui 0.9.8 → 0.9.9

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +13 -8
  3. package/README.zh-CN.md +13 -8
  4. package/SKILL.md +5 -5
  5. package/lib/client.js +3 -3
  6. package/lib/index.js +1273 -338
  7. package/lib/types/client/blocks/advanced.d.ts +1 -1
  8. package/lib/types/client/blocks/checkbox.d.ts +17 -0
  9. package/lib/types/client/blocks/forms.d.ts +3 -8
  10. package/lib/types/client/blocks/state.d.ts +18 -14
  11. package/lib/types/client/dom-fence.d.ts +1 -1
  12. package/lib/types/client/genui-runtime/diagnostics.d.ts +18 -0
  13. package/lib/types/client/genui-runtime/index.d.ts +1 -0
  14. package/lib/types/client/genui-runtime/limits.d.ts +54 -0
  15. package/lib/types/client/genui-runtime/normalize.d.ts +15 -0
  16. package/lib/types/client/genui-runtime/schema.d.ts +73 -0
  17. package/lib/types/client/genui-runtime/value-utils.d.ts +18 -0
  18. package/lib/types/client/guard.d.ts +60 -71
  19. package/lib/types/client/interaction-store.d.ts +7 -2
  20. package/lib/types/client/panel-command.d.ts +1 -1
  21. package/lib/types/client/primitive-labels.d.ts +11 -0
  22. package/lib/types/client/spec.d.ts +17 -17
  23. package/lib/types/plugin/chart-contract.d.ts +6 -4
  24. package/lib/types/plugin/index.d.ts +1 -4
  25. package/package.json +74 -13
  26. package/src/client/GenuiBlock.tsx +27 -11
  27. package/src/client/blocks/advanced.tsx +6 -5
  28. package/src/client/blocks/charts.tsx +1 -1
  29. package/src/client/blocks/checkbox.tsx +60 -0
  30. package/src/client/blocks/diagram/index.tsx +1 -1
  31. package/src/client/blocks/forms.tsx +51 -28
  32. package/src/client/blocks/render-node.tsx +3 -16
  33. package/src/client/blocks/state.ts +18 -14
  34. package/src/client/dom-fence.tsx +2 -1
  35. package/src/client/fence-render.tsx +26 -15
  36. package/src/client/genui-runtime/diagnostics.ts +143 -0
  37. package/src/client/genui-runtime/index.ts +1 -0
  38. package/src/client/genui-runtime/limits.ts +54 -0
  39. package/src/client/genui-runtime/normalize.ts +115 -0
  40. package/src/client/genui-runtime/schema.ts +291 -0
  41. package/src/client/genui-runtime/value-utils.ts +67 -0
  42. package/src/client/guard.ts +314 -169
  43. package/src/client/index.tsx +3 -2
  44. package/src/client/interaction-store.ts +7 -2
  45. package/src/client/panel-command.ts +2 -1
  46. package/src/client/panel.tsx +1 -1
  47. package/src/client/primitive-labels.ts +37 -0
  48. package/src/client/spec.ts +17 -17
  49. package/src/client/toolview.tsx +2 -1
  50. package/src/index.ts +3 -6
  51. package/src/plugin/chart-contract.ts +7 -97
  52. package/src/plugin/index.ts +2 -6
  53. package/src/plugin/tool.ts +60 -40
package/lib/index.js CHANGED
@@ -21,136 +21,8 @@ function wrapSingleComponentRoot(value) {
21
21
  return root;
22
22
  }
23
23
  //#endregion
24
- //#region src/client/guard.ts
25
- /** Hard resource limits enforced by repair (and mirrored at render time). */
26
- const GENUI_LIMITS = {
27
- /** Maximum nesting depth of the component tree. */
28
- maxDepth: 8,
29
- /** Maximum total nodes across the whole spec. */
30
- maxNodes: 200,
31
- /** Maximum length of any plain string field. */
32
- maxString: 2e3,
33
- /** Maximum length of a `code` body. */
34
- maxCode: 12e3,
35
- /** Maximum length of a mermaid source. */
36
- maxMermaid: 8e3,
37
- /** Maximum `grid` columns. */
38
- maxGridCols: 12,
39
- /** Maximum `tabs` count. */
40
- maxTabs: 12,
41
- /** Maximum `accordion` items. */
42
- maxAccordionItems: 24,
43
- /** Maximum `list` items. */
44
- maxListItems: 50,
45
- /** Maximum `select`/`radio` options. */
46
- maxOptions: 50,
47
- /** Maximum `table` rows / columns. */
48
- maxTableRows: 50,
49
- maxTableCols: 12,
50
- /** Maximum `chart` data points per series. */
51
- maxChartPoints: 60,
52
- /** Maximum `plot` series and per-series parameters. */
53
- maxPlotSeries: 8,
54
- maxPlotParams: 6,
55
- /** Maximum `scene3d` meshes. */
56
- maxMeshes: 5,
57
- /** Maximum `quiz` options. */
58
- maxQuizOptions: 8,
59
- /** Maximum `steps` / `timeline` / `breadcrumb` / `keyvalue` entries. */
60
- maxSteps: 24,
61
- maxTimelineItems: 24,
62
- maxBreadcrumbItems: 12,
63
- maxKeyValuePairs: 24,
64
- /** Maximum `file-tree` nesting. */
65
- maxTreeDepth: 6,
66
- /** Maximum `diagram` nodes / edges / zones / focal accents (editorial
67
- * complexity budget, mirroring diagram-design's §7 limits). */
68
- maxDiagramNodes: 9,
69
- maxDiagramEdges: 12,
70
- maxDiagramZones: 3,
71
- maxDiagramFocal: 2,
72
- maxDiagramLabel: 14,
73
- /** Maximum depth of an `echart` option object (prevents pathological nested
74
- * ECharts configs from stalling the guard walk). */
75
- maxEChartOptionDepth: 10,
76
- /** Maximum length of any single array inside an `echart` option (prevents
77
- * a model from stalling rendering with `series.data` of hundreds of
78
- * thousands of points). */
79
- maxEChartArrayLen: 500,
80
- /** Maximum total entries (object keys + array elements) traversed while
81
- * sanitizing an `echart` option. Bounds the walk so a pathologically
82
- * large option object cannot stall the guard. */
83
- maxEChartOptionNodes: 2e3
84
- };
85
- /** Is `v` one of `values`? (enum guard) */
86
- function inEnum(v, values) {
87
- return typeof v === "string" && values.includes(v);
88
- }
89
- /** String field: truncate a string to `cap`, or undefined when not a string. */
90
- function str(v, cap) {
91
- return typeof v === "string" ? v.slice(0, cap) : void 0;
92
- }
93
- /**
94
- * Color field: the value lands in an inline `style` (background/stroke) or
95
- * THREE.Color. Arbitrary CSS values are an exfiltration channel — a model
96
- * (or a hostile spec) could emit `url(https://attacker/track?...)` and the
97
- * browser would fetch it. Only formats that name a color pass: hex, rgb/hsl
98
- * functions, and host design tokens (`var(--dsw-*)`). Anything else degrades
99
- * to the component's default palette.
100
- */
101
- const SAFE_COLOR_RE = /^(?:#[\da-fA-F]{3,8}|rgba?\([^)]{0,64}\)|hsla?\([^)]{0,64}\)|var\(--dsw-[\w-]+(?:,\s*#[0-9a-fA-F]{3,8})?\))$/;
102
- function color(v) {
103
- if (typeof v !== "string") return void 0;
104
- const s = v.trim();
105
- return s.length <= 64 && SAFE_COLOR_RE.test(s) ? s : void 0;
106
- }
107
- /**
108
- * Link target field: only http(s) and mailto survive. `javascript:`/`data:`
109
- * and every other scheme degrade to a plain-text node — the model's link is
110
- * display, not an execution channel.
111
- */
112
- function safeHref(v) {
113
- if (typeof v !== "string") return void 0;
114
- const s = v.trim();
115
- if (s.length > 2048) return void 0;
116
- return /^https?:\/\//i.test(s) || /^mailto:[^@\s]+@[^@\s]+$/i.test(s) ? s : void 0;
117
- }
118
- /** Media loads bytes, so accept only browser-reachable http(s) or same-origin
119
- * relative paths. Active/local schemes and protocol-relative URLs are
120
- * rejected. The renderer always keeps playback user-controlled. */
121
- function safeMediaSrc(v) {
122
- if (typeof v !== "string") return void 0;
123
- const s = v.trim();
124
- if (s === "" || s.length > 2048) return void 0;
125
- if (/^https?:\/\//i.test(s)) return s;
126
- if (/^[a-z][a-z0-9+.-]*:/i.test(s) || /^[/\\]{2}/.test(s)) return void 0;
127
- return s;
128
- }
129
- /** Finite-number field: clamp into [min, max], or undefined when not finite. */
130
- function num(v, min, max) {
131
- return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, v)) : void 0;
132
- }
133
- /** Integer field: clamp into [min, max], or undefined when not a finite integer. */
134
- function int(v, min, max) {
135
- return typeof v === "number" && Number.isFinite(v) ? Math.min(max, Math.max(min, Math.trunc(v))) : void 0;
136
- }
137
- /** Optional enum field: the value when it matches, otherwise undefined. */
138
- function enu(v, values) {
139
- return inEnum(v, values) ? v : void 0;
140
- }
141
- /** Plain object (not array, not null). */
142
- function obj$1(v) {
143
- return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
144
- }
145
- /**
146
- * Optional-field spread helper. `exactOptionalPropertyTypes` forbids
147
- * `{ gap: number | undefined }`; computing the value into a const first and
148
- * spreading `opt('gap', g)` keeps every optional field either absent or a
149
- * plain value.
150
- */
151
- function opt(key, value) {
152
- return value === void 0 ? {} : { [key]: value };
153
- }
24
+ //#region src/client/genui-runtime/schema.ts
25
+ /** Canonical enum domains shared by schema validation and repair. */
154
26
  const TEXT_SIZES = [
155
27
  "h1",
156
28
  "h2",
@@ -174,7 +46,8 @@ const BADGE_TONES = [
174
46
  const INPUT_TYPES = [
175
47
  "text",
176
48
  "email",
177
- "password"
49
+ "password",
50
+ "color"
178
51
  ];
179
52
  const CALLOUT_TONES = [
180
53
  "info",
@@ -267,6 +140,880 @@ const ECHART_PRESETS = [
267
140
  "pie",
268
141
  "scatter"
269
142
  ];
143
+ const schema = (required, fields, aliases = {}, options = {}) => {
144
+ const optional = Object.fromEntries(Object.entries(fields).filter(([field]) => field !== "type" && !required.includes(field)));
145
+ const oneOfRequired = options.oneOfRequired ?? [];
146
+ const conditionalRequired = options.conditionalRequired ?? [];
147
+ return {
148
+ required,
149
+ fields,
150
+ optional,
151
+ aliases,
152
+ oneOfRequired,
153
+ conditionalRequired,
154
+ rules: [...oneOfRequired.map((fieldsInRule) => ({
155
+ kind: "one-of-required",
156
+ fields: fieldsInRule
157
+ })), ...conditionalRequired],
158
+ enums: options.enums ?? {},
159
+ nested: options.nested ?? {},
160
+ ...options.validator === void 0 ? {} : { validator: options.validator }
161
+ };
162
+ };
163
+ const recordSchema = (required, fields, nested = {}, enums = {}) => ({
164
+ required,
165
+ fields,
166
+ enums,
167
+ nested
168
+ });
169
+ const nodeFields = { type: "string" };
170
+ const chartDatumSchema = recordSchema(["label", "value"], {
171
+ label: "string",
172
+ value: "number",
173
+ color: "string"
174
+ });
175
+ const chartSeriesSchema = recordSchema(["label", "data"], {
176
+ label: "string",
177
+ color: "string",
178
+ data: "array"
179
+ }, { data: chartDatumSchema });
180
+ const stepsRecordSchema = recordSchema(["title"], {
181
+ title: "string",
182
+ desc: "string"
183
+ });
184
+ const keyValueRecordSchema = recordSchema(["key", "value"], {
185
+ key: "string",
186
+ value: "string"
187
+ });
188
+ const timelineRecordSchema = recordSchema(["title"], {
189
+ title: "string",
190
+ desc: "string",
191
+ time: "string"
192
+ });
193
+ const diffRecordSchema = recordSchema(["path", "newText"], {
194
+ path: "string",
195
+ oldText: "string-or-null",
196
+ newText: "string"
197
+ });
198
+ const plotSeriesSchema = recordSchema(["expr"], {
199
+ expr: "string",
200
+ label: "string",
201
+ color: "string",
202
+ kind: "string",
203
+ params: "array"
204
+ }, { params: recordSchema(["name", "value"], {
205
+ name: "string",
206
+ value: "number",
207
+ min: "number",
208
+ max: "number",
209
+ step: "number",
210
+ animateTo: "number",
211
+ durationMs: "number",
212
+ loop: "boolean"
213
+ }) }, { kind: PLOT_KINDS });
214
+ const sceneMeshSchema = recordSchema(["shape"], {
215
+ shape: "string",
216
+ color: "string",
217
+ position: "array",
218
+ rotation: "array",
219
+ scale: "unknown",
220
+ size: "unknown"
221
+ }, {}, { shape: MESH_SHAPES });
222
+ function fileTreeRecordSchema(depth) {
223
+ return recordSchema(["name"], {
224
+ name: "string",
225
+ type: "string",
226
+ children: "array"
227
+ }, depth > 0 ? { children: fileTreeRecordSchema(depth - 1) } : {}, { type: FILE_TYPES });
228
+ }
229
+ const fileTreeNodeSchema = fileTreeRecordSchema(6);
230
+ const tabHolderSchema = recordSchema(["label", "items"], {
231
+ label: "string",
232
+ items: "nodes",
233
+ content: "nodes"
234
+ });
235
+ const accordionHolderSchema = recordSchema(["title", "items"], {
236
+ title: "string",
237
+ items: "nodes"
238
+ });
239
+ const diagramNodeSchema = recordSchema(["id", "label"], {
240
+ id: "string",
241
+ label: "string",
242
+ sub: "string",
243
+ type: "string",
244
+ x: "number",
245
+ y: "number",
246
+ w: "number",
247
+ h: "number",
248
+ tag: "string"
249
+ }, {}, { type: DIAGRAM_NODE_TYPES });
250
+ const diagramEdgeSchema = recordSchema(["from", "to"], {
251
+ from: "string",
252
+ to: "string",
253
+ label: "string",
254
+ kind: "string",
255
+ route: "string"
256
+ }, {}, {
257
+ kind: DIAGRAM_EDGE_KINDS,
258
+ route: DIAGRAM_ROUTES
259
+ });
260
+ const diagramZoneSchema = recordSchema(["label"], {
261
+ label: "string",
262
+ x: "number",
263
+ y: "number",
264
+ w: "number",
265
+ h: "number"
266
+ });
267
+ const diagramThemeSchema = recordSchema([], {
268
+ paper: "string",
269
+ "paper-2": "string",
270
+ ink: "string",
271
+ muted: "string",
272
+ soft: "string",
273
+ rule: "string",
274
+ accent: "string",
275
+ "accent-tint": "string",
276
+ link: "string"
277
+ });
278
+ /** Root GenUI specification metadata used by diagnostics. */
279
+ const GENUI_SPEC_SCHEMA = schema(["items"], {
280
+ title: "string",
281
+ gap: "number",
282
+ panel: "boolean",
283
+ append: "boolean",
284
+ items: "nodes"
285
+ });
286
+ /**
287
+ * Native component field metadata.
288
+ *
289
+ * This is intentionally explicit rather than inferred from TypeScript
290
+ * interfaces: the registry is also consumed at runtime by normalization and
291
+ * diagnostics, where erased interfaces are unavailable.
292
+ */
293
+ const COMPONENT_SCHEMAS = {
294
+ accordion: schema(["items"], {
295
+ ...nodeFields,
296
+ items: "array"
297
+ }, {}, { nested: { items: accordionHolderSchema } }),
298
+ audio: schema(["src"], {
299
+ ...nodeFields,
300
+ src: "string",
301
+ alt: "string",
302
+ loop: "boolean"
303
+ }),
304
+ avatar: schema(["name"], {
305
+ ...nodeFields,
306
+ name: "string",
307
+ color: "string"
308
+ }),
309
+ badge: schema(["label"], {
310
+ ...nodeFields,
311
+ label: "string",
312
+ tone: "string",
313
+ icon: "string"
314
+ }, {
315
+ text: "label",
316
+ value: "label"
317
+ }, { enums: { tone: BADGE_TONES } }),
318
+ breadcrumb: schema(["items"], {
319
+ ...nodeFields,
320
+ items: "array"
321
+ }),
322
+ button: schema(["label"], {
323
+ ...nodeFields,
324
+ label: "string",
325
+ tone: "string",
326
+ full: "boolean",
327
+ small: "boolean",
328
+ icon: "string",
329
+ action: "string"
330
+ }, {}, { enums: { tone: BUTTON_TONES } }),
331
+ callout: schema(["content"], {
332
+ ...nodeFields,
333
+ title: "string",
334
+ content: "string",
335
+ tone: "string"
336
+ }, { kind: "tone" }, { enums: { tone: CALLOUT_TONES } }),
337
+ card: schema(["items"], {
338
+ ...nodeFields,
339
+ title: "string",
340
+ items: "nodes"
341
+ }, {
342
+ label: "title",
343
+ content: "items"
344
+ }),
345
+ chart: schema([], {
346
+ ...nodeFields,
347
+ kind: "string",
348
+ data: "array",
349
+ series: "array"
350
+ }, {}, {
351
+ oneOfRequired: [["data", "series"]],
352
+ conditionalRequired: [{
353
+ kind: "required-if",
354
+ when: {
355
+ field: "kind",
356
+ equals: "line"
357
+ },
358
+ required: ["data"]
359
+ }, {
360
+ kind: "required-if",
361
+ when: {
362
+ field: "kind",
363
+ equals: "donut"
364
+ },
365
+ required: ["data"]
366
+ }],
367
+ nested: {
368
+ data: chartDatumSchema,
369
+ series: chartSeriesSchema
370
+ },
371
+ enums: { kind: CHART_KINDS },
372
+ validator: { name: "chart-renderability" }
373
+ }),
374
+ checkbox: schema(["label"], {
375
+ ...nodeFields,
376
+ label: "string",
377
+ checked: "boolean",
378
+ action: "string",
379
+ group: "string"
380
+ }),
381
+ code: schema(["code"], {
382
+ ...nodeFields,
383
+ lang: "string",
384
+ code: "string"
385
+ }),
386
+ col: schema(["items"], {
387
+ ...nodeFields,
388
+ items: "nodes",
389
+ gap: "number"
390
+ }),
391
+ copy: schema(["text"], {
392
+ ...nodeFields,
393
+ label: "string",
394
+ text: "string"
395
+ }),
396
+ diagram: schema(["kind", "nodes"], {
397
+ ...nodeFields,
398
+ kind: "string",
399
+ variant: "string",
400
+ title: "string",
401
+ nodes: "array",
402
+ edges: "array",
403
+ zones: "array",
404
+ theme: "object"
405
+ }, {}, {
406
+ nested: {
407
+ nodes: diagramNodeSchema,
408
+ edges: diagramEdgeSchema,
409
+ zones: diagramZoneSchema,
410
+ theme: diagramThemeSchema
411
+ },
412
+ enums: {
413
+ kind: DIAGRAM_KINDS,
414
+ variant: DIAGRAM_VARIANTS
415
+ }
416
+ }),
417
+ diff: schema(["diffs"], {
418
+ ...nodeFields,
419
+ diffs: "array"
420
+ }, {}, { nested: { diffs: diffRecordSchema } }),
421
+ divider: schema([], nodeFields),
422
+ echart: schema([], {
423
+ ...nodeFields,
424
+ title: "string",
425
+ height: "number",
426
+ preset: "string",
427
+ data: "array",
428
+ series: "array",
429
+ option: "object"
430
+ }, {}, {
431
+ oneOfRequired: [[
432
+ "option",
433
+ "data",
434
+ "series"
435
+ ]],
436
+ enums: { preset: ECHART_PRESETS }
437
+ }),
438
+ "file-tree": schema(["items"], {
439
+ ...nodeFields,
440
+ items: "array"
441
+ }, {}, { nested: { items: fileTreeNodeSchema } }),
442
+ grid: schema(["items"], {
443
+ ...nodeFields,
444
+ cols: "number",
445
+ items: "nodes"
446
+ }),
447
+ image: schema(["src"], {
448
+ ...nodeFields,
449
+ src: "string",
450
+ alt: "string"
451
+ }),
452
+ input: schema([], {
453
+ ...nodeFields,
454
+ label: "string",
455
+ placeholder: "string",
456
+ value: "string",
457
+ inputType: "string",
458
+ action: "string",
459
+ id: "string"
460
+ }, {}, { enums: { inputType: INPUT_TYPES } }),
461
+ json: schema(["value"], {
462
+ ...nodeFields,
463
+ value: "unknown"
464
+ }),
465
+ keyvalue: schema(["pairs"], {
466
+ ...nodeFields,
467
+ pairs: "array"
468
+ }, {}, { nested: { pairs: keyValueRecordSchema } }),
469
+ link: schema(["label"], {
470
+ ...nodeFields,
471
+ label: "string",
472
+ href: "string"
473
+ }),
474
+ list: schema(["items"], {
475
+ ...nodeFields,
476
+ items: "array"
477
+ }),
478
+ mermaid: schema(["code"], {
479
+ ...nodeFields,
480
+ code: "string"
481
+ }),
482
+ plot: schema(["series"], {
483
+ ...nodeFields,
484
+ series: "array",
485
+ xMin: "number",
486
+ xMax: "number",
487
+ yMin: "number",
488
+ yMax: "number",
489
+ title: "string"
490
+ }, {}, { nested: { series: plotSeriesSchema } }),
491
+ progress: schema(["value"], {
492
+ ...nodeFields,
493
+ value: "number",
494
+ label: "string",
495
+ valueLabel: "string"
496
+ }),
497
+ quiz: schema(["question", "options"], {
498
+ ...nodeFields,
499
+ question: "string",
500
+ options: "array",
501
+ explanation: "string",
502
+ id: "string",
503
+ action: "string"
504
+ }),
505
+ radio: schema(["options"], {
506
+ ...nodeFields,
507
+ label: "string",
508
+ options: "array",
509
+ selected: "number",
510
+ action: "string",
511
+ group: "string",
512
+ answer: "unknown",
513
+ explanation: "string"
514
+ }),
515
+ row: schema(["items"], {
516
+ ...nodeFields,
517
+ items: "nodes",
518
+ wrap: "boolean",
519
+ spacer: "boolean"
520
+ }),
521
+ scene3d: schema(["meshes"], {
522
+ ...nodeFields,
523
+ title: "string",
524
+ meshes: "array",
525
+ ambient: "number",
526
+ background: "string"
527
+ }, {}, { nested: { meshes: sceneMeshSchema } }),
528
+ select: schema(["options"], {
529
+ ...nodeFields,
530
+ label: "string",
531
+ options: "array",
532
+ action: "string",
533
+ selected: "number",
534
+ id: "string"
535
+ }),
536
+ slider: schema([], {
537
+ ...nodeFields,
538
+ label: "string",
539
+ min: "number",
540
+ max: "number",
541
+ step: "number",
542
+ value: "number",
543
+ action: "string",
544
+ id: "string"
545
+ }),
546
+ spacer: schema([], nodeFields),
547
+ stat: schema(["label", "value"], {
548
+ ...nodeFields,
549
+ label: "string",
550
+ value: "string",
551
+ delta: "string"
552
+ }),
553
+ steps: schema(["steps"], {
554
+ ...nodeFields,
555
+ steps: "array",
556
+ current: "number"
557
+ }, { items: "steps" }, { nested: { steps: stepsRecordSchema } }),
558
+ submit: schema(["label"], {
559
+ ...nodeFields,
560
+ label: "string",
561
+ action: "string",
562
+ resetAction: "string",
563
+ groups: "array"
564
+ }),
565
+ switch: schema(["label"], {
566
+ ...nodeFields,
567
+ label: "string",
568
+ checked: "boolean",
569
+ action: "string"
570
+ }),
571
+ table: schema(["columns", "rows"], {
572
+ ...nodeFields,
573
+ columns: "array",
574
+ rows: "array"
575
+ }, {
576
+ headers: "columns",
577
+ data: "rows"
578
+ }),
579
+ tabs: schema(["tabs"], {
580
+ ...nodeFields,
581
+ tabs: "array"
582
+ }, {}, { nested: { tabs: tabHolderSchema } }),
583
+ text: schema(["content"], {
584
+ ...nodeFields,
585
+ content: "string",
586
+ size: "string",
587
+ center: "boolean"
588
+ }, { text: "content" }, { enums: { size: TEXT_SIZES } }),
589
+ textarea: schema([], {
590
+ ...nodeFields,
591
+ label: "string",
592
+ placeholder: "string",
593
+ rows: "number",
594
+ value: "string",
595
+ action: "string",
596
+ id: "string"
597
+ }),
598
+ timeline: schema(["items"], {
599
+ ...nodeFields,
600
+ items: "array"
601
+ }, {}, { nested: { items: timelineRecordSchema } }),
602
+ video: schema(["src"], {
603
+ ...nodeFields,
604
+ src: "string",
605
+ alt: "string",
606
+ poster: "string",
607
+ loop: "boolean",
608
+ muted: "boolean",
609
+ aspectRatio: "string"
610
+ }, {}, { enums: { aspectRatio: MEDIA_ASPECT_RATIOS } })
611
+ };
612
+ const GENUI_NATIVE_TYPES = new Set(Object.keys(COMPONENT_SCHEMAS));
613
+ //#endregion
614
+ //#region src/client/genui-runtime/normalize.ts
615
+ /** Deterministic GenUI alias and structural normalization. */
616
+ function record$1(value) {
617
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
618
+ }
619
+ function isNode$1(value) {
620
+ const candidate = record$1(value);
621
+ return candidate !== void 0 && typeof candidate.type === "string";
622
+ }
623
+ function normalizeAliasFields(value, path, type, warnings) {
624
+ const definition = COMPONENT_SCHEMAS[type];
625
+ if (definition === void 0) return value;
626
+ const out = { ...value };
627
+ for (const [alias, canonical] of Object.entries(definition.aliases)) {
628
+ if (!(alias in out)) continue;
629
+ const aliasPath = `${path}.${alias}`;
630
+ const keptCanonical = canonical in out;
631
+ if (!keptCanonical) out[canonical] = out[alias];
632
+ delete out[alias];
633
+ warnings.push({
634
+ kind: "alias",
635
+ path: aliasPath,
636
+ message: keptCanonical ? `${aliasPath} is ignored because canonical field '${canonical}' is present` : `${aliasPath} normalized/adopted as '${canonical}'`,
637
+ type,
638
+ field: alias,
639
+ canonical
640
+ });
641
+ }
642
+ return out;
643
+ }
644
+ function normalizeNode(value, path, warnings) {
645
+ if (!isNode$1(value)) return value;
646
+ const type = value.type;
647
+ if (COMPONENT_SCHEMAS[type] === void 0) return value;
648
+ const out = normalizeAliasFields(value, path, type, warnings);
649
+ const normalizeNodeValue = (child, childPath) => normalizeNode(child, childPath, warnings);
650
+ const normalizeNodeArray = (children, childPath) => Array.isArray(children) ? children.map((child, index) => normalizeNodeValue(child, `${childPath}[${index}]`)) : children;
651
+ if (type === "row" || type === "col" || type === "grid" || type === "card" || type === "file-tree" || type === "timeline" || type === "breadcrumb") {
652
+ if (type !== "file-tree" && type !== "timeline" && type !== "breadcrumb") out.items = normalizeNodeArray(out.items, `${path}.items`);
653
+ } else if (type === "list" && Array.isArray(out.items)) out.items = out.items.map((child, index) => isNode$1(child) ? normalizeNodeValue(child, `${path}.items[${index}]`) : child);
654
+ else if (type === "tabs" && Array.isArray(out.tabs)) out.tabs = out.tabs.map((tab, index) => {
655
+ const holder = record$1(tab);
656
+ if (holder === void 0) return tab;
657
+ const normalizedHolder = { ...holder };
658
+ if ("content" in normalizedHolder) {
659
+ const tabPath = `${path}.tabs[${index}].content`;
660
+ const hasItems = "items" in normalizedHolder;
661
+ if (!hasItems) normalizedHolder.items = normalizedHolder.content;
662
+ delete normalizedHolder.content;
663
+ warnings.push({
664
+ kind: "alias",
665
+ path: tabPath,
666
+ message: hasItems ? `${tabPath} is ignored because canonical field 'items' is present` : `${tabPath} normalized/adopted as 'items'`,
667
+ type,
668
+ field: "content",
669
+ canonical: "items"
670
+ });
671
+ }
672
+ normalizedHolder.items = Array.isArray(normalizedHolder.items) ? normalizedHolder.items.map((child, childIndex) => normalizeNodeValue(child, `${path}.tabs[${index}].items[${childIndex}]`)) : normalizedHolder.items === void 0 ? normalizedHolder.items : [normalizeNodeValue(normalizedHolder.items, `${path}.tabs[${index}].items[0]`)];
673
+ return normalizedHolder;
674
+ });
675
+ else if (type === "accordion" && Array.isArray(out.items)) out.items = out.items.map((item, index) => {
676
+ const holder = record$1(item);
677
+ if (holder === void 0) return item;
678
+ return {
679
+ ...holder,
680
+ items: Array.isArray(holder.items) ? holder.items.map((child, childIndex) => normalizeNodeValue(child, `${path}.items[${index}].items[${childIndex}]`)) : holder.items
681
+ };
682
+ });
683
+ return out;
684
+ }
685
+ /**
686
+ * Normalize a raw GenUI value into canonical field names.
687
+ *
688
+ * Only deterministic aliases and structural aliases are changed. Resource
689
+ * limits, type repair, security filtering, and semantic validation remain in
690
+ * the guard layer. Unknown component types are returned opaque.
691
+ *
692
+ * @param value - Raw GenUI spec or bare native component.
693
+ * @returns Canonical value and stable alias diagnostics.
694
+ */
695
+ function normalizeGenuiSpec(value) {
696
+ const warnings = [];
697
+ const root = record$1(value);
698
+ if (root === void 0) return {
699
+ value,
700
+ warnings
701
+ };
702
+ const out = { ...root };
703
+ if (Array.isArray(out.items)) out.items = out.items.map((item, index) => normalizeNode(item, `items[${index}]`, warnings));
704
+ else if (typeof out.type === "string") return {
705
+ value: normalizeNode(out, "spec", warnings),
706
+ warnings
707
+ };
708
+ return {
709
+ value: out,
710
+ warnings
711
+ };
712
+ }
713
+ //#endregion
714
+ //#region src/client/genui-runtime/diagnostics.ts
715
+ /** GenUI runtime diagnostics for aliases and unknown fields. */
716
+ function record(value) {
717
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
718
+ }
719
+ function isNode(value) {
720
+ const candidate = record(value);
721
+ return candidate !== void 0 && typeof candidate.type === "string";
722
+ }
723
+ function visitNativeNodes(value, path, visit) {
724
+ if (!isNode(value)) return;
725
+ const type = value.type;
726
+ const definition = COMPONENT_SCHEMAS[type];
727
+ if (definition === void 0) return;
728
+ visit(value, path, definition);
729
+ const children = (child, childPath) => visitNativeNodes(child, childPath, visit);
730
+ if ((type === "row" || type === "col" || type === "grid" || type === "card") && Array.isArray(value.items)) value.items.forEach((child, index) => children(child, `${path}.items[${index}]`));
731
+ else if (type === "list" && Array.isArray(value.items)) value.items.forEach((child, index) => children(child, `${path}.items[${index}]`));
732
+ else if (type === "tabs" && Array.isArray(value.tabs)) value.tabs.forEach((tab, index) => {
733
+ const holder = record(tab);
734
+ if (holder === void 0) return;
735
+ if (Array.isArray(holder.items)) holder.items.forEach((child, childIndex) => children(child, `${path}.tabs[${index}].items[${childIndex}]`));
736
+ else children(holder.items, `${path}.tabs[${index}].items`);
737
+ });
738
+ else if (type === "accordion" && Array.isArray(value.items)) value.items.forEach((item, index) => {
739
+ const holder = record(item);
740
+ if (holder?.items !== void 0 && Array.isArray(holder.items)) holder.items.forEach((child, childIndex) => children(child, `${path}.items[${index}].items[${childIndex}]`));
741
+ });
742
+ }
743
+ function pushUnknownField(warnings, path, field, type) {
744
+ warnings.push({
745
+ kind: "unknown-field",
746
+ path: `${path}.${field}`,
747
+ message: `${path}.${field}: unknown field for '${type}'`,
748
+ type,
749
+ field
750
+ });
751
+ }
752
+ function diagnoseRecordFields(value, path, definition, type, warnings) {
753
+ const holder = record(value);
754
+ if (holder === void 0) return;
755
+ for (const field of Object.keys(holder)) {
756
+ if (field in definition.fields) continue;
757
+ pushUnknownField(warnings, path, field, type);
758
+ }
759
+ for (const [field, nested] of Object.entries(definition.nested)) {
760
+ const nestedValue = holder[field];
761
+ if (Array.isArray(nestedValue)) nestedValue.forEach((item, index) => diagnoseRecordFields(item, `${path}.${field}[${index}]`, nested, type, warnings));
762
+ else if (nestedValue !== void 0) diagnoseRecordFields(nestedValue, `${path}.${field}`, nested, type, warnings);
763
+ }
764
+ }
765
+ function diagnoseNestedFields(node, path, definition, warnings) {
766
+ for (const [field, nested] of Object.entries(definition.nested)) {
767
+ const nestedValue = node[field];
768
+ if (Array.isArray(nestedValue)) nestedValue.forEach((item, index) => diagnoseRecordFields(item, `${path}.${field}[${index}]`, nested, node.type, warnings));
769
+ else if (nestedValue !== void 0) diagnoseRecordFields(nestedValue, `${path}.${field}`, nested, node.type, warnings);
770
+ }
771
+ }
772
+ /**
773
+ * Diagnose unknown direct fields on native nodes.
774
+ *
775
+ * Unknown types are intentionally skipped so custom renderers retain their
776
+ * opaque extension payloads. Native unknown fields are warnings, not errors.
777
+ *
778
+ * @param value - Canonical or raw GenUI value.
779
+ * @returns Stable field diagnostics in tree order.
780
+ */
781
+ function diagnoseUnknownGenuiFields(value) {
782
+ const warnings = [];
783
+ const root = record(value);
784
+ if (root === void 0) return warnings;
785
+ const visit = (node, path, definition) => {
786
+ for (const field of Object.keys(node)) {
787
+ if (field === "type" || field in definition.fields) continue;
788
+ pushUnknownField(warnings, path, field, node.type);
789
+ }
790
+ diagnoseNestedFields(node, path, definition, warnings);
791
+ };
792
+ if (Array.isArray(root.items)) {
793
+ for (const field of Object.keys(root)) {
794
+ if (field in GENUI_SPEC_SCHEMA.fields) continue;
795
+ pushUnknownField(warnings, "spec", field, "spec");
796
+ }
797
+ root.items.forEach((item, index) => visitNativeNodes(item, `items[${index}]`, visit));
798
+ } else if (typeof root.type === "string") visitNativeNodes(root, "spec", visit);
799
+ else for (const field of Object.keys(root)) {
800
+ if (field in GENUI_SPEC_SCHEMA.fields) continue;
801
+ pushUnknownField(warnings, "spec", field, "spec");
802
+ }
803
+ return warnings;
804
+ }
805
+ //#endregion
806
+ //#region src/client/genui-runtime/limits.ts
807
+ /** Resource limits shared by GenUI repair, validation, and rendering. */
808
+ const GENUI_LIMITS = {
809
+ /** Maximum nesting depth of the component tree. */
810
+ maxDepth: 8,
811
+ /** Maximum total nodes across the whole spec. */
812
+ maxNodes: 200,
813
+ /** Maximum length of any plain string field. */
814
+ maxString: 2e3,
815
+ /** Maximum length of a `code` body. */
816
+ maxCode: 12e3,
817
+ /** Maximum length of a mermaid source. */
818
+ maxMermaid: 8e3,
819
+ /** Maximum `grid` columns. */
820
+ maxGridCols: 12,
821
+ /** Maximum `tabs` count. */
822
+ maxTabs: 12,
823
+ /** Maximum `accordion` items. */
824
+ maxAccordionItems: 24,
825
+ /** Maximum `list` items. */
826
+ maxListItems: 50,
827
+ /** Maximum `select`/`radio` options. */
828
+ maxOptions: 50,
829
+ /** Maximum `table` rows / columns. */
830
+ maxTableRows: 50,
831
+ maxTableCols: 12,
832
+ /** Maximum `chart` data points per series. */
833
+ maxChartPoints: 60,
834
+ /** Maximum `plot` series and per-series parameters. */
835
+ maxPlotSeries: 8,
836
+ maxPlotParams: 6,
837
+ /** Maximum `scene3d` meshes. */
838
+ maxMeshes: 5,
839
+ /** Maximum `quiz` options. */
840
+ maxQuizOptions: 8,
841
+ /** Maximum `steps` / `timeline` / `breadcrumb` / `keyvalue` entries. */
842
+ maxSteps: 24,
843
+ maxTimelineItems: 24,
844
+ maxBreadcrumbItems: 12,
845
+ maxKeyValuePairs: 24,
846
+ /** Maximum `file-tree` nesting. */
847
+ maxTreeDepth: 6,
848
+ /** Maximum `diagram` nodes / edges / zones / focal accents. */
849
+ maxDiagramNodes: 9,
850
+ maxDiagramEdges: 12,
851
+ maxDiagramZones: 3,
852
+ maxDiagramFocal: 2,
853
+ maxDiagramLabel: 14,
854
+ /** Maximum depth of an `echart` option object. */
855
+ maxEChartOptionDepth: 10,
856
+ /** Maximum length of any single array inside an `echart` option. */
857
+ maxEChartArrayLen: 500,
858
+ /** Maximum entries traversed while sanitizing an `echart` option. */
859
+ maxEChartOptionNodes: 2e3
860
+ };
861
+ //#endregion
862
+ //#region src/client/genui-runtime/value-utils.ts
863
+ /** Shared primitive sanitizers used by the GenUI guard. */
864
+ /** Is `value` one of `values`? */
865
+ function inEnum(value, values) {
866
+ return typeof value === "string" && values.includes(value);
867
+ }
868
+ /** String field: truncate a string to `cap`, or undefined when not a string. */
869
+ function str(value, cap) {
870
+ return typeof value === "string" ? value.slice(0, cap) : void 0;
871
+ }
872
+ /**
873
+ * Color field: the value lands in an inline `style` (background/stroke) or
874
+ * THREE.Color. Arbitrary CSS values are an exfiltration channel, so only
875
+ * literal color formats and host design tokens are accepted.
876
+ */
877
+ const SAFE_COLOR_RE = /^(?:#[\da-fA-F]{3,8}|rgba?\([^)]{0,64}\)|hsla?\([^)]{0,64}\)|var\(--dsw-[\w-]+(?:,\s*#[0-9a-fA-F]{3,8})?\))$/;
878
+ function color(value) {
879
+ if (typeof value !== "string") return void 0;
880
+ const normalized = value.trim();
881
+ return normalized.length <= 64 && SAFE_COLOR_RE.test(normalized) ? normalized : void 0;
882
+ }
883
+ /** Keep only http(s) and mailto link targets. */
884
+ function safeHref(value) {
885
+ if (typeof value !== "string") return void 0;
886
+ const normalized = value.trim();
887
+ if (normalized.length > 2048) return void 0;
888
+ return /^https?:\/\//i.test(normalized) || /^mailto:[^@\s]+@[^@\s]+$/i.test(normalized) ? normalized : void 0;
889
+ }
890
+ /** Keep browser-reachable http(s) or same-origin relative media paths. */
891
+ function safeMediaSrc(value) {
892
+ if (typeof value !== "string") return void 0;
893
+ const normalized = value.trim();
894
+ if (normalized === "" || normalized.length > 2048) return void 0;
895
+ if (/^https?:\/\//i.test(normalized)) return normalized;
896
+ if (/^[a-z][a-z0-9+.-]*:/i.test(normalized) || /^[/\\]{2}/.test(normalized)) return void 0;
897
+ return normalized;
898
+ }
899
+ /** Finite-number field: clamp into [min, max], or undefined when not finite. */
900
+ function num(value, min, max) {
901
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : void 0;
902
+ }
903
+ /** Integer field: clamp into [min, max], or undefined when not a finite number. */
904
+ function int(value, min, max) {
905
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : void 0;
906
+ }
907
+ /** Optional enum field: the value when it matches, otherwise undefined. */
908
+ function enu(value, values) {
909
+ return inEnum(value, values) ? value : void 0;
910
+ }
911
+ /** Plain object (not array, not null). */
912
+ function obj(value) {
913
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
914
+ }
915
+ /** Preserve an optional field only when its value exists. */
916
+ function opt(key, value) {
917
+ return value === void 0 ? {} : { [key]: value };
918
+ }
919
+ //#endregion
920
+ //#region src/client/guard.ts
921
+ function fieldKindMatches(value, kind) {
922
+ switch (kind) {
923
+ case "string": return typeof value === "string";
924
+ case "string-or-null": return value === null || typeof value === "string";
925
+ case "number": return typeof value === "number" && Number.isFinite(value);
926
+ case "boolean": return typeof value === "boolean";
927
+ case "nodes": return Array.isArray(value);
928
+ case "array": return Array.isArray(value);
929
+ case "object": return obj(value) !== void 0;
930
+ case "unknown": return true;
931
+ }
932
+ }
933
+ function fieldKindLabel(kind) {
934
+ switch (kind) {
935
+ case "string-or-null": return "a string or null";
936
+ case "number": return "a finite number";
937
+ case "boolean": return "a boolean";
938
+ case "nodes":
939
+ case "array": return "an array";
940
+ case "object": return "an object";
941
+ case "unknown": return "a value";
942
+ default: return "a string";
943
+ }
944
+ }
945
+ /** Detect an existing field diagnostic using both canonical and legacy messages. */
946
+ function hasFieldError(errors, at, field) {
947
+ return errors.some((error) => error.startsWith(`${at}.${field} `) || error.includes(`'${field}'`) || error.includes(`requires ${field}`));
948
+ }
949
+ /** Validate present fields against the runtime schema without duplicating errors. */
950
+ function validateSchemaFieldKinds(value, at, definition, errors, excludedFields = []) {
951
+ for (const [field, kind] of Object.entries(definition.fields)) {
952
+ if (excludedFields.includes(field) || value[field] === void 0) continue;
953
+ if (!fieldKindMatches(value[field], kind) && !hasFieldError(errors, at, field)) errors.push(`${at}.${field} must be ${fieldKindLabel(kind)}`);
954
+ }
955
+ for (const [field, values] of Object.entries(definition.enums)) {
956
+ if (excludedFields.includes(field) || value[field] === void 0 || !fieldKindMatches(value[field], "string")) continue;
957
+ if (!values.includes(value[field]) && !hasFieldError(errors, at, field)) errors.push(`${at}.${field} must be one of ${values.join(", ")}`);
958
+ }
959
+ }
960
+ /** Validate one schema-declared nested record and all records below it. */
961
+ function validateRecordSchema(value, at, definition, errors) {
962
+ const holder = obj(value);
963
+ if (holder === void 0) {
964
+ errors.push(`${at} must be an object`);
965
+ return;
966
+ }
967
+ for (const field of definition.required) {
968
+ const kind = definition.fields[field];
969
+ if (holder[field] === void 0) {
970
+ if (!hasFieldError(errors, at, field)) errors.push(`${at}: requires ${field}${kind === void 0 ? "" : ` (${fieldKindLabel(kind)})`}`);
971
+ } else if (kind !== void 0 && !fieldKindMatches(holder[field], kind)) {
972
+ if (!hasFieldError(errors, at, field)) errors.push(`${at}.${field} must be ${fieldKindLabel(kind)}`);
973
+ }
974
+ }
975
+ for (const [field, kind] of Object.entries(definition.fields)) if (holder[field] !== void 0 && !fieldKindMatches(holder[field], kind)) {
976
+ if (!hasFieldError(errors, at, field)) errors.push(`${at}.${field} must be ${fieldKindLabel(kind)}`);
977
+ }
978
+ for (const [field, values] of Object.entries(definition.enums)) if (holder[field] !== void 0 && typeof holder[field] === "string" && !values.includes(holder[field])) {
979
+ if (!hasFieldError(errors, at, field)) errors.push(`${at}.${field} must be one of ${values.join(", ")}`);
980
+ }
981
+ for (const [field, nested] of Object.entries(definition.nested)) {
982
+ const nestedValue = holder[field];
983
+ if (nestedValue === void 0) continue;
984
+ if (Array.isArray(nestedValue)) nestedValue.forEach((item, index) => validateRecordSchema(item, `${at}.${field}[${index}]`, nested, errors));
985
+ else validateRecordSchema(nestedValue, `${at}.${field}`, nested, errors);
986
+ }
987
+ }
988
+ /** Validate registry-declared nested records before repair can discard them. */
989
+ function validateNestedRecordSchemas(value, at, definition, errors) {
990
+ for (const [field, nested] of Object.entries(definition.nested)) {
991
+ const nestedValue = value[field];
992
+ if (nestedValue === void 0) continue;
993
+ if (Array.isArray(nestedValue)) nestedValue.forEach((item, index) => validateRecordSchema(item, `${at}.${field}[${index}]`, nested, errors));
994
+ else validateRecordSchema(nestedValue, `${at}.${field}`, nested, errors);
995
+ }
996
+ }
997
+ /** Validate the registry-declared presence and primitive shape of a native node. */
998
+ function validateRegistryFields(value, at, definition, errors) {
999
+ const type = String(value.type);
1000
+ const alreadyReported = (field) => hasFieldError(errors, at, field);
1001
+ for (const field of definition.required) {
1002
+ const kind = definition.fields[field];
1003
+ if (kind === void 0 || value[field] === void 0) {
1004
+ if (!alreadyReported(field)) errors.push(`${at}: type '${type}' requires ${field}${kind === void 0 ? "" : ` (${fieldKindLabel(kind)})`}`);
1005
+ continue;
1006
+ }
1007
+ if (!fieldKindMatches(value[field], kind) && !alreadyReported(field)) errors.push(`${at}.${field} must be ${fieldKindLabel(kind)}`);
1008
+ }
1009
+ validateSchemaFieldKinds(value, at, definition, errors, ["type"]);
1010
+ for (const group of definition.oneOfRequired) if (!group.some((field) => value[field] !== void 0) && !errors.some((error) => error.includes(`requires ${group.join(" or ")}`))) errors.push(`${at}: type '${type}' requires one of ${group.join(" or ")}`);
1011
+ for (const rule of definition.conditionalRequired) {
1012
+ if (value[rule.when.field] !== rule.when.equals) continue;
1013
+ for (const field of rule.required) if (value[field] === void 0 && !alreadyReported(field)) errors.push(`${at}: type '${type}' requires ${field}${rule.message === void 0 ? "" : ` (${rule.message})`}`);
1014
+ }
1015
+ validateNestedRecordSchemas(value, at, definition, errors);
1016
+ }
270
1017
  /** Walk `list` with the shared node budget; drops invalid entries. */
271
1018
  function repairItems(list, ctx, depth) {
272
1019
  if (!Array.isArray(list)) return [];
@@ -281,7 +1028,7 @@ function repairItems(list, ctx, depth) {
281
1028
  }
282
1029
  function repairNode(value, ctx, depth) {
283
1030
  if (depth > GENUI_LIMITS.maxDepth) return null;
284
- const v = obj$1(value);
1031
+ const v = obj(value);
285
1032
  if (v === void 0) return null;
286
1033
  const type = v.type;
287
1034
  if (typeof type !== "string") return null;
@@ -358,7 +1105,8 @@ function repairNode(value, ctx, depth) {
358
1105
  type: "checkbox",
359
1106
  label,
360
1107
  ...opt("checked", v.checked === true ? true : void 0),
361
- ...opt("action", str(v.action, 200))
1108
+ ...opt("action", str(v.action, 200)),
1109
+ ...opt("group", str(v.group, 200))
362
1110
  };
363
1111
  }
364
1112
  case "link": {
@@ -730,7 +1478,7 @@ function repairListItems(v, cap, ctx, depth) {
730
1478
  out.push(item.slice(0, GENUI_LIMITS.maxString));
731
1479
  continue;
732
1480
  }
733
- const o = obj$1(item);
1481
+ const o = obj(item);
734
1482
  const title = o === void 0 ? void 0 : str(o.title, GENUI_LIMITS.maxString);
735
1483
  if (title !== void 0) {
736
1484
  out.push({
@@ -769,7 +1517,7 @@ function repairChartData(v, cap) {
769
1517
  const out = [];
770
1518
  for (const datum of v) {
771
1519
  if (out.length >= cap) break;
772
- const o = obj$1(datum);
1520
+ const o = obj(datum);
773
1521
  const label = o === void 0 ? void 0 : str(o.label, 128);
774
1522
  const value = o === void 0 ? void 0 : num(o.value, -0xe8d4a51000, 0xe8d4a51000);
775
1523
  if (label === void 0 || value === void 0) continue;
@@ -786,7 +1534,7 @@ function repairSeries(v, cap, pointCap) {
786
1534
  const out = [];
787
1535
  for (const s of v) {
788
1536
  if (out.length >= cap) break;
789
- const o = obj$1(s);
1537
+ const o = obj(s);
790
1538
  const label = o === void 0 ? void 0 : str(o.label, 128);
791
1539
  const data = o === void 0 ? void 0 : repairChartData(o.data, pointCap);
792
1540
  if (label === void 0 || data === void 0) continue;
@@ -803,7 +1551,7 @@ function repairTabs(v, ctx, depth) {
803
1551
  const out = [];
804
1552
  for (const tab of v) {
805
1553
  if (out.length >= GENUI_LIMITS.maxTabs) break;
806
- const o = obj$1(tab);
1554
+ const o = obj(tab);
807
1555
  const label = o === void 0 ? void 0 : str(o.label, 128);
808
1556
  if (label === void 0 || o === void 0) continue;
809
1557
  const rawItems = o.items !== void 0 ? o.items : o.content !== void 0 ? Array.isArray(o.content) ? o.content : [o.content] : void 0;
@@ -816,7 +1564,7 @@ function repairTabs(v, ctx, depth) {
816
1564
  }
817
1565
  /** Header text for an object-shaped table column ({title,key} antd style). */
818
1566
  function columnHeaderText(c) {
819
- const o = obj$1(c);
1567
+ const o = obj(c);
820
1568
  if (o === void 0) return String(c);
821
1569
  for (const k of [
822
1570
  "title",
@@ -831,7 +1579,7 @@ function columnHeaderText(c) {
831
1579
  }
832
1580
  /** Row key for an object-shaped column, mirroring columnHeaderText's order. */
833
1581
  function columnKeyOf(c) {
834
- const o = obj$1(c);
1582
+ const o = obj(c);
835
1583
  if (o === void 0) return void 0;
836
1584
  for (const k of [
837
1585
  "key",
@@ -857,13 +1605,13 @@ function repairPlotSeries(v, cap) {
857
1605
  const out = [];
858
1606
  for (const s of v) {
859
1607
  if (out.length >= cap) break;
860
- const o = obj$1(s);
1608
+ const o = obj(s);
861
1609
  const expr = o === void 0 ? void 0 : str(o.expr, 512);
862
1610
  if (expr === void 0 || o === void 0) continue;
863
1611
  const params = [];
864
1612
  if (Array.isArray(o.params)) for (const p of o.params) {
865
1613
  if (params.length >= GENUI_LIMITS.maxPlotParams) break;
866
- const po = obj$1(p);
1614
+ const po = obj(p);
867
1615
  const name = po === void 0 ? void 0 : str(po.name, 64);
868
1616
  const value = po === void 0 ? void 0 : num(po.value, -1e9, 1e9);
869
1617
  if (name === void 0 || value === void 0) continue;
@@ -893,7 +1641,7 @@ function repairSteps(v) {
893
1641
  const out = [];
894
1642
  for (const s of v) {
895
1643
  if (out.length >= GENUI_LIMITS.maxSteps) break;
896
- const o = obj$1(s);
1644
+ const o = obj(s);
897
1645
  const title = o === void 0 ? void 0 : str(o.title, 256);
898
1646
  if (title === void 0) continue;
899
1647
  out.push({
@@ -908,7 +1656,7 @@ function repairPairs(v, cap) {
908
1656
  const out = [];
909
1657
  for (const p of v) {
910
1658
  if (out.length >= cap) break;
911
- const o = obj$1(p);
1659
+ const o = obj(p);
912
1660
  const key = o === void 0 ? void 0 : str(o.key, 256);
913
1661
  const value = o === void 0 ? void 0 : str(o.value, GENUI_LIMITS.maxString);
914
1662
  if (key === void 0 || value === void 0) continue;
@@ -924,7 +1672,7 @@ function repairDiffs(v) {
924
1672
  const out = [];
925
1673
  for (const d of v) {
926
1674
  if (out.length >= 24) break;
927
- const o = obj$1(d);
1675
+ const o = obj(d);
928
1676
  const path = o === void 0 ? void 0 : str(o.path, 1024);
929
1677
  const newText = o === void 0 ? void 0 : str(o.newText, 2e4);
930
1678
  if (path === void 0 || newText === void 0) continue;
@@ -942,7 +1690,7 @@ function repairAccordion(v, ctx, depth) {
942
1690
  const out = [];
943
1691
  for (const item of v) {
944
1692
  if (out.length >= GENUI_LIMITS.maxAccordionItems) break;
945
- const o = obj$1(item);
1693
+ const o = obj(item);
946
1694
  const title = o === void 0 ? void 0 : str(o.title, 256);
947
1695
  if (title === void 0 || o === void 0) continue;
948
1696
  out.push({
@@ -957,7 +1705,7 @@ function repairMeshes(v) {
957
1705
  const out = [];
958
1706
  for (const m of v) {
959
1707
  if (out.length >= GENUI_LIMITS.maxMeshes) break;
960
- const o = obj$1(m);
1708
+ const o = obj(m);
961
1709
  const shape = o === void 0 ? void 0 : enu(o.shape, MESH_SHAPES);
962
1710
  if (shape === void 0) continue;
963
1711
  const scale = o === void 0 ? void 0 : num(o.scale, -1e6, 1e6) ?? tuple3(o.scale);
@@ -993,7 +1741,7 @@ function repairDiagramNodes(v) {
993
1741
  const seen = /* @__PURE__ */ new Set();
994
1742
  for (const raw of v) {
995
1743
  if (out.length >= GENUI_LIMITS.maxDiagramNodes) break;
996
- const o = obj$1(raw);
1744
+ const o = obj(raw);
997
1745
  if (o === void 0) continue;
998
1746
  const id = str(o.id, 128);
999
1747
  const label = str(o.label, GENUI_LIMITS.maxString);
@@ -1025,7 +1773,7 @@ function repairDiagramEdges(v) {
1025
1773
  const out = [];
1026
1774
  for (const raw of v) {
1027
1775
  if (out.length >= GENUI_LIMITS.maxDiagramEdges) break;
1028
- const o = obj$1(raw);
1776
+ const o = obj(raw);
1029
1777
  if (o === void 0) continue;
1030
1778
  const from = str(o.from, 128);
1031
1779
  const to = str(o.to, 128);
@@ -1041,7 +1789,7 @@ function repairDiagramEdges(v) {
1041
1789
  return out;
1042
1790
  }
1043
1791
  function repairDiagramTheme(v) {
1044
- const o = obj$1(v);
1792
+ const o = obj(v);
1045
1793
  if (o === void 0) return void 0;
1046
1794
  const out = {};
1047
1795
  for (const key of [
@@ -1066,7 +1814,7 @@ function repairDiagramZones(v) {
1066
1814
  const out = [];
1067
1815
  for (const raw of v) {
1068
1816
  if (out.length >= GENUI_LIMITS.maxDiagramZones) break;
1069
- const o = obj$1(raw);
1817
+ const o = obj(raw);
1070
1818
  if (o === void 0) continue;
1071
1819
  const label = str(o.label, 64);
1072
1820
  if (label === void 0) continue;
@@ -1081,7 +1829,7 @@ function repairDiagramZones(v) {
1081
1829
  return out;
1082
1830
  }
1083
1831
  function repairDiagram(v) {
1084
- const o = obj$1(v);
1832
+ const o = obj(v);
1085
1833
  if (o === void 0) return null;
1086
1834
  const kind = enu(o.kind, DIAGRAM_KINDS);
1087
1835
  if (kind === void 0) return null;
@@ -1107,7 +1855,7 @@ function repairTimeline(v, cap) {
1107
1855
  const out = [];
1108
1856
  for (const item of v) {
1109
1857
  if (out.length >= cap) break;
1110
- const o = obj$1(item);
1858
+ const o = obj(item);
1111
1859
  const title = o === void 0 ? void 0 : str(o.title, 256);
1112
1860
  if (title === void 0) continue;
1113
1861
  out.push({
@@ -1126,7 +1874,7 @@ function walkTree(v, cap, depthLeft) {
1126
1874
  const out = [];
1127
1875
  for (const item of v) {
1128
1876
  if (out.length >= cap) break;
1129
- const o = obj$1(item);
1877
+ const o = obj(item);
1130
1878
  const name = o === void 0 ? void 0 : str(o.name, 256);
1131
1879
  if (name === void 0) continue;
1132
1880
  const children = o !== void 0 && depthLeft > 0 && Array.isArray(o.children) ? walkTree(o.children, cap, depthLeft - 1) : void 0;
@@ -1143,7 +1891,7 @@ function repairQuizOptions(v, answer) {
1143
1891
  const out = [];
1144
1892
  for (const optItem of v) {
1145
1893
  if (out.length >= GENUI_LIMITS.maxQuizOptions) break;
1146
- const o = obj$1(optItem);
1894
+ const o = obj(optItem);
1147
1895
  const label = typeof optItem === "string" ? str(optItem, 512) : o === void 0 ? void 0 : str(o.label, 512);
1148
1896
  if (label === void 0) continue;
1149
1897
  out.push({
@@ -1210,7 +1958,7 @@ function sanitizeEChartOption(v, depth, budget) {
1210
1958
  }
1211
1959
  return arr.length > 0 ? arr : void 0;
1212
1960
  }
1213
- const o = obj$1(v);
1961
+ const o = obj(v);
1214
1962
  if (o === void 0) return void 0;
1215
1963
  const out = {};
1216
1964
  for (const [key, val] of Object.entries(o)) {
@@ -1229,13 +1977,13 @@ function sanitizeEChartOption(v, depth, budget) {
1229
1977
  * dropping/clamping/truncating. Idempotent: repairing a repaired spec is a
1230
1978
  * no-op.
1231
1979
  */
1232
- function repairGenuiSpec(value) {
1233
- const v = obj$1(value);
1980
+ function repairCanonicalGenuiSpec(value) {
1981
+ const v = obj(value);
1234
1982
  if (v === void 0) return null;
1235
1983
  if (!Array.isArray(v.items)) {
1236
1984
  const wrapped = wrapSingleComponentRoot(value);
1237
1985
  if (wrapped === null) return null;
1238
- return repairGenuiSpec(wrapped);
1986
+ return repairCanonicalGenuiSpec(wrapped);
1239
1987
  }
1240
1988
  const ctx = { remaining: GENUI_LIMITS.maxNodes };
1241
1989
  return {
@@ -1262,81 +2010,34 @@ function countGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1262
2010
  for (const item of list) {
1263
2011
  if (count >= cap) return;
1264
2012
  count += 1;
1265
- const v = obj$1(item);
2013
+ const v = obj(item);
1266
2014
  if (v === void 0) continue;
1267
2015
  if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) {
1268
2016
  if (count >= cap) return;
1269
- const to = obj$1(t);
2017
+ const to = obj(t);
1270
2018
  if (to !== void 0) walk(to.items);
1271
2019
  }
1272
2020
  else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) {
1273
2021
  if (count >= cap) return;
1274
- const io = obj$1(it);
2022
+ const io = obj(it);
1275
2023
  if (io !== void 0) walk(io.items);
1276
2024
  }
1277
2025
  else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
1278
- else if (v.type === "file-tree" && Array.isArray(v.items)) walk(v.items);
1279
- else if (v.type === "list" && Array.isArray(v.items)) for (const li of v.items) {
2026
+ else if (v.type === "file-tree") {} else if (v.type === "list" && Array.isArray(v.items)) for (const li of v.items) {
1280
2027
  if (count >= cap) return;
1281
- const lo = obj$1(li);
2028
+ const lo = obj(li);
1282
2029
  if (lo !== void 0 && typeof lo.type === "string") walk([lo]);
1283
2030
  }
1284
2031
  }
1285
2032
  };
1286
- const root = obj$1(value);
2033
+ const root = obj(value);
1287
2034
  walk(root === void 0 ? [] : root.items);
1288
2035
  return count;
1289
2036
  }
1290
2037
  /** Every white-listed node `type`. Keep in sync with the repairNode switch —
1291
2038
  * validate_dsh_ui uses it to tell declared GenUI nodes apart from unrelated
1292
2039
  * `"type"` strings (e.g. file-tree's `{type:'file'}` children). */
1293
- const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
1294
- "accordion",
1295
- "audio",
1296
- "avatar",
1297
- "badge",
1298
- "breadcrumb",
1299
- "button",
1300
- "callout",
1301
- "card",
1302
- "chart",
1303
- "checkbox",
1304
- "code",
1305
- "col",
1306
- "copy",
1307
- "diff",
1308
- "divider",
1309
- "file-tree",
1310
- "grid",
1311
- "image",
1312
- "input",
1313
- "json",
1314
- "keyvalue",
1315
- "link",
1316
- "list",
1317
- "mermaid",
1318
- "plot",
1319
- "progress",
1320
- "quiz",
1321
- "radio",
1322
- "row",
1323
- "scene3d",
1324
- "select",
1325
- "slider",
1326
- "spacer",
1327
- "stat",
1328
- "steps",
1329
- "submit",
1330
- "switch",
1331
- "table",
1332
- "tabs",
1333
- "text",
1334
- "textarea",
1335
- "timeline",
1336
- "video",
1337
- "echart",
1338
- "diagram"
1339
- ]);
2040
+ const GENUI_NODE_TYPES = GENUI_NATIVE_TYPES;
1340
2041
  /**
1341
2042
  * Visit and count declared nodes in a raw spec tree: objects whose `type` is a
1342
2043
  * white-listed string, descending the same containers `countGenuiNodes`
@@ -1346,7 +2047,7 @@ const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
1346
2047
  function visitDeclaredGenuiNodes(value, cap, visit) {
1347
2048
  let count = 0;
1348
2049
  const declared = (candidate) => {
1349
- const o = obj$1(candidate);
2050
+ const o = obj(candidate);
1350
2051
  return o !== void 0 && typeof o.type === "string" && GENUI_NODE_TYPES.has(o.type);
1351
2052
  };
1352
2053
  function walk(list, path) {
@@ -1358,7 +2059,7 @@ function visitDeclaredGenuiNodes(value, cap, visit) {
1358
2059
  }
1359
2060
  function walkNode(item, at) {
1360
2061
  if (count >= cap || !declared(item)) return;
1361
- const v = obj$1(item);
2062
+ const v = obj(item);
1362
2063
  if (v === void 0) return;
1363
2064
  count += 1;
1364
2065
  visit(v, at);
@@ -1368,13 +2069,13 @@ function visitDeclaredGenuiNodes(value, cap, visit) {
1368
2069
  else if (v.type === "list" && Array.isArray(v.items)) for (let row = 0; row < v.items.length; row++) walkNode(v.items[row], `${at}.items[${row}]`);
1369
2070
  }
1370
2071
  function walkItemsOf(holder, path) {
1371
- const o = obj$1(holder);
2072
+ const o = obj(holder);
1372
2073
  if (o === void 0) return;
1373
2074
  const items = o.items !== void 0 ? o.items : o.content;
1374
2075
  if (Array.isArray(items)) walk(items, `${path}.items`);
1375
2076
  else walkNode(items, `${path}.items`);
1376
2077
  }
1377
- const root = obj$1(value);
2078
+ const root = obj(value);
1378
2079
  if (root === void 0) return count;
1379
2080
  if (!Array.isArray(root.items) && declared(value)) walkNode(value, "spec");
1380
2081
  else walk(root.items, "items");
@@ -1389,23 +2090,106 @@ function visitDeclaredGenuiNodes(value, cap, visit) {
1389
2090
  function countDeclaredGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1390
2091
  return visitDeclaredGenuiNodes(value, cap, () => {});
1391
2092
  }
2093
+ /** Count native nodes that survived repair, excluding opaque custom nodes. */
2094
+ function countRenderedNativeGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
2095
+ return countDeclaredGenuiNodes(value, cap);
2096
+ }
1392
2097
  /**
1393
- * Return field-level chart errors without changing other repairable component families.
1394
- * @param value - raw GenUI spec.
1395
- * @returns chart semantic errors in tree order.
2098
+ * Validate a raw spec value against the white list and limits, collecting
2099
+ * human-readable problems. Unlike repair this never mutates: it is a
2100
+ * diagnostic for tests and tooling. Unknown `type`s are reported (a plugin
2101
+ * custom type is valid only when a renderer is registered — the guard cannot
2102
+ * know, so it flags them as warnings).
1396
2103
  */
1397
- function validateGenuiChartSemantics(value) {
2104
+ function validateCanonicalGenuiSpec(value) {
1398
2105
  const errors = [];
1399
- visitDeclaredGenuiNodes(value, GENUI_LIMITS.maxNodes + 1, (node, at) => {
1400
- if (node.type !== "chart") return;
1401
- validateChartNode(node, at, errors);
1402
- });
1403
- return errors;
2106
+ const v = obj(value);
2107
+ if (v === void 0) return {
2108
+ ok: false,
2109
+ errors: ["spec root must be an object"]
2110
+ };
2111
+ if (!Array.isArray(v.items)) {
2112
+ const wrapped = wrapSingleComponentRoot(value);
2113
+ if (wrapped !== null) return validateCanonicalGenuiSpec(wrapped);
2114
+ return {
2115
+ ok: false,
2116
+ errors: ["spec.items must be an array"]
2117
+ };
2118
+ }
2119
+ validateSchemaFieldKinds(v, "spec", GENUI_SPEC_SCHEMA, errors, ["items"]);
2120
+ let count = 0;
2121
+ let capped = false;
2122
+ const walk = (list, depth, path) => {
2123
+ if (capped) return;
2124
+ if (!Array.isArray(list)) {
2125
+ errors.push(`${path} must be an array`);
2126
+ return;
2127
+ }
2128
+ for (let i = 0; i < list.length; i++) {
2129
+ if (capped || count >= GENUI_LIMITS.maxNodes) {
2130
+ if (!capped) {
2131
+ errors.push(`spec exceeds ${GENUI_LIMITS.maxNodes} nodes; tail elided`);
2132
+ capped = true;
2133
+ }
2134
+ return;
2135
+ }
2136
+ count += 1;
2137
+ const at = `${path}[${i}]`;
2138
+ validateNode(list[i], depth, at, errors, walk);
2139
+ }
2140
+ };
2141
+ walk(v.items, 0, "items");
2142
+ return {
2143
+ ok: errors.length === 0,
2144
+ errors
2145
+ };
2146
+ }
2147
+ /**
2148
+ * Run the shared canonical GenUI pipeline.
2149
+ *
2150
+ * The pipeline normalizes aliases, applies the existing deterministic repair
2151
+ * and security filters, validates the canonical input, and reports native
2152
+ * declarations that disappeared during repair. Custom nodes stay opaque and
2153
+ * do not participate in native unknown-field diagnostics.
2154
+ *
2155
+ * @param value - Raw GenUI spec or bare component.
2156
+ * @returns Canonical input, repaired output, diagnostics, and node counts.
2157
+ */
2158
+ function processGenuiSpec(value) {
2159
+ const normalized = normalizeGenuiSpec(value);
2160
+ const repaired = repairCanonicalGenuiSpec(normalized.value);
2161
+ const validation = validateCanonicalGenuiSpec(normalized.value);
2162
+ const declaredNativeCount = countDeclaredGenuiNodes(normalized.value, GENUI_LIMITS.maxNodes + 1);
2163
+ const renderedNativeCount = repaired === null ? 0 : countRenderedNativeGenuiNodes(repaired);
2164
+ const renderedTotalCount = repaired === null ? 0 : countGenuiNodes(repaired);
2165
+ const errors = validation.errors.filter((error) => !error.includes(": unknown type "));
2166
+ if (declaredNativeCount > renderedNativeCount) errors.push(`repair dropped ${declaredNativeCount - renderedNativeCount} declared native node(s): declared ${declaredNativeCount}, rendered ${renderedNativeCount}`);
2167
+ return {
2168
+ value: normalized.value,
2169
+ normalized: normalized.value,
2170
+ repaired,
2171
+ spec: repaired,
2172
+ errors,
2173
+ warnings: [...normalized.warnings, ...diagnoseUnknownGenuiFields(normalized.value)],
2174
+ declaredCount: declaredNativeCount,
2175
+ renderedCount: renderedTotalCount,
2176
+ declaredNativeCount,
2177
+ renderedNativeCount,
2178
+ renderedTotalCount
2179
+ };
2180
+ }
2181
+ /** Return whether the only process errors describe an intentional budget tail cut. */
2182
+ function isIntentionalBudgetCut(processed) {
2183
+ return processed.errors.length > 0 && processed.errors.every((error) => error.startsWith("spec exceeds ") || error.startsWith("repair dropped ")) && processed.renderedNativeCount === GENUI_LIMITS.maxNodes && processed.declaredNativeCount === GENUI_LIMITS.maxNodes + 1;
2184
+ }
2185
+ /** Decide whether a repaired spec is safe to expose to any GenUI renderer. */
2186
+ function isRenderableProcess(processed) {
2187
+ return processed.spec !== null && (processed.errors.length === 0 || isIntentionalBudgetCut(processed));
1404
2188
  }
1405
2189
  function validateChartData(value, at, errors) {
1406
2190
  if (!Array.isArray(value)) return;
1407
2191
  for (let index = 0; index < value.length; index++) {
1408
- const datum = obj$1(value[index]);
2192
+ const datum = obj(value[index]);
1409
2193
  const path = `${at}[${index}]`;
1410
2194
  if (datum === void 0) {
1411
2195
  errors.push(`${path} must be an object`);
@@ -1419,7 +2203,7 @@ function validateChartData(value, at, errors) {
1419
2203
  function validateChartSeries(value, at, errors) {
1420
2204
  if (!Array.isArray(value)) return;
1421
2205
  for (let index = 0; index < value.length; index++) {
1422
- const series = obj$1(value[index]);
2206
+ const series = obj(value[index]);
1423
2207
  const path = `${at}[${index}]`;
1424
2208
  if (series === void 0) {
1425
2209
  errors.push(`${path} must be an object`);
@@ -1437,9 +2221,220 @@ function validateChartNode(v, at, errors) {
1437
2221
  if (v.data !== void 0 && !Array.isArray(v.data)) errors.push(`${at}.data must be an array`);
1438
2222
  if (v.series !== void 0 && !Array.isArray(v.series)) errors.push(`${at}.series must be an array`);
1439
2223
  if (v.kind !== void 0 && (typeof v.kind !== "string" || !CHART_KINDS.includes(v.kind))) errors.push(`${at}.kind must be bars, line, or donut`);
2224
+ const kind = v.kind === void 0 ? "bars" : v.kind;
2225
+ if (Array.isArray(v.data) && v.data.length === 0) errors.push(`${at}.data must not be empty`);
2226
+ if (Array.isArray(v.series)) {
2227
+ if (v.series.length === 0) errors.push(`${at}.series must not be empty`);
2228
+ if (kind === "line" || kind === "donut") errors.push(`${at}.series is only supported for bars`);
2229
+ for (let index = 0; index < v.series.length; index++) {
2230
+ const series = obj(v.series[index]);
2231
+ if (series !== void 0 && Array.isArray(series.data) && series.data.length === 0) errors.push(`${at}.series[${index}].data must not be empty`);
2232
+ }
2233
+ }
2234
+ if ((kind === "line" || kind === "donut") && v.data === void 0) errors.push(`${at}.data is required for ${kind}`);
1440
2235
  validateChartData(v.data, `${at}.data`, errors);
1441
2236
  validateChartSeries(v.series, `${at}.series`, errors);
1442
2237
  }
2238
+ /** Validate table rows before repair can silently remove malformed cells. */
2239
+ function validateTableRows(value, at, errors) {
2240
+ if (!Array.isArray(value)) return;
2241
+ for (let rowIndex = 0; rowIndex < value.length; rowIndex++) {
2242
+ const row = value[rowIndex];
2243
+ if (obj(row) !== void 0) continue;
2244
+ if (!Array.isArray(row)) {
2245
+ errors.push(`${at}[${rowIndex}] must be an array or object`);
2246
+ continue;
2247
+ }
2248
+ for (let cellIndex = 0; cellIndex < row.length; cellIndex++) {
2249
+ const cell = row[cellIndex];
2250
+ if (typeof cell !== "string" && (typeof cell !== "number" || !Number.isFinite(cell))) errors.push(`${at}[${rowIndex}][${cellIndex}] must be a string or finite number`);
2251
+ }
2252
+ }
2253
+ }
2254
+ function validateNode(value, depth, at, errors, walk) {
2255
+ if (depth > GENUI_LIMITS.maxDepth) {
2256
+ errors.push(`${at}: exceeds max depth ${GENUI_LIMITS.maxDepth}`);
2257
+ return;
2258
+ }
2259
+ const v = obj(value);
2260
+ if (v === void 0) {
2261
+ errors.push(`${at}: must be an object`);
2262
+ return;
2263
+ }
2264
+ const type = v.type;
2265
+ if (typeof type !== "string") {
2266
+ errors.push(`${at}: missing string 'type'`);
2267
+ return;
2268
+ }
2269
+ const isStr = (name) => {
2270
+ if (v[name] !== void 0 && typeof v[name] !== "string") errors.push(`${at}: '${name}' must be a string`);
2271
+ };
2272
+ const isNum = (name) => {
2273
+ if (v[name] !== void 0 && (typeof v[name] !== "number" || !Number.isFinite(v[name]))) errors.push(`${at}: '${name}' must be a finite number`);
2274
+ };
2275
+ switch (type) {
2276
+ case "text":
2277
+ if (typeof v.content !== "string" && typeof v.text !== "string") errors.push(`${at}: type 'text' requires content or text (string)`);
2278
+ isStr("content");
2279
+ isStr("text");
2280
+ break;
2281
+ case "row":
2282
+ case "col":
2283
+ case "card":
2284
+ case "grid":
2285
+ if (!Array.isArray(v.items)) errors.push(`${at}: type '${type}' requires items (array)`);
2286
+ walk(v.items, depth + 1, `${at}.items`);
2287
+ if (type === "grid") isNum("cols");
2288
+ break;
2289
+ case "button":
2290
+ case "checkbox":
2291
+ case "link":
2292
+ case "switch":
2293
+ if (typeof v.label !== "string") errors.push(`${at}: type '${type}' requires label (string)`);
2294
+ isStr("label");
2295
+ break;
2296
+ case "image":
2297
+ case "audio":
2298
+ case "video":
2299
+ if (typeof v.src !== "string") errors.push(`${at}: type '${type}' requires src (string)`);
2300
+ isStr("src");
2301
+ isStr("alt");
2302
+ if (type === "video") isStr("poster");
2303
+ break;
2304
+ case "slider":
2305
+ isStr("label");
2306
+ isNum("min");
2307
+ isNum("max");
2308
+ isNum("step");
2309
+ isNum("value");
2310
+ break;
2311
+ case "input":
2312
+ case "textarea":
2313
+ isStr("label");
2314
+ isStr("placeholder");
2315
+ isStr("value");
2316
+ break;
2317
+ case "select":
2318
+ case "radio":
2319
+ if (!Array.isArray(v.options)) errors.push(`${at}: type '${type}' requires options (array)`);
2320
+ break;
2321
+ case "submit":
2322
+ if (typeof v.label !== "string") errors.push(`${at}: type 'submit' requires label (string)`);
2323
+ break;
2324
+ case "badge":
2325
+ if (typeof v.label !== "string" && typeof v.text !== "string" && typeof v.value !== "string") errors.push(`${at}: type 'badge' requires label, text, or value (string)`);
2326
+ isStr("label");
2327
+ isStr("text");
2328
+ isStr("value");
2329
+ break;
2330
+ case "stat":
2331
+ if (typeof v.label !== "string") errors.push(`${at}: type 'stat' requires label (string)`);
2332
+ if (typeof v.value !== "string") errors.push(`${at}: type 'stat' requires value (string)`);
2333
+ isStr("delta");
2334
+ break;
2335
+ case "progress":
2336
+ if (typeof v.value !== "number" || !Number.isFinite(v.value) || v.value < 0 || v.value > 100) errors.push(`${at}: type 'progress' requires value (number 0..100)`);
2337
+ isNum("value");
2338
+ break;
2339
+ case "avatar":
2340
+ if (typeof v.name !== "string") errors.push(`${at}: type 'avatar' requires name (string)`);
2341
+ break;
2342
+ case "list":
2343
+ if (!Array.isArray(v.items)) errors.push(`${at}: type 'list' requires items (array)`);
2344
+ if (Array.isArray(v.items)) for (let i = 0; i < v.items.length; i++) {
2345
+ const item = obj(v.items[i]);
2346
+ if (item !== void 0 && typeof item.type === "string") validateNode(item, depth + 1, `${at}.items[${i}]`, errors, walk);
2347
+ }
2348
+ break;
2349
+ case "table":
2350
+ if (!Array.isArray(v.columns)) errors.push(`${at}: type 'table' requires columns (array)`);
2351
+ if (!Array.isArray(v.rows)) errors.push(`${at}: type 'table' requires rows (array)`);
2352
+ validateTableRows(v.rows, `${at}.rows`, errors);
2353
+ break;
2354
+ case "chart":
2355
+ validateChartNode(v, at, errors);
2356
+ break;
2357
+ case "tabs":
2358
+ if (!Array.isArray(v.tabs)) errors.push(`${at}: type 'tabs' requires tabs (array)`);
2359
+ if (Array.isArray(v.tabs)) for (let i = 0; i < v.tabs.length; i++) {
2360
+ const t = obj(v.tabs[i]);
2361
+ if (t === void 0) {
2362
+ errors.push(`${at}.tabs[${i}] must be an object`);
2363
+ continue;
2364
+ }
2365
+ if (typeof t.label !== "string") errors.push(`${at}.tabs[${i}].label must be a string`);
2366
+ walk(t.items, depth + 1, `${at}.tabs[${i}].items`);
2367
+ }
2368
+ break;
2369
+ case "plot":
2370
+ if (!Array.isArray(v.series)) errors.push(`${at}: type 'plot' requires series (array)`);
2371
+ break;
2372
+ case "callout":
2373
+ if (typeof v.content !== "string") errors.push(`${at}: type 'callout' requires content (string)`);
2374
+ break;
2375
+ case "steps":
2376
+ if (!Array.isArray(v.steps)) errors.push(`${at}: type 'steps' requires steps (array)`);
2377
+ break;
2378
+ case "keyvalue":
2379
+ if (!Array.isArray(v.pairs)) errors.push(`${at}: type 'keyvalue' requires pairs (array)`);
2380
+ break;
2381
+ case "diff":
2382
+ if (!Array.isArray(v.diffs)) errors.push(`${at}: type 'diff' requires diffs (array)`);
2383
+ break;
2384
+ case "json":
2385
+ if (!("value" in v)) errors.push(`${at}: type 'json' requires value`);
2386
+ break;
2387
+ case "code":
2388
+ if (typeof v.code !== "string") errors.push(`${at}: type 'code' requires code (string)`);
2389
+ break;
2390
+ case "accordion":
2391
+ if (!Array.isArray(v.items)) errors.push(`${at}: type 'accordion' requires items (array)`);
2392
+ if (Array.isArray(v.items)) for (let i = 0; i < v.items.length; i++) {
2393
+ const item = obj(v.items[i]);
2394
+ if (item === void 0) {
2395
+ errors.push(`${at}.items[${i}] must be an object`);
2396
+ continue;
2397
+ }
2398
+ if (typeof item.title !== "string") errors.push(`${at}.items[${i}].title must be a string`);
2399
+ walk(item.items, depth + 1, `${at}.items[${i}].items`);
2400
+ }
2401
+ break;
2402
+ case "copy":
2403
+ if (typeof v.text !== "string") errors.push(`${at}: type 'copy' requires text (string)`);
2404
+ break;
2405
+ case "mermaid":
2406
+ if (typeof v.code !== "string") errors.push(`${at}: type 'mermaid' requires code (string)`);
2407
+ break;
2408
+ case "scene3d":
2409
+ if (!Array.isArray(v.meshes)) errors.push(`${at}: type 'scene3d' requires meshes (array)`);
2410
+ break;
2411
+ case "timeline":
2412
+ if (!Array.isArray(v.items)) errors.push(`${at}: type 'timeline' requires items (array)`);
2413
+ break;
2414
+ case "file-tree":
2415
+ if (!Array.isArray(v.items)) errors.push(`${at}: type 'file-tree' requires items (array)`);
2416
+ break;
2417
+ case "breadcrumb":
2418
+ if (!Array.isArray(v.items)) errors.push(`${at}: type 'breadcrumb' requires items (array)`);
2419
+ break;
2420
+ case "quiz":
2421
+ if (typeof v.question !== "string") errors.push(`${at}: type 'quiz' requires question (string)`);
2422
+ if (!Array.isArray(v.options)) errors.push(`${at}: type 'quiz' requires options (array)`);
2423
+ break;
2424
+ case "diagram":
2425
+ if (typeof v.kind !== "string") errors.push(`${at}: type 'diagram' requires kind (string)`);
2426
+ if (!Array.isArray(v.nodes)) errors.push(`${at}: type 'diagram' requires nodes (array)`);
2427
+ if (v.edges !== void 0 && !Array.isArray(v.edges)) errors.push(`${at}: type 'diagram' requires edges (array) when present`);
2428
+ break;
2429
+ case "echart":
2430
+ if (v.option === void 0 && v.data === void 0 && v.series === void 0) errors.push(`${at}: type 'echart' requires option, data, or series`);
2431
+ isNum("height");
2432
+ break;
2433
+ default: errors.push(`${at}: unknown type '${type}' (custom renderer?)`);
2434
+ }
2435
+ const definition = COMPONENT_SCHEMAS[type];
2436
+ if (definition !== void 0) validateRegistryFields(v, at, definition, errors);
2437
+ }
1443
2438
  //#endregion
1444
2439
  //#region src/shared/fence-repair.ts
1445
2440
  /**
@@ -1550,83 +2545,6 @@ function completeFenceJson(raw) {
1550
2545
  }
1551
2546
  }
1552
2547
  //#endregion
1553
- //#region src/plugin/chart-contract.ts
1554
- /** Plain object helper for raw model-authored specs. */
1555
- function obj(value) {
1556
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1557
- }
1558
- /**
1559
- * Visit chart nodes through the component-bearing containers understood by
1560
- * the GenUI guard. This deliberately does not recursively inspect arbitrary
1561
- * objects (for example `json.value` or `echart.option`), where a nested
1562
- * `{type:"chart"}` is data rather than a GenUI node.
1563
- */
1564
- function visitChartNodes(value, visit) {
1565
- function walkNode(value, at) {
1566
- const node = obj(value);
1567
- if (node === void 0 || typeof node.type !== "string") return;
1568
- if (node.type === "chart") visit(node, at);
1569
- if ((node.type === "row" || node.type === "col" || node.type === "grid" || node.type === "card") && Array.isArray(node.items)) {
1570
- walk(node.items, `${at}.items`);
1571
- return;
1572
- }
1573
- if (node.type === "tabs" && Array.isArray(node.tabs)) {
1574
- for (let index = 0; index < node.tabs.length; index++) {
1575
- const tab = obj(node.tabs[index]);
1576
- if (tab === void 0) continue;
1577
- const items = tab.items !== void 0 ? tab.items : tab.content;
1578
- if (Array.isArray(items)) walk(items, `${at}.tabs[${index}].items`);
1579
- else walkNode(items, `${at}.tabs[${index}].items`);
1580
- }
1581
- return;
1582
- }
1583
- if (node.type === "accordion" && Array.isArray(node.items)) {
1584
- for (let index = 0; index < node.items.length; index++) {
1585
- const item = obj(node.items[index]);
1586
- if (item === void 0) continue;
1587
- const items = item.items !== void 0 ? item.items : item.content;
1588
- if (Array.isArray(items)) walk(items, `${at}.items[${index}].items`);
1589
- else walkNode(items, `${at}.items[${index}].items`);
1590
- }
1591
- return;
1592
- }
1593
- if (node.type === "list" && Array.isArray(node.items)) for (let index = 0; index < node.items.length; index++) walkNode(node.items[index], `${at}.items[${index}]`);
1594
- }
1595
- function walk(list, path) {
1596
- if (!Array.isArray(list)) return;
1597
- for (let index = 0; index < list.length; index++) walkNode(list[index], `${path}[${index}]`);
1598
- }
1599
- const root = obj(value);
1600
- if (root === void 0) return;
1601
- if (!Array.isArray(root.items) && root.type === "chart") walkNode(root, "spec");
1602
- else walk(root.items, "items");
1603
- }
1604
- /**
1605
- * Validate the native chart contract all the way to something the renderer
1606
- * can actually draw. The shared guard still owns field/type validation; this
1607
- * layer adds renderer-specific invariants that would otherwise produce an
1608
- * empty line/donut chart or a grouped-bars shell with no points.
1609
- */
1610
- function validateRenderableChartSemantics(value) {
1611
- const errors = validateGenuiChartSemantics(value);
1612
- visitChartNodes(value, (node, at) => {
1613
- const kind = node.kind === void 0 ? "bars" : node.kind;
1614
- if (Array.isArray(node.data) && node.data.length === 0) errors.push(`${at}.data must not be empty`);
1615
- if (Array.isArray(node.series)) {
1616
- if (node.series.length === 0) errors.push(`${at}.series must not be empty`);
1617
- for (let index = 0; index < node.series.length; index++) {
1618
- const series = obj(node.series[index]);
1619
- if (series !== void 0 && Array.isArray(series.data) && series.data.length === 0) errors.push(`${at}.series[${index}].data must not be empty`);
1620
- }
1621
- }
1622
- if (kind === "line" || kind === "donut") {
1623
- if (node.series !== void 0) errors.push(`${at}.series is only supported for bars`);
1624
- if (node.data === void 0) errors.push(`${at}.data is required for ${kind}`);
1625
- }
1626
- });
1627
- return errors;
1628
- }
1629
- //#endregion
1630
2548
  //#region src/plugin/tool.ts
1631
2549
  /**
1632
2550
  * Arguments schema: an open `spec` slot. The schema must NOT reject anything
@@ -1731,17 +2649,37 @@ function parseSpecJson(raw, shape) {
1731
2649
  return;
1732
2650
  }
1733
2651
  }
1734
- /** Total node count of a repaired spec — the shared guard traversal
1735
- * (`countGenuiNodes`) so the tool reports the SAME number the panel fold and
1736
- * validation use. A local walker used to under-count specs whose content
1737
- * lives inside tabs/accordion/file-tree (their children are not `.items`). */
1738
- function countNodes(spec) {
1739
- return countGenuiNodes(spec, GENUI_LIMITS.maxNodes);
2652
+ /** Process a raw tool value once for all render-time decisions. */
2653
+ function processRenderableValue(value) {
2654
+ return processGenuiSpec(value);
2655
+ }
2656
+ /** Render process diagnostics as stable model-facing warning lines. */
2657
+ function formatProcessWarnings(processed) {
2658
+ return processed.warnings.map((warning) => {
2659
+ if (warning.kind === "alias" && warning.canonical !== void 0) {
2660
+ const separator = warning.path.lastIndexOf(".");
2661
+ const canonicalPath = `${separator < 0 ? "" : warning.path.slice(0, separator + 1)}${warning.canonical}`;
2662
+ return warning.message.includes("ignored") ? `⚠️ 已忽略别名字段:${warning.path} → ${canonicalPath}(ignored because canonical field '${warning.canonical}' is present)` : `⚠️ 已规范化字段:${warning.path} → ${canonicalPath}(normalized/adopted as '${warning.canonical}')`;
2663
+ }
2664
+ return `⚠️ ${warning.message}`;
2665
+ });
2666
+ }
2667
+ /** Format chart-specific process errors while keeping other schema errors generic. */
2668
+ function formatProcessFailure(processed) {
2669
+ const chartErrors = processed.errors.filter((error) => /(?:variant is unsupported|kind must be bars, line, or donut|requires data or series|(?:data|series) is required for|(?:\.data|\.series)(?:\[\d+\])?(?:\.(?:data|label|value|color))? must|series is only supported for bars)/.test(error));
2670
+ return chartErrors.length === 0 ? void 0 : `❌ chart 字段验证失败:\n- ${chartErrors.join("\n- ")}`;
2671
+ }
2672
+ /** Return the legacy validation text for a process that dropped native nodes. */
2673
+ function droppedNodeFailure(processed) {
2674
+ if (!processed.errors.some((error) => error.startsWith("repair dropped "))) return void 0;
2675
+ const dropped = processed.declaredNativeCount - processed.renderedNativeCount;
2676
+ return `❌ 验证未通过:检测到声明了 ${processed.declaredNativeCount} 个组件,但仅成功解析出 ${processed.renderedNativeCount} 个(有 ${dropped} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`;
1740
2677
  }
1741
2678
  /** Tool-call title shared by the pending and completed presentations. */
1742
2679
  function cardTitle(args) {
1743
- const spec = repairGenuiSpec(specOf(args));
1744
- return spec === null ? void 0 : `渲染 UI:${spec.title ?? "未命名"}`;
2680
+ const processed = processRenderableValue(specOf(args));
2681
+ if (!isRenderableProcess(processed) || processed.spec === null) return void 0;
2682
+ return `渲染 UI:${processed.spec.title ?? "未命名"}`;
1745
2683
  }
1746
2684
  /**
1747
2685
  * Build the render_ui tool definition. Registered by the plugin node half;
@@ -1761,16 +2699,18 @@ function createRenderUiTool() {
1761
2699
  }];
1762
2700
  },
1763
2701
  presentationMeta(args) {
1764
- return repairGenuiSpec(specOf(args));
2702
+ const processed = processRenderableValue(specOf(args));
2703
+ return isRenderableProcess(processed) ? processed.spec : null;
1765
2704
  }
1766
2705
  },
1767
2706
  async execute(args) {
1768
- const input = specOf(args);
1769
- const chartErrors = validateRenderableChartSemantics(input);
1770
- if (chartErrors.length > 0) throw new Error("render_ui spec invalid: " + chartErrors.join("; "));
1771
- const spec = repairGenuiSpec(input);
1772
- if (spec === null) return "render_ui:spec 无效 —— 根对象需要 \"items\" 数组(组件树白名单见系统提示词),请修正后重试。";
1773
- return `已渲染 UI「${spec.title ?? "未命名"}」(${countNodes(spec)} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。`;
2707
+ const processed = processRenderableValue(specOf(args));
2708
+ if (processed.spec === null) return "render_ui:spec 无效 —— 根对象需要 \"items\" 数组(组件树白名单见系统提示词),请修正后重试。";
2709
+ if (!isRenderableProcess(processed)) throw new Error("render_ui spec invalid: " + processed.errors.join("; "));
2710
+ const title = processed.spec.title ?? "未命名";
2711
+ const warnings = formatProcessWarnings(processed);
2712
+ const warningText = warnings.length === 0 ? "" : `\n${warnings.join("\n")}`;
2713
+ return `已渲染 UI「${title}」(${processed.renderedCount} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。${warningText}`;
1774
2714
  },
1775
2715
  presentCall(args) {
1776
2716
  const title = cardTitle(args);
@@ -1871,11 +2811,6 @@ function bracketDiagnostic(raw) {
1871
2811
  return diffs.length === 0 ? "" : ` 括号计数:${diffs.join(";")}(长表格最易在收尾处错位,如把 ]]}]} 写成 ]}]}]})\n`;
1872
2812
  }
1873
2813
  const COMMON_CAUSES = "常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 \"(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合";
1874
- /** Return the model-facing chart field failure for one parsed fence body. */
1875
- function chartValidationFailure(value) {
1876
- const errors = validateRenderableChartSemantics(value);
1877
- return errors.length === 0 ? void 0 : `❌ chart 字段验证失败:\n- ${errors.join("\n- ")}`;
1878
- }
1879
2814
  /** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
1880
2815
  function createValidateDshUiTool() {
1881
2816
  return {
@@ -1904,21 +2839,23 @@ function createValidateDshUiTool() {
1904
2839
  const detail = error instanceof Error ? error.message : String(error);
1905
2840
  const repaired = completeFenceJson(raw);
1906
2841
  if (repaired !== null) {
1907
- const repairedValue = JSON.parse(repaired.text);
1908
- const chartFailure = chartValidationFailure(repairedValue);
2842
+ const processed = processRenderableValue(JSON.parse(repaired.text));
2843
+ const chartFailure = formatProcessFailure(processed);
1909
2844
  if (chartFailure !== void 0) return chartFailure;
1910
- if (repairGenuiSpec(repairedValue) !== null) return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
2845
+ if (processed.spec !== null && processed.errors.length === 0) {
2846
+ const warnings = formatProcessWarnings(processed);
2847
+ const warningText = warnings.length === 0 ? "" : `${warnings.join("\n")}\n`;
2848
+ return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)}${warningText} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
2849
+ }
1911
2850
  }
1912
2851
  return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`;
1913
2852
  }
1914
- const chartFailure = chartValidationFailure(parsed);
2853
+ const processed = processRenderableValue(parsed);
2854
+ const chartFailure = formatProcessFailure(processed);
1915
2855
  if (chartFailure !== void 0) return chartFailure;
1916
- const spec = repairGenuiSpec(parsed);
1917
- if (spec === null) return "❌ 不是合法 GenUI spec:根对象需要 \"items\" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。";
1918
- const validCount = countNodes(spec);
1919
- const declaredCount = countDeclaredGenuiNodes(parsed, GENUI_LIMITS.maxNodes + 1);
1920
- if (declaredCount > validCount) return `❌ 验证未通过:检测到声明了 ${declaredCount} 个组件,但仅成功解析出 ${validCount} 个(有 ${declaredCount - validCount} 个组件因字段格式异常被丢弃)。常见原因:table 的 columns/rows 不是二维字符串数组、tabs 的 items/content 缺失、嵌套组件字段类型不符。请修正后重新验证。`;
1921
- return `✅ dsh-ui spec 合法(${validCount} 个组件),可以发出围栏。`;
2856
+ if (processed.spec === null || processed.errors.length > 0) return droppedNodeFailure(processed) ?? `❌ 不是合法 GenUI spec:${processed.errors.join(";") || "根对象需要 \"items\" 数组,且每个节点 type 必须在白名单内(见系统提示词)"}。请修正后重新验证。`;
2857
+ const warnings = formatProcessWarnings(processed);
2858
+ return [`✅ dsh-ui spec 合法(${processed.renderedCount} 个组件),可以发出围栏。`, ...warnings].join("\n");
1922
2859
  },
1923
2860
  presentCall() {
1924
2861
  return {
@@ -1937,8 +2874,6 @@ function createValidateDshUiTool() {
1937
2874
  }
1938
2875
  //#endregion
1939
2876
  //#region src/plugin/index.ts
1940
- /** Convention: tool guidance uses 100–199; bash's section is 104. */
1941
- const GENUI_SECTION_ORDER = 105;
1942
2877
  /**
1943
2878
  * The mermaid/three engines ship as standalone IIFE bundles under
1944
2879
  * `lib/assets/` and are fetched by the client ONLY when a spec needs them.
@@ -2074,7 +3009,7 @@ function bundledSkillProvider() {
2074
3009
  function apply(ctx) {
2075
3010
  ctx.systemPrompt.section({
2076
3011
  name: "genui:fence",
2077
- order: 105,
3012
+ order: ctx.systemPrompt.getSectionOrder("STRUCTURED_OUTPUT"),
2078
3013
  text: GENUI_SECTION_TEXT
2079
3014
  });
2080
3015
  let registered = false;
@@ -2111,4 +3046,4 @@ function apply(ctx) {
2111
3046
  });
2112
3047
  }
2113
3048
  //#endregion
2114
- export { GENUI_SECTION_ORDER, GENUI_SECTION_TEXT, apply, inject };
3049
+ export { GENUI_SECTION_TEXT, apply, inject };