@bbigbang/protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/panel.js ADDED
@@ -0,0 +1,2116 @@
1
+ export const DEFAULT_PANEL_PAGE_SIZE = 10;
2
+ export const MIN_PANEL_PAGE_SIZE = 1;
3
+ export const MAX_PANEL_PAGE_SIZE = 50;
4
+ export const MIN_PANEL_REFRESH_INTERVAL_MS = 1000;
5
+ export const MAX_PANEL_REFRESH_INTERVAL_MS = 60_000;
6
+ export const INLINE_ROWS_MAX = 1000;
7
+ export const PANEL_API_JSONL_MAX_URLS = 16;
8
+ export const PANEL_MEDIA_FETCH_CONCURRENCY = 6;
9
+ export const PANEL_MEDIA_REMOTE_FETCH_TIMEOUT_MS = 10_000;
10
+ export const PANEL_IMAGE_MEDIA_MAX_BYTES = 5 * 1024 * 1024;
11
+ export const PANEL_TEXT_MEDIA_MAX_BYTES = 1024 * 1024;
12
+ const PANEL_API_JSONL_LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
13
+ const PANEL_API_JSONL_AUTH_PROFILE_RE = /^[A-Za-z0-9_.:-]{1,64}$/u;
14
+ export function normalizePanelApiJsonlAuthProfileName(rawProfile) {
15
+ const trimmed = rawProfile.trim();
16
+ if (!PANEL_API_JSONL_AUTH_PROFILE_RE.test(trimmed))
17
+ return null;
18
+ return trimmed;
19
+ }
20
+ export function normalizePanelApiJsonlAuthProfileNames(rawProfiles) {
21
+ const entries = typeof rawProfiles === 'string'
22
+ ? rawProfiles.split(/[,\s]+/u)
23
+ : Array.isArray(rawProfiles)
24
+ ? [...rawProfiles]
25
+ : [];
26
+ const seen = new Set();
27
+ const profiles = [];
28
+ for (const entry of entries) {
29
+ if (typeof entry !== 'string')
30
+ continue;
31
+ const normalized = normalizePanelApiJsonlAuthProfileName(entry);
32
+ if (!normalized || seen.has(normalized))
33
+ continue;
34
+ seen.add(normalized);
35
+ profiles.push(normalized);
36
+ }
37
+ return profiles;
38
+ }
39
+ export function validatePanelApiJsonlAuthProfile(rawAuth, options = {}) {
40
+ if (rawAuth === undefined || rawAuth === null)
41
+ return { ok: true, authProfile: null };
42
+ if (typeof rawAuth !== 'object' || Array.isArray(rawAuth)) {
43
+ return { ok: false, reason: 'dataset.source.auth must be an object with a profile name.' };
44
+ }
45
+ const rawProfile = rawAuth.profile;
46
+ const profile = typeof rawProfile === 'string' ? normalizePanelApiJsonlAuthProfileName(rawProfile) : null;
47
+ if (!profile) {
48
+ return { ok: false, reason: 'dataset.source.auth.profile must be 1-64 chars of letters, numbers, dot, underscore, colon, or dash.' };
49
+ }
50
+ const allowedProfiles = normalizePanelApiJsonlAuthProfileNames(options.allowedAuthProfiles);
51
+ if (!allowedProfiles.includes(profile)) {
52
+ return { ok: false, reason: 'dataset.source.auth.profile must match an auth profile advertised by the agent node.' };
53
+ }
54
+ return { ok: true, authProfile: profile };
55
+ }
56
+ export function normalizePanelApiJsonlAllowedOrigin(rawOrigin) {
57
+ const trimmed = rawOrigin.trim();
58
+ if (!trimmed || trimmed.includes('\0') || /[\r\n]/u.test(trimmed))
59
+ return null;
60
+ let parsed;
61
+ try {
62
+ parsed = new URL(trimmed);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
68
+ return null;
69
+ if (parsed.username || parsed.password)
70
+ return null;
71
+ if ((parsed.pathname && parsed.pathname !== '/') || parsed.search || parsed.hash)
72
+ return null;
73
+ return parsed.origin;
74
+ }
75
+ export function normalizePanelApiJsonlAllowedOrigins(rawOrigins) {
76
+ const entries = typeof rawOrigins === 'string'
77
+ ? rawOrigins.split(/[,\s]+/u)
78
+ : Array.isArray(rawOrigins)
79
+ ? [...rawOrigins]
80
+ : [];
81
+ const seen = new Set();
82
+ const origins = [];
83
+ for (const entry of entries) {
84
+ if (typeof entry !== 'string')
85
+ continue;
86
+ const trimmed = entry.trim();
87
+ if (!trimmed)
88
+ continue;
89
+ const normalized = normalizePanelApiJsonlAllowedOrigin(trimmed);
90
+ if (!normalized || seen.has(normalized))
91
+ continue;
92
+ seen.add(normalized);
93
+ origins.push(normalized);
94
+ }
95
+ return origins;
96
+ }
97
+ export function normalizePanelApiJsonlAllowedUrlPrefix(rawPrefix) {
98
+ const trimmed = rawPrefix.trim();
99
+ if (!trimmed || trimmed.includes('\0') || /[\r\n]/u.test(trimmed))
100
+ return null;
101
+ let parsed;
102
+ try {
103
+ parsed = new URL(trimmed);
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
109
+ return null;
110
+ if (parsed.username || parsed.password)
111
+ return null;
112
+ if (parsed.search || parsed.hash)
113
+ return null;
114
+ return `${parsed.origin}${parsed.pathname}`;
115
+ }
116
+ export function normalizePanelApiJsonlAllowedUrlPrefixes(rawPrefixes) {
117
+ const entries = typeof rawPrefixes === 'string'
118
+ ? rawPrefixes.split(/[,\s]+/u)
119
+ : Array.isArray(rawPrefixes)
120
+ ? [...rawPrefixes]
121
+ : [];
122
+ const seen = new Set();
123
+ const prefixes = [];
124
+ for (const entry of entries) {
125
+ if (typeof entry !== 'string')
126
+ continue;
127
+ const normalized = normalizePanelApiJsonlAllowedUrlPrefix(entry);
128
+ if (!normalized || seen.has(normalized))
129
+ continue;
130
+ seen.add(normalized);
131
+ prefixes.push(normalized);
132
+ }
133
+ return prefixes;
134
+ }
135
+ export function matchPanelApiJsonlAllowedUrlPrefix(parsed, rawPrefixes) {
136
+ for (const prefix of normalizePanelApiJsonlAllowedUrlPrefixes(rawPrefixes)) {
137
+ const parsedPrefix = new URL(prefix);
138
+ if (parsed.origin !== parsedPrefix.origin)
139
+ continue;
140
+ const prefixPath = parsedPrefix.pathname;
141
+ const pathMatches = prefixPath.endsWith('/')
142
+ ? parsed.pathname.startsWith(prefixPath)
143
+ : parsed.pathname === prefixPath || parsed.pathname.startsWith(`${prefixPath}/`);
144
+ if (!pathMatches)
145
+ continue;
146
+ return prefix;
147
+ }
148
+ return null;
149
+ }
150
+ export function isPanelApiJsonlDefaultLoopbackUrl(parsed) {
151
+ return parsed.protocol === 'http:' && PANEL_API_JSONL_LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase());
152
+ }
153
+ export function validatePanelApiJsonlUrl(rawUrl, options = {}) {
154
+ const trimmed = rawUrl.trim();
155
+ if (!trimmed || trimmed.includes('\0') || /[\r\n]/u.test(trimmed)) {
156
+ return { ok: false, reason: 'api_jsonl url must be a non-empty URL without control characters.' };
157
+ }
158
+ let parsed;
159
+ try {
160
+ parsed = new URL(trimmed);
161
+ }
162
+ catch {
163
+ return { ok: false, reason: 'api_jsonl url must be a valid URL.' };
164
+ }
165
+ if (parsed.username || parsed.password) {
166
+ return { ok: false, reason: 'api_jsonl url must not include credentials.' };
167
+ }
168
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
169
+ return {
170
+ ok: false,
171
+ reason: 'api_jsonl only supports http:// loopback endpoints or configured http(s) origins / URL prefixes.',
172
+ };
173
+ }
174
+ const defaultLoopback = isPanelApiJsonlDefaultLoopbackUrl(parsed);
175
+ const allowedOrigins = normalizePanelApiJsonlAllowedOrigins(options.allowedOrigins);
176
+ const matchedAllowedPrefix = matchPanelApiJsonlAllowedUrlPrefix(parsed, options.allowedUrlPrefixes);
177
+ if (!defaultLoopback && !allowedOrigins.includes(parsed.origin) && !matchedAllowedPrefix) {
178
+ return {
179
+ ok: false,
180
+ reason: 'api_jsonl endpoints must target localhost, 127.0.0.1, or ::1, or match a configured allowlisted origin or URL prefix on the agent node.',
181
+ };
182
+ }
183
+ return {
184
+ ok: true,
185
+ url: parsed.toString(),
186
+ origin: parsed.origin,
187
+ defaultLoopback,
188
+ matchedAllowedPrefix,
189
+ };
190
+ }
191
+ export function validatePanelApiJsonlUrlList(source, options = {}) {
192
+ const hasUrl = source.url !== undefined;
193
+ const hasUrls = source.urls !== undefined;
194
+ if (hasUrl && hasUrls) {
195
+ return {
196
+ ok: false,
197
+ reason: 'dataset.source.url and dataset.source.urls are mutually exclusive.',
198
+ };
199
+ }
200
+ if (!hasUrl && !hasUrls) {
201
+ return {
202
+ ok: false,
203
+ reason: 'dataset.source.url or dataset.source.urls is required.',
204
+ missingField: 'dataset.source.url',
205
+ };
206
+ }
207
+ const rawUrls = hasUrl
208
+ ? [source.url]
209
+ : Array.isArray(source.urls)
210
+ ? source.urls
211
+ : null;
212
+ if (!rawUrls) {
213
+ return {
214
+ ok: false,
215
+ reason: 'dataset.source.urls must be an array of URL strings.',
216
+ };
217
+ }
218
+ if (rawUrls.length < 1) {
219
+ return {
220
+ ok: false,
221
+ reason: 'dataset.source.urls must include at least one URL.',
222
+ };
223
+ }
224
+ if (rawUrls.length > PANEL_API_JSONL_MAX_URLS) {
225
+ return {
226
+ ok: false,
227
+ reason: `dataset.source.urls supports at most ${PANEL_API_JSONL_MAX_URLS} URLs.`,
228
+ };
229
+ }
230
+ const seen = new Set();
231
+ const urls = [];
232
+ for (let index = 0; index < rawUrls.length; index += 1) {
233
+ const rawUrl = rawUrls[index];
234
+ if (typeof rawUrl !== 'string') {
235
+ return {
236
+ ok: false,
237
+ reason: hasUrl ? 'dataset.source.url must be a URL string.' : `dataset.source.urls[${index}] must be a URL string.`,
238
+ };
239
+ }
240
+ const validation = validatePanelApiJsonlUrl(rawUrl, options);
241
+ if (!validation.ok) {
242
+ return {
243
+ ok: false,
244
+ reason: hasUrl ? validation.reason : `dataset.source.urls[${index}]: ${validation.reason}`,
245
+ };
246
+ }
247
+ if (seen.has(validation.url)) {
248
+ return {
249
+ ok: false,
250
+ reason: `dataset.source.urls[${index}] duplicates an earlier URL.`,
251
+ };
252
+ }
253
+ seen.add(validation.url);
254
+ urls.push(validation.url);
255
+ }
256
+ const authValidation = validatePanelApiJsonlAuthProfile(source.auth, {
257
+ allowedAuthProfiles: options.allowedAuthProfiles,
258
+ });
259
+ if (!authValidation.ok) {
260
+ return { ok: false, reason: authValidation.reason };
261
+ }
262
+ return { ok: true, urls, authProfile: authValidation.authProfile };
263
+ }
264
+ export function isPanelTemplateContainerNode(node) {
265
+ return node?.type === 'Row'
266
+ || node?.type === 'Column'
267
+ || node?.type === 'Card'
268
+ || node?.type === 'Tabs'
269
+ || node?.type === 'Accordion'
270
+ || node?.type === 'Columns'
271
+ || node?.type === 'Stack'
272
+ || node?.type === 'Inline'
273
+ || node?.type === 'Grid';
274
+ }
275
+ export function walkPanelTemplateNodes(node, visitor) {
276
+ if (!node)
277
+ return;
278
+ const visit = (current, context) => {
279
+ if (visitor(current, context) === false)
280
+ return;
281
+ for (const child of getPanelTemplateChildEntries(current)) {
282
+ visit(child.node, {
283
+ parent: current,
284
+ depth: context.depth + 1,
285
+ index: child.index,
286
+ ...(child.columnIndex !== undefined ? { columnIndex: child.columnIndex } : {}),
287
+ ...(child.tabIndex !== undefined ? { tabIndex: child.tabIndex } : {}),
288
+ });
289
+ }
290
+ };
291
+ visit(node, { parent: null, depth: 0, index: null });
292
+ }
293
+ export function collectPanelTemplateUnsupportedNodes(node, policy = {}) {
294
+ const unsupported = [];
295
+ walkPanelTemplateNodes(node, (current) => {
296
+ const interactiveBlocked = policy.allowInteractiveTypes !== true
297
+ && panelTemplateNodeTypeIn(current.type, policy.interactiveTypes);
298
+ if (interactiveBlocked
299
+ || panelTemplateNodeTypeIn(current.type, policy.unsupportedTypes)
300
+ || policy.predicate?.(current) === true) {
301
+ unsupported.push(current);
302
+ }
303
+ });
304
+ return unsupported;
305
+ }
306
+ export function isPanelLevelContainerNode(node) {
307
+ return node?.type === 'Section'
308
+ || node?.type === 'ParameterFormSection'
309
+ || node?.type === 'Card'
310
+ || node?.type === 'Columns'
311
+ || node?.type === 'Stack'
312
+ || node?.type === 'Inline'
313
+ || node?.type === 'Grid';
314
+ }
315
+ export function isPanelLevelActionBarNode(node) {
316
+ return node?.type === 'ActionBar';
317
+ }
318
+ export function isPanelLevelParameterInputNode(node) {
319
+ switch (node?.type) {
320
+ case 'TextInput':
321
+ case 'TextArea':
322
+ case 'NumberInput':
323
+ case 'Checkbox':
324
+ case 'Select':
325
+ case 'MultiSelect':
326
+ case 'TagInput':
327
+ case 'Slider':
328
+ case 'DatePicker':
329
+ case 'FileUpload':
330
+ return true;
331
+ default:
332
+ return false;
333
+ }
334
+ }
335
+ export function isPanelLevelDatasetAggregateNode(node) {
336
+ return node?.type === 'AggregateValue' && node.scope === 'dataset';
337
+ }
338
+ export function walkPanelLevelNodes(node, visitor) {
339
+ if (!node)
340
+ return;
341
+ const visit = (current, context) => {
342
+ if (visitor(current, context) === false)
343
+ return;
344
+ for (const child of getPanelLevelChildEntries(current)) {
345
+ visit(child.node, {
346
+ parent: current,
347
+ depth: context.depth + 1,
348
+ index: child.index,
349
+ ...(child.columnIndex !== undefined ? { columnIndex: child.columnIndex } : {}),
350
+ });
351
+ }
352
+ };
353
+ visit(node, { parent: null, depth: 0, index: null });
354
+ }
355
+ export function mapPanelLevelNodes(node, mapper, options = {}) {
356
+ if (!node)
357
+ return null;
358
+ const visit = (current, context) => {
359
+ const mapped = mapper(current, context);
360
+ if (mapped === null)
361
+ return { node: null, changed: true };
362
+ const base = mapped ?? current;
363
+ let changed = mapped !== undefined && mapped !== current;
364
+ switch (base.type) {
365
+ case 'Section':
366
+ case 'ParameterFormSection':
367
+ case 'Card':
368
+ case 'Stack':
369
+ case 'Inline':
370
+ case 'Grid': {
371
+ const sourceChildren = Array.isArray(base.children)
372
+ ? [...base.children]
373
+ : [];
374
+ const children = [];
375
+ let childrenChanged = !Array.isArray(base.children);
376
+ for (let index = 0; index < sourceChildren.length; index += 1) {
377
+ const child = sourceChildren[index];
378
+ const childResult = visit(child, {
379
+ parent: base,
380
+ depth: context.depth + 1,
381
+ index,
382
+ });
383
+ if (childResult.node) {
384
+ children.push(childResult.node);
385
+ }
386
+ else {
387
+ childrenChanged = true;
388
+ }
389
+ if (childResult.changed || childResult.node !== child)
390
+ childrenChanged = true;
391
+ }
392
+ if (options.pruneEmptyContainers === true && children.length === 0) {
393
+ return { node: null, changed: true };
394
+ }
395
+ if (options.collapseSingleChildContainers === true
396
+ && children.length === 1
397
+ && children.length !== sourceChildren.length) {
398
+ return { node: children[0], changed: true };
399
+ }
400
+ if (childrenChanged || children.length !== sourceChildren.length) {
401
+ changed = true;
402
+ return { node: { ...base, children }, changed };
403
+ }
404
+ return { node: base, changed };
405
+ }
406
+ case 'Columns': {
407
+ const sourceColumns = Array.isArray(base.columns)
408
+ ? [...base.columns]
409
+ : [];
410
+ const columns = [];
411
+ let columnsChanged = !Array.isArray(base.columns);
412
+ for (let columnIndex = 0; columnIndex < sourceColumns.length; columnIndex += 1) {
413
+ const column = sourceColumns[columnIndex];
414
+ if (!isRecord(column) || !Array.isArray(column.children)) {
415
+ columnsChanged = true;
416
+ continue;
417
+ }
418
+ const children = [];
419
+ let childrenChanged = false;
420
+ for (let index = 0; index < column.children.length; index += 1) {
421
+ const child = column.children[index];
422
+ const childResult = visit(child, {
423
+ parent: base,
424
+ depth: context.depth + 1,
425
+ index,
426
+ columnIndex,
427
+ });
428
+ if (childResult.node) {
429
+ children.push(childResult.node);
430
+ }
431
+ else {
432
+ childrenChanged = true;
433
+ }
434
+ if (childResult.changed || childResult.node !== child)
435
+ childrenChanged = true;
436
+ }
437
+ if (options.pruneEmptyColumns === true && children.length === 0) {
438
+ columnsChanged = true;
439
+ continue;
440
+ }
441
+ const nextColumn = {
442
+ ...(typeof column.width === 'string' ? { width: column.width } : {}),
443
+ children,
444
+ };
445
+ columns.push(nextColumn);
446
+ if (childrenChanged || children.length !== column.children.length) {
447
+ columnsChanged = true;
448
+ }
449
+ }
450
+ if (options.pruneEmptyContainers === true && columns.length === 0) {
451
+ return { node: null, changed: true };
452
+ }
453
+ if (columnsChanged || columns.length !== sourceColumns.length) {
454
+ changed = true;
455
+ return { node: { type: 'Columns', columns }, changed };
456
+ }
457
+ return { node: base, changed };
458
+ }
459
+ default:
460
+ return { node: base, changed };
461
+ }
462
+ };
463
+ return visit(node, { parent: null, depth: 0, index: null }).node;
464
+ }
465
+ export function collectPanelLevelActionBars(node) {
466
+ const actionBars = [];
467
+ walkPanelLevelNodes(node, (current) => {
468
+ if (isPanelLevelActionBarNode(current))
469
+ actionBars.push(current);
470
+ });
471
+ return actionBars;
472
+ }
473
+ export function panelLevelNodeHasActionBar(node) {
474
+ return collectPanelLevelActionBars(node).length > 0;
475
+ }
476
+ export function collectPanelLevelParameterInputs(node) {
477
+ const inputs = [];
478
+ walkPanelLevelNodes(node, (current) => {
479
+ if (isPanelLevelParameterInputNode(current))
480
+ inputs.push(current);
481
+ });
482
+ return inputs;
483
+ }
484
+ export function collectDatasetAggregateNodes(node) {
485
+ const aggregates = [];
486
+ walkPanelLevelNodes(node, (current) => {
487
+ if (isPanelLevelDatasetAggregateNode(current))
488
+ aggregates.push(current);
489
+ });
490
+ return aggregates;
491
+ }
492
+ export function panelLevelNodeUsesDatasetAggregate(node) {
493
+ return collectDatasetAggregateNodes(node).length > 0;
494
+ }
495
+ export function panelLevelNodeContainsEmptyLayoutContainer(node) {
496
+ if (!node)
497
+ return false;
498
+ switch (node.type) {
499
+ case 'Section':
500
+ case 'ParameterFormSection':
501
+ case 'Card':
502
+ case 'Stack':
503
+ case 'Inline':
504
+ case 'Grid': {
505
+ const children = Array.isArray(node.children)
506
+ ? node.children
507
+ : [];
508
+ return children.length === 0 || children.some(panelLevelNodeContainsEmptyLayoutContainer);
509
+ }
510
+ case 'Columns': {
511
+ const columns = Array.isArray(node.columns)
512
+ ? node.columns
513
+ : [];
514
+ return columns.length === 0
515
+ || columns.some((column) => (!isRecord(column)
516
+ || !Array.isArray(column.children)
517
+ || column.children.length === 0
518
+ || column.children.some(panelLevelNodeContainsEmptyLayoutContainer)));
519
+ }
520
+ default:
521
+ return false;
522
+ }
523
+ }
524
+ export function isPanelLevelNodeEffectivelyEmpty(node) {
525
+ if (!node)
526
+ return true;
527
+ switch (node.type) {
528
+ case 'Section':
529
+ case 'ParameterFormSection':
530
+ case 'Card':
531
+ case 'Stack':
532
+ case 'Inline':
533
+ case 'Grid': {
534
+ const children = Array.isArray(node.children)
535
+ ? node.children
536
+ : [];
537
+ return children.length === 0 || children.every(isPanelLevelNodeEffectivelyEmpty);
538
+ }
539
+ case 'Columns': {
540
+ const columns = Array.isArray(node.columns)
541
+ ? node.columns
542
+ : [];
543
+ return columns.length === 0
544
+ || columns.every((column) => (!isRecord(column)
545
+ || !Array.isArray(column.children)
546
+ || column.children.length === 0
547
+ || column.children.every(isPanelLevelNodeEffectivelyEmpty)));
548
+ }
549
+ default:
550
+ return false;
551
+ }
552
+ }
553
+ export function collectPanelLevelUnsupportedNodes(node, policy = {}) {
554
+ const unsupported = [];
555
+ walkPanelLevelNodes(node, (current) => {
556
+ const interactiveBlocked = policy.allowInteractiveTypes !== true
557
+ && panelLevelNodeTypeIn(current.type, policy.interactiveTypes);
558
+ if (interactiveBlocked
559
+ || panelLevelNodeTypeIn(current.type, policy.unsupportedTypes)
560
+ || policy.predicate?.(current) === true) {
561
+ unsupported.push(current);
562
+ }
563
+ });
564
+ return unsupported;
565
+ }
566
+ function getPanelLevelChildEntries(node) {
567
+ switch (node.type) {
568
+ case 'Section':
569
+ case 'ParameterFormSection':
570
+ case 'Card':
571
+ case 'Stack':
572
+ case 'Inline':
573
+ case 'Grid': {
574
+ const children = Array.isArray(node.children)
575
+ ? node.children
576
+ : [];
577
+ return children.map((child, index) => ({ node: child, index }));
578
+ }
579
+ case 'Columns': {
580
+ const columns = Array.isArray(node.columns)
581
+ ? node.columns
582
+ : [];
583
+ return columns.flatMap((column, columnIndex) => {
584
+ if (!isRecord(column) || !Array.isArray(column.children))
585
+ return [];
586
+ return column.children.map((child, index) => ({ node: child, index, columnIndex }));
587
+ });
588
+ }
589
+ default:
590
+ return [];
591
+ }
592
+ }
593
+ function getPanelTemplateChildEntries(node) {
594
+ switch (node.type) {
595
+ case 'Row':
596
+ case 'Column':
597
+ case 'Card':
598
+ case 'Accordion':
599
+ case 'Stack':
600
+ case 'Inline':
601
+ case 'Grid': {
602
+ const children = Array.isArray(node.children)
603
+ ? node.children
604
+ : [];
605
+ return children.map((child, index) => ({ node: child, index }));
606
+ }
607
+ case 'Tabs': {
608
+ const items = Array.isArray(node.items)
609
+ ? node.items
610
+ : [];
611
+ return items.flatMap((item, itemIndex) => (isRecord(item) && isRecord(item.node)
612
+ ? [{ node: item.node, index: itemIndex, tabIndex: itemIndex }]
613
+ : []));
614
+ }
615
+ case 'Columns': {
616
+ const columns = Array.isArray(node.columns)
617
+ ? node.columns
618
+ : [];
619
+ return columns.flatMap((column, columnIndex) => {
620
+ if (!isRecord(column) || !Array.isArray(column.children))
621
+ return [];
622
+ return column.children.map((child, index) => ({ node: child, index, columnIndex }));
623
+ });
624
+ }
625
+ default:
626
+ return [];
627
+ }
628
+ }
629
+ function panelLevelNodeTypeIn(type, types) {
630
+ if (!types)
631
+ return false;
632
+ if (Array.isArray(types))
633
+ return types.includes(type);
634
+ return types.has(type);
635
+ }
636
+ function panelTemplateNodeTypeIn(type, types) {
637
+ if (!types)
638
+ return false;
639
+ if (Array.isArray(types))
640
+ return types.includes(type);
641
+ return types.has(type);
642
+ }
643
+ export const COMPONENT_CONTRACTS = {
644
+ ImageReviewGrid: {
645
+ name: 'ImageReviewGrid',
646
+ displayName: 'Image Review Grid',
647
+ description: 'Split-layout grid for reviewing images with LaTeX annotations. Left column shows image + label; right column renders LaTeX text.',
648
+ dataModel: { kind: 'paged_rows' },
649
+ propsSchema: {
650
+ type: 'object',
651
+ properties: {
652
+ title: { type: 'string' },
653
+ showScore: { type: 'boolean' },
654
+ pageSize: {
655
+ type: 'integer',
656
+ minimum: MIN_PANEL_PAGE_SIZE,
657
+ maximum: MAX_PANEL_PAGE_SIZE,
658
+ default: DEFAULT_PANEL_PAGE_SIZE,
659
+ description: 'Optional row page size. Defaults to 10 and is limited to 1-50.',
660
+ },
661
+ refreshIntervalMs: {
662
+ type: 'integer',
663
+ minimum: MIN_PANEL_REFRESH_INTERVAL_MS,
664
+ maximum: MAX_PANEL_REFRESH_INTERVAL_MS,
665
+ description: 'Optional auto-refresh interval for dynamic query_handle or api_jsonl rows. Omit to disable polling. Limited to 1000-60000 ms.',
666
+ },
667
+ },
668
+ },
669
+ datasetSchema: {
670
+ fields: [
671
+ { name: 'label', label: 'Label', type: 'string', filterable: true, sortable: true },
672
+ { name: 'score', label: 'Score', type: 'number', filterable: true, sortable: true },
673
+ { name: 'category', label: 'Category', type: 'string', filterable: true, sortable: true },
674
+ { name: 'latex', label: 'LaTeX', type: 'string' },
675
+ ],
676
+ mediaSlots: [
677
+ { name: 'img', kind: 'workspace_path', displayType: 'img' },
678
+ ],
679
+ },
680
+ stateSchema: {
681
+ type: 'object',
682
+ properties: {
683
+ filters: { type: 'object' },
684
+ sort: { type: 'object' },
685
+ selection: { type: 'array' },
686
+ },
687
+ },
688
+ sharedStateKeys: ['selection'],
689
+ clientInteractions: ['filter', 'sort', 'select', 'hover', 'zoom'],
690
+ supportedActions: [
691
+ {
692
+ id: 'ocr',
693
+ label: 'OCR 选中项',
694
+ description: 'Run OCR on selected images',
695
+ },
696
+ ],
697
+ },
698
+ RowTemplateGrid: {
699
+ name: 'RowTemplateGrid',
700
+ displayName: 'Row Template Grid',
701
+ description: 'Paged row grid that renders each row through a safe declarative template made from registered primitives: Row, Column, Card, Tabs, TextField, LatexBlock, Badge, ComputedValue, ImageSlot, Checkbox, FormField, ActionButton, SubmitButton, Sparkline, Accordion, Divider, Callout, Columns, DiffView, CodeBlock, JsonView, and MarkdownText. Template nodes can use visibleWhen for bounded row-field conditional rendering. Badge supports bounded valueMap label/variant mapping for declared row values. FormField supports text, number, checkbox, textarea, bounded static select, and form-only tags inputs for row-level form deltas; tags may not bind dataset fields. ComputedValue supports fixed cross-field operations such as sum, ratio, percent, average, weighted_sum, and concat without arbitrary expressions or JavaScript. ImageSlot can optionally render editable annotation boxes when bound to annotationsField, including shortcutLabels for single-key label/color updates and autosaved staged form state. Supports optional summary and parameterForm panel-level slots, including Section/ParameterFormSection, Card, Columns, AggregateValue page or server-side dataset aggregates, BarChart/LineChart with optional zoomable brush, Histogram, grouped/stacked BarChart, ProgressBar, StatusBadge, Timeline, LogBlock, Tool-only RunStatusCard/RunLogViewer/ArtifactList widgets, ActionBar plus TextInput, TextArea, NumberInput, DatePicker, FileUpload with clipboard paste, Checkbox, Select, MultiSelect, TagInput, Slider, and Button controls. Select and MultiSelect option objects may use visibleWhen against current parameter form values for bounded option cascading.',
702
+ dataModel: { kind: 'paged_rows' },
703
+ propsSchema: {
704
+ type: 'object',
705
+ required: ['fields', 'template'],
706
+ properties: {
707
+ title: { type: 'string' },
708
+ pageSize: {
709
+ type: 'integer',
710
+ minimum: MIN_PANEL_PAGE_SIZE,
711
+ maximum: MAX_PANEL_PAGE_SIZE,
712
+ default: DEFAULT_PANEL_PAGE_SIZE,
713
+ description: 'Optional row page size. Defaults to 10 and is limited to 1-50.',
714
+ },
715
+ refreshIntervalMs: {
716
+ type: 'integer',
717
+ minimum: MIN_PANEL_REFRESH_INTERVAL_MS,
718
+ maximum: MAX_PANEL_REFRESH_INTERVAL_MS,
719
+ description: 'Optional auto-refresh interval for dynamic query_handle or api_jsonl rows. Omit to disable polling. Limited to 1000-60000 ms.',
720
+ },
721
+ fields: { type: 'array' },
722
+ mediaSlots: { type: 'array' },
723
+ template: { type: 'object' },
724
+ summary: {
725
+ type: 'object',
726
+ description: 'Optional panel-level slot. Supports MetricCard, AggregateValue, BarChart, LineChart, PieChart, Histogram, ProgressBar, StatusBadge, Timeline, LogBlock, and Tool-only RunStatusCard/RunLogViewer/ArtifactList widgets when rendered in a tool-scoped surface.',
727
+ },
728
+ parameterForm: {
729
+ type: 'object',
730
+ description: 'Optional panel-level form/action slot. Supports Section/ParameterFormSection, Card, Columns, TextInput, TextArea, NumberInput, Checkbox, Select, MultiSelect, TagInput, Slider, DatePicker, FileUpload with clipboard paste, Button, and ActionBar.',
731
+ },
732
+ },
733
+ },
734
+ datasetSchema: {
735
+ fields: [],
736
+ mediaSlots: [],
737
+ },
738
+ stateSchema: {
739
+ type: 'object',
740
+ properties: {
741
+ filters: { type: 'object' },
742
+ sort: { type: 'object' },
743
+ selection: { type: 'array' },
744
+ formValues: { type: 'object' },
745
+ },
746
+ },
747
+ sharedStateKeys: ['selection'],
748
+ clientInteractions: ['filter', 'sort', 'select', 'hover', 'zoom', 'edit', 'autosave'],
749
+ },
750
+ };
751
+ export function getComponentContract(name) {
752
+ return COMPONENT_CONTRACTS[name];
753
+ }
754
+ export function listComponentContracts() {
755
+ return Object.values(COMPONENT_CONTRACTS);
756
+ }
757
+ const FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_:-]{0,63}$/;
758
+ const FIELD_PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_:-]{0,63}(?:\.[A-Za-z_][A-Za-z0-9_:-]{0,63}){0,15}$/;
759
+ const MAX_TEMPLATE_DEPTH = 8;
760
+ const MAX_TEMPLATE_CHILDREN = 12;
761
+ const MAX_FORM_FIELD_OPTIONS = 50;
762
+ const MAX_PANEL_LEVEL_DEPTH = 8;
763
+ const MAX_PANEL_LEVEL_CHILDREN = 50;
764
+ const ROW_CONTEXT_NODE_TYPES = new Set([
765
+ 'TextField', 'LatexBlock', 'MarkdownField', 'CodeField', 'JsonField', 'DiffField',
766
+ 'Badge', 'ComputedValue', 'ImageSlot', 'MediaBlock', 'FormField',
767
+ 'ActionButton', 'SubmitButton', 'Sparkline',
768
+ ]);
769
+ const PANEL_LEVEL_ONLY_TYPES = new Set([
770
+ 'Section', 'ParameterFormSection', 'MetricCard', 'BarChart', 'LineChart', 'PieChart', 'Histogram',
771
+ 'ProgressBar', 'StatusBadge', 'Timeline', 'LogBlock', 'RunStatusCard', 'RunLogViewer', 'ArtifactList', 'TextInput', 'TextArea', 'NumberInput', 'Select',
772
+ 'MultiSelect', 'TagInput', 'Slider', 'DatePicker', 'FileUpload', 'Button',
773
+ 'ActionBar', 'AggregateValue',
774
+ ]);
775
+ function isRecord(value) {
776
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
777
+ }
778
+ function isPrimitiveMapValue(value) {
779
+ return value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
780
+ }
781
+ function isFormFieldOptionValue(value) {
782
+ return typeof value === 'string'
783
+ || (typeof value === 'number' && Number.isFinite(value))
784
+ || typeof value === 'boolean';
785
+ }
786
+ function isValidBadgeVariant(value) {
787
+ return value === 'default' || value === 'success' || value === 'warning' || value === 'danger';
788
+ }
789
+ function isValidKeyValueTone(value) {
790
+ return value === 'default' || value === 'success' || value === 'warning' || value === 'danger';
791
+ }
792
+ function validateLayoutGap(value, path, errors, allowNone = true) {
793
+ if (value !== undefined
794
+ && value !== 'sm'
795
+ && value !== 'md'
796
+ && value !== 'lg'
797
+ && (allowNone !== true || value !== 'none')) {
798
+ errors.push(`${path} must be ${allowNone ? 'none, ' : ''}sm, md, or lg`);
799
+ }
800
+ }
801
+ function validateLayoutAlign(value, path, errors, allowStretch = true) {
802
+ if (value !== undefined
803
+ && value !== 'start'
804
+ && value !== 'center'
805
+ && value !== 'end'
806
+ && (allowStretch !== true || value !== 'stretch')) {
807
+ errors.push(`${path} must be start, center, end${allowStretch ? ', or stretch' : ''}`);
808
+ }
809
+ }
810
+ function validateToolRunSelector(value, path, errors) {
811
+ if (value === undefined || value === 'latest')
812
+ return;
813
+ if (!isRecord(value)) {
814
+ errors.push(`${path} must be "latest" or { id: string }`);
815
+ return;
816
+ }
817
+ hasOnlyKeys(value, ['id'], path, errors);
818
+ if (typeof value.id !== 'string' || value.id.trim().length === 0) {
819
+ errors.push(`${path}.id must be a non-empty string`);
820
+ }
821
+ }
822
+ function validateStringArrayOption(value, path, errors) {
823
+ if (value === undefined)
824
+ return;
825
+ if (!Array.isArray(value)) {
826
+ errors.push(`${path} must be an array`);
827
+ return;
828
+ }
829
+ if (value.length > 50) {
830
+ errors.push(`${path} exceeds maximum 50 items`);
831
+ }
832
+ value.forEach((item, index) => {
833
+ if (typeof item !== 'string' || item.trim().length === 0) {
834
+ errors.push(`${path}[${index}] must be a non-empty string`);
835
+ }
836
+ });
837
+ }
838
+ function validateKeyValueListNode(node, options) {
839
+ const { path, errors, fieldNames, template } = options;
840
+ const keyValidator = template ? hasOnlyTemplateKeys : hasOnlyKeys;
841
+ keyValidator(node, ['type', 'items'], path, errors);
842
+ if (!Array.isArray(node.items)) {
843
+ errors.push(`${path}.items must be an array`);
844
+ return;
845
+ }
846
+ if (node.items.length > 50) {
847
+ errors.push(`${path}.items exceeds max 50`);
848
+ }
849
+ node.items.forEach((item, index) => {
850
+ const itemPath = `${path}.items[${index}]`;
851
+ if (!isRecord(item)) {
852
+ errors.push(`${itemPath} must be an object`);
853
+ return;
854
+ }
855
+ hasOnlyKeys(item, ['label', 'field', 'source', 'value', 'fallback', 'tone'], itemPath, errors);
856
+ if (typeof item.label !== 'string' || item.label.trim().length === 0) {
857
+ errors.push(`${itemPath}.label must be a non-empty string`);
858
+ }
859
+ if (item.field !== undefined && (typeof item.field !== 'string' || !fieldNames.has(item.field))) {
860
+ errors.push(`${itemPath}.field references an unknown field`);
861
+ }
862
+ if (item.source !== undefined && typeof item.source !== 'string') {
863
+ errors.push(`${itemPath}.source must be a string`);
864
+ }
865
+ if (template && item.source !== undefined) {
866
+ errors.push(`${itemPath}.source is only supported in panel-level KeyValueList nodes`);
867
+ }
868
+ if (item.fallback !== undefined && typeof item.fallback !== 'string') {
869
+ errors.push(`${itemPath}.fallback must be a string`);
870
+ }
871
+ if (item.tone !== undefined && !isValidKeyValueTone(item.tone)) {
872
+ errors.push(`${itemPath}.tone must be default, success, warning, or danger`);
873
+ }
874
+ if (!Object.prototype.hasOwnProperty.call(item, 'value')
875
+ && item.field === undefined
876
+ && item.source === undefined) {
877
+ errors.push(`${itemPath} must declare field, source, or value`);
878
+ }
879
+ });
880
+ }
881
+ function isPanelLevelOnlyType(type) {
882
+ return PANEL_LEVEL_ONLY_TYPES.has(type);
883
+ }
884
+ function isRowContextNodeType(type) {
885
+ return ROW_CONTEXT_NODE_TYPES.has(type);
886
+ }
887
+ function validateFieldSchemas(value, path) {
888
+ const errors = [];
889
+ if (!Array.isArray(value)) {
890
+ return { fields: [], errors: [`${path} must be an array`] };
891
+ }
892
+ const seen = new Set();
893
+ const fields = [];
894
+ value.forEach((item, index) => {
895
+ const itemPath = `${path}[${index}]`;
896
+ if (!isRecord(item)) {
897
+ errors.push(`${itemPath} must be an object`);
898
+ return;
899
+ }
900
+ const name = item.name;
901
+ const type = item.type;
902
+ if (typeof name !== 'string' || !FIELD_PATH_PATTERN.test(name)) {
903
+ errors.push(`${itemPath}.name must be a safe field name or dot path`);
904
+ return;
905
+ }
906
+ if (seen.has(name)) {
907
+ errors.push(`${itemPath}.name duplicates "${name}"`);
908
+ return;
909
+ }
910
+ if (type !== 'string' && type !== 'number' && type !== 'boolean') {
911
+ errors.push(`${itemPath}.type must be string, number, or boolean`);
912
+ return;
913
+ }
914
+ if (item.label !== undefined && typeof item.label !== 'string') {
915
+ errors.push(`${itemPath}.label must be a string`);
916
+ return;
917
+ }
918
+ const label = typeof item.label === 'string' && item.label.trim()
919
+ ? item.label.trim()
920
+ : undefined;
921
+ seen.add(name);
922
+ fields.push({
923
+ name,
924
+ ...(label ? { label } : {}),
925
+ type,
926
+ filterable: item.filterable === true,
927
+ sortable: item.sortable === true,
928
+ });
929
+ });
930
+ return { fields, errors };
931
+ }
932
+ function validateMediaSlotSchemas(value, path) {
933
+ const errors = [];
934
+ if (value === undefined)
935
+ return { mediaSlots: [], errors };
936
+ if (!Array.isArray(value)) {
937
+ return { mediaSlots: [], errors: [`${path} must be an array`] };
938
+ }
939
+ const seen = new Set();
940
+ const mediaSlots = [];
941
+ value.forEach((item, index) => {
942
+ const itemPath = `${path}[${index}]`;
943
+ if (!isRecord(item)) {
944
+ errors.push(`${itemPath} must be an object`);
945
+ return;
946
+ }
947
+ const name = item.name;
948
+ const kind = item.kind;
949
+ const displayType = item.displayType;
950
+ if (typeof name !== 'string' || !FIELD_NAME_PATTERN.test(name)) {
951
+ errors.push(`${itemPath}.name must be a safe media slot name`);
952
+ return;
953
+ }
954
+ if (seen.has(name)) {
955
+ errors.push(`${itemPath}.name duplicates "${name}"`);
956
+ return;
957
+ }
958
+ if (kind !== 'workspace_path' && kind !== 'asset') {
959
+ errors.push(`${itemPath}.kind must be workspace_path or asset`);
960
+ return;
961
+ }
962
+ if (displayType !== 'img' && displayType !== 'latex' && displayType !== 'text') {
963
+ errors.push(`${itemPath}.displayType must be img, latex, or text`);
964
+ return;
965
+ }
966
+ seen.add(name);
967
+ mediaSlots.push({ name, kind, displayType });
968
+ });
969
+ return { mediaSlots, errors };
970
+ }
971
+ function hasOnlyKeys(value, keys, path, errors) {
972
+ const allowed = new Set(keys);
973
+ for (const key of Object.keys(value)) {
974
+ if (!allowed.has(key)) {
975
+ errors.push(`${path}.${key} is not allowed`);
976
+ }
977
+ }
978
+ }
979
+ function hasOnlyTemplateKeys(value, keys, path, errors) {
980
+ hasOnlyKeys(value, [...keys, 'visibleWhen'], path, errors);
981
+ }
982
+ function validateVisibilityCondition(value, options) {
983
+ const { fieldNames, path, errors } = options;
984
+ if (value === undefined)
985
+ return;
986
+ if (!isRecord(value)) {
987
+ errors.push(`${path}.visibleWhen must be an object`);
988
+ return;
989
+ }
990
+ hasOnlyKeys(value, ['field', 'op', 'value', 'values'], `${path}.visibleWhen`, errors);
991
+ if (typeof value.field !== 'string' || !fieldNames.has(value.field)) {
992
+ errors.push(`${path}.visibleWhen.field references an unknown field`);
993
+ }
994
+ const op = value.op ?? 'truthy';
995
+ const validOps = new Set(['exists', 'not_empty', 'equals', 'not_equals', 'in', 'not_in', 'contains', 'truthy', 'falsy']);
996
+ if (typeof op !== 'string' || !validOps.has(op)) {
997
+ errors.push(`${path}.visibleWhen.op must be exists, not_empty, equals, not_equals, in, not_in, contains, truthy, or falsy`);
998
+ return;
999
+ }
1000
+ if ((op === 'equals' || op === 'not_equals' || op === 'contains') && !Object.prototype.hasOwnProperty.call(value, 'value')) {
1001
+ errors.push(`${path}.visibleWhen.value is required for ${op}`);
1002
+ }
1003
+ if (op === 'in' || op === 'not_in') {
1004
+ if (!Array.isArray(value.values)) {
1005
+ errors.push(`${path}.visibleWhen.values must be an array for ${op}`);
1006
+ }
1007
+ else if (value.values.length > 50) {
1008
+ errors.push(`${path}.visibleWhen.values exceeds max 50`);
1009
+ }
1010
+ }
1011
+ }
1012
+ export function validatePanelLevelNode(node, options) {
1013
+ const { path, depth, errors, slotName, actionIds = new Set(), fieldNames = new Set() } = options;
1014
+ if (depth > MAX_PANEL_LEVEL_DEPTH) {
1015
+ errors.push(`${path} exceeds max depth ${MAX_PANEL_LEVEL_DEPTH}`);
1016
+ return;
1017
+ }
1018
+ if (!isRecord(node)) {
1019
+ errors.push(`${path} must be an object`);
1020
+ return;
1021
+ }
1022
+ const type = node.type;
1023
+ if (typeof type !== 'string') {
1024
+ errors.push(`${path}.type is required`);
1025
+ return;
1026
+ }
1027
+ if (type === 'FormField') {
1028
+ if (slotName === 'parameterForm') {
1029
+ errors.push('Parameter form slot may not contain row-context field binding: FormField');
1030
+ }
1031
+ else {
1032
+ errors.push(`Panel-level slot ${slotName} may not contain row-context node: ${type}`);
1033
+ }
1034
+ return;
1035
+ }
1036
+ if (type === 'Sparkline') {
1037
+ errors.push(`Panel-level slot ${slotName} may not contain row-level node: Sparkline`);
1038
+ return;
1039
+ }
1040
+ if (isRowContextNodeType(type)) {
1041
+ errors.push(`Panel-level slot ${slotName} may not contain row-context node: ${type}`);
1042
+ return;
1043
+ }
1044
+ switch (type) {
1045
+ case 'Section':
1046
+ case 'ParameterFormSection':
1047
+ case 'Card': {
1048
+ hasOnlyKeys(node, type === 'Card' ? ['type', 'title', 'children'] : ['type', 'children'], path, errors);
1049
+ if (type === 'Card' && node.title !== undefined && typeof node.title !== 'string') {
1050
+ errors.push(`${path}.title must be a string`);
1051
+ }
1052
+ if (!Array.isArray(node.children)) {
1053
+ errors.push(`${path}.children must be an array`);
1054
+ return;
1055
+ }
1056
+ if (node.children.length > MAX_PANEL_LEVEL_CHILDREN) {
1057
+ errors.push(`${path}.children exceeds maximum children per container (${MAX_PANEL_LEVEL_CHILDREN})`);
1058
+ }
1059
+ node.children.forEach((child, index) => validatePanelLevelNode(child, {
1060
+ path: `${path}.children[${index}]`,
1061
+ depth: depth + 1,
1062
+ errors,
1063
+ slotName,
1064
+ actionIds,
1065
+ fieldNames,
1066
+ }));
1067
+ break;
1068
+ }
1069
+ case 'Stack':
1070
+ case 'Inline':
1071
+ case 'Grid': {
1072
+ const allowedKeys = type === 'Stack'
1073
+ ? ['type', 'children', 'direction', 'gap', 'align', 'wrap']
1074
+ : type === 'Inline'
1075
+ ? ['type', 'children', 'gap', 'align', 'wrap']
1076
+ : ['type', 'children', 'minColumnWidth', 'maxColumns', 'gap'];
1077
+ hasOnlyKeys(node, allowedKeys, path, errors);
1078
+ if (type === 'Stack' && node.direction !== undefined && node.direction !== 'vertical' && node.direction !== 'horizontal') {
1079
+ errors.push(`${path}.direction must be vertical or horizontal`);
1080
+ }
1081
+ if (type === 'Stack' || type === 'Inline') {
1082
+ validateLayoutGap(node.gap, `${path}.gap`, errors);
1083
+ validateLayoutAlign(node.align, `${path}.align`, errors, type === 'Stack');
1084
+ if (node.wrap !== undefined && typeof node.wrap !== 'boolean') {
1085
+ errors.push(`${path}.wrap must be a boolean`);
1086
+ }
1087
+ }
1088
+ else {
1089
+ validateLayoutGap(node.gap, `${path}.gap`, errors, false);
1090
+ if (node.minColumnWidth !== undefined && (typeof node.minColumnWidth !== 'number'
1091
+ || !Number.isInteger(node.minColumnWidth)
1092
+ || node.minColumnWidth < 80
1093
+ || node.minColumnWidth > 640)) {
1094
+ errors.push(`${path}.minColumnWidth must be an integer from 80 to 640`);
1095
+ }
1096
+ if (node.maxColumns !== undefined && (typeof node.maxColumns !== 'number'
1097
+ || !Number.isInteger(node.maxColumns)
1098
+ || node.maxColumns < 1
1099
+ || node.maxColumns > 12)) {
1100
+ errors.push(`${path}.maxColumns must be an integer from 1 to 12`);
1101
+ }
1102
+ }
1103
+ if (!Array.isArray(node.children)) {
1104
+ errors.push(`${path}.children must be an array`);
1105
+ return;
1106
+ }
1107
+ if (node.children.length > MAX_PANEL_LEVEL_CHILDREN) {
1108
+ errors.push(`${path}.children exceeds maximum children per container (${MAX_PANEL_LEVEL_CHILDREN})`);
1109
+ }
1110
+ node.children.forEach((child, index) => validatePanelLevelNode(child, {
1111
+ path: `${path}.children[${index}]`,
1112
+ depth: depth + 1,
1113
+ errors,
1114
+ slotName,
1115
+ actionIds,
1116
+ fieldNames,
1117
+ }));
1118
+ break;
1119
+ }
1120
+ case 'Columns': {
1121
+ hasOnlyKeys(node, ['type', 'columns'], path, errors);
1122
+ if (!Array.isArray(node.columns)) {
1123
+ errors.push(`${path}.columns must be an array`);
1124
+ break;
1125
+ }
1126
+ if (node.columns.length > MAX_PANEL_LEVEL_CHILDREN) {
1127
+ errors.push(`${path}.columns exceeds maximum children per container (${MAX_PANEL_LEVEL_CHILDREN})`);
1128
+ }
1129
+ node.columns.forEach((col, index) => {
1130
+ const colPath = `${path}.columns[${index}]`;
1131
+ if (!isRecord(col)) {
1132
+ errors.push(`${colPath} must be an object`);
1133
+ return;
1134
+ }
1135
+ hasOnlyKeys(col, ['width', 'children'], colPath, errors);
1136
+ if (col.width !== undefined && typeof col.width !== 'string') {
1137
+ errors.push(`${colPath}.width must be a string`);
1138
+ }
1139
+ if (!Array.isArray(col.children)) {
1140
+ errors.push(`${colPath}.children must be an array`);
1141
+ return;
1142
+ }
1143
+ if (col.children.length > MAX_PANEL_LEVEL_CHILDREN) {
1144
+ errors.push(`${colPath}.children exceeds maximum children per container (${MAX_PANEL_LEVEL_CHILDREN})`);
1145
+ }
1146
+ col.children.forEach((child, childIndex) => validatePanelLevelNode(child, {
1147
+ path: `${colPath}.children[${childIndex}]`,
1148
+ depth: depth + 1,
1149
+ errors,
1150
+ slotName,
1151
+ actionIds,
1152
+ fieldNames,
1153
+ }));
1154
+ });
1155
+ break;
1156
+ }
1157
+ case 'KeyValueList':
1158
+ validateKeyValueListNode(node, { path, errors, fieldNames, template: false });
1159
+ break;
1160
+ case 'MetricCard':
1161
+ case 'BarChart':
1162
+ case 'LineChart':
1163
+ case 'PieChart':
1164
+ case 'Histogram':
1165
+ hasOnlyKeys(node, ['type', 'props'], path, errors);
1166
+ break;
1167
+ case 'ProgressBar':
1168
+ case 'StatusBadge':
1169
+ case 'Timeline':
1170
+ hasOnlyKeys(node, ['type', 'source', 'label'], path, errors);
1171
+ if (node.source !== undefined && typeof node.source !== 'string') {
1172
+ errors.push(`${path}.source must be a string`);
1173
+ }
1174
+ if (node.label !== undefined && typeof node.label !== 'string') {
1175
+ errors.push(`${path}.label must be a string`);
1176
+ }
1177
+ break;
1178
+ case 'LogBlock':
1179
+ hasOnlyKeys(node, ['type', 'source', 'title', 'maxLines', 'emptyText'], path, errors);
1180
+ if (node.source !== undefined && typeof node.source !== 'string') {
1181
+ errors.push(`${path}.source must be a string`);
1182
+ }
1183
+ if (node.title !== undefined && typeof node.title !== 'string') {
1184
+ errors.push(`${path}.title must be a string`);
1185
+ }
1186
+ if (node.maxLines !== undefined && (typeof node.maxLines !== 'number'
1187
+ || !Number.isInteger(node.maxLines)
1188
+ || node.maxLines < 1
1189
+ || node.maxLines > 500)) {
1190
+ errors.push(`${path}.maxLines must be an integer from 1 to 500`);
1191
+ }
1192
+ if (node.emptyText !== undefined && typeof node.emptyText !== 'string') {
1193
+ errors.push(`${path}.emptyText must be a string`);
1194
+ }
1195
+ break;
1196
+ case 'RunStatusCard':
1197
+ hasOnlyKeys(node, ['type', 'run', 'showTarget', 'showRevision'], path, errors);
1198
+ validateToolRunSelector(node.run, `${path}.run`, errors);
1199
+ if (node.showTarget !== undefined && typeof node.showTarget !== 'boolean') {
1200
+ errors.push(`${path}.showTarget must be a boolean`);
1201
+ }
1202
+ if (node.showRevision !== undefined && typeof node.showRevision !== 'boolean') {
1203
+ errors.push(`${path}.showRevision must be a boolean`);
1204
+ }
1205
+ break;
1206
+ case 'RunLogViewer':
1207
+ hasOnlyKeys(node, ['type', 'run', 'maxLines', 'levels', 'emptyText'], path, errors);
1208
+ validateToolRunSelector(node.run, `${path}.run`, errors);
1209
+ if (node.maxLines !== undefined && (typeof node.maxLines !== 'number'
1210
+ || !Number.isInteger(node.maxLines)
1211
+ || node.maxLines < 1
1212
+ || node.maxLines > 500)) {
1213
+ errors.push(`${path}.maxLines must be an integer from 1 to 500`);
1214
+ }
1215
+ validateStringArrayOption(node.levels, `${path}.levels`, errors);
1216
+ if (node.emptyText !== undefined && typeof node.emptyText !== 'string') {
1217
+ errors.push(`${path}.emptyText must be a string`);
1218
+ }
1219
+ break;
1220
+ case 'ArtifactList':
1221
+ hasOnlyKeys(node, ['type', 'run', 'kinds', 'emptyText'], path, errors);
1222
+ validateToolRunSelector(node.run, `${path}.run`, errors);
1223
+ validateStringArrayOption(node.kinds, `${path}.kinds`, errors);
1224
+ if (node.emptyText !== undefined && typeof node.emptyText !== 'string') {
1225
+ errors.push(`${path}.emptyText must be a string`);
1226
+ }
1227
+ break;
1228
+ case 'AggregateValue': {
1229
+ hasOnlyKeys(node, ['type', 'op', 'scope', 'field', 'label', 'precision', 'prefix', 'suffix', 'fallback'], path, errors);
1230
+ const validOps = new Set(['count', 'sum', 'avg', 'min', 'max']);
1231
+ if (typeof node.op !== 'string' || !validOps.has(node.op)) {
1232
+ errors.push(`${path}.op must be count, sum, avg, min, or max`);
1233
+ }
1234
+ if (node.scope !== undefined && node.scope !== 'page' && node.scope !== 'dataset') {
1235
+ errors.push(`${path}.scope must be page or dataset`);
1236
+ }
1237
+ if (node.field !== undefined && (typeof node.field !== 'string' || !fieldNames.has(node.field))) {
1238
+ errors.push(`${path}.field references an unknown field`);
1239
+ }
1240
+ if (node.op !== 'count' && typeof node.field !== 'string') {
1241
+ errors.push(`${path}.field is required for ${node.op}`);
1242
+ }
1243
+ if (node.label !== undefined && typeof node.label !== 'string') {
1244
+ errors.push(`${path}.label must be a string`);
1245
+ }
1246
+ if (node.precision !== undefined && (typeof node.precision !== 'number'
1247
+ || !Number.isInteger(node.precision)
1248
+ || node.precision < 0
1249
+ || node.precision > 6)) {
1250
+ errors.push(`${path}.precision must be an integer from 0 to 6`);
1251
+ }
1252
+ if (node.prefix !== undefined && typeof node.prefix !== 'string') {
1253
+ errors.push(`${path}.prefix must be a string`);
1254
+ }
1255
+ if (node.suffix !== undefined && typeof node.suffix !== 'string') {
1256
+ errors.push(`${path}.suffix must be a string`);
1257
+ }
1258
+ if (node.fallback !== undefined && typeof node.fallback !== 'string') {
1259
+ errors.push(`${path}.fallback must be a string`);
1260
+ }
1261
+ break;
1262
+ }
1263
+ case 'TextInput':
1264
+ case 'TextArea':
1265
+ case 'NumberInput':
1266
+ case 'Checkbox':
1267
+ case 'Select':
1268
+ case 'MultiSelect':
1269
+ case 'TagInput':
1270
+ case 'Slider':
1271
+ case 'DatePicker':
1272
+ case 'FileUpload':
1273
+ hasOnlyKeys(node, ['type', 'props'], path, errors);
1274
+ break;
1275
+ case 'ActionBar':
1276
+ hasOnlyKeys(node, ['type', 'actionIds', 'layout', 'title', 'showDescriptions', 'showHelp'], path, errors);
1277
+ if (node.actionIds !== undefined) {
1278
+ if (!Array.isArray(node.actionIds)) {
1279
+ errors.push(`${path}.actionIds must be an array`);
1280
+ }
1281
+ else {
1282
+ node.actionIds.forEach((actionId, index) => {
1283
+ if (typeof actionId !== 'string' || !actionIds.has(actionId)) {
1284
+ errors.push(`${path}.actionIds[${index}] references an unknown action id`);
1285
+ }
1286
+ });
1287
+ }
1288
+ }
1289
+ if (node.layout !== undefined && node.layout !== 'wrap' && node.layout !== 'stack') {
1290
+ errors.push(`${path}.layout must be wrap or stack`);
1291
+ }
1292
+ if (node.title !== undefined && typeof node.title !== 'string') {
1293
+ errors.push(`${path}.title must be a string`);
1294
+ }
1295
+ if (node.showDescriptions !== undefined && typeof node.showDescriptions !== 'boolean') {
1296
+ errors.push(`${path}.showDescriptions must be a boolean`);
1297
+ }
1298
+ if (node.showHelp !== undefined && typeof node.showHelp !== 'boolean') {
1299
+ errors.push(`${path}.showHelp must be a boolean`);
1300
+ }
1301
+ break;
1302
+ case 'Button':
1303
+ hasOnlyKeys(node, ['type', 'props'], path, errors);
1304
+ break;
1305
+ default:
1306
+ errors.push(`Unknown panel-level node type: ${type}`);
1307
+ break;
1308
+ }
1309
+ }
1310
+ function validateTemplateNode(node, options) {
1311
+ const { fieldNames, fieldTypes, mediaSlotNames, mediaSlotDisplayTypes, actionIds, path, depth, errors } = options;
1312
+ if (depth > MAX_TEMPLATE_DEPTH) {
1313
+ errors.push(`${path} exceeds max depth ${MAX_TEMPLATE_DEPTH}`);
1314
+ return;
1315
+ }
1316
+ if (!isRecord(node)) {
1317
+ errors.push(`${path} must be an object`);
1318
+ return;
1319
+ }
1320
+ const type = node.type;
1321
+ if (typeof type !== 'string') {
1322
+ errors.push(`${path}.type is required`);
1323
+ return;
1324
+ }
1325
+ if (isPanelLevelOnlyType(type)) {
1326
+ errors.push(`Row template may not contain panel-level node: ${type}`);
1327
+ return;
1328
+ }
1329
+ validateVisibilityCondition(node.visibleWhen, { fieldNames, path, errors });
1330
+ const requireStringRef = (key, refs) => {
1331
+ const value = node[key];
1332
+ if (typeof value !== 'string' || !refs.has(value)) {
1333
+ errors.push(`${path}.${key} references an unknown ${key === 'slot' ? 'media slot' : 'field'}`);
1334
+ }
1335
+ };
1336
+ switch (type) {
1337
+ case 'Row':
1338
+ case 'Column': {
1339
+ hasOnlyTemplateKeys(node, ['type', 'children', 'gap', 'align'], path, errors);
1340
+ if (node.gap !== undefined && node.gap !== 'none' && node.gap !== 'sm' && node.gap !== 'md' && node.gap !== 'lg') {
1341
+ errors.push(`${path}.gap must be none, sm, md, or lg`);
1342
+ }
1343
+ if (node.align !== undefined && node.align !== 'start' && node.align !== 'center' && node.align !== 'end' && node.align !== 'stretch') {
1344
+ errors.push(`${path}.align must be start, center, end, or stretch`);
1345
+ }
1346
+ if (!Array.isArray(node.children)) {
1347
+ errors.push(`${path}.children must be an array`);
1348
+ return;
1349
+ }
1350
+ if (node.children.length > MAX_TEMPLATE_CHILDREN) {
1351
+ errors.push(`${path}.children exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1352
+ }
1353
+ node.children.forEach((child, index) => validateTemplateNode(child, {
1354
+ fieldNames,
1355
+ fieldTypes,
1356
+ mediaSlotNames,
1357
+ mediaSlotDisplayTypes,
1358
+ actionIds,
1359
+ path: `${path}.children[${index}]`,
1360
+ depth: depth + 1,
1361
+ errors,
1362
+ }));
1363
+ break;
1364
+ }
1365
+ case 'Card': {
1366
+ hasOnlyTemplateKeys(node, ['type', 'children', 'title', 'titleField'], path, errors);
1367
+ if (node.title !== undefined && typeof node.title !== 'string')
1368
+ errors.push(`${path}.title must be a string`);
1369
+ if (node.titleField !== undefined && (typeof node.titleField !== 'string' || !fieldNames.has(node.titleField))) {
1370
+ errors.push(`${path}.titleField references an unknown field`);
1371
+ }
1372
+ if (!Array.isArray(node.children)) {
1373
+ errors.push(`${path}.children must be an array`);
1374
+ return;
1375
+ }
1376
+ if (node.children.length > MAX_TEMPLATE_CHILDREN) {
1377
+ errors.push(`${path}.children exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1378
+ }
1379
+ node.children.forEach((child, index) => validateTemplateNode(child, {
1380
+ fieldNames,
1381
+ fieldTypes,
1382
+ mediaSlotNames,
1383
+ mediaSlotDisplayTypes,
1384
+ actionIds,
1385
+ path: `${path}.children[${index}]`,
1386
+ depth: depth + 1,
1387
+ errors,
1388
+ }));
1389
+ break;
1390
+ }
1391
+ case 'Tabs': {
1392
+ hasOnlyTemplateKeys(node, ['type', 'items', 'defaultIndex'], path, errors);
1393
+ const defaultIndex = node.defaultIndex;
1394
+ if (defaultIndex !== undefined && (typeof defaultIndex !== 'number' || !Number.isInteger(defaultIndex) || defaultIndex < 0)) {
1395
+ errors.push(`${path}.defaultIndex must be a non-negative integer`);
1396
+ }
1397
+ if (!Array.isArray(node.items)) {
1398
+ errors.push(`${path}.items must be an array`);
1399
+ return;
1400
+ }
1401
+ if (node.items.length > MAX_TEMPLATE_CHILDREN) {
1402
+ errors.push(`${path}.items exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1403
+ }
1404
+ node.items.forEach((item, index) => {
1405
+ const itemPath = `${path}.items[${index}]`;
1406
+ if (!isRecord(item)) {
1407
+ errors.push(`${itemPath} must be an object`);
1408
+ return;
1409
+ }
1410
+ hasOnlyKeys(item, ['label', 'node'], itemPath, errors);
1411
+ if (typeof item.label !== 'string')
1412
+ errors.push(`${itemPath}.label must be a string`);
1413
+ validateTemplateNode(item.node, {
1414
+ fieldNames,
1415
+ fieldTypes,
1416
+ mediaSlotNames,
1417
+ mediaSlotDisplayTypes,
1418
+ actionIds,
1419
+ path: `${itemPath}.node`,
1420
+ depth: depth + 1,
1421
+ errors,
1422
+ });
1423
+ });
1424
+ break;
1425
+ }
1426
+ case 'Stack':
1427
+ case 'Inline':
1428
+ case 'Grid': {
1429
+ const allowedKeys = type === 'Stack'
1430
+ ? ['type', 'children', 'direction', 'gap', 'align', 'wrap']
1431
+ : type === 'Inline'
1432
+ ? ['type', 'children', 'gap', 'align', 'wrap']
1433
+ : ['type', 'children', 'minColumnWidth', 'maxColumns', 'gap'];
1434
+ hasOnlyTemplateKeys(node, allowedKeys, path, errors);
1435
+ if (type === 'Stack' && node.direction !== undefined && node.direction !== 'vertical' && node.direction !== 'horizontal') {
1436
+ errors.push(`${path}.direction must be vertical or horizontal`);
1437
+ }
1438
+ if (type === 'Stack' || type === 'Inline') {
1439
+ validateLayoutGap(node.gap, `${path}.gap`, errors);
1440
+ validateLayoutAlign(node.align, `${path}.align`, errors, type === 'Stack');
1441
+ if (node.wrap !== undefined && typeof node.wrap !== 'boolean') {
1442
+ errors.push(`${path}.wrap must be a boolean`);
1443
+ }
1444
+ }
1445
+ else {
1446
+ validateLayoutGap(node.gap, `${path}.gap`, errors, false);
1447
+ if (node.minColumnWidth !== undefined && (typeof node.minColumnWidth !== 'number'
1448
+ || !Number.isInteger(node.minColumnWidth)
1449
+ || node.minColumnWidth < 80
1450
+ || node.minColumnWidth > 640)) {
1451
+ errors.push(`${path}.minColumnWidth must be an integer from 80 to 640`);
1452
+ }
1453
+ if (node.maxColumns !== undefined && (typeof node.maxColumns !== 'number'
1454
+ || !Number.isInteger(node.maxColumns)
1455
+ || node.maxColumns < 1
1456
+ || node.maxColumns > 12)) {
1457
+ errors.push(`${path}.maxColumns must be an integer from 1 to 12`);
1458
+ }
1459
+ }
1460
+ if (!Array.isArray(node.children)) {
1461
+ errors.push(`${path}.children must be an array`);
1462
+ return;
1463
+ }
1464
+ if (node.children.length > MAX_TEMPLATE_CHILDREN) {
1465
+ errors.push(`${path}.children exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1466
+ }
1467
+ node.children.forEach((child, index) => validateTemplateNode(child, {
1468
+ fieldNames,
1469
+ fieldTypes,
1470
+ mediaSlotNames,
1471
+ mediaSlotDisplayTypes,
1472
+ actionIds,
1473
+ path: `${path}.children[${index}]`,
1474
+ depth: depth + 1,
1475
+ errors,
1476
+ }));
1477
+ break;
1478
+ }
1479
+ case 'KeyValueList':
1480
+ validateKeyValueListNode(node, { path, errors, fieldNames, template: true });
1481
+ break;
1482
+ case 'TextField':
1483
+ hasOnlyTemplateKeys(node, ['type', 'field', 'label', 'fallback'], path, errors);
1484
+ requireStringRef('field', fieldNames);
1485
+ if (node.label !== undefined && typeof node.label !== 'string')
1486
+ errors.push(`${path}.label must be a string`);
1487
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1488
+ errors.push(`${path}.fallback must be a string`);
1489
+ break;
1490
+ case 'LatexBlock':
1491
+ hasOnlyTemplateKeys(node, ['type', 'field', 'fallback'], path, errors);
1492
+ requireStringRef('field', fieldNames);
1493
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1494
+ errors.push(`${path}.fallback must be a string`);
1495
+ break;
1496
+ case 'MarkdownField':
1497
+ hasOnlyTemplateKeys(node, ['type', 'field', 'fallback'], path, errors);
1498
+ requireStringRef('field', fieldNames);
1499
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1500
+ errors.push(`${path}.fallback must be a string`);
1501
+ break;
1502
+ case 'CodeField':
1503
+ hasOnlyTemplateKeys(node, ['type', 'field', 'language', 'languageField', 'fallback'], path, errors);
1504
+ requireStringRef('field', fieldNames);
1505
+ if (node.language !== undefined && typeof node.language !== 'string')
1506
+ errors.push(`${path}.language must be a string`);
1507
+ if (node.languageField !== undefined && (typeof node.languageField !== 'string' || !fieldNames.has(node.languageField))) {
1508
+ errors.push(`${path}.languageField references an unknown field`);
1509
+ }
1510
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1511
+ errors.push(`${path}.fallback must be a string`);
1512
+ break;
1513
+ case 'JsonField':
1514
+ hasOnlyTemplateKeys(node, ['type', 'field', 'expanded', 'fallback'], path, errors);
1515
+ requireStringRef('field', fieldNames);
1516
+ if (node.expanded !== undefined && typeof node.expanded !== 'boolean')
1517
+ errors.push(`${path}.expanded must be a boolean`);
1518
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1519
+ errors.push(`${path}.fallback must be a string`);
1520
+ break;
1521
+ case 'DiffField':
1522
+ hasOnlyTemplateKeys(node, ['type', 'leftField', 'rightField', 'mode', 'title'], path, errors);
1523
+ if (typeof node.leftField !== 'string' || !fieldNames.has(node.leftField)) {
1524
+ errors.push(`${path}.leftField references an unknown field`);
1525
+ }
1526
+ if (typeof node.rightField !== 'string' || !fieldNames.has(node.rightField)) {
1527
+ errors.push(`${path}.rightField references an unknown field`);
1528
+ }
1529
+ if (node.mode !== undefined && node.mode !== 'line' && node.mode !== 'char') {
1530
+ errors.push(`${path}.mode must be line or char`);
1531
+ }
1532
+ if (node.title !== undefined && typeof node.title !== 'string')
1533
+ errors.push(`${path}.title must be a string`);
1534
+ break;
1535
+ case 'Badge':
1536
+ hasOnlyTemplateKeys(node, ['type', 'field', 'label', 'variant', 'valueMap', 'fallback'], path, errors);
1537
+ requireStringRef('field', fieldNames);
1538
+ if (node.label !== undefined && typeof node.label !== 'string')
1539
+ errors.push(`${path}.label must be a string`);
1540
+ if (node.variant !== undefined && !isValidBadgeVariant(node.variant)) {
1541
+ errors.push(`${path}.variant must be default, success, warning, or danger`);
1542
+ }
1543
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1544
+ errors.push(`${path}.fallback must be a string`);
1545
+ if (node.valueMap !== undefined) {
1546
+ if (!Array.isArray(node.valueMap)) {
1547
+ errors.push(`${path}.valueMap must be an array`);
1548
+ }
1549
+ else if (node.valueMap.length > 50) {
1550
+ errors.push(`${path}.valueMap exceeds max 50`);
1551
+ }
1552
+ else {
1553
+ node.valueMap.forEach((entry, index) => {
1554
+ const entryPath = `${path}.valueMap[${index}]`;
1555
+ if (!isRecord(entry)) {
1556
+ errors.push(`${entryPath} must be an object`);
1557
+ return;
1558
+ }
1559
+ hasOnlyKeys(entry, ['value', 'label', 'variant'], entryPath, errors);
1560
+ if (!Object.prototype.hasOwnProperty.call(entry, 'value')
1561
+ || !isPrimitiveMapValue(entry.value)
1562
+ || (typeof entry.value === 'number' && !Number.isFinite(entry.value))) {
1563
+ errors.push(`${entryPath}.value must be a string, finite number, boolean, or null`);
1564
+ }
1565
+ if (entry.label !== undefined && typeof entry.label !== 'string')
1566
+ errors.push(`${entryPath}.label must be a string`);
1567
+ if (entry.variant !== undefined && !isValidBadgeVariant(entry.variant)) {
1568
+ errors.push(`${entryPath}.variant must be default, success, warning, or danger`);
1569
+ }
1570
+ });
1571
+ }
1572
+ }
1573
+ break;
1574
+ case 'ComputedValue': {
1575
+ hasOnlyTemplateKeys(node, ['type', 'op', 'fields', 'weights', 'label', 'precision', 'prefix', 'suffix', 'separator', 'fallback'], path, errors);
1576
+ const op = node.op;
1577
+ const validOps = new Set(['sum', 'difference', 'product', 'ratio', 'percent', 'average', 'weighted_sum', 'concat']);
1578
+ if (typeof op !== 'string' || !validOps.has(op)) {
1579
+ errors.push(`${path}.op must be sum, difference, product, ratio, percent, average, weighted_sum, or concat`);
1580
+ }
1581
+ if (!Array.isArray(node.fields)) {
1582
+ errors.push(`${path}.fields must be an array`);
1583
+ break;
1584
+ }
1585
+ if (node.fields.length === 0 || node.fields.length > 8) {
1586
+ errors.push(`${path}.fields must contain 1-8 fields`);
1587
+ }
1588
+ if ((op === 'difference' || op === 'ratio' || op === 'percent') && node.fields.length !== 2) {
1589
+ errors.push(`${path}.fields must contain exactly 2 fields for ${op}`);
1590
+ }
1591
+ if (op === 'weighted_sum') {
1592
+ if (!Array.isArray(node.weights)) {
1593
+ errors.push(`${path}.weights must be an array for weighted_sum`);
1594
+ }
1595
+ else {
1596
+ if (node.weights.length !== node.fields.length) {
1597
+ errors.push(`${path}.weights must have the same length as fields for weighted_sum`);
1598
+ }
1599
+ node.weights.forEach((weight, index) => {
1600
+ if (typeof weight !== 'number' || !Number.isFinite(weight)) {
1601
+ errors.push(`${path}.weights[${index}] must be a finite number`);
1602
+ }
1603
+ });
1604
+ }
1605
+ }
1606
+ else if (node.weights !== undefined) {
1607
+ errors.push(`${path}.weights is only allowed for weighted_sum`);
1608
+ }
1609
+ node.fields.forEach((field, index) => {
1610
+ if (typeof field !== 'string' || !fieldNames.has(field)) {
1611
+ errors.push(`${path}.fields[${index}] references an unknown field`);
1612
+ return;
1613
+ }
1614
+ if (op !== 'concat' && fieldTypes.get(field) !== 'number') {
1615
+ errors.push(`${path}.fields[${index}] must reference a number field for ${op}`);
1616
+ }
1617
+ });
1618
+ if (node.label !== undefined && typeof node.label !== 'string')
1619
+ errors.push(`${path}.label must be a string`);
1620
+ if (node.precision !== undefined
1621
+ && (typeof node.precision !== 'number' || !Number.isInteger(node.precision) || node.precision < 0 || node.precision > 6)) {
1622
+ errors.push(`${path}.precision must be an integer from 0 to 6`);
1623
+ }
1624
+ if (node.prefix !== undefined && typeof node.prefix !== 'string')
1625
+ errors.push(`${path}.prefix must be a string`);
1626
+ if (node.suffix !== undefined && typeof node.suffix !== 'string')
1627
+ errors.push(`${path}.suffix must be a string`);
1628
+ if (node.separator !== undefined && typeof node.separator !== 'string')
1629
+ errors.push(`${path}.separator must be a string`);
1630
+ if (node.fallback !== undefined && typeof node.fallback !== 'string')
1631
+ errors.push(`${path}.fallback must be a string`);
1632
+ break;
1633
+ }
1634
+ case 'ImageSlot':
1635
+ hasOnlyTemplateKeys(node, ['type', 'slot', 'labelField', 'annotationsField', 'editable', 'newAnnotationLabel', 'shortcutLabels'], path, errors);
1636
+ requireStringRef('slot', mediaSlotNames);
1637
+ if (node.labelField !== undefined && (typeof node.labelField !== 'string' || !fieldNames.has(node.labelField))) {
1638
+ errors.push(`${path}.labelField references an unknown field`);
1639
+ }
1640
+ if (node.annotationsField !== undefined && (typeof node.annotationsField !== 'string' || !fieldNames.has(node.annotationsField))) {
1641
+ errors.push(`${path}.annotationsField references an unknown field`);
1642
+ }
1643
+ if (node.editable !== undefined && typeof node.editable !== 'boolean') {
1644
+ errors.push(`${path}.editable must be a boolean`);
1645
+ }
1646
+ if (node.editable === true && typeof node.annotationsField !== 'string') {
1647
+ errors.push(`${path}.editable requires annotationsField`);
1648
+ }
1649
+ if (node.newAnnotationLabel !== undefined && typeof node.newAnnotationLabel !== 'string') {
1650
+ errors.push(`${path}.newAnnotationLabel must be a string`);
1651
+ }
1652
+ if (node.shortcutLabels !== undefined) {
1653
+ if (!Array.isArray(node.shortcutLabels)) {
1654
+ errors.push(`${path}.shortcutLabels must be an array`);
1655
+ }
1656
+ else {
1657
+ if (node.shortcutLabels.length > 20) {
1658
+ errors.push(`${path}.shortcutLabels exceeds max 20`);
1659
+ }
1660
+ node.shortcutLabels.forEach((shortcut, index) => {
1661
+ const shortcutPath = `${path}.shortcutLabels[${index}]`;
1662
+ if (!isRecord(shortcut)) {
1663
+ errors.push(`${shortcutPath} must be an object`);
1664
+ return;
1665
+ }
1666
+ hasOnlyKeys(shortcut, ['key', 'label', 'color'], shortcutPath, errors);
1667
+ if (typeof shortcut.key !== 'string' || shortcut.key.trim().length === 0 || shortcut.key.length > 10) {
1668
+ errors.push(`${shortcutPath}.key must be a non-empty string up to 10 characters`);
1669
+ }
1670
+ if (typeof shortcut.label !== 'string' || shortcut.label.trim().length === 0) {
1671
+ errors.push(`${shortcutPath}.label must be a non-empty string`);
1672
+ }
1673
+ if (shortcut.color !== undefined && typeof shortcut.color !== 'string') {
1674
+ errors.push(`${shortcutPath}.color must be a string`);
1675
+ }
1676
+ });
1677
+ }
1678
+ }
1679
+ break;
1680
+ case 'MediaBlock':
1681
+ hasOnlyTemplateKeys(node, ['type', 'slot', 'labelField', 'displayType', 'maxLines', 'annotationsField', 'editable', 'newAnnotationLabel', 'shortcutLabels'], path, errors);
1682
+ requireStringRef('slot', mediaSlotNames);
1683
+ if (node.labelField !== undefined && (typeof node.labelField !== 'string' || !fieldNames.has(node.labelField))) {
1684
+ errors.push(`${path}.labelField references an unknown field`);
1685
+ }
1686
+ if (node.displayType !== undefined && node.displayType !== 'img' && node.displayType !== 'text' && node.displayType !== 'latex') {
1687
+ errors.push(`${path}.displayType must be img, text, or latex`);
1688
+ }
1689
+ if (typeof node.slot === 'string' && mediaSlotDisplayTypes.has(node.slot) && node.displayType !== undefined && mediaSlotDisplayTypes.get(node.slot) !== node.displayType) {
1690
+ errors.push(`${path}.displayType must match declared media slot displayType`);
1691
+ }
1692
+ const effectiveMediaDisplayType = node.displayType ?? (typeof node.slot === 'string' ? mediaSlotDisplayTypes.get(node.slot) : undefined);
1693
+ if (effectiveMediaDisplayType === 'text' || effectiveMediaDisplayType === 'latex') {
1694
+ for (const imageOnlyKey of ['annotationsField', 'editable', 'newAnnotationLabel', 'shortcutLabels']) {
1695
+ if (node[imageOnlyKey] !== undefined) {
1696
+ errors.push(`${path}.${imageOnlyKey} is only allowed when displayType is img`);
1697
+ }
1698
+ }
1699
+ }
1700
+ if (node.maxLines !== undefined && (typeof node.maxLines !== 'number'
1701
+ || !Number.isInteger(node.maxLines)
1702
+ || node.maxLines < 1
1703
+ || node.maxLines > 500)) {
1704
+ errors.push(`${path}.maxLines must be an integer from 1 to 500`);
1705
+ }
1706
+ if (node.annotationsField !== undefined && (typeof node.annotationsField !== 'string' || !fieldNames.has(node.annotationsField))) {
1707
+ errors.push(`${path}.annotationsField references an unknown field`);
1708
+ }
1709
+ if (node.editable !== undefined && typeof node.editable !== 'boolean') {
1710
+ errors.push(`${path}.editable must be a boolean`);
1711
+ }
1712
+ if (node.editable === true && typeof node.annotationsField !== 'string') {
1713
+ errors.push(`${path}.editable requires annotationsField`);
1714
+ }
1715
+ if (node.newAnnotationLabel !== undefined && typeof node.newAnnotationLabel !== 'string') {
1716
+ errors.push(`${path}.newAnnotationLabel must be a string`);
1717
+ }
1718
+ if (node.shortcutLabels !== undefined) {
1719
+ if (!Array.isArray(node.shortcutLabels)) {
1720
+ errors.push(`${path}.shortcutLabels must be an array`);
1721
+ }
1722
+ else {
1723
+ if (node.shortcutLabels.length > 20) {
1724
+ errors.push(`${path}.shortcutLabels exceeds max 20`);
1725
+ }
1726
+ node.shortcutLabels.forEach((shortcut, index) => {
1727
+ const shortcutPath = `${path}.shortcutLabels[${index}]`;
1728
+ if (!isRecord(shortcut)) {
1729
+ errors.push(`${shortcutPath} must be an object`);
1730
+ return;
1731
+ }
1732
+ hasOnlyKeys(shortcut, ['key', 'label', 'color'], shortcutPath, errors);
1733
+ if (typeof shortcut.key !== 'string' || shortcut.key.trim().length === 0 || shortcut.key.length > 10) {
1734
+ errors.push(`${shortcutPath}.key must be a non-empty string up to 10 characters`);
1735
+ }
1736
+ if (typeof shortcut.label !== 'string' || shortcut.label.trim().length === 0) {
1737
+ errors.push(`${shortcutPath}.label must be a non-empty string`);
1738
+ }
1739
+ if (shortcut.color !== undefined && typeof shortcut.color !== 'string') {
1740
+ errors.push(`${shortcutPath}.color must be a string`);
1741
+ }
1742
+ });
1743
+ }
1744
+ }
1745
+ break;
1746
+ case 'Checkbox':
1747
+ hasOnlyTemplateKeys(node, ['type', 'label'], path, errors);
1748
+ if (node.label !== undefined && typeof node.label !== 'string')
1749
+ errors.push(`${path}.label must be a string`);
1750
+ break;
1751
+ case 'FormField':
1752
+ hasOnlyTemplateKeys(node, ['type', 'name', 'label', 'field', 'input', 'placeholder', 'options'], path, errors);
1753
+ if (typeof node.name !== 'string' || !FIELD_NAME_PATTERN.test(node.name)) {
1754
+ errors.push(`${path}.name must be a safe field name`);
1755
+ }
1756
+ if (node.label !== undefined && typeof node.label !== 'string')
1757
+ errors.push(`${path}.label must be a string`);
1758
+ if (node.field !== undefined && (typeof node.field !== 'string' || !fieldNames.has(node.field))) {
1759
+ errors.push(`${path}.field references an unknown field`);
1760
+ }
1761
+ if (node.input !== undefined
1762
+ && node.input !== 'text'
1763
+ && node.input !== 'number'
1764
+ && node.input !== 'checkbox'
1765
+ && node.input !== 'textarea'
1766
+ && node.input !== 'select'
1767
+ && node.input !== 'tags') {
1768
+ errors.push(`${path}.input must be text, number, checkbox, textarea, select, or tags`);
1769
+ }
1770
+ if (typeof node.field === 'string' && fieldNames.has(node.field) && typeof node.input === 'string') {
1771
+ const fieldType = fieldTypes.get(node.field);
1772
+ if (node.input === 'number' && fieldType !== 'number') {
1773
+ errors.push(`${path}.input number requires a number field`);
1774
+ }
1775
+ if (node.input === 'checkbox' && fieldType !== 'boolean') {
1776
+ errors.push(`${path}.input checkbox requires a boolean field`);
1777
+ }
1778
+ if (node.input === 'tags') {
1779
+ errors.push(`${path}.input tags may not bind field because dataset fields are scalar`);
1780
+ }
1781
+ }
1782
+ if (node.placeholder !== undefined && typeof node.placeholder !== 'string')
1783
+ errors.push(`${path}.placeholder must be a string`);
1784
+ if (node.options !== undefined && node.input !== 'select') {
1785
+ errors.push(`${path}.options is only allowed when input is select`);
1786
+ }
1787
+ if (node.input === 'select') {
1788
+ if (!Array.isArray(node.options)) {
1789
+ errors.push(`${path}.options must be a non-empty array for select`);
1790
+ }
1791
+ else if (node.options.length === 0) {
1792
+ errors.push(`${path}.options must include at least one option for select`);
1793
+ }
1794
+ else if (node.options.length > MAX_FORM_FIELD_OPTIONS) {
1795
+ errors.push(`${path}.options exceeds max ${MAX_FORM_FIELD_OPTIONS}`);
1796
+ }
1797
+ else {
1798
+ const fieldType = typeof node.field === 'string' ? fieldTypes.get(node.field) : undefined;
1799
+ node.options.forEach((option, index) => {
1800
+ const optionPath = `${path}.options[${index}]`;
1801
+ if (!isRecord(option)) {
1802
+ errors.push(`${optionPath} must be an object`);
1803
+ return;
1804
+ }
1805
+ hasOnlyKeys(option, ['label', 'value'], optionPath, errors);
1806
+ if (!Object.prototype.hasOwnProperty.call(option, 'value') || !isFormFieldOptionValue(option.value)) {
1807
+ errors.push(`${optionPath}.value must be a string, finite number, or boolean`);
1808
+ }
1809
+ else if (fieldType !== undefined && typeof option.value !== fieldType) {
1810
+ errors.push(`${optionPath}.value must match selected field type ${fieldType}`);
1811
+ }
1812
+ if (option.label !== undefined && typeof option.label !== 'string') {
1813
+ errors.push(`${optionPath}.label must be a string`);
1814
+ }
1815
+ });
1816
+ }
1817
+ }
1818
+ break;
1819
+ case 'Sparkline':
1820
+ hasOnlyTemplateKeys(node, ['type', 'field'], path, errors);
1821
+ if (typeof node.field !== 'string' || !fieldNames.has(node.field)) {
1822
+ errors.push(`${path}.field references an unknown field`);
1823
+ }
1824
+ break;
1825
+ case 'Accordion': {
1826
+ hasOnlyTemplateKeys(node, ['type', 'title', 'children', 'expanded'], path, errors);
1827
+ if (typeof node.title !== 'string') {
1828
+ errors.push(`${path}.title must be a string`);
1829
+ }
1830
+ if (node.expanded !== undefined && typeof node.expanded !== 'boolean') {
1831
+ errors.push(`${path}.expanded must be a boolean`);
1832
+ }
1833
+ if (!Array.isArray(node.children)) {
1834
+ errors.push(`${path}.children must be an array`);
1835
+ break;
1836
+ }
1837
+ if (node.children.length > MAX_TEMPLATE_CHILDREN) {
1838
+ errors.push(`${path}.children exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1839
+ }
1840
+ node.children.forEach((child, index) => validateTemplateNode(child, {
1841
+ fieldNames,
1842
+ fieldTypes,
1843
+ mediaSlotNames,
1844
+ mediaSlotDisplayTypes,
1845
+ actionIds,
1846
+ path: `${path}.children[${index}]`,
1847
+ depth: depth + 1,
1848
+ errors,
1849
+ }));
1850
+ break;
1851
+ }
1852
+ case 'Divider': {
1853
+ hasOnlyTemplateKeys(node, ['type', 'title'], path, errors);
1854
+ if (node.title !== undefined && typeof node.title !== 'string') {
1855
+ errors.push(`${path}.title must be a string`);
1856
+ }
1857
+ break;
1858
+ }
1859
+ case 'Callout': {
1860
+ hasOnlyTemplateKeys(node, ['type', 'calloutType', 'title', 'content'], path, errors);
1861
+ if (!['info', 'warning', 'success', 'error'].includes(node.calloutType)) {
1862
+ errors.push(`${path}.calloutType must be info, warning, success, or error`);
1863
+ }
1864
+ if (node.title !== undefined && typeof node.title !== 'string') {
1865
+ errors.push(`${path}.title must be a string`);
1866
+ }
1867
+ if (node.content !== undefined && typeof node.content !== 'string') {
1868
+ errors.push(`${path}.content must be a string`);
1869
+ }
1870
+ break;
1871
+ }
1872
+ case 'Columns': {
1873
+ hasOnlyTemplateKeys(node, ['type', 'columns'], path, errors);
1874
+ if (!Array.isArray(node.columns)) {
1875
+ errors.push(`${path}.columns must be an array`);
1876
+ break;
1877
+ }
1878
+ if (node.columns.length > MAX_TEMPLATE_CHILDREN) {
1879
+ errors.push(`${path}.columns exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1880
+ }
1881
+ node.columns.forEach((col, index) => {
1882
+ const colPath = `${path}.columns[${index}]`;
1883
+ if (!isRecord(col)) {
1884
+ errors.push(`${colPath} must be an object`);
1885
+ return;
1886
+ }
1887
+ hasOnlyKeys(col, ['width', 'children'], colPath, errors);
1888
+ if (col.width !== undefined && typeof col.width !== 'string') {
1889
+ errors.push(`${colPath}.width must be a string`);
1890
+ }
1891
+ if (!Array.isArray(col.children)) {
1892
+ errors.push(`${colPath}.children must be an array`);
1893
+ return;
1894
+ }
1895
+ if (col.children.length > MAX_TEMPLATE_CHILDREN) {
1896
+ errors.push(`${colPath}.children exceeds max ${MAX_TEMPLATE_CHILDREN}`);
1897
+ }
1898
+ col.children.forEach((child, childIndex) => validateTemplateNode(child, {
1899
+ fieldNames,
1900
+ fieldTypes,
1901
+ mediaSlotNames,
1902
+ mediaSlotDisplayTypes,
1903
+ actionIds,
1904
+ path: `${colPath}.children[${childIndex}]`,
1905
+ depth: depth + 1,
1906
+ errors,
1907
+ }));
1908
+ });
1909
+ break;
1910
+ }
1911
+ case 'ActionButton':
1912
+ hasOnlyTemplateKeys(node, ['type', 'actionId', 'label', 'help', 'showHelp'], path, errors);
1913
+ if (typeof node.actionId !== 'string' || !actionIds.has(node.actionId)) {
1914
+ errors.push(`${path}.actionId references an unknown action id`);
1915
+ }
1916
+ if (node.label !== undefined && typeof node.label !== 'string')
1917
+ errors.push(`${path}.label must be a string`);
1918
+ if (node.help !== undefined && typeof node.help !== 'string')
1919
+ errors.push(`${path}.help must be a string`);
1920
+ if (node.showHelp !== undefined && typeof node.showHelp !== 'boolean')
1921
+ errors.push(`${path}.showHelp must be a boolean`);
1922
+ break;
1923
+ case 'SubmitButton':
1924
+ hasOnlyTemplateKeys(node, ['type', 'submitKind', 'label', 'variant', 'help', 'showHelp'], path, errors);
1925
+ if (node.submitKind !== 'form' && node.submitKind !== 'apply' && node.submitKind !== 'done') {
1926
+ errors.push(`${path}.submitKind must be form, apply, or done`);
1927
+ }
1928
+ if (node.label !== undefined && typeof node.label !== 'string')
1929
+ errors.push(`${path}.label must be a string`);
1930
+ if (node.variant !== undefined && node.variant !== 'primary' && node.variant !== 'secondary' && node.variant !== 'danger') {
1931
+ errors.push(`${path}.variant must be primary, secondary, or danger`);
1932
+ }
1933
+ if (node.help !== undefined && typeof node.help !== 'string')
1934
+ errors.push(`${path}.help must be a string`);
1935
+ if (node.showHelp !== undefined && typeof node.showHelp !== 'boolean')
1936
+ errors.push(`${path}.showHelp must be a boolean`);
1937
+ break;
1938
+ case 'DiffView':
1939
+ hasOnlyTemplateKeys(node, ['type', 'left', 'right', 'mode'], path, errors);
1940
+ if (typeof node.left !== 'string')
1941
+ errors.push(`${path}.left must be a string`);
1942
+ if (typeof node.right !== 'string')
1943
+ errors.push(`${path}.right must be a string`);
1944
+ if (node.mode !== undefined && node.mode !== 'line' && node.mode !== 'char') {
1945
+ errors.push(`${path}.mode must be line or char`);
1946
+ }
1947
+ break;
1948
+ case 'CodeBlock':
1949
+ hasOnlyTemplateKeys(node, ['type', 'code', 'language'], path, errors);
1950
+ if (typeof node.code !== 'string')
1951
+ errors.push(`${path}.code must be a string`);
1952
+ if (node.language !== undefined && typeof node.language !== 'string')
1953
+ errors.push(`${path}.language must be a string`);
1954
+ break;
1955
+ case 'JsonView':
1956
+ hasOnlyTemplateKeys(node, ['type', 'data', 'expanded'], path, errors);
1957
+ if (node.data === undefined)
1958
+ errors.push(`${path}.data is required`);
1959
+ if (node.expanded !== undefined && typeof node.expanded !== 'boolean')
1960
+ errors.push(`${path}.expanded must be a boolean`);
1961
+ break;
1962
+ case 'MarkdownText':
1963
+ hasOnlyTemplateKeys(node, ['type', 'content'], path, errors);
1964
+ if (typeof node.content !== 'string')
1965
+ errors.push(`${path}.content must be a string`);
1966
+ break;
1967
+ default:
1968
+ errors.push(`${path}.type "${type}" is not allowed`);
1969
+ break;
1970
+ }
1971
+ }
1972
+ export function getEffectiveDatasetSchema(componentOrContract, props) {
1973
+ const contract = typeof componentOrContract === 'string'
1974
+ ? getComponentContract(componentOrContract)
1975
+ : componentOrContract;
1976
+ if (!contract)
1977
+ return undefined;
1978
+ if (contract.name !== 'RowTemplateGrid')
1979
+ return contract.datasetSchema;
1980
+ const fieldResult = validateFieldSchemas(props?.fields, 'props.fields');
1981
+ const mediaResult = validateMediaSlotSchemas(props?.mediaSlots, 'props.mediaSlots');
1982
+ if (fieldResult.errors.length > 0 || mediaResult.errors.length > 0) {
1983
+ return { fields: [], mediaSlots: [] };
1984
+ }
1985
+ return {
1986
+ fields: fieldResult.fields,
1987
+ mediaSlots: mediaResult.mediaSlots,
1988
+ };
1989
+ }
1990
+ export function validatePanelTemplateProps(componentOrContract, props, actions = []) {
1991
+ const contract = typeof componentOrContract === 'string'
1992
+ ? getComponentContract(componentOrContract)
1993
+ : componentOrContract;
1994
+ if (!contract) {
1995
+ return { ok: false, errors: ['Unknown component contract'] };
1996
+ }
1997
+ if (contract.name !== 'RowTemplateGrid') {
1998
+ return { ok: true, datasetSchema: contract.datasetSchema ?? { fields: [], mediaSlots: [] } };
1999
+ }
2000
+ const fieldResult = validateFieldSchemas(props.fields, 'props.fields');
2001
+ const mediaResult = validateMediaSlotSchemas(props.mediaSlots, 'props.mediaSlots');
2002
+ const errors = [...fieldResult.errors, ...mediaResult.errors];
2003
+ if (fieldResult.fields.length === 0) {
2004
+ errors.push('props.fields must declare at least one field');
2005
+ }
2006
+ if (props.title !== undefined && typeof props.title !== 'string') {
2007
+ errors.push('props.title must be a string');
2008
+ }
2009
+ const actionIds = new Set();
2010
+ for (const action of actions) {
2011
+ if (typeof action.id === 'string' && action.id.trim())
2012
+ actionIds.add(action.id);
2013
+ }
2014
+ validateTemplateNode(props.template, {
2015
+ fieldNames: new Set(fieldResult.fields.map((field) => field.name)),
2016
+ fieldTypes: new Map(fieldResult.fields.map((field) => [field.name, field.type])),
2017
+ mediaSlotNames: new Set(mediaResult.mediaSlots.map((slot) => slot.name)),
2018
+ mediaSlotDisplayTypes: new Map(mediaResult.mediaSlots.map((slot) => [slot.name, slot.displayType])),
2019
+ actionIds,
2020
+ path: 'props.template',
2021
+ depth: 1,
2022
+ errors,
2023
+ });
2024
+ if (props.summary !== undefined && props.summary !== null) {
2025
+ validatePanelLevelNode(props.summary, {
2026
+ path: 'props.summary',
2027
+ depth: 1,
2028
+ errors,
2029
+ slotName: 'summary',
2030
+ actionIds,
2031
+ fieldNames: new Set(fieldResult.fields.map((field) => field.name)),
2032
+ });
2033
+ }
2034
+ if (props.parameterForm !== undefined && props.parameterForm !== null) {
2035
+ validatePanelLevelNode(props.parameterForm, {
2036
+ path: 'props.parameterForm',
2037
+ depth: 1,
2038
+ errors,
2039
+ slotName: 'parameterForm',
2040
+ actionIds,
2041
+ fieldNames: new Set(fieldResult.fields.map((field) => field.name)),
2042
+ });
2043
+ }
2044
+ if (errors.length > 0) {
2045
+ return { ok: false, errors };
2046
+ }
2047
+ return {
2048
+ ok: true,
2049
+ datasetSchema: {
2050
+ fields: fieldResult.fields,
2051
+ mediaSlots: mediaResult.mediaSlots,
2052
+ },
2053
+ };
2054
+ }
2055
+ const COMPONENT_PROPOSAL_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
2056
+ export function validateComponentProposal(input, existingContractNames) {
2057
+ const errors = [];
2058
+ if (!isRecord(input)) {
2059
+ return { ok: false, errors: [{ field: '', message: 'Proposal must be an object' }] };
2060
+ }
2061
+ const name = input.name;
2062
+ if (typeof name !== 'string' || !COMPONENT_PROPOSAL_NAME_PATTERN.test(name)) {
2063
+ errors.push({ field: 'name', message: 'name must be a PascalCase identifier (1-64 chars, alphanumeric and underscore, starting with a letter)' });
2064
+ }
2065
+ else if (existingContractNames.has(name)) {
2066
+ errors.push({ field: 'name', message: `name "${name}" collides with an existing component contract` });
2067
+ }
2068
+ const displayName = input.displayName;
2069
+ if (typeof displayName !== 'string' || displayName.trim().length === 0) {
2070
+ errors.push({ field: 'displayName', message: 'displayName is required and must be a non-empty string' });
2071
+ }
2072
+ const description = input.description;
2073
+ if (typeof description !== 'string' || description.trim().length === 0) {
2074
+ errors.push({ field: 'description', message: 'description is required and must be a non-empty string' });
2075
+ }
2076
+ const dataModel = input.dataModel;
2077
+ if (!isRecord(dataModel)) {
2078
+ errors.push({ field: 'dataModel', message: 'dataModel is required and must be an object' });
2079
+ }
2080
+ else {
2081
+ const kind = dataModel.kind;
2082
+ if (kind !== 'inline' && kind !== 'paged_rows') {
2083
+ errors.push({ field: 'dataModel.kind', message: 'dataModel.kind must be "inline" or "paged_rows"' });
2084
+ }
2085
+ }
2086
+ if (input.propsSchema === undefined) {
2087
+ errors.push({ field: 'propsSchema', message: 'propsSchema is required and must be an object' });
2088
+ }
2089
+ else if (!isRecord(input.propsSchema)) {
2090
+ errors.push({ field: 'propsSchema', message: 'propsSchema must be an object' });
2091
+ }
2092
+ if (input.datasetSchema === undefined) {
2093
+ errors.push({ field: 'datasetSchema', message: 'datasetSchema is required and must be an object' });
2094
+ }
2095
+ else {
2096
+ const ds = input.datasetSchema;
2097
+ if (!isRecord(ds)) {
2098
+ errors.push({ field: 'datasetSchema', message: 'datasetSchema must be an object' });
2099
+ }
2100
+ else {
2101
+ if (!Array.isArray(ds.fields)) {
2102
+ errors.push({ field: 'datasetSchema.fields', message: 'datasetSchema.fields must be an array' });
2103
+ }
2104
+ if (ds.mediaSlots !== undefined && !Array.isArray(ds.mediaSlots)) {
2105
+ errors.push({ field: 'datasetSchema.mediaSlots', message: 'datasetSchema.mediaSlots must be an array when provided' });
2106
+ }
2107
+ }
2108
+ }
2109
+ if (typeof input.rationale !== 'string' || input.rationale.trim().length === 0) {
2110
+ errors.push({ field: 'rationale', message: 'rationale is required and must be a non-empty string' });
2111
+ }
2112
+ if (errors.length > 0) {
2113
+ return { ok: false, errors };
2114
+ }
2115
+ return { ok: true };
2116
+ }