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.
Files changed (163) hide show
  1. package/CLAUDE.md +114 -9
  2. package/README.md +45 -4
  3. package/ai/behaviorContracts.cjs +311 -0
  4. package/ai/chartSuggestions.cjs +291 -0
  5. package/ai/cli.js +255 -30
  6. package/ai/componentMetadata.cjs +107 -0
  7. package/ai/dist/mcp-server.js +907 -227
  8. package/ai/schema.json +3954 -2537
  9. package/ai/system-prompt.md +23 -4
  10. package/dist/components/LinkedCharts.d.ts +5 -1
  11. package/dist/components/Tooltip/Tooltip.d.ts +1 -1
  12. package/dist/components/charts/custom/NetworkCustomChart.d.ts +64 -0
  13. package/dist/components/charts/custom/OrdinalCustomChart.d.ts +71 -0
  14. package/dist/components/charts/custom/XYCustomChart.d.ts +59 -0
  15. package/dist/components/charts/geo/ChoroplethMap.d.ts +93 -2
  16. package/dist/components/charts/geo/DistanceCartogram.d.ts +51 -4
  17. package/dist/components/charts/geo/FlowMap.d.ts +55 -0
  18. package/dist/components/charts/geo/ProportionalSymbolMap.d.ts +53 -0
  19. package/dist/components/charts/index.d.ts +6 -0
  20. package/dist/components/charts/network/ChordDiagram.d.ts +34 -2
  21. package/dist/components/charts/network/CirclePack.d.ts +36 -1
  22. package/dist/components/charts/network/ForceDirectedGraph.d.ts +130 -2
  23. package/dist/components/charts/network/OrbitDiagram.d.ts +37 -0
  24. package/dist/components/charts/network/SankeyDiagram.d.ts +51 -2
  25. package/dist/components/charts/network/TreeDiagram.d.ts +37 -2
  26. package/dist/components/charts/network/Treemap.d.ts +36 -2
  27. package/dist/components/charts/ordinal/BarChart.d.ts +113 -1
  28. package/dist/components/charts/ordinal/BoxPlot.d.ts +33 -0
  29. package/dist/components/charts/ordinal/DonutChart.d.ts +36 -0
  30. package/dist/components/charts/ordinal/DotPlot.d.ts +33 -0
  31. package/dist/components/charts/ordinal/FunnelChart.d.ts +40 -0
  32. package/dist/components/charts/ordinal/GaugeChart.d.ts +45 -0
  33. package/dist/components/charts/ordinal/GroupedBarChart.d.ts +40 -0
  34. package/dist/components/charts/ordinal/Histogram.d.ts +97 -0
  35. package/dist/components/charts/ordinal/LikertChart.d.ts +44 -0
  36. package/dist/components/charts/ordinal/PieChart.d.ts +90 -1
  37. package/dist/components/charts/ordinal/RidgelinePlot.d.ts +29 -0
  38. package/dist/components/charts/ordinal/StackedBarChart.d.ts +40 -0
  39. package/dist/components/charts/ordinal/SwarmPlot.d.ts +38 -0
  40. package/dist/components/charts/ordinal/SwimlaneChart.d.ts +62 -0
  41. package/dist/components/charts/ordinal/ViolinPlot.d.ts +34 -0
  42. package/dist/components/charts/realtime/RealtimeHeatmap.d.ts +22 -4
  43. package/dist/components/charts/realtime/RealtimeHistogram.d.ts +5 -2
  44. package/dist/components/charts/realtime/RealtimeLineChart.d.ts +24 -3
  45. package/dist/components/charts/realtime/RealtimeSwarmChart.d.ts +12 -0
  46. package/dist/components/charts/realtime/RealtimeWaterfallChart.d.ts +14 -0
  47. package/dist/components/charts/realtime/defaultRealtimeTooltip.d.ts +67 -0
  48. package/dist/components/charts/realtime/resolveWindowSize.d.ts +26 -0
  49. package/dist/components/charts/shared/chartSpecs.d.ts +91 -0
  50. package/dist/components/charts/shared/colorPalettes.d.ts +62 -0
  51. package/dist/components/charts/shared/colorUtils.d.ts +9 -10
  52. package/dist/components/charts/shared/numberFormat.d.ts +58 -0
  53. package/dist/components/charts/shared/sparseArray.d.ts +27 -0
  54. package/dist/components/charts/shared/streamPropsHelpers.d.ts +113 -0
  55. package/dist/components/charts/shared/timeFormat.d.ts +60 -0
  56. package/dist/components/charts/shared/useChartSetup.d.ts +8 -0
  57. package/dist/components/charts/shared/useCustomChartSetup.d.ts +84 -0
  58. package/dist/components/charts/shared/useFrameImperativeHandle.d.ts +28 -0
  59. package/dist/components/charts/shared/useOrdinalStreaming.d.ts +6 -19
  60. package/dist/components/charts/shared/useStreamingLegend.d.ts +27 -11
  61. package/dist/components/charts/shared/validateProps.d.ts +2 -2
  62. package/dist/components/charts/shared/validationMap.d.ts +2 -1
  63. package/dist/components/charts/shared/withChartWrapper.d.ts +13 -4
  64. package/dist/components/charts/xy/AreaChart.d.ts +44 -1
  65. package/dist/components/charts/xy/BubbleChart.d.ts +4 -0
  66. package/dist/components/charts/xy/CandlestickChart.d.ts +37 -6
  67. package/dist/components/charts/xy/ConnectedScatterplot.d.ts +28 -0
  68. package/dist/components/charts/xy/Heatmap.d.ts +4 -0
  69. package/dist/components/charts/xy/LineChart.d.ts +12 -0
  70. package/dist/components/charts/xy/MinimapChart.d.ts +58 -0
  71. package/dist/components/charts/xy/MultiAxisLineChart.d.ts +27 -0
  72. package/dist/components/charts/xy/QuadrantChart.d.ts +21 -0
  73. package/dist/components/charts/xy/Scatterplot.d.ts +38 -2
  74. package/dist/components/charts/xy/ScatterplotMatrix.d.ts +16 -0
  75. package/dist/components/charts/xy/StackedAreaChart.d.ts +61 -1
  76. package/dist/components/realtime/types.d.ts +2 -4
  77. package/dist/components/recipes/bullet.d.ts +86 -0
  78. package/dist/components/recipes/calendar.d.ts +43 -0
  79. package/dist/components/recipes/dagre.d.ts +56 -0
  80. package/dist/components/recipes/flextree.d.ts +55 -0
  81. package/dist/components/recipes/marimekko.d.ts +55 -0
  82. package/dist/components/recipes/parallelCoordinates.d.ts +97 -0
  83. package/dist/components/recipes/recipeUtils.d.ts +27 -0
  84. package/dist/components/recipes/waffle.d.ts +46 -0
  85. package/dist/components/semiotic-ai.d.ts +4 -0
  86. package/dist/components/semiotic-network.d.ts +3 -0
  87. package/dist/components/semiotic-ordinal.d.ts +3 -0
  88. package/dist/components/semiotic-recipes.d.ts +24 -0
  89. package/dist/components/semiotic-xy.d.ts +3 -0
  90. package/dist/components/semiotic.d.ts +2 -2
  91. package/dist/components/server/renderToStaticSVG.d.ts +8 -2
  92. package/dist/components/server/serverChartConfigs.d.ts +47 -1
  93. package/dist/components/server/staticAnnotations.d.ts +6 -0
  94. package/dist/components/store/ObservationStore.d.ts +1 -3
  95. package/dist/components/store/SelectionStore.d.ts +1 -3
  96. package/dist/components/store/ThemeStore.d.ts +4 -4
  97. package/dist/components/store/TooltipStore.d.ts +1 -3
  98. package/dist/components/store/createStore.d.ts +4 -2
  99. package/dist/components/stream/CanvasHitTester.d.ts +10 -8
  100. package/dist/components/stream/DataSourceAdapter.d.ts +9 -0
  101. package/dist/components/stream/GeoPipelineStore.d.ts +9 -0
  102. package/dist/components/stream/GeoTileRenderer.d.ts +14 -0
  103. package/dist/components/stream/NetworkPipelineStore.d.ts +25 -0
  104. package/dist/components/stream/OrdinalPipelineStore.d.ts +12 -0
  105. package/dist/components/stream/PipelineStore.d.ts +51 -0
  106. package/dist/components/stream/SVGOverlay.d.ts +12 -0
  107. package/dist/components/stream/SceneGraph.d.ts +15 -1
  108. package/dist/components/stream/SceneToSVG.d.ts +1 -1
  109. package/dist/components/stream/categoryDomain.d.ts +4 -0
  110. package/dist/components/stream/composeOverlays.d.ts +15 -0
  111. package/dist/components/stream/customLayout.d.ts +76 -0
  112. package/dist/components/stream/customLayoutPalette.d.ts +29 -0
  113. package/dist/components/stream/geoTypes.d.ts +13 -0
  114. package/dist/components/stream/hoverUtils.d.ts +4 -10
  115. package/dist/components/stream/networkCustomLayout.d.ts +67 -0
  116. package/dist/components/stream/networkTypes.d.ts +45 -0
  117. package/dist/components/stream/ordinalCustomLayout.d.ts +84 -0
  118. package/dist/components/stream/ordinalTypes.d.ts +35 -1
  119. package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +9 -1
  120. package/dist/components/stream/renderers/canvasRenderHelpers.d.ts +92 -0
  121. package/dist/components/stream/sampleCurvePath.d.ts +9 -0
  122. package/dist/components/stream/types.d.ts +44 -1
  123. package/dist/components/stream/useHydration.d.ts +89 -0
  124. package/dist/components/stream/useStableShallow.d.ts +1 -0
  125. package/dist/components/stream/xySceneBuilders/types.d.ts +4 -0
  126. package/dist/geo.min.js +2 -1
  127. package/dist/geo.module.min.js +2 -1
  128. package/dist/network.min.js +2 -1
  129. package/dist/network.module.min.js +2 -1
  130. package/dist/ordinal.min.js +2 -1
  131. package/dist/ordinal.module.min.js +2 -1
  132. package/dist/realtime.min.js +2 -1
  133. package/dist/realtime.module.min.js +2 -1
  134. package/dist/semiotic-ai.d.ts +69 -65
  135. package/dist/semiotic-ai.min.js +2 -1
  136. package/dist/semiotic-ai.module.min.js +2 -1
  137. package/dist/semiotic-data.d.ts +4 -4
  138. package/dist/semiotic-geo.d.ts +15 -15
  139. package/dist/semiotic-network.d.ts +19 -16
  140. package/dist/semiotic-ordinal.d.ts +31 -28
  141. package/dist/semiotic-realtime.d.ts +17 -17
  142. package/dist/semiotic-recipes.d.ts +24 -0
  143. package/dist/semiotic-recipes.min.js +1 -0
  144. package/dist/semiotic-recipes.module.min.js +1 -0
  145. package/dist/semiotic-server.d.ts +6 -6
  146. package/dist/semiotic-statisticalOverlays-C3DsOgr_.js +1 -0
  147. package/dist/semiotic-themes.d.ts +3 -3
  148. package/dist/semiotic-themes.min.js +2 -1
  149. package/dist/semiotic-themes.module.min.js +2 -1
  150. package/dist/semiotic-utils.d.ts +23 -23
  151. package/dist/semiotic-utils.min.js +2 -1
  152. package/dist/semiotic-utils.module.min.js +2 -1
  153. package/dist/semiotic-xy.d.ts +27 -24
  154. package/dist/semiotic.d.ts +63 -63
  155. package/dist/semiotic.min.js +2 -1
  156. package/dist/semiotic.module.min.js +2 -1
  157. package/dist/server.min.js +1 -1
  158. package/dist/server.module.min.js +1 -1
  159. package/dist/test-utils/canvasMock.d.ts +34 -5
  160. package/dist/xy.min.js +2 -1
  161. package/dist/xy.module.min.js +2 -1
  162. package/package.json +38 -17
  163. 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 --schema Print ai/schema.json (tool definitions)
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 + props JSON from stdin
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
- // --doctor: validate component + props from stdin or argv
36
- if (flag === "--doctor") {
37
- let input = ""
38
- if (process.argv[3]) {
39
- // npx semiotic-ai --doctor '{"component":"LineChart","props":{...}}'
40
- input = process.argv.slice(3).join(" ")
41
- } else if (!process.stdin.isTTY) {
42
- // echo '...' | npx semiotic-ai --doctor
43
- input = fs.readFileSync("/dev/stdin", "utf-8")
44
- } else {
45
- console.error("Usage: npx semiotic-ai --doctor '{\"component\":\"LineChart\",\"props\":{\"data\":[...]}}'")
46
- console.error(" echo '{...}' | npx semiotic-ai --doctor")
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
- const mod = require(distPath)
62
- diagnoseConfig = mod.diagnoseConfig
63
- validateProps = mod.validateProps
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
- console.error("Could not load semiotic/ai dist. Run 'npm run dist' first.")
66
- process.exit(1)
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
- console.error("diagnoseConfig/validateProps not found in semiotic/ai exports.")
71
- process.exit(1)
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 (result.ok && result.diagnoses.length === 0) {
303
+ if (ok && diagnoses.length === 0) {
85
304
  console.log(`✓ ${component}: configuration looks good.`)
86
- } else if (result.ok) {
305
+ } else if (ok) {
87
306
  console.log(`✓ ${component}: configuration OK with warnings:`)
88
- for (const d of result.diagnoses) {
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 result.diagnoses) {
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
- if (result.valid) {
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 result.errors) {
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.message}`)
338
+ console.error(`Failed to parse input: ${errorMessage(err)}`)
114
339
  process.exit(1)
115
340
  }
116
341
  process.exit(0)