@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
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { Fragment, useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type Key, type ReactNode } from 'react'
|
|
18
18
|
import { CodeBlock } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
19
|
+
import { CODE_BLOCK_LABELS } from './primitive-labels.ts'
|
|
19
20
|
import { ErrorBoundary } from './ErrorBoundary.tsx'
|
|
20
21
|
import { GenuiBlock } from './GenuiBlock.tsx'
|
|
21
|
-
import {
|
|
22
|
+
import { isRenderableProcess, processGenuiSpec } from './guard.ts'
|
|
22
23
|
import { fenceStateKey } from './interaction-store.ts'
|
|
23
24
|
import { parsePartialGenuiSpec } from './parse-partial.ts'
|
|
24
25
|
import { applyPanelOperation, diagnosePanelBudget, type PanelOperationStatus } from './panel-store.ts'
|
|
25
26
|
import type { GenuiSpec } from './spec.ts'
|
|
26
27
|
import { completeFenceJson, describeJsonFailure, isCompleteJson, repairFenceJson } from '../shared/fence-repair.ts'
|
|
27
|
-
import { validateRenderableChartSemantics } from '../plugin/chart-contract.ts'
|
|
28
28
|
|
|
29
29
|
/** Settled fence source identity (data shape, host-independent). */
|
|
30
30
|
export interface GenuiFenceSource {
|
|
@@ -54,12 +54,22 @@ const FENCE_ERROR_STYLE: CSSProperties = {
|
|
|
54
54
|
whiteSpace: 'pre-wrap',
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
/**
|
|
58
|
-
function
|
|
57
|
+
/** Format chart-specific process errors without maintaining a second validator. */
|
|
58
|
+
function formatChartProcessErrors(errors: string[]): string | null {
|
|
59
|
+
const chartErrors = 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))
|
|
60
|
+
return chartErrors.length === 0 ? null : chartErrors.join(';')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return a semantic/schema diagnostic for parseable raw fence content. */
|
|
64
|
+
function processSemanticFailure(raw: string): string | null {
|
|
59
65
|
const parsed = parsePartialGenuiSpec(raw)
|
|
60
66
|
if (parsed === null) return null
|
|
61
|
-
const
|
|
62
|
-
|
|
67
|
+
const processed = processGenuiSpec(parsed)
|
|
68
|
+
if (isRenderableProcess(processed)) return null
|
|
69
|
+
const chartErrors = formatChartProcessErrors(processed.errors)
|
|
70
|
+
return chartErrors === null
|
|
71
|
+
? `GenUI 字段验证失败:${processed.errors.join(';')}`
|
|
72
|
+
: `chart 字段验证失败:${chartErrors}`
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/**
|
|
@@ -86,21 +96,21 @@ function FenceFallback({ raw, fenceKey }: { raw: string; fenceKey: Key }) {
|
|
|
86
96
|
const node = ref.current
|
|
87
97
|
if (node !== null && node.closest('[data-streaming]') === null) setSettled(true)
|
|
88
98
|
})
|
|
89
|
-
const
|
|
90
|
-
const parseDiagnostic = settled &&
|
|
99
|
+
const processDiagnostic = settled ? processSemanticFailure(raw) : null
|
|
100
|
+
const parseDiagnostic = settled && processDiagnostic === null && raw.trim() !== '' ? describeJsonFailure(raw) : null
|
|
91
101
|
return (
|
|
92
102
|
<div ref={ref}>
|
|
93
|
-
{
|
|
103
|
+
{processDiagnostic !== null && (
|
|
94
104
|
<div style={FENCE_ERROR_STYLE} role="alert">
|
|
95
|
-
⚠️ dsh-ui
|
|
105
|
+
⚠️ dsh-ui {processDiagnostic} —— 围栏保持为代码块;请修正后重发。
|
|
96
106
|
</div>
|
|
97
107
|
)}
|
|
98
|
-
{
|
|
108
|
+
{processDiagnostic === null && parseDiagnostic !== null && (
|
|
99
109
|
<div style={FENCE_ERROR_STYLE} role="alert">
|
|
100
110
|
⚠️ dsh-ui fence JSON 解析失败{parseDiagnostic} —— 围栏保持为代码块;请让模型检查并修复 JSON 后重发。
|
|
101
111
|
</div>
|
|
102
112
|
)}
|
|
103
|
-
<CodeBlock key={fenceKey} code={`${raw}\n`} lang="dsh-ui" />
|
|
113
|
+
<CodeBlock key={fenceKey} {...CODE_BLOCK_LABELS} code={`${raw}\n`} lang="dsh-ui" />
|
|
104
114
|
</div>
|
|
105
115
|
)
|
|
106
116
|
}
|
|
@@ -130,10 +140,11 @@ function FencePanelPublisher({ sessionId, sourceId, order, spec }: {
|
|
|
130
140
|
return null
|
|
131
141
|
}
|
|
132
142
|
|
|
133
|
-
/**
|
|
143
|
+
/** Process one parsed value and return its canonical repaired spec only when the shared pipeline is error-free. */
|
|
134
144
|
function repairRenderableSpec(value: unknown): GenuiSpec | null {
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
const processed = processGenuiSpec(value)
|
|
146
|
+
if (!isRenderableProcess(processed)) return null
|
|
147
|
+
return processed.spec
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
/**
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** GenUI runtime diagnostics for aliases and unknown fields. */
|
|
2
|
+
import { COMPONENT_SCHEMAS, GENUI_SPEC_SCHEMA } from './schema.ts'
|
|
3
|
+
import type { ComponentRecordSchema, ComponentSchema } from './schema.ts'
|
|
4
|
+
|
|
5
|
+
export interface GenuiDiagnostic {
|
|
6
|
+
readonly kind: 'alias' | 'unknown-field'
|
|
7
|
+
readonly path: string
|
|
8
|
+
readonly message: string
|
|
9
|
+
readonly type?: string
|
|
10
|
+
readonly field?: string
|
|
11
|
+
readonly canonical?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
17
|
+
? value as Record<string, unknown>
|
|
18
|
+
: undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isNode(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
const candidate = record(value)
|
|
23
|
+
return candidate !== undefined && typeof candidate.type === 'string'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function visitNativeNodes(value: unknown, path: string, visit: (node: Record<string, unknown>, path: string, schema: ComponentSchema) => void): void {
|
|
27
|
+
if (!isNode(value)) return
|
|
28
|
+
const type = value.type as string
|
|
29
|
+
const definition = COMPONENT_SCHEMAS[type]
|
|
30
|
+
if (definition === undefined) return
|
|
31
|
+
visit(value, path, definition)
|
|
32
|
+
const children = (child: unknown, childPath: string): void => visitNativeNodes(child, childPath, visit)
|
|
33
|
+
if ((type === 'row' || type === 'col' || type === 'grid' || type === 'card') && Array.isArray(value.items)) {
|
|
34
|
+
value.items.forEach((child, index) => children(child, `${path}.items[${index}]`))
|
|
35
|
+
} else if (type === 'list' && Array.isArray(value.items)) {
|
|
36
|
+
value.items.forEach((child, index) => children(child, `${path}.items[${index}]`))
|
|
37
|
+
} else if (type === 'tabs' && Array.isArray(value.tabs)) {
|
|
38
|
+
value.tabs.forEach((tab, index) => {
|
|
39
|
+
const holder = record(tab)
|
|
40
|
+
if (holder === undefined) return
|
|
41
|
+
if (Array.isArray(holder.items)) holder.items.forEach((child, childIndex) => children(child, `${path}.tabs[${index}].items[${childIndex}]`))
|
|
42
|
+
else children(holder.items, `${path}.tabs[${index}].items`)
|
|
43
|
+
})
|
|
44
|
+
} else if (type === 'accordion' && Array.isArray(value.items)) {
|
|
45
|
+
value.items.forEach((item, index) => {
|
|
46
|
+
const holder = record(item)
|
|
47
|
+
if (holder?.items !== undefined && Array.isArray(holder.items)) holder.items.forEach((child, childIndex) => children(child, `${path}.items[${index}].items[${childIndex}]`))
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pushUnknownField(
|
|
53
|
+
warnings: GenuiDiagnostic[],
|
|
54
|
+
path: string,
|
|
55
|
+
field: string,
|
|
56
|
+
type: string,
|
|
57
|
+
): void {
|
|
58
|
+
warnings.push({
|
|
59
|
+
kind: 'unknown-field',
|
|
60
|
+
path: `${path}.${field}`,
|
|
61
|
+
message: `${path}.${field}: unknown field for '${type}'`,
|
|
62
|
+
type,
|
|
63
|
+
field,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function diagnoseRecordFields(
|
|
68
|
+
value: unknown,
|
|
69
|
+
path: string,
|
|
70
|
+
definition: ComponentRecordSchema,
|
|
71
|
+
type: string,
|
|
72
|
+
warnings: GenuiDiagnostic[],
|
|
73
|
+
): void {
|
|
74
|
+
const holder = record(value)
|
|
75
|
+
if (holder === undefined) return
|
|
76
|
+
for (const field of Object.keys(holder)) {
|
|
77
|
+
if (field in definition.fields) continue
|
|
78
|
+
pushUnknownField(warnings, path, field, type)
|
|
79
|
+
}
|
|
80
|
+
for (const [field, nested] of Object.entries(definition.nested)) {
|
|
81
|
+
const nestedValue = holder[field]
|
|
82
|
+
if (Array.isArray(nestedValue)) {
|
|
83
|
+
nestedValue.forEach((item, index) => diagnoseRecordFields(item, `${path}.${field}[${index}]`, nested, type, warnings))
|
|
84
|
+
} else if (nestedValue !== undefined) {
|
|
85
|
+
diagnoseRecordFields(nestedValue, `${path}.${field}`, nested, type, warnings)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function diagnoseNestedFields(
|
|
91
|
+
node: Record<string, unknown>,
|
|
92
|
+
path: string,
|
|
93
|
+
definition: ComponentSchema,
|
|
94
|
+
warnings: GenuiDiagnostic[],
|
|
95
|
+
): void {
|
|
96
|
+
for (const [field, nested] of Object.entries(definition.nested)) {
|
|
97
|
+
const nestedValue = node[field]
|
|
98
|
+
if (Array.isArray(nestedValue)) {
|
|
99
|
+
nestedValue.forEach((item, index) => diagnoseRecordFields(item, `${path}.${field}[${index}]`, nested, node.type as string, warnings))
|
|
100
|
+
} else if (nestedValue !== undefined) {
|
|
101
|
+
diagnoseRecordFields(nestedValue, `${path}.${field}`, nested, node.type as string, warnings)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Diagnose unknown direct fields on native nodes.
|
|
108
|
+
*
|
|
109
|
+
* Unknown types are intentionally skipped so custom renderers retain their
|
|
110
|
+
* opaque extension payloads. Native unknown fields are warnings, not errors.
|
|
111
|
+
*
|
|
112
|
+
* @param value - Canonical or raw GenUI value.
|
|
113
|
+
* @returns Stable field diagnostics in tree order.
|
|
114
|
+
*/
|
|
115
|
+
export function diagnoseUnknownGenuiFields(value: unknown): GenuiDiagnostic[] {
|
|
116
|
+
const warnings: GenuiDiagnostic[] = []
|
|
117
|
+
const root = record(value)
|
|
118
|
+
if (root === undefined) return warnings
|
|
119
|
+
const visit = (node: Record<string, unknown>, path: string, definition: ComponentSchema): void => {
|
|
120
|
+
for (const field of Object.keys(node)) {
|
|
121
|
+
if (field === 'type' || field in definition.fields) continue
|
|
122
|
+
pushUnknownField(warnings, path, field, node.type as string)
|
|
123
|
+
}
|
|
124
|
+
diagnoseNestedFields(node, path, definition, warnings)
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(root.items)) {
|
|
127
|
+
for (const field of Object.keys(root)) {
|
|
128
|
+
if (field in GENUI_SPEC_SCHEMA.fields) continue
|
|
129
|
+
pushUnknownField(warnings, 'spec', field, 'spec')
|
|
130
|
+
}
|
|
131
|
+
root.items.forEach((item, index) => visitNativeNodes(item, `items[${index}]`, visit))
|
|
132
|
+
} else if (typeof root.type === 'string') {
|
|
133
|
+
// A bare component root is a documented shorthand, so its `type` belongs
|
|
134
|
+
// to the native node schema rather than the root specification schema.
|
|
135
|
+
visitNativeNodes(root, 'spec', visit)
|
|
136
|
+
} else {
|
|
137
|
+
for (const field of Object.keys(root)) {
|
|
138
|
+
if (field in GENUI_SPEC_SCHEMA.fields) continue
|
|
139
|
+
pushUnknownField(warnings, 'spec', field, 'spec')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return warnings
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GENUI_LIMITS } from './limits.ts'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Resource limits shared by GenUI repair, validation, and rendering. */
|
|
2
|
+
export const GENUI_LIMITS = {
|
|
3
|
+
/** Maximum nesting depth of the component tree. */
|
|
4
|
+
maxDepth: 8,
|
|
5
|
+
/** Maximum total nodes across the whole spec. */
|
|
6
|
+
maxNodes: 200,
|
|
7
|
+
/** Maximum length of any plain string field. */
|
|
8
|
+
maxString: 2000,
|
|
9
|
+
/** Maximum length of a `code` body. */
|
|
10
|
+
maxCode: 12_000,
|
|
11
|
+
/** Maximum length of a mermaid source. */
|
|
12
|
+
maxMermaid: 8000,
|
|
13
|
+
/** Maximum `grid` columns. */
|
|
14
|
+
maxGridCols: 12,
|
|
15
|
+
/** Maximum `tabs` count. */
|
|
16
|
+
maxTabs: 12,
|
|
17
|
+
/** Maximum `accordion` items. */
|
|
18
|
+
maxAccordionItems: 24,
|
|
19
|
+
/** Maximum `list` items. */
|
|
20
|
+
maxListItems: 50,
|
|
21
|
+
/** Maximum `select`/`radio` options. */
|
|
22
|
+
maxOptions: 50,
|
|
23
|
+
/** Maximum `table` rows / columns. */
|
|
24
|
+
maxTableRows: 50,
|
|
25
|
+
maxTableCols: 12,
|
|
26
|
+
/** Maximum `chart` data points per series. */
|
|
27
|
+
maxChartPoints: 60,
|
|
28
|
+
/** Maximum `plot` series and per-series parameters. */
|
|
29
|
+
maxPlotSeries: 8,
|
|
30
|
+
maxPlotParams: 6,
|
|
31
|
+
/** Maximum `scene3d` meshes. */
|
|
32
|
+
maxMeshes: 5,
|
|
33
|
+
/** Maximum `quiz` options. */
|
|
34
|
+
maxQuizOptions: 8,
|
|
35
|
+
/** Maximum `steps` / `timeline` / `breadcrumb` / `keyvalue` entries. */
|
|
36
|
+
maxSteps: 24,
|
|
37
|
+
maxTimelineItems: 24,
|
|
38
|
+
maxBreadcrumbItems: 12,
|
|
39
|
+
maxKeyValuePairs: 24,
|
|
40
|
+
/** Maximum `file-tree` nesting. */
|
|
41
|
+
maxTreeDepth: 6,
|
|
42
|
+
/** Maximum `diagram` nodes / edges / zones / focal accents. */
|
|
43
|
+
maxDiagramNodes: 9,
|
|
44
|
+
maxDiagramEdges: 12,
|
|
45
|
+
maxDiagramZones: 3,
|
|
46
|
+
maxDiagramFocal: 2,
|
|
47
|
+
maxDiagramLabel: 14,
|
|
48
|
+
/** Maximum depth of an `echart` option object. */
|
|
49
|
+
maxEChartOptionDepth: 10,
|
|
50
|
+
/** Maximum length of any single array inside an `echart` option. */
|
|
51
|
+
maxEChartArrayLen: 500,
|
|
52
|
+
/** Maximum entries traversed while sanitizing an `echart` option. */
|
|
53
|
+
maxEChartOptionNodes: 2000,
|
|
54
|
+
} as const
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** Deterministic GenUI alias and structural normalization. */
|
|
2
|
+
import { COMPONENT_SCHEMAS } from './schema.ts'
|
|
3
|
+
import type { GenuiDiagnostic } from './diagnostics.ts'
|
|
4
|
+
|
|
5
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
7
|
+
? value as Record<string, unknown>
|
|
8
|
+
: undefined
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isNode(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
const candidate = record(value)
|
|
13
|
+
return candidate !== undefined && typeof candidate.type === 'string'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeAliasFields(value: Record<string, unknown>, path: string, type: string, warnings: GenuiDiagnostic[]): Record<string, unknown> {
|
|
17
|
+
const definition = COMPONENT_SCHEMAS[type]
|
|
18
|
+
if (definition === undefined) return value
|
|
19
|
+
const out: Record<string, unknown> = { ...value }
|
|
20
|
+
for (const [alias, canonical] of Object.entries(definition.aliases)) {
|
|
21
|
+
if (!(alias in out)) continue
|
|
22
|
+
const aliasPath = `${path}.${alias}`
|
|
23
|
+
const keptCanonical = canonical in out
|
|
24
|
+
if (!keptCanonical) out[canonical] = out[alias]
|
|
25
|
+
delete out[alias]
|
|
26
|
+
warnings.push({
|
|
27
|
+
kind: 'alias',
|
|
28
|
+
path: aliasPath,
|
|
29
|
+
message: keptCanonical
|
|
30
|
+
? `${aliasPath} is ignored because canonical field '${canonical}' is present`
|
|
31
|
+
: `${aliasPath} normalized/adopted as '${canonical}'`,
|
|
32
|
+
type,
|
|
33
|
+
field: alias,
|
|
34
|
+
canonical,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeNode(value: unknown, path: string, warnings: GenuiDiagnostic[]): unknown {
|
|
41
|
+
if (!isNode(value)) return value
|
|
42
|
+
const type = value.type as string
|
|
43
|
+
const definition = COMPONENT_SCHEMAS[type]
|
|
44
|
+
// Custom nodes are opaque by contract: don't inspect or rewrite their data.
|
|
45
|
+
if (definition === undefined) return value
|
|
46
|
+
const out = normalizeAliasFields(value, path, type, warnings)
|
|
47
|
+
const normalizeNodeValue = (child: unknown, childPath: string): unknown => normalizeNode(child, childPath, warnings)
|
|
48
|
+
const normalizeNodeArray = (children: unknown, childPath: string): unknown => Array.isArray(children)
|
|
49
|
+
? children.map((child, index) => normalizeNodeValue(child, `${childPath}[${index}]`))
|
|
50
|
+
: children
|
|
51
|
+
|
|
52
|
+
if (type === 'row' || type === 'col' || type === 'grid' || type === 'card' || type === 'file-tree' || type === 'timeline' || type === 'breadcrumb') {
|
|
53
|
+
if (type !== 'file-tree' && type !== 'timeline' && type !== 'breadcrumb') out.items = normalizeNodeArray(out.items, `${path}.items`)
|
|
54
|
+
} else if (type === 'list' && Array.isArray(out.items)) {
|
|
55
|
+
out.items = out.items.map((child, index) => isNode(child) ? normalizeNodeValue(child, `${path}.items[${index}]`) : child)
|
|
56
|
+
} else if (type === 'tabs' && Array.isArray(out.tabs)) {
|
|
57
|
+
out.tabs = out.tabs.map((tab, index) => {
|
|
58
|
+
const holder = record(tab)
|
|
59
|
+
if (holder === undefined) return tab
|
|
60
|
+
const normalizedHolder = { ...holder }
|
|
61
|
+
if ('content' in normalizedHolder) {
|
|
62
|
+
const tabPath = `${path}.tabs[${index}].content`
|
|
63
|
+
const hasItems = 'items' in normalizedHolder
|
|
64
|
+
if (!hasItems) normalizedHolder.items = normalizedHolder.content
|
|
65
|
+
delete normalizedHolder.content
|
|
66
|
+
warnings.push({
|
|
67
|
+
kind: 'alias',
|
|
68
|
+
path: tabPath,
|
|
69
|
+
message: hasItems
|
|
70
|
+
? `${tabPath} is ignored because canonical field 'items' is present`
|
|
71
|
+
: `${tabPath} normalized/adopted as 'items'`,
|
|
72
|
+
type,
|
|
73
|
+
field: 'content',
|
|
74
|
+
canonical: 'items',
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
normalizedHolder.items = Array.isArray(normalizedHolder.items)
|
|
78
|
+
? normalizedHolder.items.map((child, childIndex) => normalizeNodeValue(child, `${path}.tabs[${index}].items[${childIndex}]`))
|
|
79
|
+
: normalizedHolder.items === undefined
|
|
80
|
+
? normalizedHolder.items
|
|
81
|
+
: [normalizeNodeValue(normalizedHolder.items, `${path}.tabs[${index}].items[0]`)]
|
|
82
|
+
return normalizedHolder
|
|
83
|
+
})
|
|
84
|
+
} else if (type === 'accordion' && Array.isArray(out.items)) {
|
|
85
|
+
out.items = out.items.map((item, index) => {
|
|
86
|
+
const holder = record(item)
|
|
87
|
+
if (holder === undefined) return item
|
|
88
|
+
return { ...holder, items: Array.isArray(holder.items) ? holder.items.map((child, childIndex) => normalizeNodeValue(child, `${path}.items[${index}].items[${childIndex}]`)) : holder.items }
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Normalize a raw GenUI value into canonical field names.
|
|
96
|
+
*
|
|
97
|
+
* Only deterministic aliases and structural aliases are changed. Resource
|
|
98
|
+
* limits, type repair, security filtering, and semantic validation remain in
|
|
99
|
+
* the guard layer. Unknown component types are returned opaque.
|
|
100
|
+
*
|
|
101
|
+
* @param value - Raw GenUI spec or bare native component.
|
|
102
|
+
* @returns Canonical value and stable alias diagnostics.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeGenuiSpec(value: unknown): { value: unknown; warnings: GenuiDiagnostic[] } {
|
|
105
|
+
const warnings: GenuiDiagnostic[] = []
|
|
106
|
+
const root = record(value)
|
|
107
|
+
if (root === undefined) return { value, warnings }
|
|
108
|
+
const out = { ...root }
|
|
109
|
+
if (Array.isArray(out.items)) {
|
|
110
|
+
out.items = out.items.map((item, index) => normalizeNode(item, `items[${index}]`, warnings))
|
|
111
|
+
} else if (typeof out.type === 'string') {
|
|
112
|
+
return { value: normalizeNode(out, 'spec', warnings), warnings }
|
|
113
|
+
}
|
|
114
|
+
return { value: out, warnings }
|
|
115
|
+
}
|