semiotic 3.4.2 → 3.5.1
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/CLAUDE.md +114 -9
- package/README.md +45 -4
- package/ai/behaviorContracts.cjs +311 -0
- package/ai/chartSuggestions.cjs +291 -0
- package/ai/cli.js +255 -30
- package/ai/componentMetadata.cjs +107 -0
- package/ai/dist/mcp-server.js +907 -227
- package/ai/schema.json +3954 -2537
- package/ai/system-prompt.md +23 -4
- package/dist/components/LinkedCharts.d.ts +5 -1
- package/dist/components/Tooltip/Tooltip.d.ts +1 -1
- package/dist/components/charts/custom/NetworkCustomChart.d.ts +64 -0
- package/dist/components/charts/custom/OrdinalCustomChart.d.ts +71 -0
- package/dist/components/charts/custom/XYCustomChart.d.ts +59 -0
- package/dist/components/charts/geo/ChoroplethMap.d.ts +93 -2
- package/dist/components/charts/geo/DistanceCartogram.d.ts +51 -4
- package/dist/components/charts/geo/FlowMap.d.ts +55 -0
- package/dist/components/charts/geo/ProportionalSymbolMap.d.ts +53 -0
- package/dist/components/charts/index.d.ts +6 -0
- package/dist/components/charts/network/ChordDiagram.d.ts +34 -2
- package/dist/components/charts/network/CirclePack.d.ts +36 -1
- package/dist/components/charts/network/ForceDirectedGraph.d.ts +130 -2
- package/dist/components/charts/network/OrbitDiagram.d.ts +37 -0
- package/dist/components/charts/network/SankeyDiagram.d.ts +51 -2
- package/dist/components/charts/network/TreeDiagram.d.ts +37 -2
- package/dist/components/charts/network/Treemap.d.ts +36 -2
- package/dist/components/charts/ordinal/BarChart.d.ts +113 -1
- package/dist/components/charts/ordinal/BoxPlot.d.ts +33 -0
- package/dist/components/charts/ordinal/DonutChart.d.ts +36 -0
- package/dist/components/charts/ordinal/DotPlot.d.ts +33 -0
- package/dist/components/charts/ordinal/FunnelChart.d.ts +40 -0
- package/dist/components/charts/ordinal/GaugeChart.d.ts +45 -0
- package/dist/components/charts/ordinal/GroupedBarChart.d.ts +40 -0
- package/dist/components/charts/ordinal/Histogram.d.ts +97 -0
- package/dist/components/charts/ordinal/LikertChart.d.ts +44 -0
- package/dist/components/charts/ordinal/PieChart.d.ts +90 -1
- package/dist/components/charts/ordinal/RidgelinePlot.d.ts +29 -0
- package/dist/components/charts/ordinal/StackedBarChart.d.ts +40 -0
- package/dist/components/charts/ordinal/SwarmPlot.d.ts +38 -0
- package/dist/components/charts/ordinal/SwimlaneChart.d.ts +62 -0
- package/dist/components/charts/ordinal/ViolinPlot.d.ts +34 -0
- package/dist/components/charts/realtime/RealtimeHeatmap.d.ts +22 -4
- package/dist/components/charts/realtime/RealtimeHistogram.d.ts +5 -2
- package/dist/components/charts/realtime/RealtimeLineChart.d.ts +24 -3
- package/dist/components/charts/realtime/RealtimeSwarmChart.d.ts +12 -0
- package/dist/components/charts/realtime/RealtimeWaterfallChart.d.ts +14 -0
- package/dist/components/charts/realtime/defaultRealtimeTooltip.d.ts +67 -0
- package/dist/components/charts/realtime/resolveWindowSize.d.ts +26 -0
- package/dist/components/charts/shared/chartSpecs.d.ts +91 -0
- package/dist/components/charts/shared/colorPalettes.d.ts +62 -0
- package/dist/components/charts/shared/colorUtils.d.ts +9 -10
- package/dist/components/charts/shared/numberFormat.d.ts +58 -0
- package/dist/components/charts/shared/sparseArray.d.ts +27 -0
- package/dist/components/charts/shared/streamPropsHelpers.d.ts +113 -0
- package/dist/components/charts/shared/timeFormat.d.ts +60 -0
- package/dist/components/charts/shared/useChartSetup.d.ts +8 -0
- package/dist/components/charts/shared/useCustomChartSetup.d.ts +84 -0
- package/dist/components/charts/shared/useFrameImperativeHandle.d.ts +28 -0
- package/dist/components/charts/shared/useOrdinalStreaming.d.ts +6 -19
- package/dist/components/charts/shared/useStreamingLegend.d.ts +27 -11
- package/dist/components/charts/shared/validateProps.d.ts +2 -2
- package/dist/components/charts/shared/validationMap.d.ts +2 -1
- package/dist/components/charts/shared/withChartWrapper.d.ts +13 -4
- package/dist/components/charts/xy/AreaChart.d.ts +44 -1
- package/dist/components/charts/xy/BubbleChart.d.ts +4 -0
- package/dist/components/charts/xy/CandlestickChart.d.ts +37 -6
- package/dist/components/charts/xy/ConnectedScatterplot.d.ts +28 -0
- package/dist/components/charts/xy/Heatmap.d.ts +4 -0
- package/dist/components/charts/xy/LineChart.d.ts +12 -0
- package/dist/components/charts/xy/MinimapChart.d.ts +58 -0
- package/dist/components/charts/xy/MultiAxisLineChart.d.ts +27 -0
- package/dist/components/charts/xy/QuadrantChart.d.ts +21 -0
- package/dist/components/charts/xy/Scatterplot.d.ts +38 -2
- package/dist/components/charts/xy/ScatterplotMatrix.d.ts +16 -0
- package/dist/components/charts/xy/StackedAreaChart.d.ts +61 -1
- package/dist/components/realtime/types.d.ts +2 -4
- package/dist/components/recipes/bullet.d.ts +86 -0
- package/dist/components/recipes/calendar.d.ts +43 -0
- package/dist/components/recipes/dagre.d.ts +56 -0
- package/dist/components/recipes/flextree.d.ts +55 -0
- package/dist/components/recipes/marimekko.d.ts +55 -0
- package/dist/components/recipes/parallelCoordinates.d.ts +97 -0
- package/dist/components/recipes/recipeUtils.d.ts +27 -0
- package/dist/components/recipes/waffle.d.ts +46 -0
- package/dist/components/semiotic-ai.d.ts +4 -0
- package/dist/components/semiotic-network.d.ts +3 -0
- package/dist/components/semiotic-ordinal.d.ts +3 -0
- package/dist/components/semiotic-recipes.d.ts +24 -0
- package/dist/components/semiotic-xy.d.ts +3 -0
- package/dist/components/semiotic.d.ts +2 -2
- package/dist/components/server/renderToStaticSVG.d.ts +8 -2
- package/dist/components/server/serverChartConfigs.d.ts +47 -1
- package/dist/components/server/staticAnnotations.d.ts +6 -0
- package/dist/components/store/ObservationStore.d.ts +1 -3
- package/dist/components/store/SelectionStore.d.ts +1 -3
- package/dist/components/store/ThemeStore.d.ts +4 -4
- package/dist/components/store/TooltipStore.d.ts +1 -3
- package/dist/components/store/createStore.d.ts +4 -2
- package/dist/components/stream/CanvasHitTester.d.ts +10 -8
- package/dist/components/stream/DataSourceAdapter.d.ts +9 -0
- package/dist/components/stream/GeoPipelineStore.d.ts +9 -0
- package/dist/components/stream/GeoTileRenderer.d.ts +14 -0
- package/dist/components/stream/NetworkPipelineStore.d.ts +25 -0
- package/dist/components/stream/OrdinalPipelineStore.d.ts +12 -0
- package/dist/components/stream/PipelineStore.d.ts +51 -0
- package/dist/components/stream/SVGOverlay.d.ts +12 -0
- package/dist/components/stream/SceneGraph.d.ts +15 -1
- package/dist/components/stream/SceneToSVG.d.ts +1 -1
- package/dist/components/stream/categoryDomain.d.ts +4 -0
- package/dist/components/stream/composeOverlays.d.ts +15 -0
- package/dist/components/stream/customLayout.d.ts +76 -0
- package/dist/components/stream/customLayoutPalette.d.ts +29 -0
- package/dist/components/stream/geoTypes.d.ts +13 -0
- package/dist/components/stream/hoverUtils.d.ts +4 -10
- package/dist/components/stream/networkCustomLayout.d.ts +67 -0
- package/dist/components/stream/networkTypes.d.ts +45 -0
- package/dist/components/stream/ordinalCustomLayout.d.ts +84 -0
- package/dist/components/stream/ordinalTypes.d.ts +35 -1
- package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +9 -1
- package/dist/components/stream/renderers/canvasRenderHelpers.d.ts +92 -0
- package/dist/components/stream/sampleCurvePath.d.ts +9 -0
- package/dist/components/stream/types.d.ts +44 -1
- package/dist/components/stream/useHydration.d.ts +89 -0
- package/dist/components/stream/useStableShallow.d.ts +1 -0
- package/dist/components/stream/xySceneBuilders/types.d.ts +4 -0
- package/dist/geo.min.js +2 -1
- package/dist/geo.module.min.js +2 -1
- package/dist/network.min.js +2 -1
- package/dist/network.module.min.js +2 -1
- package/dist/ordinal.min.js +2 -1
- package/dist/ordinal.module.min.js +2 -1
- package/dist/realtime.min.js +2 -1
- package/dist/realtime.module.min.js +2 -1
- package/dist/semiotic-ai.d.ts +69 -65
- package/dist/semiotic-ai.min.js +2 -1
- package/dist/semiotic-ai.module.min.js +2 -1
- package/dist/semiotic-data.d.ts +4 -4
- package/dist/semiotic-geo.d.ts +15 -15
- package/dist/semiotic-network.d.ts +19 -16
- package/dist/semiotic-ordinal.d.ts +31 -28
- package/dist/semiotic-realtime.d.ts +17 -17
- package/dist/semiotic-recipes.d.ts +24 -0
- package/dist/semiotic-recipes.min.js +1 -0
- package/dist/semiotic-recipes.module.min.js +1 -0
- package/dist/semiotic-server.d.ts +6 -6
- package/dist/semiotic-statisticalOverlays-C3DsOgr_.js +1 -0
- package/dist/semiotic-themes.d.ts +3 -3
- package/dist/semiotic-themes.min.js +2 -1
- package/dist/semiotic-themes.module.min.js +2 -1
- package/dist/semiotic-utils.d.ts +23 -23
- package/dist/semiotic-utils.min.js +2 -1
- package/dist/semiotic-utils.module.min.js +2 -1
- package/dist/semiotic-xy.d.ts +27 -24
- package/dist/semiotic.d.ts +63 -63
- package/dist/semiotic.min.js +2 -1
- package/dist/semiotic.module.min.js +2 -1
- package/dist/server.min.js +1 -1
- package/dist/server.module.min.js +1 -1
- package/dist/test-utils/canvasMock.d.ts +34 -5
- package/dist/xy.min.js +2 -1
- package/dist/xy.module.min.js +2 -1
- package/package.json +38 -17
- package/dist/semiotic-statisticalOverlays-Ckd_jM8z.js +0 -1
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const VALID_INTENTS = [
|
|
4
|
+
"comparison", "trend", "distribution", "relationship", "composition",
|
|
5
|
+
"geographic", "network", "hierarchy",
|
|
6
|
+
]
|
|
7
|
+
const MAX_SAMPLE_SIZE = 5
|
|
8
|
+
|
|
9
|
+
function summarizeFields(data, keys) {
|
|
10
|
+
const numericFields = []
|
|
11
|
+
const stringFields = []
|
|
12
|
+
const dateFields = []
|
|
13
|
+
const geoFields = {}
|
|
14
|
+
const networkFields = {}
|
|
15
|
+
const hierarchyFields = {}
|
|
16
|
+
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const values = data.map((d) => d[key]).filter((v) => v != null)
|
|
19
|
+
if (values.length === 0) continue
|
|
20
|
+
|
|
21
|
+
const first = values[0]
|
|
22
|
+
if (typeof first === "number") {
|
|
23
|
+
numericFields.push(key)
|
|
24
|
+
} else if (typeof first === "string") {
|
|
25
|
+
if (/^\d{4}[-/]\d{2}/.test(first) && !Number.isNaN(Date.parse(first))) {
|
|
26
|
+
dateFields.push(key)
|
|
27
|
+
} else {
|
|
28
|
+
stringFields.push(key)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const kl = key.toLowerCase()
|
|
33
|
+
if (kl === "lat" || kl === "latitude") geoFields.lat = key
|
|
34
|
+
if (kl === "lon" || kl === "lng" || kl === "longitude") geoFields.lon = key
|
|
35
|
+
if (kl === "source" || kl === "from") networkFields.source = key
|
|
36
|
+
if (kl === "target" || kl === "to") networkFields.target = key
|
|
37
|
+
if (kl === "value" || kl === "weight" || kl === "amount") networkFields.value = key
|
|
38
|
+
if (kl === "children" || kl === "values") hierarchyFields.children = key
|
|
39
|
+
if (kl === "parent") hierarchyFields.parent = key
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
keys,
|
|
44
|
+
numericFields,
|
|
45
|
+
stringFields,
|
|
46
|
+
dateFields,
|
|
47
|
+
geoFields,
|
|
48
|
+
networkFields,
|
|
49
|
+
hierarchyFields,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function jsxString(value) {
|
|
54
|
+
return JSON.stringify(String(value))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function jsxExpression(value) {
|
|
58
|
+
return `{${value}}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function uniqueNetworkNodes(data, sourceField, targetField) {
|
|
62
|
+
const ids = new Set()
|
|
63
|
+
for (const datum of data) {
|
|
64
|
+
const source = datum[sourceField]
|
|
65
|
+
const target = datum[targetField]
|
|
66
|
+
if (source != null) ids.add(source)
|
|
67
|
+
if (target != null) ids.add(target)
|
|
68
|
+
}
|
|
69
|
+
return Array.from(ids).map((id) => ({ id }))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function suggestCharts(args = {}) {
|
|
73
|
+
const data = args.data
|
|
74
|
+
const intent = args.intent
|
|
75
|
+
|
|
76
|
+
if (intent && !VALID_INTENTS.includes(intent)) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: `Unknown intent "${intent}". Expected one of: ${VALID_INTENTS.join(", ")}.`,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: "Pass { data: [{ ... }, ...] } with 1-5 sample data objects. Optionally include intent: 'comparison' | 'trend' | 'distribution' | 'relationship' | 'composition' | 'geographic' | 'network' | 'hierarchy'.",
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (data.length > MAX_SAMPLE_SIZE) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: `Pass 1-${MAX_SAMPLE_SIZE} sample data objects; received ${data.length}. Use a representative sample instead of the full dataset.`,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sample = data[0]
|
|
98
|
+
if (!sample || typeof sample !== "object" || Array.isArray(sample)) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
error: "Data items must be objects with key-value pairs.",
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const keys = Object.keys(sample)
|
|
106
|
+
const fields = summarizeFields(data, keys)
|
|
107
|
+
const suggestions = []
|
|
108
|
+
|
|
109
|
+
const { numericFields, stringFields, dateFields, geoFields, networkFields, hierarchyFields } = fields
|
|
110
|
+
const hasTime = dateFields.length > 0
|
|
111
|
+
const hasCat = stringFields.length > 0
|
|
112
|
+
const hasNum = numericFields.length > 0
|
|
113
|
+
const hasGeo = Boolean(geoFields.lat && geoFields.lon)
|
|
114
|
+
const hasNetwork = Boolean(networkFields.source && networkFields.target)
|
|
115
|
+
const hasHierarchy = Boolean(hierarchyFields.children || hierarchyFields.parent)
|
|
116
|
+
|
|
117
|
+
if (hasNetwork && (!intent || intent === "network")) {
|
|
118
|
+
const src = networkFields.source
|
|
119
|
+
const tgt = networkFields.target
|
|
120
|
+
if (networkFields.value) {
|
|
121
|
+
suggestions.push({
|
|
122
|
+
component: "SankeyDiagram",
|
|
123
|
+
confidence: "high",
|
|
124
|
+
reason: `Data has ${src}->${tgt} with ${networkFields.value} - ideal for flow visualization`,
|
|
125
|
+
props: { edges: jsxExpression("data"), sourceAccessor: jsxString(src), targetAccessor: jsxString(tgt), valueAccessor: jsxString(networkFields.value) },
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
const nodes = uniqueNetworkNodes(data, src, tgt)
|
|
129
|
+
suggestions.push({
|
|
130
|
+
component: "ForceDirectedGraph",
|
|
131
|
+
confidence: networkFields.value ? "medium" : "high",
|
|
132
|
+
reason: `Data has ${src}->${tgt} edges - force layout shows network structure. ForceDirectedGraph requires explicit nodes, derived here from unique source/target IDs.`,
|
|
133
|
+
setup: [`const nodes = ${JSON.stringify(nodes, null, 2)}`],
|
|
134
|
+
derivedData: { nodes },
|
|
135
|
+
props: { nodes: jsxExpression("nodes"), edges: jsxExpression("data"), nodeIDAccessor: jsxString("id"), sourceAccessor: jsxString(src), targetAccessor: jsxString(tgt) },
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (hasHierarchy && hierarchyFields.children && Array.isArray(sample[hierarchyFields.children]) && (!intent || intent === "hierarchy")) {
|
|
140
|
+
const childrenAccessor = hierarchyFields.children
|
|
141
|
+
suggestions.push({
|
|
142
|
+
component: "Treemap",
|
|
143
|
+
confidence: "high",
|
|
144
|
+
reason: `Data has nested ${childrenAccessor} structure - treemap shows hierarchical proportions. Use data[0] as the root node from the provided sample.`,
|
|
145
|
+
props: { data: jsxExpression("data[0]"), childrenAccessor: jsxString(childrenAccessor), ...(numericFields[0] ? { valueAccessor: jsxString(numericFields[0]) } : {}) },
|
|
146
|
+
})
|
|
147
|
+
suggestions.push({
|
|
148
|
+
component: "TreeDiagram",
|
|
149
|
+
confidence: "medium",
|
|
150
|
+
reason: `Tree layout shows hierarchical relationships. Use data[0] as the root node from the provided sample.`,
|
|
151
|
+
props: { data: jsxExpression("data[0]"), childrenAccessor: jsxString(childrenAccessor) },
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (hasGeo && (!intent || intent === "geographic")) {
|
|
156
|
+
const sizeField = numericFields.find((f) => f !== geoFields.lat && f !== geoFields.lon)
|
|
157
|
+
suggestions.push({
|
|
158
|
+
component: "ProportionalSymbolMap",
|
|
159
|
+
confidence: "high",
|
|
160
|
+
reason: `Data has ${geoFields.lat}/${geoFields.lon} coordinates - map shows spatial distribution`,
|
|
161
|
+
props: { points: jsxExpression("data"), xAccessor: jsxString(geoFields.lon), yAccessor: jsxString(geoFields.lat), ...(sizeField ? { sizeBy: jsxString(sizeField) } : {}) },
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (hasTime && hasNum && (!intent || intent === "trend")) {
|
|
166
|
+
const timeField = dateFields[0]
|
|
167
|
+
const valueField = numericFields[0]
|
|
168
|
+
suggestions.push({
|
|
169
|
+
component: "LineChart",
|
|
170
|
+
confidence: "high",
|
|
171
|
+
reason: `Data has dates (${timeField}) and numeric values (${valueField}) - line chart shows trends over time`,
|
|
172
|
+
props: { data: jsxExpression("data"), xAccessor: jsxString(timeField), yAccessor: jsxString(valueField), ...(hasCat ? { lineBy: jsxString(stringFields[0]), colorBy: jsxString(stringFields[0]) } : {}) },
|
|
173
|
+
})
|
|
174
|
+
if (hasCat) {
|
|
175
|
+
suggestions.push({
|
|
176
|
+
component: "StackedAreaChart",
|
|
177
|
+
confidence: "medium",
|
|
178
|
+
reason: `Multiple categories (${stringFields[0]}) over time - stacked area shows composition trends`,
|
|
179
|
+
props: { data: jsxExpression("data"), xAccessor: jsxString(timeField), yAccessor: jsxString(valueField), areaBy: jsxString(stringFields[0]), colorBy: jsxString(stringFields[0]) },
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (hasCat && hasNum && (!intent || intent === "comparison" || intent === "composition" || intent === "distribution")) {
|
|
185
|
+
const catField = stringFields[0]
|
|
186
|
+
const valField = numericFields[0]
|
|
187
|
+
|
|
188
|
+
if (!intent || intent === "comparison") {
|
|
189
|
+
suggestions.push({
|
|
190
|
+
component: "BarChart",
|
|
191
|
+
confidence: hasTime ? "medium" : "high",
|
|
192
|
+
reason: `Categorical field (${catField}) with values (${valField}) - bar chart for comparison`,
|
|
193
|
+
props: { data: jsxExpression("data"), categoryAccessor: jsxString(catField), valueAccessor: jsxString(valField) },
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (stringFields.length >= 2 && (!intent || intent === "composition")) {
|
|
198
|
+
suggestions.push({
|
|
199
|
+
component: "StackedBarChart",
|
|
200
|
+
confidence: "medium",
|
|
201
|
+
reason: `Two categorical fields (${stringFields.join(", ")}) - stacked bar shows composition within categories`,
|
|
202
|
+
props: { data: jsxExpression("data"), categoryAccessor: jsxString(catField), valueAccessor: jsxString(valField), stackBy: jsxString(stringFields[1]) },
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!intent || intent === "distribution") {
|
|
207
|
+
suggestions.push({
|
|
208
|
+
component: "Histogram",
|
|
209
|
+
confidence: "medium",
|
|
210
|
+
reason: `Numeric distribution of ${valField} - histogram shows value spread`,
|
|
211
|
+
props: { data: jsxExpression("data"), categoryAccessor: jsxString(catField), valueAccessor: jsxString(valField) },
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!intent || intent === "composition") {
|
|
216
|
+
const uniqueCats = new Set(data.map((d) => d[catField])).size
|
|
217
|
+
if (uniqueCats <= 8) {
|
|
218
|
+
suggestions.push({
|
|
219
|
+
component: "DonutChart",
|
|
220
|
+
confidence: "medium",
|
|
221
|
+
reason: `${uniqueCats} categories - donut chart shows proportional composition`,
|
|
222
|
+
props: { data: jsxExpression("data"), categoryAccessor: jsxString(catField), valueAccessor: jsxString(valField) },
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (numericFields.length >= 2 && (!intent || intent === "relationship")) {
|
|
229
|
+
const xField = numericFields[0]
|
|
230
|
+
const yField = numericFields[1]
|
|
231
|
+
suggestions.push({
|
|
232
|
+
component: "Scatterplot",
|
|
233
|
+
confidence: "high",
|
|
234
|
+
reason: `Two numeric fields (${xField}, ${yField}) - scatterplot shows relationships`,
|
|
235
|
+
props: { data: jsxExpression("data"), xAccessor: jsxString(xField), yAccessor: jsxString(yField), ...(hasCat ? { colorBy: jsxString(stringFields[0]) } : {}), ...(numericFields[2] ? { sizeBy: jsxString(numericFields[2]) } : {}) },
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
if (numericFields.length >= 3) {
|
|
239
|
+
suggestions.push({
|
|
240
|
+
component: "BubbleChart",
|
|
241
|
+
confidence: "medium",
|
|
242
|
+
reason: "Three numeric fields - bubble chart adds size dimension to scatter",
|
|
243
|
+
props: { data: jsxExpression("data"), xAccessor: jsxString(xField), yAccessor: jsxString(yField), sizeBy: jsxString(numericFields[2]) },
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (stringFields.length >= 2 && numericFields.length >= 1 && (!intent || intent === "relationship" || intent === "distribution" || intent === "composition")) {
|
|
249
|
+
const xField = stringFields[0]
|
|
250
|
+
const yField = stringFields[1]
|
|
251
|
+
const valueField = numericFields[0]
|
|
252
|
+
suggestions.push({
|
|
253
|
+
component: "Heatmap",
|
|
254
|
+
confidence: "medium",
|
|
255
|
+
reason: `Two categorical fields (${xField}, ${yField}) plus numeric values (${valueField}) - heatmap shows intensity across dimensions`,
|
|
256
|
+
props: { data: jsxExpression("data"), xAccessor: jsxString(xField), yAccessor: jsxString(yField), valueAccessor: jsxString(valueField) },
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
ok: true,
|
|
262
|
+
intent,
|
|
263
|
+
fieldSummary: `Fields: ${keys.join(", ")} (${numericFields.length} numeric, ${stringFields.length} categorical, ${dateFields.length} date)`,
|
|
264
|
+
fields,
|
|
265
|
+
suggestions,
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatSuggestionReport(result) {
|
|
270
|
+
if (!result.ok) return result.error
|
|
271
|
+
|
|
272
|
+
if (result.suggestions.length === 0) {
|
|
273
|
+
return `Could not confidently recommend a chart type.\n\n${result.fieldSummary}\n\nTry providing intent ('${VALID_INTENTS.join("', '")}') to narrow recommendations, or use getSchema to browse available components.`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const lines = result.suggestions.map((suggestion, i) => {
|
|
277
|
+
const propsStr = Object.entries(suggestion.props).map(([k, v]) => `${k}=${v}`).join(" ")
|
|
278
|
+
const setup = suggestion.setup ? `${suggestion.setup.join("\n")}\n` : ""
|
|
279
|
+
return `${i + 1}. **${suggestion.component}** (${suggestion.confidence} confidence)\n ${suggestion.reason}\n\`\`\`tsx\n${setup}<${suggestion.component} ${propsStr} />\n\`\`\``
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
const themingTip = `\n---\n**Styling**: All charts respond to CSS custom properties on any ancestor element:\n\`\`\`css\n.my-theme {\n --semiotic-bg: #fff;\n --semiotic-text: #333;\n --semiotic-text-secondary: #666;\n --semiotic-grid: #e0e0e0;\n --semiotic-border: #e0e0e0;\n --semiotic-font-family: sans-serif;\n --semiotic-tooltip-bg: rgba(0,0,0,0.85);\n --semiotic-tooltip-text: white;\n --semiotic-tooltip-radius: 6px;\n}\n\`\`\`\nOr use \`<ThemeProvider theme="dark">\` / \`<ThemeProvider theme={{ colors: {...}, typography: {...} }}>\`.\nFor accessibility, use \`colorScheme={COLOR_BLIND_SAFE_CATEGORICAL}\` (import from \`semiotic/themes\`) - 8-color palette safe for all forms of color blindness.`
|
|
283
|
+
|
|
284
|
+
return lines.join("\n\n") + themingTip
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = {
|
|
288
|
+
VALID_INTENTS,
|
|
289
|
+
formatSuggestionReport,
|
|
290
|
+
suggestCharts,
|
|
291
|
+
}
|
package/ai/cli.js
CHANGED
|
@@ -5,6 +5,23 @@ const fs = require("fs")
|
|
|
5
5
|
const path = require("path")
|
|
6
6
|
|
|
7
7
|
const pkgRoot = path.resolve(__dirname, "..")
|
|
8
|
+
const {
|
|
9
|
+
CATEGORY_ORDER,
|
|
10
|
+
componentIndexFromSchema,
|
|
11
|
+
findComponent,
|
|
12
|
+
metadataForComponent,
|
|
13
|
+
schemaEntries,
|
|
14
|
+
} = require("./componentMetadata.cjs")
|
|
15
|
+
const {
|
|
16
|
+
formatSuggestionReport,
|
|
17
|
+
suggestCharts,
|
|
18
|
+
} = require("./chartSuggestions.cjs")
|
|
19
|
+
const {
|
|
20
|
+
behaviorContractsFor,
|
|
21
|
+
dataRequiredForUsageMode,
|
|
22
|
+
formatDoctorBehaviorContracts,
|
|
23
|
+
normalizeUsageMode,
|
|
24
|
+
} = require("./behaviorContracts.cjs")
|
|
8
25
|
|
|
9
26
|
const FILES = {
|
|
10
27
|
default: path.join(pkgRoot, "CLAUDE.md"),
|
|
@@ -13,67 +30,269 @@ const FILES = {
|
|
|
13
30
|
"--examples": path.join(__dirname, "examples.md"),
|
|
14
31
|
}
|
|
15
32
|
|
|
33
|
+
function errorMessage(err) {
|
|
34
|
+
return err instanceof Error ? err.message : String(err)
|
|
35
|
+
}
|
|
36
|
+
|
|
16
37
|
const HELP = `
|
|
17
38
|
semiotic-ai — Dump Semiotic AI context to stdout
|
|
18
39
|
|
|
19
40
|
Usage:
|
|
20
41
|
npx semiotic-ai Print CLAUDE.md (full reference)
|
|
21
|
-
npx semiotic-ai --
|
|
42
|
+
npx semiotic-ai --list List components, categories, imports, and renderability
|
|
43
|
+
npx semiotic-ai --list --json Print component index as JSON
|
|
44
|
+
npx semiotic-ai --schema Print ai/schema.json (all tool definitions)
|
|
45
|
+
npx semiotic-ai --schema BarChart
|
|
46
|
+
Print one component schema plus AI metadata
|
|
47
|
+
npx semiotic-ai --suggest Recommend charts from { data, intent? } JSON
|
|
22
48
|
npx semiotic-ai --compact Print ai/system-prompt.md (compact prompt)
|
|
23
49
|
npx semiotic-ai --examples Print ai/examples.md (copy-paste examples)
|
|
24
|
-
npx semiotic-ai --doctor Validate component
|
|
50
|
+
npx semiotic-ai --doctor Validate { component, props, usageMode? } JSON from stdin
|
|
25
51
|
npx semiotic-ai --help Show this help message
|
|
26
52
|
`.trim()
|
|
27
53
|
|
|
28
54
|
const flag = process.argv[2]
|
|
29
55
|
|
|
56
|
+
function loadSchema() {
|
|
57
|
+
return JSON.parse(fs.readFileSync(FILES["--schema"], "utf-8"))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function componentIndex() {
|
|
61
|
+
return componentIndexFromSchema(loadSchema())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printComponentList(asJSON) {
|
|
65
|
+
const index = componentIndex()
|
|
66
|
+
if (asJSON) {
|
|
67
|
+
console.log(JSON.stringify(index, null, 2))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`Semiotic components (${index.totalComponents} total, ${index.renderableComponents} renderable)`)
|
|
72
|
+
for (const category of CATEGORY_ORDER) {
|
|
73
|
+
const names = index.categories[category] || []
|
|
74
|
+
if (names.length === 0) continue
|
|
75
|
+
console.log(`\n${category}:`)
|
|
76
|
+
for (const name of names) {
|
|
77
|
+
const component = index.components.find((entry) => entry.name === name)
|
|
78
|
+
const marker = component.renderable ? "renderable" : "browser-only"
|
|
79
|
+
console.log(` ${name} [${marker}] import ${component.importPath}`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function printSingleComponentSchema(componentName) {
|
|
85
|
+
const schema = loadSchema()
|
|
86
|
+
const component = findComponent(schema, componentName)
|
|
87
|
+
if (!component) {
|
|
88
|
+
const available = schemaEntries(schema).map((entry) => entry.name).sort().join(", ")
|
|
89
|
+
console.error(`Unknown component: ${componentName}`)
|
|
90
|
+
console.error(`Available components: ${available}`)
|
|
91
|
+
process.exit(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const payload = {
|
|
95
|
+
...component,
|
|
96
|
+
metadata: {
|
|
97
|
+
...metadataForComponent(component),
|
|
98
|
+
usageModes: {
|
|
99
|
+
static: {
|
|
100
|
+
dataRequired: dataRequiredForUsageMode(component.name, "static"),
|
|
101
|
+
note: "Use for renderChart, MCP previews, SSR snapshots, and static JSX examples.",
|
|
102
|
+
},
|
|
103
|
+
push: {
|
|
104
|
+
dataRequired: dataRequiredForUsageMode(component.name, "push"),
|
|
105
|
+
note: "Use for ref-based React HOCs. Omit data and push via ref.current when supported.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
behaviorContracts: behaviorContractsFor({ component: component.name, props: {} }),
|
|
110
|
+
}
|
|
111
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Both helpers are only called from `validatePropsWithSchema` below, which
|
|
115
|
+
// filters `undefined` / `null` out of `value` before reaching them — so
|
|
116
|
+
// neither guards null here. CodeQL flags the dead branches if they return.
|
|
117
|
+
|
|
118
|
+
function schemaTypeMatches(value, expectedType) {
|
|
119
|
+
const expectedTypes = Array.isArray(expectedType) ? expectedType : [expectedType]
|
|
120
|
+
return expectedTypes.some((type) => {
|
|
121
|
+
if (type === "array") return Array.isArray(value)
|
|
122
|
+
if (type === "object") return typeof value === "object" && !Array.isArray(value)
|
|
123
|
+
return typeof value === type
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function describeActualType(value) {
|
|
128
|
+
if (Array.isArray(value)) return "array"
|
|
129
|
+
return typeof value
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shouldSkipMissingRequiredProp(componentName, propName, usageMode) {
|
|
133
|
+
return propName === "data" && !dataRequiredForUsageMode(componentName, usageMode)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function filterUsageModeErrors(componentName, errors, usageMode) {
|
|
137
|
+
if (dataRequiredForUsageMode(componentName, usageMode)) return errors
|
|
138
|
+
return errors.filter((err) => err !== `"data" is required for ${componentName}.`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function validatePropsWithSchema(componentName, props, usageMode = "static") {
|
|
142
|
+
const schema = loadSchema()
|
|
143
|
+
const component = findComponent(schema, componentName)
|
|
144
|
+
if (!component) {
|
|
145
|
+
const available = schemaEntries(schema).map((entry) => entry.name).sort().join(", ")
|
|
146
|
+
return {
|
|
147
|
+
valid: false,
|
|
148
|
+
errors: [`Unknown component "${componentName}". Available components: ${available}`],
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parameters = component.parameters || {}
|
|
153
|
+
const properties = parameters.properties || {}
|
|
154
|
+
const required = parameters.required || []
|
|
155
|
+
const errors = []
|
|
156
|
+
|
|
157
|
+
for (const propName of required) {
|
|
158
|
+
if (shouldSkipMissingRequiredProp(component.name, propName, usageMode)) continue
|
|
159
|
+
if (props[propName] === undefined || props[propName] === null) {
|
|
160
|
+
errors.push(`"${propName}" is required for ${component.name}.`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const [propName, value] of Object.entries(props)) {
|
|
165
|
+
if (value === undefined || value === null) continue
|
|
166
|
+
const propSchema = properties[propName]
|
|
167
|
+
if (!propSchema) {
|
|
168
|
+
errors.push(`Unknown prop "${propName}" for ${component.name}.`)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (propSchema.type && !schemaTypeMatches(value, propSchema.type)) {
|
|
173
|
+
const expected = Array.isArray(propSchema.type) ? propSchema.type.join(" | ") : propSchema.type
|
|
174
|
+
errors.push(`"${propName}" should be ${expected}, got ${describeActualType(value)}.`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (propSchema.enum && typeof value === "string" && !propSchema.enum.includes(value)) {
|
|
178
|
+
errors.push(`"${propName}" value "${value}" is not valid. Expected one of: ${propSchema.enum.join(", ")}.`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
valid: errors.length === 0,
|
|
184
|
+
errors,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function printSchemaOnlyDoctorResult(component, props, usageMode) {
|
|
189
|
+
const result = validatePropsWithSchema(component, props, usageMode)
|
|
190
|
+
if (usageMode === "push") {
|
|
191
|
+
console.log(` Usage mode: push (data prop may be omitted; use a ref to push data)`)
|
|
192
|
+
}
|
|
193
|
+
if (result.valid) {
|
|
194
|
+
console.log(`✓ ${component}: schema-only validation passed.`)
|
|
195
|
+
} else {
|
|
196
|
+
console.log(`✗ ${component}: schema-only validation failed.`)
|
|
197
|
+
for (const err of result.errors) {
|
|
198
|
+
console.log(` • ${err}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
printDoctorBehaviorContracts(component, props)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function printDoctorBehaviorContracts(component, props) {
|
|
205
|
+
const formatted = formatDoctorBehaviorContracts(
|
|
206
|
+
behaviorContractsFor({ component, props })
|
|
207
|
+
)
|
|
208
|
+
if (formatted) {
|
|
209
|
+
console.log("")
|
|
210
|
+
console.log(formatted)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readJSONInput(usage) {
|
|
215
|
+
if (process.argv[3]) {
|
|
216
|
+
return process.argv.slice(3).join(" ")
|
|
217
|
+
}
|
|
218
|
+
if (!process.stdin.isTTY) {
|
|
219
|
+
return fs.readFileSync(0, "utf-8")
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.error(usage)
|
|
223
|
+
process.exit(1)
|
|
224
|
+
}
|
|
225
|
+
|
|
30
226
|
if (flag === "--help" || flag === "-h") {
|
|
31
227
|
console.log(HELP)
|
|
32
228
|
process.exit(0)
|
|
33
229
|
}
|
|
34
230
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
231
|
+
if (flag === "--list") {
|
|
232
|
+
printComponentList(process.argv.includes("--json"))
|
|
233
|
+
process.exit(0)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (flag === "--schema" && process.argv[3]) {
|
|
237
|
+
printSingleComponentSchema(process.argv[3])
|
|
238
|
+
process.exit(0)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (flag === "--suggest") {
|
|
242
|
+
const input = readJSONInput("Usage: npx semiotic-ai --suggest '{\"data\":[{\"category\":\"A\",\"value\":10}],\"intent\":\"comparison\"}'")
|
|
243
|
+
try {
|
|
244
|
+
const args = JSON.parse(input)
|
|
245
|
+
const result = suggestCharts(args)
|
|
246
|
+
console.log(formatSuggestionReport(result))
|
|
247
|
+
process.exit(result.ok ? 0 : 1)
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(`Failed to parse input: ${errorMessage(err)}`)
|
|
47
250
|
process.exit(1)
|
|
48
251
|
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --doctor: validate component + props from stdin or argv
|
|
255
|
+
if (flag === "--doctor") {
|
|
256
|
+
const input = readJSONInput("Usage: npx semiotic-ai --doctor '{\"component\":\"LineChart\",\"props\":{\"data\":[...]},\"usageMode\":\"static\"}'\n echo '{\"component\":\"LineChart\",\"props\":{\"xAccessor\":\"x\",\"yAccessor\":\"y\"},\"usageMode\":\"push\"}' | npx semiotic-ai --doctor")
|
|
49
257
|
|
|
50
258
|
try {
|
|
51
|
-
const { component, props } = JSON.parse(input)
|
|
259
|
+
const { component, props, usageMode: rawUsageMode } = JSON.parse(input)
|
|
52
260
|
if (!component || !props) {
|
|
53
261
|
console.error("Input must be JSON with { component, props } fields.")
|
|
54
262
|
process.exit(1)
|
|
55
263
|
}
|
|
264
|
+
const usageMode = normalizeUsageMode(rawUsageMode)
|
|
56
265
|
|
|
57
|
-
// Load diagnoseConfig from dist (falls back to validateProps)
|
|
266
|
+
// Load diagnoseConfig from dist (falls back to validateProps, then schema.json)
|
|
58
267
|
const distPath = path.join(pkgRoot, "dist", "semiotic-ai.min.js")
|
|
59
268
|
let diagnoseConfig, validateProps
|
|
60
269
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
270
|
+
if (!process.env.SEMIOTIC_AI_SCHEMA_ONLY) {
|
|
271
|
+
const mod = require(distPath)
|
|
272
|
+
diagnoseConfig = mod.diagnoseConfig
|
|
273
|
+
validateProps = mod.validateProps
|
|
274
|
+
}
|
|
64
275
|
} catch (e) {
|
|
65
|
-
|
|
66
|
-
|
|
276
|
+
// Dist is not available in a clean source checkout. Fall back to the
|
|
277
|
+
// packaged schema so the CLI still catches basic agent mistakes.
|
|
67
278
|
}
|
|
68
279
|
|
|
69
280
|
if (!diagnoseConfig && !validateProps) {
|
|
70
|
-
|
|
71
|
-
process.exit(
|
|
281
|
+
printSchemaOnlyDoctorResult(component, props, usageMode)
|
|
282
|
+
process.exit(0)
|
|
72
283
|
}
|
|
73
284
|
|
|
74
285
|
if (diagnoseConfig) {
|
|
75
286
|
// Use the full anti-pattern detector
|
|
76
287
|
const result = diagnoseConfig(component, props)
|
|
288
|
+
const diagnoses = usageMode === "push"
|
|
289
|
+
? result.diagnoses.filter((d) => d.code !== "VALIDATION" || !shouldSkipMissingRequiredProp(component, "data", usageMode) || d.message !== `"data" is required for ${component}.`)
|
|
290
|
+
: result.diagnoses
|
|
291
|
+
const ok = diagnoses.every((d) => d.severity === "warning")
|
|
292
|
+
|
|
293
|
+
if (usageMode === "push") {
|
|
294
|
+
console.log(` Usage mode: push (data prop may be omitted; use a ref to push data)`)
|
|
295
|
+
}
|
|
77
296
|
|
|
78
297
|
// Show data shape summary
|
|
79
298
|
if (props.data && Array.isArray(props.data) && props.data.length > 0) {
|
|
@@ -81,36 +300,42 @@ if (flag === "--doctor") {
|
|
|
81
300
|
console.log(` Data shape: ${props.data.length} items, keys: [${Object.keys(sample).join(", ")}]`)
|
|
82
301
|
}
|
|
83
302
|
|
|
84
|
-
if (
|
|
303
|
+
if (ok && diagnoses.length === 0) {
|
|
85
304
|
console.log(`✓ ${component}: configuration looks good.`)
|
|
86
|
-
} else if (
|
|
305
|
+
} else if (ok) {
|
|
87
306
|
console.log(`✓ ${component}: configuration OK with warnings:`)
|
|
88
|
-
for (const d of
|
|
307
|
+
for (const d of diagnoses) {
|
|
89
308
|
console.log(` ⚠ [${d.code}] ${d.message}`)
|
|
90
309
|
if (d.fix) console.log(` Fix: ${d.fix}`)
|
|
91
310
|
}
|
|
92
311
|
} else {
|
|
93
312
|
console.log(`✗ ${component}: issues detected.`)
|
|
94
|
-
for (const d of
|
|
313
|
+
for (const d of diagnoses) {
|
|
95
314
|
const icon = d.severity === "error" ? "✗" : "⚠"
|
|
96
315
|
console.log(` ${icon} [${d.code}] ${d.message}`)
|
|
97
316
|
if (d.fix) console.log(` Fix: ${d.fix}`)
|
|
98
317
|
}
|
|
99
318
|
}
|
|
319
|
+
printDoctorBehaviorContracts(component, props)
|
|
100
320
|
} else {
|
|
101
321
|
// Fallback to validateProps only
|
|
102
322
|
const result = validateProps(component, props)
|
|
103
|
-
|
|
323
|
+
const errors = filterUsageModeErrors(component, result.errors, usageMode)
|
|
324
|
+
if (usageMode === "push") {
|
|
325
|
+
console.log(` Usage mode: push (data prop may be omitted; use a ref to push data)`)
|
|
326
|
+
}
|
|
327
|
+
if (errors.length === 0) {
|
|
104
328
|
console.log(`✓ ${component}: props are valid.`)
|
|
105
329
|
} else {
|
|
106
330
|
console.log(`✗ ${component}: validation failed.`)
|
|
107
|
-
for (const err of
|
|
331
|
+
for (const err of errors) {
|
|
108
332
|
console.log(` • ${err}`)
|
|
109
333
|
}
|
|
110
334
|
}
|
|
335
|
+
printDoctorBehaviorContracts(component, props)
|
|
111
336
|
}
|
|
112
337
|
} catch (err) {
|
|
113
|
-
console.error(`Failed to parse input: ${err
|
|
338
|
+
console.error(`Failed to parse input: ${errorMessage(err)}`)
|
|
114
339
|
process.exit(1)
|
|
115
340
|
}
|
|
116
341
|
process.exit(0)
|