semiotic 3.4.2 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) 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 +111 -1
  28. package/dist/components/charts/ordinal/BoxPlot.d.ts +31 -0
  29. package/dist/components/charts/ordinal/DonutChart.d.ts +36 -0
  30. package/dist/components/charts/ordinal/DotPlot.d.ts +31 -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 +38 -0
  34. package/dist/components/charts/ordinal/Histogram.d.ts +95 -0
  35. package/dist/components/charts/ordinal/LikertChart.d.ts +42 -0
  36. package/dist/components/charts/ordinal/PieChart.d.ts +90 -1
  37. package/dist/components/charts/ordinal/RidgelinePlot.d.ts +27 -0
  38. package/dist/components/charts/ordinal/StackedBarChart.d.ts +38 -0
  39. package/dist/components/charts/ordinal/SwarmPlot.d.ts +36 -0
  40. package/dist/components/charts/ordinal/SwimlaneChart.d.ts +60 -0
  41. package/dist/components/charts/ordinal/ViolinPlot.d.ts +32 -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 +43 -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 +30 -1
  65. package/dist/components/charts/xy/CandlestickChart.d.ts +33 -6
  66. package/dist/components/charts/xy/ConnectedScatterplot.d.ts +24 -0
  67. package/dist/components/charts/xy/MinimapChart.d.ts +51 -0
  68. package/dist/components/charts/xy/MultiAxisLineChart.d.ts +27 -0
  69. package/dist/components/charts/xy/QuadrantChart.d.ts +21 -0
  70. package/dist/components/charts/xy/Scatterplot.d.ts +34 -2
  71. package/dist/components/charts/xy/ScatterplotMatrix.d.ts +16 -0
  72. package/dist/components/charts/xy/StackedAreaChart.d.ts +49 -1
  73. package/dist/components/realtime/types.d.ts +2 -4
  74. package/dist/components/recipes/bullet.d.ts +86 -0
  75. package/dist/components/recipes/calendar.d.ts +43 -0
  76. package/dist/components/recipes/dagre.d.ts +56 -0
  77. package/dist/components/recipes/flextree.d.ts +55 -0
  78. package/dist/components/recipes/marimekko.d.ts +55 -0
  79. package/dist/components/recipes/parallelCoordinates.d.ts +97 -0
  80. package/dist/components/recipes/recipeUtils.d.ts +27 -0
  81. package/dist/components/recipes/waffle.d.ts +46 -0
  82. package/dist/components/semiotic-ai.d.ts +4 -0
  83. package/dist/components/semiotic-network.d.ts +3 -0
  84. package/dist/components/semiotic-ordinal.d.ts +3 -0
  85. package/dist/components/semiotic-recipes.d.ts +24 -0
  86. package/dist/components/semiotic-xy.d.ts +3 -0
  87. package/dist/components/semiotic.d.ts +2 -2
  88. package/dist/components/server/renderToStaticSVG.d.ts +8 -2
  89. package/dist/components/server/serverChartConfigs.d.ts +47 -1
  90. package/dist/components/server/staticAnnotations.d.ts +6 -0
  91. package/dist/components/store/ObservationStore.d.ts +1 -3
  92. package/dist/components/store/SelectionStore.d.ts +1 -3
  93. package/dist/components/store/ThemeStore.d.ts +4 -4
  94. package/dist/components/store/TooltipStore.d.ts +1 -3
  95. package/dist/components/store/createStore.d.ts +4 -2
  96. package/dist/components/stream/CanvasHitTester.d.ts +10 -8
  97. package/dist/components/stream/DataSourceAdapter.d.ts +9 -0
  98. package/dist/components/stream/GeoPipelineStore.d.ts +9 -0
  99. package/dist/components/stream/GeoTileRenderer.d.ts +14 -0
  100. package/dist/components/stream/NetworkPipelineStore.d.ts +25 -0
  101. package/dist/components/stream/OrdinalPipelineStore.d.ts +12 -0
  102. package/dist/components/stream/PipelineStore.d.ts +51 -0
  103. package/dist/components/stream/SVGOverlay.d.ts +12 -0
  104. package/dist/components/stream/SceneGraph.d.ts +15 -1
  105. package/dist/components/stream/SceneToSVG.d.ts +1 -1
  106. package/dist/components/stream/categoryDomain.d.ts +4 -0
  107. package/dist/components/stream/composeOverlays.d.ts +15 -0
  108. package/dist/components/stream/customLayout.d.ts +76 -0
  109. package/dist/components/stream/customLayoutPalette.d.ts +29 -0
  110. package/dist/components/stream/geoTypes.d.ts +13 -0
  111. package/dist/components/stream/hoverUtils.d.ts +4 -10
  112. package/dist/components/stream/networkCustomLayout.d.ts +67 -0
  113. package/dist/components/stream/networkTypes.d.ts +45 -0
  114. package/dist/components/stream/ordinalCustomLayout.d.ts +84 -0
  115. package/dist/components/stream/ordinalTypes.d.ts +35 -1
  116. package/dist/components/stream/renderers/barFunnelCanvasRenderer.d.ts +9 -1
  117. package/dist/components/stream/renderers/canvasRenderHelpers.d.ts +92 -0
  118. package/dist/components/stream/sampleCurvePath.d.ts +9 -0
  119. package/dist/components/stream/types.d.ts +44 -1
  120. package/dist/components/stream/useHydration.d.ts +89 -0
  121. package/dist/components/stream/useStableShallow.d.ts +1 -0
  122. package/dist/components/stream/xySceneBuilders/types.d.ts +4 -0
  123. package/dist/geo.min.js +2 -1
  124. package/dist/geo.module.min.js +2 -1
  125. package/dist/network.min.js +2 -1
  126. package/dist/network.module.min.js +2 -1
  127. package/dist/ordinal.min.js +2 -1
  128. package/dist/ordinal.module.min.js +2 -1
  129. package/dist/realtime.min.js +2 -1
  130. package/dist/realtime.module.min.js +2 -1
  131. package/dist/semiotic-ai.d.ts +69 -65
  132. package/dist/semiotic-ai.min.js +2 -1
  133. package/dist/semiotic-ai.module.min.js +2 -1
  134. package/dist/semiotic-data.d.ts +4 -4
  135. package/dist/semiotic-geo.d.ts +15 -15
  136. package/dist/semiotic-network.d.ts +19 -16
  137. package/dist/semiotic-ordinal.d.ts +31 -28
  138. package/dist/semiotic-realtime.d.ts +17 -17
  139. package/dist/semiotic-recipes.d.ts +24 -0
  140. package/dist/semiotic-recipes.min.js +1 -0
  141. package/dist/semiotic-recipes.module.min.js +1 -0
  142. package/dist/semiotic-server.d.ts +6 -6
  143. package/dist/semiotic-statisticalOverlays-C3DsOgr_.js +1 -0
  144. package/dist/semiotic-themes.d.ts +3 -3
  145. package/dist/semiotic-themes.min.js +2 -1
  146. package/dist/semiotic-themes.module.min.js +2 -1
  147. package/dist/semiotic-utils.d.ts +23 -23
  148. package/dist/semiotic-utils.min.js +2 -1
  149. package/dist/semiotic-utils.module.min.js +2 -1
  150. package/dist/semiotic-xy.d.ts +27 -24
  151. package/dist/semiotic.d.ts +63 -63
  152. package/dist/semiotic.min.js +2 -1
  153. package/dist/semiotic.module.min.js +2 -1
  154. package/dist/server.min.js +1 -1
  155. package/dist/server.module.min.js +1 -1
  156. package/dist/test-utils/canvasMock.d.ts +34 -5
  157. package/dist/xy.min.js +2 -1
  158. package/dist/xy.module.min.js +2 -1
  159. package/package.json +38 -17
  160. package/dist/semiotic-statisticalOverlays-Ckd_jM8z.js +0 -1
package/CLAUDE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Quick Start
4
4
  - Install: `npm install semiotic`
5
- - **Use sub-path imports** — `semiotic/xy` (78KB gz), `semiotic/ordinal` (65KB), `semiotic/network` (54KB), `semiotic/geo` (53KB), `semiotic/realtime` (77KB), `semiotic/server` (58KB), `semiotic/utils` (19KB), `semiotic/themes` (3KB), `semiotic/data` (3KB). Full `semiotic` is 158KB gz.
5
+ - **Use sub-path imports** — `semiotic/xy` (77KB gz), `semiotic/ordinal` (64KB), `semiotic/network` (51KB), `semiotic/geo` (49KB), `semiotic/realtime` (84KB), `semiotic/server` (64KB), `semiotic/recipes` (4KB), `semiotic/utils` (20KB), `semiotic/themes` (4KB), `semiotic/data` (3KB). Full `semiotic` is 165KB gz.
6
6
  - CLI: `npx semiotic-ai [--schema|--compact|--examples|--doctor]`
7
7
  - MCP: `npx semiotic-mcp`
8
8
 
@@ -20,9 +20,9 @@
20
20
 
21
21
  ## XY Charts (`semiotic/xy`)
22
22
 
23
- **LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time")
24
- **AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true)
25
- **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`. No `lineBy`/`lineDataAccessor`.
23
+ **LineChart** — `data`, `xAccessor` ("x"), `yAccessor` ("y"), `lineBy`, `lineDataAccessor`, `colorBy`, `colorScheme`, `curve`, `lineWidth` (2), `showPoints`, `pointRadius` (3), `fillArea` (boolean|string[]), `areaOpacity` (0.3), `lineGradient`, `anomaly`, `forecast`, `directLabel`, `gapStrategy`, `xScaleType`/`yScaleType` ("linear"|"log"|"time"), `tooltip="multi"` for hover-anywhere series comparison
24
+ **AreaChart** — LineChart props + `areaBy`, `y0Accessor`, `gradientFill`, `areaOpacity` (0.7), `showLine` (true), `tooltip="multi"` for hover-anywhere area comparison
25
+ **StackedAreaChart** — flat array + `areaBy` (required), `colorBy`, `normalize`, `baseline` (`"zero"` default | `"wiggle"` for streamgraph | `"silhouette"` for centered), `stackOrder` (`"key"` default alpha | `"insideOut"` largest-in-middle | `"asc"`/`"desc"` by total). For canonical streamgraph aesthetic: `baseline="wiggle"` + `stackOrder="insideOut"` puts the largest series in the middle as a central anchor with smaller series wrapping outward. `baseline` is mutually exclusive with `normalize`. No `lineBy`/`lineDataAccessor`. Pass `tooltip="multi"` for hover-anywhere mode: a single tooltip lists every stacked series at the hovered x, with values interpolated between rendered path samples.
26
26
  **Scatterplot** — `data`, `xAccessor`, `yAccessor`, `colorBy`, `sizeBy`, `sizeRange`, `pointRadius` (5), `pointOpacity` (0.8), `marginalGraphics`
27
27
  **BubbleChart** — Scatterplot + `sizeBy` (required), `sizeRange` ([5,40])
28
28
  **ConnectedScatterplot** — + `orderAccessor`
@@ -101,6 +101,88 @@ ref.current.getScales() // returns {o, r, projection}
101
101
  `remove()` and `update()` require an ID accessor: `pointIdAccessor` on XY/realtime charts, `dataIdAccessor` on ordinal charts. `replace()` is ordinal-only and routes through a bounded-ingest path that preserves category insertion-order memory and the transition position snapshot — what aggregator HOCs like LikertChart use under the hood to re-aggregate streaming input without shuffling categories or losing animations. Network HOC refs also use `remove(id)`/`update(id, updater)` (operates on nodes). For edge-level operations, use `StreamNetworkFrameHandle` directly: `removeNode(id)`, `removeEdge(sourceId, targetId)` or `removeEdge(edgeId)` (requires `edgeIdAccessor`), `updateNode(id, updater)`, `updateEdge(sourceId, targetId, updater)`.
102
102
  Not supported: Tree, Treemap, CirclePack, Orbit, ChoroplethMap, FlowMap, ScatterplotMatrix.
103
103
 
104
+ ## Custom Charts (escape hatch)
105
+
106
+ When the catalog doesn't fit, three HOCs let you supply a layout function that emits scene primitives directly. The frame still owns hit testing, transitions, decay, theme cascade, and SSR — your layout owns geometry only.
107
+
108
+ - **`XYCustomChart`** (`semiotic/xy`) — XY layouts: waffle, calendar heatmap, custom point/line/area arrangements
109
+ - **`OrdinalCustomChart`** (`semiotic/ordinal`) — category × value layouts: marimekko, parallel coordinates, bullet, fan chart, slope graph
110
+ - **`NetworkCustomChart`** (`semiotic/network`) — graph layouts: flextree, dagre, custom force/radial
111
+
112
+ All three accept `layout` and `layoutConfig` (your own typed config), but the layout context and return shape differ by chart family:
113
+
114
+ - **`XYCustomChart` / `OrdinalCustomChart`** — `layout: (ctx) => { nodes, overlays? }`. Context exposes `data`, `scales`, `dimensions` (with plot rect — center-anchored for radial ordinal, top-left otherwise), `theme` (semantic + categorical), `resolveColor(key)`, and `config`. XY scales: `{ x, y }` (linear). Ordinal scales: `{ o, r, projection }` (band + linear).
115
+ - **`NetworkCustomChart`** — `layout: (ctx) => { sceneNodes?, sceneEdges?, labels?, overlays? }`. Context exposes `nodes`, `edges`, `dimensions`, `theme`, `resolveColor(key)`, and `config` — graph data, no `data`/`scales`. Network layouts often run an external positioner (`d3-flextree`, `dagre`) on nodes/edges, then emit network scene primitives (`circle`, `rect`, `arc` for nodes; `line`, `bezier`, `curved` for edges).
116
+
117
+ XY/ordinal frames render whatever you put in `nodes` (rect, point, area, line, wedge, connector, etc.). Network frames split node-shaped scenes from edge-shaped scenes — give them `sceneNodes` for the round/rect/arc visuals and `sceneEdges` for the connecting paths. All three frames handle painting, hit testing, accessibility, transitions, decay, and SSR for you.
118
+
119
+ ```tsx
120
+ import { XYCustomChart } from "semiotic/xy"
121
+ import { OrdinalCustomChart } from "semiotic/ordinal"
122
+ import { NetworkCustomChart } from "semiotic/network"
123
+ import {
124
+ waffleLayout, calendarLayout, // XY recipes
125
+ marimekkoLayout, bulletLayout, parallelCoordinatesLayout, // ordinal
126
+ flextreeLayout, dagreLayout, // network
127
+ } from "semiotic/recipes"
128
+
129
+ <XYCustomChart data={cells} layout={waffleLayout} layoutConfig={{ rows: 10, columns: 10, ... }} />
130
+ <OrdinalCustomChart data={revenue} layout={marimekkoLayout} layoutConfig={{ ... }} />
131
+ <NetworkCustomChart nodes={nodes} edges={edges} layout={flextreeLayout} layoutConfig={{ ... }} />
132
+ ```
133
+
134
+ **Recipes subpath** (`semiotic/recipes`, 4KB gz) ships pure layout functions. They emit standard SceneNodes — no chart code. BYO heavy deps (`d3-flextree`, `dagre`) live in user code.
135
+
136
+ ### Chrome (labels, axes, legends): the recipe owns it
137
+
138
+ Custom layouts often need chrome the standard chart axes can't render — variable-width bars, per-row independent value scales, parallel axes, asymmetric tree branches. The unified pattern: **the recipe emits its own labels/axes/ticks via the `overlays` return field** (a ReactNode painted on top of the canvas). Built-in axes (via `showAxes` on the HOC) work for layouts that respect the standard scale; everything else, the recipe handles itself.
139
+
140
+ Convention across shipped recipes — every recipe takes a consistent set of label/axis toggles in `layoutConfig`:
141
+
142
+ | Recipe | Toggle | Default | What it draws |
143
+ |---|---|---|---|
144
+ | `marimekkoLayout` | `showCategoryLabels` | `true` | Category names under each variable-width bar |
145
+ | `bulletLayout` | `showLabels` | `true` | Metric name to the left of each row |
146
+ | `bulletLayout` | `showTicks` | `true` | Per-row value-axis ticks (each row independently scaled) |
147
+ | `parallelCoordinatesLayout` | `showAxes` | `true` | Vertical axis line + field name + 5 ticks per axis |
148
+ | `flextreeLayout` / `dagreLayout` | `showLabels` | `true` | Node text rendered inside each rect |
149
+ | `waffleLayout` / `calendarLayout` | — | — | No chrome needed (uniform grid + cell color tells the story) |
150
+
151
+ Writing your own recipe: prefer `showXxx` boolean toggles for chrome opt-out, `xxxFormat` callbacks for custom number/string formatting, and reserve plot-rect padding (`labelWidth`, `labelPadding`, `axisLabelPadding`) when chrome eats space.
152
+
153
+ ### Interaction (hover, brush, selection): the parent component owns it
154
+
155
+ Recipes are pure functions, so they can't carry interactive state (hover, brush ranges, selection). The pattern: recipes accept a **predicate prop** (e.g. `parallelCoordinatesLayout`'s `highlightFn?: (d) => boolean`) and the parent component manages state. Wire `onObservation` (`{ type: "hover" | "hover-end" | ... }`) on `OrdinalCustomChart` / `XYCustomChart` / `NetworkCustomChart` to update parent state, then feed a derived predicate back into `layoutConfig`. Matching rows render at full opacity; non-matching dim. Highlighted rows z-order on top so neighbors don't cover them.
156
+
157
+ ```tsx
158
+ const [hovered, setHovered] = useState(null)
159
+ <OrdinalCustomChart
160
+ data={rows}
161
+ layout={parallelCoordinatesLayout}
162
+ layoutConfig={{
163
+ fields: ["mpg", "hp", "weight"],
164
+ highlightFn: hovered ? (d) => d.name === hovered : undefined,
165
+ }}
166
+ onObservation={(obs) => {
167
+ if (obs.type === "hover") setHovered(obs.datum?.data?.name ?? obs.datum?.name)
168
+ else if (obs.type === "hover-end") setHovered(null)
169
+ }}
170
+ />
171
+ ```
172
+
173
+ The same predicate hook is the integration point for richer interactions on the roadmap: a future `<ParallelCoordinatesBrushes>` overlay that drag-brushes per-axis ranges, and `useBrushSelection`-style linked brushing across coordinated charts (selection store would carry per-field range constraints, downstream charts consume them via the same hook).
174
+
175
+ ### Built-in chrome works when the layout uses standard scales
176
+
177
+ Layouts that draw *within* the frame's standard scales can just use the HOC's chrome. Pass `showAxes` on `XYCustomChart` / `OrdinalCustomChart` and the frame will render the same x/y or o/r axes the built-in HOCs do — useful for custom XY layouts that overlay points/lines on regular axes. The recipe-managed chrome is for the cases where standard axes can't help (variable-width bars under a band scale, per-row independent scales, etc.).
178
+
179
+ **Notes:**
180
+ - Coords are plot-relative (the frame translates the canvas/SVG group by `margin`). Read `ctx.dimensions.plot` for the drawing rect. Radial ordinal projection is the one exception: `plot.x = -width/2`, `plot.y = -height/2` because the canvas ctx is center-translated.
181
+ - Layouts that need axis domains: pass `xExtent`/`yExtent` (XY) or `oExtent`/`rExtent` (ordinal) — those flow through scale construction *before* the layout runs.
182
+ - Streaming layouts: ingest data via the chart's ref (`push`/`pushMany`); the layout re-runs on each ingest. Custom overlays update on data-change paths, NOT on per-frame animation rebuilds (intentional — would force a React re-render per frame).
183
+ - Custom layouts own their colors. Always prefer `ctx.resolveColor(key)` over hardcoded literals so `ThemeProvider` / `colorScheme` flow through. `CategoryColorProvider` integration is XY-only; for cross-chart category sync on network/ordinal customLayouts, pass a matching `colorScheme` to each chart.
184
+ - Tooltips: emit datum keys that match the user-visible accessor names (e.g. when `categoryAccessor: "region"` and `valueAccessor: "revenue"` are passed, put `region` and `revenue` on each rect's `datum`). The default tooltip looks them up by accessor name and the user's custom tooltip will read whatever fields they expect. Avoid underscored synthetic keys — the default tooltip filters those out.
185
+
104
186
  ## Coordinated Views
105
187
 
106
188
  **LinkedCharts** — `selections`, **CategoryColorProvider** — `colors`|`categories` + `colorScheme`
@@ -137,9 +219,9 @@ Server SVGs include `role="img"`, `<title>`, `<desc>`, grid, legend, annotations
137
219
  - **GroupedBarChart** — `data`, `categoryAccessor`, `valueAccessor`, `groupBy` (required).
138
220
  - **PieChart/DonutChart** — `data`, `categoryAccessor`, `valueAccessor`.
139
221
  - **FunnelChart** — `data`, `stepAccessor` ("step"), `valueAccessor` ("value"). Renders with trapezoid connectors, no axes.
140
- - **GaugeChart** — `value`, `thresholds` (array of `{value, color, label}`). Optional: `min`, `max`, `sweep`, `arcWidth`.
222
+ - **GaugeChart** — `value`. Optional: `thresholds` (array of `{value, color, label}`), `min`, `max`, `sweep`, `arcWidth`.
141
223
  - **SwimlaneChart** — `data`, `categoryAccessor`, `subcategoryAccessor` (required), `valueAccessor`.
142
- - **ForceDirectedGraph** — `edges` (required). `nodes` optional (inferred from edges).
224
+ - **ForceDirectedGraph** — `nodes`, `edges` (both required). If deriving nodes from edge endpoints, materialize `nodes` before returning JSX/renderChart props.
143
225
  - **SankeyDiagram** — `edges` (required), `valueAccessor`.
144
226
  - **ChoroplethMap** — `areas` (GeoJSON features, pre-resolved).
145
227
 
@@ -165,7 +247,7 @@ CSS custom properties: `--semiotic-bg`, `--semiotic-text`, `--semiotic-text-seco
165
247
  <ThemeProvider theme={{ mode: "dark", colors: { categorical: [...] } }}> {/* Merge onto dark base */}
166
248
  ```
167
249
 
168
- **Color priority** (with `colorBy`): explicit `colorScheme` > ThemeProvider `colors.categorical` > `"category10"`.
250
+ **Color priority** (with `colorBy`): CategoryColorProvider/LinkedCharts category map > explicit `colorScheme` fallback > ThemeProvider `colors.categorical` > `"category10"`.
169
251
  Presets: `light`, `dark`, `high-contrast`, `pastels`(-dark), `bi-tool`(-dark), `italian`(-dark), `tufte`(-dark), `journalist`(-dark), `playful`(-dark), `carbon`(-dark).
170
252
  Serialization: `themeToCSS(theme, selector)`, `themeToTokens(theme)`, `resolveThemePreset(name)`.
171
253
 
@@ -182,11 +264,34 @@ Canvas scene builders read CSS variables via `getComputedStyle` on the canvas DO
182
264
  ## AI Features
183
265
  `onObservation`/`useChartObserver`, `toConfig`/`fromConfig`/`toURL`/`fromURL`/`copyConfig`/`configToJSX`, `validateProps(component, props)`, `diagnoseConfig(component, props)`, `exportChart(div, { format })`, `npx semiotic-ai --doctor`
184
266
 
267
+
268
+ ## AI Behavior Contracts
269
+
270
+ <!-- semiotic-behavior-contracts:start -->
271
+
272
+ These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `semiotic-ai --doctor`, MCP resources, and docs checks.
273
+
274
+ - **Data required by usage mode** (`props.data-required-by-usage-mode`): Static usage (`renderChart`, MCP previews, SSR snapshots, and copy/paste examples with immediate data) requires data in props. React push mode selects live ingestion by omitting data and mutating through a ref.
275
+ Agent action: Pass usageMode="push" to `semiotic-ai --doctor` when validating ref-based JSX with no data prop. Keep usageMode="static" or omit it for renderChart/MCP/static configs where data must be present.
276
+ - **Categorical color precedence** (`color.category-precedence`): When colorBy is set, CategoryColorProvider/LinkedCharts category maps win for mapped categories. Unmapped categories fall back to explicit colorScheme, then ThemeProvider colors.categorical, then the built-in categorical fallback.
277
+ Agent action: Use colorBy for categorical encodings. Use CategoryColorProvider or LinkedCharts for cross-chart consistency, colorScheme for per-chart fallback palettes, and avoid frameProps style functions unless intentionally bypassing HOC color resolution.
278
+ - **Required prop combinations** (`props.required-combinations`): Some chart families need semantic props beyond data. These combinations are enforced by validation/schema for static configs and remain required in push mode unless explicitly noted.
279
+ Agent action: Before returning code, check the selected component against the required combinations list. For push mode, omit data but keep semantic props such as areaBy, sizeBy, stackBy, and groupBy.
280
+ Required combinations: StackedAreaChart: static data + areaBy; push areaBy. Stacked areas need a flat data array plus areaBy to identify the stacked series. BubbleChart: static data + sizeBy; push sizeBy. Bubbles need sizeBy in addition to x/y accessors so radius encodes data rather than a constant point size. StackedBarChart: static data + stackBy; push stackBy. Stacked bars need stackBy to split each category into stack segments. GroupedBarChart: static data + groupBy; push groupBy. Grouped bars need groupBy to split each category into side-by-side bars. SwimlaneChart: static data + subcategoryAccessor; push subcategoryAccessor. Swimlanes need subcategoryAccessor; colorBy defaults to the same field when not provided. GaugeChart: static value; push not supported. GaugeChart is value-only. thresholds, min, max, sweep, and arcWidth are optional. ForceDirectedGraph: static nodes + edges; push nodes + edges. ForceDirectedGraph schema/rendering requires nodes and edges. If an agent infers nodes from edge endpoints, it must materialize a nodes array before returning code.
281
+ - **Push mode omits data** (`streaming.push-mode-data`): HOC push mode is selected by omitting the data prop entirely. Passing data={[]} is static empty data and can clear/reinitialize the frame on render.
282
+ Agent action: For live charts, create a ref, omit data, then call ref.current.push() or pushMany(). For static renderChart/MCP snapshots, provide data because renderChart cannot push later.
283
+ - **Ref mutations need stable IDs** (`streaming.ref-mutations-require-id-accessors`): push() and pushMany() can append without IDs, but remove(id) and update(id, updater) require a stable ID accessor: pointIdAccessor for XY/realtime charts, dataIdAccessor for ordinal charts, and nodeIDAccessor/edgeIdAccessor for network operations.
284
+ Agent action: When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field.
285
+ - **renderChart uses static props only** (`rendering.renderchart-static-props`): MCP renderChart and semiotic/server renderChart render a single static SVG/PNG snapshot. Browser-only realtime components and future ref pushes are not renderable through that path.
286
+ Agent action: Use renderChart only with renderable HOC components and complete static data. For live behavior, return React code with a ref and do not promise MCP-rendered output.
287
+
288
+ <!-- semiotic-behavior-contracts:end -->
289
+
185
290
  ## Accessibility
186
291
 
187
292
  `role="group"` (outer) + `role="img"` (inner canvas). Keyboard: arrows navigate points, Enter cycles neighbors, Home/End/PageUp/PageDown. Shape-adaptive focus ring (`--semiotic-focus`). `accessibleTable` (default true) for sr-only data summary. Auto-detects `prefers-reduced-motion` and `forced-colors`. Hooks: `useReducedMotion()`, `useHighContrast()`.
188
293
 
189
- ## Known Pitfalls
294
+ ## Usage Notes
190
295
 
191
296
  - **Tooltip datum shape**: HOC tooltips get raw data. Frame `tooltipContent` gets wrapped — use `d.data`.
192
297
  - **Legend**: "bottom" expands margin ~80px. MultiAxisLineChart: use `legendPosition="bottom"`.
@@ -201,7 +306,7 @@ Canvas scene builders read CSS variables via `getComputedStyle` on the canvas DO
201
306
  - **Geo imports**: Always `semiotic/geo`, never `semiotic`, to avoid d3-geo in non-geo bundles.
202
307
  - **fillArea**: `fillArea={["seriesA"]}` fills named series only. Names must match `lineBy`/`colorBy` keys.
203
308
  - **hoverHighlight**: Requires `colorBy` as a string field.
204
- - **tooltip="multi"**: Shows all series at hovered X. Custom fn receives `datum.allSeries`.
309
+ - **tooltip="multi"**: Shows all series at hovered X for LineChart, AreaChart, and StackedAreaChart. Custom fn receives `datum.allSeries`.
205
310
  - **Axis config**: `frameProps.axes: [{ orient, includeMax, autoRotate, gridStyle, landmarkTicks }]`
206
311
  - **xScaleType: "time"**: Creates `scaleTime`. Required for landmark ticks with timestamps.
207
312
  - **scalePadding**: Pixel inset on scale ranges. Pass via `frameProps={{ scalePadding: 12 }}`.
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
  [![npm version](https://img.shields.io/npm/v/semiotic.svg)](https://www.npmjs.com/package/semiotic)
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-built--in-blue.svg)](https://www.typescriptlang.org/)
6
6
  [![semiotic MCP server](https://glama.ai/mcp/servers/nteract/semiotic/badges/card.svg)](https://glama.ai/mcp/servers/nteract/semiotic)
7
+ [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/nteract-semiotic-badge.png)](https://mseep.ai/app/nteract-semiotic)
7
8
 
8
9
  A React data visualization library designed for AI-assisted development.
9
10
 
@@ -356,7 +357,9 @@ const gif = await renderToAnimatedGif("line", data, { ... }, { fps: 12 })
356
357
 
357
358
  ## MCP Server
358
359
 
359
- Semiotic ships with an [MCP server](https://modelcontextprotocol.io) that lets AI coding assistants render charts, diagnose configuration problems, discover schemas, and get chart recommendations via tool calls.
360
+ mcp-name: io.github.nteract/semiotic
361
+
362
+ Semiotic ships with an [MCP server](https://modelcontextprotocol.io) that lets AI coding assistants render charts, diagnose configuration problems, discover schemas, read packaged AI guidance, and get chart recommendations via tool calls.
360
363
 
361
364
  ### Setup
362
365
 
@@ -373,17 +376,34 @@ Add to your MCP client config (e.g. `claude_desktop_config.json` for Claude Desk
373
376
  }
374
377
  ```
375
378
 
376
- No API keys or authentication required. The server runs locally via stdio.
379
+ No API keys or authentication required. The server runs locally via stdio. HTTP mode is also available for inspectors and web clients: `npx semiotic-mcp --http --port 3001`.
377
380
 
378
381
  ### Tools
379
382
 
380
383
  | Tool | Description |
381
384
  |------|-------------|
382
385
  | **`renderChart`** | Render a Semiotic chart to static SVG. Supports the components returned by `getSchema` that are marked `[renderable]`. Pass `{ component: "LineChart", props: { data: [...], xAccessor: "x", yAccessor: "y" } }`. Returns SVG string or validation errors with fix suggestions. |
383
- | **`getSchema`** | Return the prop schema for a specific component. Pass `{ component: "LineChart" }` to get its props, or omit `component` to list all 30 chart types. Use before `renderChart` to look up valid props. |
386
+ | **`getSchema`** | Return the prop schema for a specific component. Pass `{ component: "LineChart" }` to get its props, or omit `component` to list all 43 chart schemas. Components marked `[renderable]` are available through `renderChart`; realtime charts require a browser/live environment. |
384
387
  | **`suggestChart`** | Recommend chart types for a data sample. Pass `{ data: [{...}, ...] }` with 1–5 sample objects. Optionally include `intent` (`"comparison"`, `"trend"`, `"distribution"`, `"relationship"`, `"composition"`, `"geographic"`, `"network"`, `"hierarchy"`). Returns ranked suggestions with example props. |
385
388
  | **`diagnoseConfig`** | Check a chart configuration for common problems — empty data, bad dimensions, missing accessors, wrong data shape, and more. Returns a human-readable diagnostic report with actionable fixes. |
386
389
  | **`reportIssue`** | Generate a pre-filled GitHub issue URL for bug reports or feature requests. Pass `{ title: "...", body: "...", labels: ["bug"] }`. Returns a URL the user can open to submit. |
390
+ | **`applyTheme`** | List named theme presets or return ThemeProvider/CSS/token usage for a preset such as `{ name: "tufte" }`. |
391
+
392
+ ### Resources
393
+
394
+ | Resource | Description |
395
+ |----------|-------------|
396
+ | **`semiotic://schema`** | Full machine-readable component schema JSON. |
397
+ | **`semiotic://components`** | Component index showing renderable/browser-only status and MCP categories. |
398
+ | **`semiotic://system-prompt`** | Compact AI instructions with import rules, chart props, SSR guidance, and pitfalls. |
399
+ | **`semiotic://examples`** | Copy-paste chart examples by data shape. |
400
+
401
+ ### Prompts
402
+
403
+ | Prompt | Description |
404
+ |--------|-------------|
405
+ | **`build-semiotic-chart`** | Reusable workflow for choosing a chart, reading schema, diagnosing props, and rendering a preview. |
406
+ | **`debug-semiotic-chart`** | Reusable workflow for debugging invalid props, rendering failures, and issue reports. |
387
407
 
388
408
  ### Example: get schema for a component
389
409
 
@@ -453,17 +473,38 @@ Args: {
453
473
  For quick validation without an MCP client:
454
474
 
455
475
  ```bash
476
+ npx semiotic-ai --list # list components with import paths and renderability
477
+ npx semiotic-ai --list --json # machine-readable component index
478
+ npx semiotic-ai --schema GaugeChart
479
+ npx semiotic-ai --suggest '{"data":[{"category":"A","value":10}],"intent":"comparison"}'
456
480
  npx semiotic-ai --doctor # validate component + props JSON
457
481
  npx semiotic-ai --schema # dump all chart schemas
458
482
  npx semiotic-ai --compact # compact schema (fewer tokens)
459
483
  ```
460
484
 
485
+ `--doctor` uses the full `diagnoseConfig` checks when `dist` is available and falls back to schema-only validation in clean source checkouts.
486
+
487
+ ## Where to find Semiotic for AI assistants
488
+
489
+ Semiotic is indexed by AI-coding-agent documentation tools so your assistant (Claude Code, Cursor, Cline, Copilot, etc.) can pull current docs and tools without copy-paste:
490
+
491
+ - **Context7** — [context7.com/nteract/semiotic](https://context7.com/nteract/semiotic) (configured via `context7.json`)
492
+ - **DeepWiki** — [deepwiki.com/nteract/semiotic](https://deepwiki.com/nteract/semiotic)
493
+ - **GitMCP** — [gitmcp.io/nteract/semiotic](https://gitmcp.io/nteract/semiotic) (exposes the repo as an MCP endpoint directly)
494
+ - **Official MCP Registry** — search "semiotic" at [registry.modelcontextprotocol.io](https://registry.modelcontextprotocol.io)
495
+ - **Smithery** — [smithery.ai/server/nteract/semiotic](https://smithery.ai/server/nteract/semiotic)
496
+
497
+ Agent-facing API surface:
498
+
499
+ - **`CLAUDE.md`**, **`ai/schema.json`**, **`ai/behaviorContracts.cjs`** — bundled in the npm tarball (see `package.json#files`); agents that install Semiotic locally read these directly. `CLAUDE.md` is the quick-start cheat sheet (HOC props, push API, theming, usage notes); `ai/schema.json` is the JSON Schema for every chart's prop surface (43 charts); `ai/behaviorContracts.cjs` carries the agent-visible semantic rules (color precedence, push-mode requirements, ID-accessor contracts).
500
+ - [**`semiotic.nteract.io/llms.txt`**](https://semiotic.nteract.io/llms.txt) + [**`/llms-full.txt`**](https://semiotic.nteract.io/llms-full.txt) — deployed at the docs site per the [llms.txt standard](https://llmstxt.org). Agents fetch the navigation map (`llms.txt`) or the full inlined docs (`llms-full.txt`) over HTTP; they're not part of the npm package itself.
501
+
461
502
  ## Documentation
462
503
 
463
504
  [Interactive docs and examples](https://semiotic.nteract.io)
464
505
 
465
506
  - [Getting Started](https://semiotic.nteract.io/getting-started)
466
- - [Charts](https://semiotic.nteract.io/charts) — all 38 chart types with live examples
507
+ - [Charts](https://semiotic.nteract.io/charts) — chart types with live examples
467
508
  - [Frames](https://semiotic.nteract.io/frames) — full Frame API reference
468
509
  - [Features](https://semiotic.nteract.io/features) — axes, annotations, tooltips, styling, Vega-Lite translator
469
510
  - [Cookbook](https://semiotic.nteract.io/cookbook) — advanced patterns and recipes
@@ -0,0 +1,311 @@
1
+ "use strict"
2
+
3
+ // Maintenance note: when you change the agent-facing rules in this
4
+ // file (especially anything in CONTRACTS), update the parallel `rules`
5
+ // array in `context7.json` so the Context7 index stays in sync. The
6
+ // `check:context7` gate validates format (255-char-per-rule limit,
7
+ // folder references, sub-path drift vs `package.json` exports) but
8
+ // can't detect *content* drift between this file and the manifest —
9
+ // keeping them aligned is part of the same edit.
10
+
11
+ const path = require("path")
12
+ const fs = require("fs")
13
+
14
+ const DOC_MARKER_START = "<!-- semiotic-behavior-contracts:start -->"
15
+ const DOC_MARKER_END = "<!-- semiotic-behavior-contracts:end -->"
16
+
17
+ // Components whose static config requires `data` are derived from
18
+ // `ai/schema.json` rather than maintained as a hand-curated list. The schema
19
+ // already declares which components require data — duplicating that here led
20
+ // to drift (Heatmap, FunnelChart, MinimapChart, ScatterplotMatrix, and the
21
+ // hierarchy charts were schema-required but missing from the local list,
22
+ // which made `dataRequiredForUsageMode` incorrectly return `false` and
23
+ // suppressed the "data is required" error in --doctor / MCP diagnoseConfig
24
+ // even when usageMode wasn't `push`).
25
+ //
26
+ // `STATIC_DATA_COMPONENTS` stays exported as a Set for test/legacy callers
27
+ // that probe the surface, and is rebuilt from disk at module load time.
28
+ function loadStaticDataComponentsFromSchema() {
29
+ // Source layout has this file at `<repo>/ai/behaviorContracts.cjs` and the
30
+ // schema at `<repo>/ai/schema.json` — `__dirname` works directly.
31
+ // The MCP server bundles this module into `<repo>/ai/dist/mcp-server.js`
32
+ // via esbuild, so when invoked from there `__dirname` is `<repo>/ai/dist/`
33
+ // and the schema lives one directory up. Try both layouts; use whichever
34
+ // resolves first.
35
+ const candidates = [
36
+ path.join(__dirname, "schema.json"),
37
+ path.join(__dirname, "..", "schema.json"),
38
+ ]
39
+ for (const schemaPath of candidates) {
40
+ try {
41
+ const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"))
42
+ const out = new Set()
43
+ for (const tool of schema.tools || []) {
44
+ const required = tool.function?.parameters?.required || []
45
+ if (required.includes("data")) out.add(tool.function.name)
46
+ }
47
+ if (out.size > 0) return out
48
+ } catch {
49
+ // try next candidate
50
+ }
51
+ }
52
+ // Defensive fallback: if schema.json is unavailable (e.g. unusual install
53
+ // layout), fall back to an empty set so callers fail safe — they'll see
54
+ // "data not required" everywhere, which is permissive but won't crash.
55
+ return new Set()
56
+ }
57
+
58
+ const REQUIRED_COMBINATIONS = [
59
+ {
60
+ component: "StackedAreaChart",
61
+ required: ["areaBy"],
62
+ staticRequired: ["data", "areaBy"],
63
+ pushRequired: ["areaBy"],
64
+ summary: "Stacked areas need a flat data array plus areaBy to identify the stacked series.",
65
+ },
66
+ {
67
+ component: "BubbleChart",
68
+ required: ["sizeBy"],
69
+ staticRequired: ["data", "sizeBy"],
70
+ pushRequired: ["sizeBy"],
71
+ summary: "Bubbles need sizeBy in addition to x/y accessors so radius encodes data rather than a constant point size.",
72
+ },
73
+ {
74
+ component: "StackedBarChart",
75
+ required: ["stackBy"],
76
+ staticRequired: ["data", "stackBy"],
77
+ pushRequired: ["stackBy"],
78
+ summary: "Stacked bars need stackBy to split each category into stack segments.",
79
+ },
80
+ {
81
+ component: "GroupedBarChart",
82
+ required: ["groupBy"],
83
+ staticRequired: ["data", "groupBy"],
84
+ pushRequired: ["groupBy"],
85
+ summary: "Grouped bars need groupBy to split each category into side-by-side bars.",
86
+ },
87
+ {
88
+ component: "SwimlaneChart",
89
+ required: ["subcategoryAccessor"],
90
+ staticRequired: ["data", "subcategoryAccessor"],
91
+ pushRequired: ["subcategoryAccessor"],
92
+ summary: "Swimlanes need subcategoryAccessor; colorBy defaults to the same field when not provided.",
93
+ },
94
+ {
95
+ component: "GaugeChart",
96
+ required: ["value"],
97
+ staticRequired: ["value"],
98
+ pushRequired: [],
99
+ summary: "GaugeChart is value-only. thresholds, min, max, sweep, and arcWidth are optional.",
100
+ },
101
+ {
102
+ component: "ForceDirectedGraph",
103
+ required: ["nodes", "edges"],
104
+ staticRequired: ["nodes", "edges"],
105
+ pushRequired: ["nodes", "edges"],
106
+ summary: "ForceDirectedGraph schema/rendering requires nodes and edges. If an agent infers nodes from edge endpoints, it must materialize a nodes array before returning code.",
107
+ },
108
+ ]
109
+
110
+ const PUSH_MODE_COMPONENTS = [
111
+ "LineChart",
112
+ "AreaChart",
113
+ "StackedAreaChart",
114
+ "Scatterplot",
115
+ "BubbleChart",
116
+ "ConnectedScatterplot",
117
+ "BarChart",
118
+ "StackedBarChart",
119
+ "GroupedBarChart",
120
+ "SwarmPlot",
121
+ "BoxPlot",
122
+ "Histogram",
123
+ "ViolinPlot",
124
+ "RidgelinePlot",
125
+ "DotPlot",
126
+ "PieChart",
127
+ "DonutChart",
128
+ "LikertChart",
129
+ "SwimlaneChart",
130
+ "ForceDirectedGraph",
131
+ "SankeyDiagram",
132
+ "ChordDiagram",
133
+ "ProportionalSymbolMap",
134
+ "DistanceCartogram",
135
+ ]
136
+
137
+ const STATIC_DATA_COMPONENTS = loadStaticDataComponentsFromSchema()
138
+
139
+ const BEHAVIOR_CONTRACTS = [
140
+ {
141
+ id: "props.data-required-by-usage-mode",
142
+ category: "required-props",
143
+ title: "Data required by usage mode",
144
+ severity: "error",
145
+ appliesTo: {
146
+ components: PUSH_MODE_COMPONENTS,
147
+ },
148
+ summary: "Static usage (`renderChart`, MCP previews, SSR snapshots, and copy/paste examples with immediate data) requires data in props. React push mode selects live ingestion by omitting data and mutating through a ref.",
149
+ agentAction: "Pass usageMode=\"push\" to `semiotic-ai --doctor` when validating ref-based JSX with no data prop. Keep usageMode=\"static\" or omit it for renderChart/MCP/static configs where data must be present.",
150
+ },
151
+ {
152
+ id: "color.category-precedence",
153
+ category: "color",
154
+ title: "Categorical color precedence",
155
+ severity: "info",
156
+ appliesTo: {
157
+ propsAny: ["colorBy", "colorScheme"],
158
+ },
159
+ summary: "When colorBy is set, CategoryColorProvider/LinkedCharts category maps win for mapped categories. Unmapped categories fall back to explicit colorScheme, then ThemeProvider colors.categorical, then the built-in categorical fallback.",
160
+ agentAction: "Use colorBy for categorical encodings. Use CategoryColorProvider or LinkedCharts for cross-chart consistency, colorScheme for per-chart fallback palettes, and avoid frameProps style functions unless intentionally bypassing HOC color resolution.",
161
+ },
162
+ {
163
+ id: "props.required-combinations",
164
+ category: "required-props",
165
+ title: "Required prop combinations",
166
+ severity: "error",
167
+ appliesTo: {
168
+ components: REQUIRED_COMBINATIONS.map((entry) => entry.component),
169
+ },
170
+ summary: "Some chart families need semantic props beyond data. These combinations are enforced by validation/schema for static configs and remain required in push mode unless explicitly noted.",
171
+ agentAction: "Before returning code, check the selected component against the required combinations list. For push mode, omit data but keep semantic props such as areaBy, sizeBy, stackBy, and groupBy.",
172
+ combinations: REQUIRED_COMBINATIONS,
173
+ },
174
+ {
175
+ id: "streaming.push-mode-data",
176
+ category: "streaming",
177
+ title: "Push mode omits data",
178
+ severity: "warning",
179
+ appliesTo: {
180
+ components: PUSH_MODE_COMPONENTS,
181
+ },
182
+ summary: "HOC push mode is selected by omitting the data prop entirely. Passing data={[]} is static empty data and can clear/reinitialize the frame on render.",
183
+ agentAction: "For live charts, create a ref, omit data, then call ref.current.push() or pushMany(). For static renderChart/MCP snapshots, provide data because renderChart cannot push later.",
184
+ },
185
+ {
186
+ id: "streaming.ref-mutations-require-id-accessors",
187
+ category: "streaming",
188
+ title: "Ref mutations need stable IDs",
189
+ severity: "warning",
190
+ appliesTo: {
191
+ components: PUSH_MODE_COMPONENTS,
192
+ },
193
+ summary: "push() and pushMany() can append without IDs, but remove(id) and update(id, updater) require a stable ID accessor: pointIdAccessor for XY/realtime charts, dataIdAccessor for ordinal charts, and nodeIDAccessor/edgeIdAccessor for network operations.",
194
+ agentAction: "When generating code that calls remove() or update(), include the matching ID accessor and make sure pushed rows carry that ID field.",
195
+ },
196
+ {
197
+ id: "rendering.renderchart-static-props",
198
+ category: "rendering",
199
+ title: "renderChart uses static props only",
200
+ severity: "warning",
201
+ appliesTo: {},
202
+ summary: "MCP renderChart and semiotic/server renderChart render a single static SVG/PNG snapshot. Browser-only realtime components and future ref pushes are not renderable through that path.",
203
+ agentAction: "Use renderChart only with renderable HOC components and complete static data. For live behavior, return React code with a ref and do not promise MCP-rendered output.",
204
+ },
205
+ ]
206
+
207
+ function hasOwn(value, key) {
208
+ return Object.prototype.hasOwnProperty.call(value, key)
209
+ }
210
+
211
+ function normalizeProps(props) {
212
+ return props && typeof props === "object" && !Array.isArray(props) ? props : {}
213
+ }
214
+
215
+ function appliesToComponent(contract, component) {
216
+ if (!component) return !contract.appliesTo?.components
217
+ const components = contract.appliesTo?.components
218
+ return !components || components.includes(component)
219
+ }
220
+
221
+ function appliesToProps(contract, props) {
222
+ const propsAny = contract.appliesTo?.propsAny
223
+ if (!propsAny || propsAny.length === 0) return true
224
+ return propsAny.some((prop) => hasOwn(props, prop) && props[prop] !== undefined)
225
+ }
226
+
227
+ function behaviorContractsFor({ component, props } = {}) {
228
+ const normalizedProps = normalizeProps(props)
229
+ return BEHAVIOR_CONTRACTS.filter((contract) =>
230
+ appliesToComponent(contract, component) && appliesToProps(contract, normalizedProps)
231
+ )
232
+ }
233
+
234
+ function normalizeUsageMode(usageMode) {
235
+ if (usageMode === "push") return "push"
236
+ if (usageMode === "static" || usageMode === "renderChart" || usageMode === "server") return "static"
237
+ return "static"
238
+ }
239
+
240
+ function dataRequiredForUsageMode(component, usageMode) {
241
+ if (!STATIC_DATA_COMPONENTS.has(component)) return false
242
+ if (normalizeUsageMode(usageMode) === "push" && PUSH_MODE_COMPONENTS.includes(component)) return false
243
+ return true
244
+ }
245
+
246
+ function requiredCombinationsFor(component) {
247
+ return REQUIRED_COMBINATIONS.filter((entry) => !component || entry.component === component)
248
+ }
249
+
250
+ function formatRequiredCombination(entry) {
251
+ const staticRequired = entry.staticRequired || entry.required
252
+ const pushRequired = entry.pushRequired || entry.required.filter((prop) => prop !== "data")
253
+ const pushText = pushRequired.length > 0 ? pushRequired.join(" + ") : "not supported"
254
+ return `${entry.component}: static ${staticRequired.join(" + ")}; push ${pushText}. ${entry.summary}`
255
+ }
256
+
257
+ function formatDoctorBehaviorContracts(contracts) {
258
+ if (!contracts || contracts.length === 0) return ""
259
+
260
+ const lines = ["Behavior contracts:"]
261
+ for (const contract of contracts) {
262
+ lines.push(` - [${contract.id}] ${contract.summary}`)
263
+ if (contract.combinations) {
264
+ for (const combo of contract.combinations) {
265
+ lines.push(` ${formatRequiredCombination(combo)}`)
266
+ }
267
+ }
268
+ lines.push(` Action: ${contract.agentAction}`)
269
+ }
270
+ return lines.join("\n")
271
+ }
272
+
273
+ function formatBehaviorContractsMarkdown({ compact = false } = {}) {
274
+ const lines = [
275
+ compact ? "## Behavior Contracts" : "## AI Behavior Contracts",
276
+ "",
277
+ DOC_MARKER_START,
278
+ "",
279
+ "These rules are generated from `ai/behaviorContracts.cjs` and are consumed by `semiotic-ai --doctor`, MCP resources, and docs checks.",
280
+ "",
281
+ ]
282
+
283
+ for (const contract of BEHAVIOR_CONTRACTS) {
284
+ lines.push(`- **${contract.title}** (\`${contract.id}\`): ${contract.summary}`)
285
+ if (!compact) {
286
+ lines.push(` Agent action: ${contract.agentAction}`)
287
+ }
288
+ if (contract.combinations) {
289
+ const combos = contract.combinations.map(formatRequiredCombination).join(" ")
290
+ lines.push(` Required combinations: ${combos}`)
291
+ }
292
+ }
293
+
294
+ lines.push("", DOC_MARKER_END)
295
+ return lines.join("\n")
296
+ }
297
+
298
+ module.exports = {
299
+ BEHAVIOR_CONTRACTS,
300
+ DOC_MARKER_END,
301
+ DOC_MARKER_START,
302
+ PUSH_MODE_COMPONENTS,
303
+ REQUIRED_COMBINATIONS,
304
+ STATIC_DATA_COMPONENTS,
305
+ behaviorContractsFor,
306
+ dataRequiredForUsageMode,
307
+ formatBehaviorContractsMarkdown,
308
+ formatDoctorBehaviorContracts,
309
+ normalizeUsageMode,
310
+ requiredCombinationsFor,
311
+ }