paseo-plugin-genui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/LICENSE +202 -0
  2. package/OVERVIEW.md +21 -0
  3. package/README.md +77 -2
  4. package/assets/fonts/Excalifont-OFL.txt +109 -0
  5. package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +1 -0
  8. package/assets/screenshot-chat.png +0 -0
  9. package/assets/screenshot-gallery.png +0 -0
  10. package/assets/screenshot-playground.png +0 -0
  11. package/client/components/content.tsx +468 -0
  12. package/client/components/data.tsx +967 -0
  13. package/client/components/dev.tsx +998 -0
  14. package/client/components/diagram.tsx +513 -0
  15. package/client/components/inputs.tsx +994 -0
  16. package/client/components/layout.tsx +239 -0
  17. package/client/components/media.tsx +428 -0
  18. package/client/components/project.tsx +641 -0
  19. package/client/components/widgets.tsx +836 -0
  20. package/client/gallery.tsx +449 -0
  21. package/client/live-ui.tsx +525 -0
  22. package/client/loaders.ts +342 -0
  23. package/client/markdown.tsx +172 -0
  24. package/client/node-renderer.tsx +149 -0
  25. package/client/render.tsx +186 -0
  26. package/client/runtime.ts +103 -0
  27. package/client/settings.tsx +57 -0
  28. package/client/web.ts +186 -0
  29. package/gallery/Acceptance-baseline.ui +1 -0
  30. package/gallery/Acceptance-checklist.ui +14 -0
  31. package/gallery/Acceptance-evidence.ui +9 -0
  32. package/gallery/Acceptance.ui +1 -0
  33. package/gallery/Badge.ui +3 -0
  34. package/gallery/Benchmark-context.ui +8 -0
  35. package/gallery/Benchmark-hyperfine.ui +1 -0
  36. package/gallery/Benchmark-runs-list.ui +9 -0
  37. package/gallery/Benchmark-runs.ui +1 -0
  38. package/gallery/Benchmark-vitest.ui +1 -0
  39. package/gallery/Benchmark.ui +9 -0
  40. package/gallery/Board-bb-tasks.ui +1 -0
  41. package/gallery/Board-gh-issues.ui +1 -0
  42. package/gallery/Board-gh-prs.ui +1 -0
  43. package/gallery/Board.ui +12 -0
  44. package/gallery/Button-destructive.ui +4 -0
  45. package/gallery/Button.ui +5 -0
  46. package/gallery/Calendar.ui +15 -0
  47. package/gallery/CalendarHeatmap-short.ui +11 -0
  48. package/gallery/CalendarHeatmap.ui +4 -0
  49. package/gallery/Callout.ui +4 -0
  50. package/gallery/Card.ui +4 -0
  51. package/gallery/Carousel.ui +8 -0
  52. package/gallery/Changes-paths.ui +1 -0
  53. package/gallery/Changes-uncommitted.ui +1 -0
  54. package/gallery/Changes.ui +1 -0
  55. package/gallery/Chart-area.ui +3 -0
  56. package/gallery/Chart-funnel.ui +3 -0
  57. package/gallery/Chart-heatmap.ui +11 -0
  58. package/gallery/Chart-pie.ui +3 -0
  59. package/gallery/Chart-radar.ui +3 -0
  60. package/gallery/Chart-scatter.ui +4 -0
  61. package/gallery/Chart-stacked.ui +5 -0
  62. package/gallery/Chart.ui +3 -0
  63. package/gallery/Checklist.ui +6 -0
  64. package/gallery/Choice.ui +8 -0
  65. package/gallery/Code.ui +3 -0
  66. package/gallery/Commits-limit.ui +1 -0
  67. package/gallery/Commits.ui +1 -0
  68. package/gallery/Compare-conditions.ui +4 -0
  69. package/gallery/Compare-three.ui +7 -0
  70. package/gallery/Compare.ui +4 -0
  71. package/gallery/Coverage-go.ui +1 -0
  72. package/gallery/Coverage-lcov.ui +1 -0
  73. package/gallery/Coverage-python.ui +1 -0
  74. package/gallery/Coverage.ui +1 -0
  75. package/gallery/DateInput.ui +5 -0
  76. package/gallery/Decision-percent.ui +10 -0
  77. package/gallery/Decision.ui +12 -0
  78. package/gallery/Dependencies-audit.ui +1 -0
  79. package/gallery/Dependencies-both.ui +1 -0
  80. package/gallery/Dependencies-list.ui +7 -0
  81. package/gallery/Dependencies-pip.ui +1 -0
  82. package/gallery/Dependencies-pnpm.ui +1 -0
  83. package/gallery/Dependencies.ui +1 -0
  84. package/gallery/Details.ui +4 -0
  85. package/gallery/Diagram-flow.ui +16 -0
  86. package/gallery/Diagram-sequence.ui +15 -0
  87. package/gallery/Diagram.ui +16 -0
  88. package/gallery/Diff.ui +3 -0
  89. package/gallery/Divider.ui +5 -0
  90. package/gallery/EditableTable.ui +9 -0
  91. package/gallery/Entity-list.ui +6 -0
  92. package/gallery/Entity.ui +8 -0
  93. package/gallery/Environments.ui +1 -0
  94. package/gallery/Estimate-points.ui +6 -0
  95. package/gallery/Estimate.ui +8 -0
  96. package/gallery/File.ui +4 -0
  97. package/gallery/Findings-biome.ui +1 -0
  98. package/gallery/Findings-eslint.ui +1 -0
  99. package/gallery/Findings-oxlint.ui +1 -0
  100. package/gallery/Findings-review.ui +1 -0
  101. package/gallery/Findings-ruff.ui +1 -0
  102. package/gallery/Findings-sarif.ui +1 -0
  103. package/gallery/Findings-ship.ui +11 -0
  104. package/gallery/Findings-tsc.ui +1 -0
  105. package/gallery/Findings.ui +7 -0
  106. package/gallery/Flashcards.ui +3 -0
  107. package/gallery/FollowUps.ui +4 -0
  108. package/gallery/Form.ui +8 -0
  109. package/gallery/Gallery-links.ui +8 -0
  110. package/gallery/Gallery-strip.ui +14 -0
  111. package/gallery/Gallery.ui +10 -0
  112. package/gallery/Gauge.ui +7 -0
  113. package/gallery/Grid.ui +8 -0
  114. package/gallery/Hotspots-photo.ui +8 -0
  115. package/gallery/Hotspots.ui +11 -0
  116. package/gallery/Icon.ui +5 -0
  117. package/gallery/Image.ui +4 -0
  118. package/gallery/ImageSearch-aspects.ui +6 -0
  119. package/gallery/ImageSearch.ui +4 -0
  120. package/gallery/Input.ui +5 -0
  121. package/gallery/Json-api.ui +1 -0
  122. package/gallery/Json.ui +8 -0
  123. package/gallery/KeyValue.ui +3 -0
  124. package/gallery/List-dots.ui +8 -0
  125. package/gallery/List.ui +8 -0
  126. package/gallery/ListEditor.ui +5 -0
  127. package/gallery/Logs-jsonl.ui +1 -0
  128. package/gallery/Logs-lines.ui +11 -0
  129. package/gallery/Logs-python.ui +1 -0
  130. package/gallery/Logs.ui +1 -0
  131. package/gallery/Map-city.ui +8 -0
  132. package/gallery/Map.ui +8 -0
  133. package/gallery/Metric.ui +3 -0
  134. package/gallery/OptionCards-multi.ui +10 -0
  135. package/gallery/OptionCards.ui +10 -0
  136. package/gallery/Pipeline-checks.ui +1 -0
  137. package/gallery/Pipeline-passed.ui +1 -0
  138. package/gallery/Pipeline-running.ui +1 -0
  139. package/gallery/Pipeline-stages.ui +13 -0
  140. package/gallery/Pipeline.ui +1 -0
  141. package/gallery/Plan.ui +9 -0
  142. package/gallery/Progress.ui +4 -0
  143. package/gallery/Provenance-deploy.ui +6 -0
  144. package/gallery/Provenance-no-receipt.ui +1 -0
  145. package/gallery/Provenance.ui +1 -0
  146. package/gallery/PullRequest.ui +1 -0
  147. package/gallery/Quiz.ui +6 -0
  148. package/gallery/Radio.ui +5 -0
  149. package/gallery/Rating.ui +5 -0
  150. package/gallery/Roadmap-sprint.ui +8 -0
  151. package/gallery/Roadmap-year.ui +9 -0
  152. package/gallery/Roadmap.ui +12 -0
  153. package/gallery/Row.ui +3 -0
  154. package/gallery/SegmentedBar-status.ui +8 -0
  155. package/gallery/SegmentedBar.ui +8 -0
  156. package/gallery/Select.ui +5 -0
  157. package/gallery/Slider.ui +7 -0
  158. package/gallery/Snippet.ui +3 -0
  159. package/gallery/Sources-chips.ui +4 -0
  160. package/gallery/Sources.ui +8 -0
  161. package/gallery/Sparkline.ui +6 -0
  162. package/gallery/Stack.ui +7 -0
  163. package/gallery/StackTrace-aggregate.ui +1 -0
  164. package/gallery/StackTrace-browser.ui +1 -0
  165. package/gallery/StackTrace-go.ui +1 -0
  166. package/gallery/StackTrace-java.ui +1 -0
  167. package/gallery/StackTrace-python.ui +1 -0
  168. package/gallery/StackTrace-rust.ui +1 -0
  169. package/gallery/StackTrace.ui +1 -0
  170. package/gallery/Status.ui +9 -0
  171. package/gallery/Stepper.ui +5 -0
  172. package/gallery/Steps.ui +8 -0
  173. package/gallery/Swatches-long-names.ui +4 -0
  174. package/gallery/Swatches-picker.ui +5 -0
  175. package/gallery/Swatches.ui +10 -0
  176. package/gallery/Table.ui +7 -0
  177. package/gallery/Tabs.ui +8 -0
  178. package/gallery/Tests-junit.ui +1 -0
  179. package/gallery/Tests-vitest-junit.ui +1 -0
  180. package/gallery/Tests-vitest.ui +1 -0
  181. package/gallery/Tests.ui +9 -0
  182. package/gallery/Text.ui +4 -0
  183. package/gallery/TextArea.ui +5 -0
  184. package/gallery/Threads-ids.ui +1 -0
  185. package/gallery/Threads.ui +1 -0
  186. package/gallery/Timeline.ui +8 -0
  187. package/gallery/Timer.ui +4 -0
  188. package/gallery/Toggle.ui +5 -0
  189. package/gallery/Transcript-text.ui +1 -0
  190. package/gallery/Transcript-untimed.ui +1 -0
  191. package/gallery/Transcript-whisper.ui +1 -0
  192. package/gallery/Transcript.ui +1 -0
  193. package/gallery/Tree.ui +3 -0
  194. package/gallery/Treemap-du.ui +1 -0
  195. package/gallery/Treemap-esbuild.ui +1 -0
  196. package/gallery/Treemap-tree.ui +7 -0
  197. package/gallery/Treemap.ui +18 -0
  198. package/gallery/Walkthrough.ui +9 -0
  199. package/gallery/Waterfall-har.ui +1 -0
  200. package/gallery/Waterfall-trace.ui +1 -0
  201. package/gallery/Waterfall.ui +11 -0
  202. package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
  203. package/gallery/data/acceptance/coil-gate.json +153 -0
  204. package/gallery/data/acceptance/orbit-baseline.json +163 -0
  205. package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
  206. package/gallery/data/acceptance/shots/gate-light.png +0 -0
  207. package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
  208. package/gallery/data/activity.json +1 -0
  209. package/gallery/data/bb-tasks-orb.json +420 -0
  210. package/gallery/data/biome.json +229 -0
  211. package/gallery/data/compare/status-after.png +0 -0
  212. package/gallery/data/compare/status-before.png +0 -0
  213. package/gallery/data/compare/status-dark-2x.png +0 -0
  214. package/gallery/data/compare/status-rerender.png +0 -0
  215. package/gallery/data/coverage-py.json +1 -0
  216. package/gallery/data/coverage-summary.json +1 -0
  217. package/gallery/data/coverage.out +23 -0
  218. package/gallery/data/deploys.json +1 -0
  219. package/gallery/data/device-runs.json +285 -0
  220. package/gallery/data/du-node-modules.txt +359 -0
  221. package/gallery/data/esbuild-meta.json +292 -0
  222. package/gallery/data/eslint.json +266 -0
  223. package/gallery/data/eslint.sarif +343 -0
  224. package/gallery/data/findings-review.json +76 -0
  225. package/gallery/data/gh-issues-nextjs.json +293 -0
  226. package/gallery/data/gh-pr-checks.json +272 -0
  227. package/gallery/data/gh-prs-nextjs.json +419 -0
  228. package/gallery/data/gh-run-failed.json +1 -0
  229. package/gallery/data/gh-run-passed.json +1 -0
  230. package/gallery/data/gh-run-running.json +1 -0
  231. package/gallery/data/github-release.json +57 -0
  232. package/gallery/data/hyperfine-startup.json +187 -0
  233. package/gallery/data/lcov.info +234 -0
  234. package/gallery/data/logs/media-sync.log +127 -0
  235. package/gallery/data/logs/thumbnailer.jsonl +17 -0
  236. package/gallery/data/logs/vitest-run.log +89 -0
  237. package/gallery/data/npm-audit.json +672 -0
  238. package/gallery/data/npm-outdated.json +179 -0
  239. package/gallery/data/oxlint.json +156 -0
  240. package/gallery/data/pip-outdated.json +68 -0
  241. package/gallery/data/pnpm-outdated.json +58 -0
  242. package/gallery/data/provenance/devicectl-install.json +35 -0
  243. package/gallery/data/provenance/install-timeout.log +4 -0
  244. package/gallery/data/provenance/iphone-install.json +72 -0
  245. package/gallery/data/provenance/iphone-no-receipt.json +57 -0
  246. package/gallery/data/provenance/live-home.jpg +0 -0
  247. package/gallery/data/provenance/live-probe.txt +10 -0
  248. package/gallery/data/provenance/xcodebuild-2811.log +19 -0
  249. package/gallery/data/provenance/xcodebuild-4712.log +19 -0
  250. package/gallery/data/pytest-junit.xml +64 -0
  251. package/gallery/data/python-org-trace.json +35 -0
  252. package/gallery/data/python-org.har +31 -0
  253. package/gallery/data/retry.ts +13 -0
  254. package/gallery/data/ruff.json +261 -0
  255. package/gallery/data/src/durations.py +63 -0
  256. package/gallery/data/src/json-logic.ts +156 -0
  257. package/gallery/data/src/mapMath.ts +135 -0
  258. package/gallery/data/src/semver/bump.go +73 -0
  259. package/gallery/data/traces/browser-error.json +5 -0
  260. package/gallery/data/traces/go/main.go +27 -0
  261. package/gallery/data/traces/go-panic.txt +16 -0
  262. package/gallery/data/traces/java-caused-by.txt +15 -0
  263. package/gallery/data/traces/node-aggregate.txt +30 -0
  264. package/gallery/data/traces/node-cause.txt +21 -0
  265. package/gallery/data/traces/py/deploy.json +5 -0
  266. package/gallery/data/traces/py/deploy.py +17 -0
  267. package/gallery/data/traces/py/settings.py +19 -0
  268. package/gallery/data/traces/python-chain.txt +30 -0
  269. package/gallery/data/traces/rust/main.rs +11 -0
  270. package/gallery/data/traces/rust-panic.txt +16 -0
  271. package/gallery/data/traces/shop/cart.mjs +19 -0
  272. package/gallery/data/traces/shop/pricing.mjs +12 -0
  273. package/gallery/data/traces/shop/status.mjs +9 -0
  274. package/gallery/data/traces/web/app.js +11 -0
  275. package/gallery/data/traces/web/index.html +1 -0
  276. package/gallery/data/traces/web/search.js +10 -0
  277. package/gallery/data/transcript/call-noisy.wav +0 -0
  278. package/gallery/data/transcript/call.json +362 -0
  279. package/gallery/data/transcript/call.wav +0 -0
  280. package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
  281. package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
  282. package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
  283. package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
  284. package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
  285. package/gallery/data/tsc.txt +4 -0
  286. package/gallery/data/vitest-bench.json +119 -0
  287. package/gallery/data/vitest-junit.xml +133 -0
  288. package/gallery/data/vitest-report.json +612 -0
  289. package/gallery/images/bell.svg +10 -0
  290. package/gallery/images/bike.svg +47 -0
  291. package/gallery/images/footer-planet.webp +0 -0
  292. package/gallery/images/front-light.svg +13 -0
  293. package/gallery/images/golden-hour.webp +0 -0
  294. package/gallery/images/option-flowers.webp +0 -0
  295. package/gallery/images/option-hilltop.webp +0 -0
  296. package/gallery/images/option-meadow.webp +0 -0
  297. package/gallery/images/option-sky.webp +0 -0
  298. package/gallery/images/u-lock.svg +10 -0
  299. package/index.client.tsx +164 -0
  300. package/index.server.ts +216 -0
  301. package/package.json +50 -4
  302. package/paseo-plugin.json +14 -0
  303. package/server/data-source.ts +407 -0
  304. package/server/project.ts +981 -0
  305. package/server/web/address.ts +57 -0
  306. package/server/web/cache.ts +85 -0
  307. package/server/web/commons.ts +126 -0
  308. package/server/web/geocode.ts +40 -0
  309. package/server/web/image.ts +35 -0
  310. package/server/web/lookups.ts +248 -0
  311. package/server/web/net.ts +210 -0
  312. package/shared/core/acceptance.ts +744 -0
  313. package/shared/core/ast.ts +76 -0
  314. package/shared/core/benchmark.ts +743 -0
  315. package/shared/core/catalog.ts +733 -0
  316. package/shared/core/compare.ts +225 -0
  317. package/shared/core/coverage.ts +456 -0
  318. package/shared/core/data.ts +334 -0
  319. package/shared/core/decisions.ts +527 -0
  320. package/shared/core/dependencies.ts +604 -0
  321. package/shared/core/diagnostics.ts +24 -0
  322. package/shared/core/environments.ts +212 -0
  323. package/shared/core/evaluate.ts +737 -0
  324. package/shared/core/fields.ts +66 -0
  325. package/shared/core/findings.ts +1172 -0
  326. package/shared/core/form.ts +69 -0
  327. package/shared/core/index.ts +44 -0
  328. package/shared/core/junit.ts +252 -0
  329. package/shared/core/lexer.ts +275 -0
  330. package/shared/core/logs.ts +496 -0
  331. package/shared/core/markdown.ts +2374 -0
  332. package/shared/core/parser.ts +355 -0
  333. package/shared/core/perf-units.ts +228 -0
  334. package/shared/core/pipeline.ts +679 -0
  335. package/shared/core/project.ts +472 -0
  336. package/shared/core/provenance.ts +725 -0
  337. package/shared/core/stack-trace.ts +718 -0
  338. package/shared/core/streaming.ts +136 -0
  339. package/shared/core/test-results.ts +562 -0
  340. package/shared/core/thread-activity.ts +203 -0
  341. package/shared/core/tracking.ts +887 -0
  342. package/shared/core/transcript.ts +1364 -0
  343. package/shared/core/treemap.ts +446 -0
  344. package/shared/core/types.ts +70 -0
  345. package/shared/core/validate.ts +574 -0
  346. package/shared/core/waterfall.ts +536 -0
  347. package/shared/diagram/flowchart.ts +205 -0
  348. package/shared/diagram/layout.ts +440 -0
  349. package/shared/diagram/markdown.ts +135 -0
  350. package/shared/diagram/sequence.ts +122 -0
  351. package/shared/diagram/zoom.ts +114 -0
  352. package/shared/fallback.ts +120 -0
  353. package/shared/genui.test.ts +122 -0
  354. package/shared/instructions.ts +144 -0
  355. package/shared/logic/calendarMath.ts +130 -0
  356. package/shared/logic/chartMath.ts +289 -0
  357. package/shared/logic/choose-logic.ts +196 -0
  358. package/shared/logic/compare-logic.ts +270 -0
  359. package/shared/logic/content-model.ts +227 -0
  360. package/shared/logic/environments-logic.ts +53 -0
  361. package/shared/logic/explain-logic.ts +315 -0
  362. package/shared/logic/fieldLogic.ts +138 -0
  363. package/shared/logic/gallery-search.ts +71 -0
  364. package/shared/logic/icons.ts +322 -0
  365. package/shared/logic/json-logic.ts +153 -0
  366. package/shared/logic/mapMath.ts +133 -0
  367. package/shared/logic/project-logic.ts +39 -0
  368. package/shared/logic/references-model.ts +201 -0
  369. package/shared/logic/sketch.ts +165 -0
  370. package/shared/logic/supersession.ts +77 -0
  371. package/shared/logic/values.ts +133 -0
  372. package/shared/logic/widget-logic.ts +163 -0
  373. package/shared/rpc.ts +252 -0
  374. package/shared/segment.ts +81 -0
  375. package/shared/settings.ts +12 -0
@@ -0,0 +1,574 @@
1
+ import type { CallExpr, Decl, Expr, Program, Statement } from "./ast";
2
+ import {
3
+ ACTIONS,
4
+ COMPONENTS,
5
+ FORBIDDEN_PROPERTIES,
6
+ FUNCTIONS,
7
+ METHODS,
8
+ type ComponentSpec,
9
+ type FunctionName,
10
+ type ParamKind,
11
+ type ParamSpec,
12
+ type StateKind,
13
+ } from "./catalog";
14
+ import type { DiagnosticList, Pos } from "./diagnostics";
15
+ import { loadPath } from "./data";
16
+ import type { Value } from "./types";
17
+
18
+ // Maps, not the catalog objects, so spec-controlled names like `constructor`
19
+ // never hit Object.prototype.
20
+ export const COMPONENT_SPECS: ReadonlyMap<string, ComponentSpec> = new Map(Object.entries(COMPONENTS));
21
+ export const ACTION_SPECS: ReadonlyMap<string, ComponentSpec> = new Map(Object.entries(ACTIONS));
22
+
23
+ type MethodName = (typeof METHODS)[keyof typeof METHODS][number];
24
+
25
+ /** Methods whose single argument is an arrow `(item, index?) => ...`. */
26
+ export const LAMBDA_METHODS: ReadonlySet<string> = new Set(["map", "filter", "find", "some", "every"]);
27
+
28
+ type Arity = readonly [min: number, max: number];
29
+
30
+ const METHOD_ARITY: ReadonlyMap<string, Arity> = new Map(
31
+ Object.entries({
32
+ map: [1, 1], filter: [1, 1], find: [1, 1], some: [1, 1], every: [1, 1],
33
+ join: [0, 1], slice: [0, 2], includes: [1, 1], indexOf: [1, 1],
34
+ split: [1, 1], trim: [0, 0], toUpperCase: [0, 0], toLowerCase: [0, 0],
35
+ startsWith: [1, 1], endsWith: [1, 1], toFixed: [0, 1],
36
+ } satisfies Record<MethodName, Arity>),
37
+ );
38
+
39
+ const FUNCTION_ARITY: ReadonlyMap<string, Arity> = new Map(
40
+ Object.entries({
41
+ round: [1, 2], fmt: [1, 2], sum: [1, 1], min: [1, Infinity], max: [1, Infinity], abs: [1, 1], Load: [1, 1],
42
+ keys: [1, 1], values: [1, 1], entries: [1, 1], floor: [1, 1], ceil: [1, 1], sqrt: [1, 1], pow: [2, 2],
43
+ } satisfies Record<FunctionName, Arity>),
44
+ );
45
+
46
+ const ACTION_LIST = [...ACTION_SPECS].map(([name, spec]) => signature(name, spec)).join(", ");
47
+ const ARROW_ONLY =
48
+ "arrow functions can only be declared as a named function (`share = p => ...`) or passed to map, filter, find, some or every";
49
+
50
+ /** The type of a value, as named in diagnostics. */
51
+ export type Kind = "null" | "boolean" | "number" | "string" | "list" | "object" | "node" | "action" | "binding";
52
+
53
+ export const DESCRIPTIONS: Record<Kind, string> = {
54
+ null: "null",
55
+ boolean: "a boolean",
56
+ number: "a number",
57
+ string: "text",
58
+ list: "a list",
59
+ object: "an object",
60
+ node: "a component",
61
+ action: "an action",
62
+ binding: "a state binding",
63
+ };
64
+
65
+ export const EXPECTED: Record<ParamKind, string> = {
66
+ string: "text",
67
+ text: "text",
68
+ number: "a number",
69
+ boolean: "true or false",
70
+ enum: "one of its allowed values",
71
+ nodes: "components or text",
72
+ list: "a list",
73
+ series: "a list of numbers or {name: [numbers]}",
74
+ binding: "a `$state`",
75
+ actions: "actions",
76
+ any: "a value",
77
+ };
78
+
79
+ const STATE_KINDS: Record<StateKind, { readonly fits: readonly Kind[]; readonly needs: string; readonly example: string }> = {
80
+ text: { fits: ["string"], needs: "a text", example: '""' },
81
+ number: { fits: ["number"], needs: "a number", example: "0" },
82
+ boolean: { fits: ["boolean"], needs: "a boolean", example: "false" },
83
+ scalar: { fits: ["string", "number", "boolean"], needs: "a text, number or boolean", example: '""' },
84
+ list: { fits: ["list"], needs: "a list", example: "[]" },
85
+ };
86
+
87
+ /** Why `kind` cannot be bound to `param` (e.g. "`Checklist` value needs a list state, ..."), or null if it can. */
88
+ export function stateMismatch(label: string, param: ParamSpec, state: string, kind: Kind, verb: string): string | null {
89
+ const holds = param.holds;
90
+ if (holds === undefined || kind === "null" || STATE_KINDS[holds].fits.includes(kind)) return null;
91
+ const { needs, example } = STATE_KINDS[holds];
92
+ return `${label} needs ${needs} state, but \`$${state}\` ${verb} ${DESCRIPTIONS[kind]}; declare it as \`$${state} = ${example}\``;
93
+ }
94
+
95
+ /**
96
+ * A lossless reading of a state value an input can't hold as-is, or
97
+ * undefined when there is none: text or a number where a list is needed
98
+ * (`"a"` → `["a"]`, `""` → `[]`), numeric text where a number is, a number or
99
+ * boolean where text is. Models often declare `$picked = ""` for a Checklist;
100
+ * reading it this way keeps the UI working instead of failing it.
101
+ */
102
+ export function coerceState(holds: StateKind | undefined, value: Value): Value | undefined {
103
+ const scalar = typeof value === "string" || typeof value === "number" || typeof value === "boolean";
104
+ if (holds === "list" && scalar) return value === "" ? [] : [value];
105
+ if (holds === "number" && typeof value === "string" && /^\s*-?\d+(\.\d+)?\s*$/.test(value)) return Number(value);
106
+ if (holds === "text" && (typeof value === "number" || typeof value === "boolean")) return String(value);
107
+ return undefined;
108
+ }
109
+
110
+ /** Params whose literal arguments can be type-checked before evaluation. */
111
+ const LITERAL_KINDS: Partial<Record<ParamKind, Kind>> = { list: "list", number: "number", boolean: "boolean" };
112
+
113
+ /** The kind an expression always evaluates to, when knowable without evaluating it. */
114
+ function staticKind(expr: Expr): Kind | undefined {
115
+ switch (expr.type) {
116
+ case "literal":
117
+ return expr.value === null ? "null" : (typeof expr.value as "string" | "number" | "boolean");
118
+ case "template":
119
+ return "string";
120
+ case "list":
121
+ return "list";
122
+ case "object":
123
+ return "object";
124
+ case "unary":
125
+ return expr.op === "!" ? "boolean" : "number";
126
+ default:
127
+ return undefined;
128
+ }
129
+ }
130
+
131
+ function signature(name: string, spec: ComponentSpec): string {
132
+ const params = spec.params.map(
133
+ (param) => `${param.kind === "binding" ? "$" : ""}${param.name}${param.optional ? "?" : ""}`,
134
+ );
135
+ return `${name}(${params.join(", ")})`;
136
+ }
137
+
138
+ function arityText([min, max]: Arity): string {
139
+ if (max === Infinity) return `at least ${min} argument${min === 1 ? "" : "s"}`;
140
+ if (min === max) return `${min} argument${min === 1 ? "" : "s"}`;
141
+ return `${min} to ${max} arguments`;
142
+ }
143
+
144
+ function editDistance(a: string, b: string): number {
145
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
146
+ for (let i = 1; i <= a.length; i++) {
147
+ const current = [i];
148
+ for (let j = 1; j <= b.length; j++) {
149
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
150
+ current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + cost);
151
+ }
152
+ previous = current;
153
+ }
154
+ return previous[b.length];
155
+ }
156
+
157
+ /** " Did you mean `x`?" for the closest candidate within edit distance 2, else "". */
158
+ function didYouMean(name: string, candidates: Iterable<string>, render = (candidate: string) => candidate): string {
159
+ let best: string | undefined;
160
+ let bestDistance = 3;
161
+ for (const candidate of candidates) {
162
+ const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
163
+ if (distance < bestDistance && distance < name.length) {
164
+ best = candidate;
165
+ bestDistance = distance;
166
+ }
167
+ }
168
+ return best === undefined ? "" : ` Did you mean \`${render(best)}\`?`;
169
+ }
170
+
171
+ interface Ref {
172
+ readonly name: string;
173
+ readonly pos: Pos;
174
+ }
175
+
176
+ /** Everything one top-level expression reads. */
177
+ interface Refs {
178
+ readonly named: Ref[];
179
+ readonly states: Ref[];
180
+ }
181
+
182
+ /** Checks parsed statements against the catalog; reports into `diagnostics`. */
183
+ export function validate(statements: readonly Statement[], diagnostics: DiagnosticList): Program {
184
+ return new Validator(diagnostics).run(statements);
185
+ }
186
+
187
+ class Validator {
188
+ private readonly states = new Map<string, { readonly index: number; readonly decl: Decl }>();
189
+ private readonly named = new Map<string, Decl>();
190
+
191
+ constructor(private readonly diagnostics: DiagnosticList) {}
192
+
193
+ run(statements: readonly Statement[]): Program {
194
+ const stateDecls: Decl[] = [];
195
+ const roots: Expr[] = [];
196
+ for (const statement of statements) {
197
+ if (statement.kind === "root") {
198
+ roots.push(statement.value);
199
+ continue;
200
+ }
201
+ this.forbidden(statement.name, statement.pos);
202
+ const isState = statement.kind === "state";
203
+ const previous = isState ? this.states.get(statement.name)?.decl : this.named.get(statement.name);
204
+ if (previous !== undefined) {
205
+ const label = isState ? `$${statement.name}` : statement.name;
206
+ this.diagnostics.report(statement.pos, `\`${label}\` is already declared on line ${previous.pos.line}`);
207
+ } else if (isState) {
208
+ this.states.set(statement.name, { index: stateDecls.length, decl: statement });
209
+ stateDecls.push(statement);
210
+ } else {
211
+ this.named.set(statement.name, statement);
212
+ }
213
+ }
214
+
215
+ const used = new Set<string>();
216
+ const namedRefs = new Map<string, Refs>();
217
+ for (const [name, decl] of this.named) {
218
+ const refs = this.declRefs(decl);
219
+ namedRefs.set(name, refs);
220
+ for (const ref of refs.named) if (ref.name !== name) used.add(ref.name);
221
+ }
222
+ for (const root of roots) for (const ref of this.refs(root).named) used.add(ref.name);
223
+
224
+ const rootDecl = this.named.get("root");
225
+ if (roots.length === 0) {
226
+ if (rootDecl === undefined) {
227
+ this.diagnostics.report(
228
+ { line: 1, column: 1 },
229
+ "Nothing to render: add a bare component expression such as Card(...)",
230
+ );
231
+ } else {
232
+ roots.push({ type: "ident", name: "root", pos: rootDecl.pos });
233
+ used.add("root");
234
+ }
235
+ }
236
+
237
+ this.checkCycles(namedRefs);
238
+ const statesOf = this.transitiveStates(namedRefs);
239
+ stateDecls.forEach((decl, index) => {
240
+ const refs = this.refs(decl.value);
241
+ for (const ref of refs.named) used.add(ref.name);
242
+ for (const ref of refs.states) {
243
+ const target = this.states.get(ref.name);
244
+ if (target === undefined || target.index < index) continue;
245
+ this.diagnostics.report(
246
+ ref.pos,
247
+ target.index === index
248
+ ? `\`$${decl.name}\` cannot use itself in its initial value`
249
+ : `\`$${ref.name}\` is declared after \`$${decl.name}\`; initial values can only use earlier state`,
250
+ );
251
+ }
252
+ for (const ref of refs.named) {
253
+ const late = [...statesOf(ref.name)].find((name) => (this.states.get(name)?.index ?? -1) >= index);
254
+ if (late !== undefined) {
255
+ this.diagnostics.report(
256
+ ref.pos,
257
+ `\`${ref.name}\` reads \`$${late}\`, which is not initialized before \`$${decl.name}\``,
258
+ );
259
+ }
260
+ }
261
+ });
262
+
263
+ for (const [name, decl] of this.named) {
264
+ if (!used.has(name)) this.diagnostics.report(decl.pos, `\`${name}\` is never used`, "warning");
265
+ }
266
+ return { states: stateDecls, named: this.named, roots };
267
+ }
268
+
269
+ private refs(expr: Expr): Refs {
270
+ const refs: Refs = { named: [], states: [] };
271
+ this.check(expr, [], refs);
272
+ return refs;
273
+ }
274
+
275
+ /** A named value's refs; a named function (`share = p => ...`) is checked with its parameters in scope. */
276
+ private declRefs(decl: Decl): Refs {
277
+ if (decl.value.type !== "arrow") return this.refs(decl.value);
278
+ if (COMPONENT_SPECS.has(decl.name) || ACTION_SPECS.has(decl.name) || FUNCTION_ARITY.has(decl.name)) {
279
+ this.diagnostics.report(decl.pos, `\`${decl.name}\` is a built-in name; give your function another name`);
280
+ }
281
+ const refs: Refs = { named: [], states: [] };
282
+ this.check(decl.value.body, decl.value.params, refs);
283
+ return refs;
284
+ }
285
+
286
+ private checkCycles(namedRefs: ReadonlyMap<string, Refs>): void {
287
+ const status = new Map<string, "visiting" | "done">();
288
+ const path: string[] = [];
289
+ const visit = (name: string): void => {
290
+ const current = status.get(name);
291
+ if (current === "done") return;
292
+ if (current === "visiting") {
293
+ const cycle = [...path.slice(path.indexOf(name)), name];
294
+ this.diagnostics.report(this.named.get(name)!.pos, `\`${name}\` depends on itself: ${cycle.join(" → ")}`);
295
+ return;
296
+ }
297
+ status.set(name, "visiting");
298
+ path.push(name);
299
+ for (const ref of namedRefs.get(name)?.named ?? []) if (this.named.has(ref.name)) visit(ref.name);
300
+ path.pop();
301
+ status.set(name, "done");
302
+ };
303
+ for (const name of this.named.keys()) visit(name);
304
+ }
305
+
306
+ /** State names each named value reads, directly or through other named values. */
307
+ private transitiveStates(namedRefs: ReadonlyMap<string, Refs>): (name: string) => ReadonlySet<string> {
308
+ const memo = new Map<string, Set<string>>();
309
+ const statesOf = (name: string): ReadonlySet<string> => {
310
+ const cached = memo.get(name);
311
+ if (cached !== undefined) return cached;
312
+ const result = new Set<string>();
313
+ memo.set(name, result); // cycles were reported; this stops the recursion
314
+ const refs = namedRefs.get(name);
315
+ for (const ref of refs?.states ?? []) result.add(ref.name);
316
+ for (const ref of refs?.named ?? []) for (const state of statesOf(ref.name)) result.add(state);
317
+ return result;
318
+ };
319
+ return statesOf;
320
+ }
321
+
322
+ private forbidden(name: string, pos: Pos): void {
323
+ if ((FORBIDDEN_PROPERTIES as readonly string[]).includes(name)) {
324
+ this.diagnostics.report(pos, `\`${name}\` is not accessible`);
325
+ }
326
+ }
327
+
328
+ private check(expr: Expr, params: readonly string[], refs: Refs): void {
329
+ switch (expr.type) {
330
+ case "literal":
331
+ return;
332
+ case "template":
333
+ for (const part of expr.parts) this.check(part, params, refs);
334
+ return;
335
+ case "state":
336
+ if (!this.states.has(expr.name)) {
337
+ const hint = this.named.has(expr.name)
338
+ ? ` \`${expr.name}\` is a named value; write it without \`$\`.`
339
+ : didYouMean(expr.name, this.states.keys(), (name) => `$${name}`) ||
340
+ ` Declare it first, as in \`$${expr.name} = 0\`.`;
341
+ this.diagnostics.report(expr.pos, `unknown state \`$${expr.name}\`.${hint}`);
342
+ }
343
+ refs.states.push(expr);
344
+ return;
345
+ case "ident":
346
+ if (params.includes(expr.name)) return;
347
+ if (this.named.has(expr.name)) {
348
+ if (this.named.get(expr.name)!.value.type === "arrow") {
349
+ this.diagnostics.report(expr.pos, `\`${expr.name}\` is a function; call it, as in \`${expr.name}(...)\``);
350
+ }
351
+ refs.named.push(expr);
352
+ return;
353
+ }
354
+ this.diagnostics.report(expr.pos, this.unknownName(expr.name, params));
355
+ return;
356
+ case "list":
357
+ for (const item of expr.items) this.check(item, params, refs);
358
+ return;
359
+ case "object":
360
+ for (const entry of expr.entries) {
361
+ this.forbidden(entry.key, entry.pos);
362
+ this.check(entry.value, params, refs);
363
+ }
364
+ return;
365
+ case "unary":
366
+ this.check(expr.operand, params, refs);
367
+ return;
368
+ case "binary":
369
+ this.check(expr.left, params, refs);
370
+ this.check(expr.right, params, refs);
371
+ return;
372
+ case "conditional":
373
+ this.check(expr.test, params, refs);
374
+ this.check(expr.then, params, refs);
375
+ this.check(expr.else, params, refs);
376
+ return;
377
+ case "member":
378
+ this.forbidden(expr.name, expr.pos);
379
+ this.check(expr.object, params, refs);
380
+ return;
381
+ case "index":
382
+ if (expr.index.type === "literal" && typeof expr.index.value === "string") {
383
+ this.forbidden(expr.index.value, expr.index.pos);
384
+ }
385
+ this.check(expr.object, params, refs);
386
+ this.check(expr.index, params, refs);
387
+ return;
388
+ case "arrow":
389
+ this.diagnostics.report(expr.pos, ARROW_ONLY);
390
+ this.check(expr.body, [...params, ...expr.params], refs);
391
+ return;
392
+ case "method":
393
+ this.method(expr, params, refs);
394
+ return;
395
+ case "call":
396
+ this.call(expr, params, refs);
397
+ return;
398
+ }
399
+ }
400
+
401
+ private unknownName(name: string, params: readonly string[]): string {
402
+ if (this.states.has(name)) return `\`${name}\` is not defined. Did you mean \`$${name}\`?`;
403
+ if (COMPONENT_SPECS.has(name) || ACTION_SPECS.has(name) || FUNCTION_ARITY.has(name)) {
404
+ return `\`${name}\` must be called, as in \`${name}(...)\``;
405
+ }
406
+ const hint = didYouMean(name, [...params, ...this.named.keys()]);
407
+ return `\`${name}\` is not defined.${hint || ` Declare it first, as in \`${name} = ...\`.`}`;
408
+ }
409
+
410
+ private method(expr: Extract<Expr, { type: "method" }>, params: readonly string[], refs: Refs): void {
411
+ this.check(expr.object, params, refs);
412
+ for (const arg of expr.named) {
413
+ this.diagnostics.report(arg.pos, "methods take positional arguments only");
414
+ this.check(arg.value, params, refs);
415
+ }
416
+ const arity = METHOD_ARITY.get(expr.name);
417
+ if (arity === undefined) {
418
+ const hint = didYouMean(expr.name, METHOD_ARITY.keys(), (name) => `.${name}()`);
419
+ this.diagnostics.report(
420
+ expr.pos,
421
+ `\`.${expr.name}()\` is not an allowed method.${hint} Lists: ${METHODS.list.join(", ")}. Text: ${METHODS.string.join(", ")}. Numbers: ${METHODS.number.join(", ")}.`,
422
+ );
423
+ } else if (expr.args.length < arity[0] || expr.args.length > arity[1]) {
424
+ this.diagnostics.report(expr.pos, `\`.${expr.name}()\` takes ${arityText(arity)}`);
425
+ }
426
+ const [first, ...rest] = expr.args;
427
+ if (LAMBDA_METHODS.has(expr.name) && first !== undefined) {
428
+ if (first.type !== "arrow") {
429
+ this.diagnostics.report(first.pos, `\`.${expr.name}()\` takes a function, as in \`x => x.price\``);
430
+ this.check(first, params, refs);
431
+ } else {
432
+ if (first.params.length > 2) {
433
+ this.diagnostics.report(first.pos, `\`.${expr.name}()\` passes (item, index); use at most 2 parameters`);
434
+ }
435
+ this.check(first.body, [...params, ...first.params], refs);
436
+ }
437
+ } else if (first !== undefined) {
438
+ this.check(first, params, refs);
439
+ }
440
+ for (const arg of rest) this.check(arg, params, refs);
441
+ }
442
+
443
+ private call(expr: CallExpr, params: readonly string[], refs: Refs): void {
444
+ const spec = COMPONENT_SPECS.get(expr.name) ?? ACTION_SPECS.get(expr.name);
445
+ const own = this.named.get(expr.name)?.value;
446
+ if (spec !== undefined) {
447
+ this.catalogCall(expr, spec);
448
+ } else if (own?.type === "arrow" && !params.includes(expr.name)) {
449
+ if (expr.named.length > 0) {
450
+ this.diagnostics.report(expr.named[0].pos, `\`${expr.name}\` takes positional arguments only`);
451
+ }
452
+ if (expr.args.length > own.params.length) {
453
+ const count = own.params.length;
454
+ this.diagnostics.report(expr.pos, `\`${expr.name}\` takes ${count} argument${count === 1 ? "" : "s"}`);
455
+ }
456
+ refs.named.push(expr);
457
+ } else {
458
+ const arity = FUNCTION_ARITY.get(expr.name);
459
+ if (arity === undefined) {
460
+ this.diagnostics.report(expr.pos, this.unknownCallee(expr.name, params));
461
+ } else {
462
+ if (expr.named.length > 0) {
463
+ this.diagnostics.report(expr.named[0].pos, `\`${expr.name}\` takes positional arguments only`);
464
+ }
465
+ if (expr.args.length < arity[0] || expr.args.length > arity[1]) {
466
+ this.diagnostics.report(expr.pos, `\`${expr.name}\` takes ${arityText(arity)}: ${FUNCTIONS[expr.name as FunctionName]}`);
467
+ } else if (expr.name === "Load") {
468
+ this.literalPath(expr.args[0], "Load");
469
+ }
470
+ }
471
+ }
472
+ for (const arg of expr.args) this.check(arg, params, refs);
473
+ for (const arg of expr.named) this.check(arg.value, params, refs);
474
+ }
475
+
476
+ private unknownCallee(name: string, params: readonly string[]): string {
477
+ if (this.named.has(name) || params.includes(name) || this.states.has(name)) {
478
+ return `\`${name}\` is a value and cannot be called; only components, actions and functions can`;
479
+ }
480
+ const isFunctionLike = name[0] === name[0].toLowerCase();
481
+ const candidates = [...COMPONENT_SPECS.keys(), ...ACTION_SPECS.keys(), ...FUNCTION_ARITY.keys()];
482
+ const fallback = isFunctionLike
483
+ ? ` Functions: ${[...FUNCTION_ARITY.keys()].join(", ")}.`
484
+ : ` Components: ${[...COMPONENT_SPECS.keys()].join(", ")}.`;
485
+ return `\`${name}\` is not a component, action or function.${didYouMean(name, candidates) || fallback}`;
486
+ }
487
+
488
+ /** The host fetches files before evaluating, so the path must be readable from the source alone. */
489
+ private literalPath(arg: Expr, owner: "Load" | "Snippet"): void {
490
+ const path = loadPath(arg);
491
+ if (path === null) {
492
+ const usage =
493
+ owner === "Load"
494
+ ? '`Load` takes a plain string path, as in Load("data/sales.csv")'
495
+ : '`Snippet` takes its path as a literal string, as in Snippet("src/app.ts", 10, 40)';
496
+ this.diagnostics.report(
497
+ arg.pos,
498
+ `${usage}; the file is read before the UI runs, so the path cannot be computed or use \`\${}\``,
499
+ );
500
+ } else if (path.trim() === "") {
501
+ this.diagnostics.report(arg.pos, `\`${owner}\` needs a file path`);
502
+ }
503
+ }
504
+
505
+ /** Maps arguments onto catalog params and checks what is knowable before evaluation. */
506
+ private catalogCall(expr: CallExpr, spec: ComponentSpec): void {
507
+ const sig = signature(expr.name, spec);
508
+ const slots: (Expr | undefined)[] = spec.params.map((_, index) => expr.args[index]);
509
+ if (expr.args.length > spec.params.length) {
510
+ this.diagnostics.report(expr.args[spec.params.length].pos, `too many arguments: ${sig}`);
511
+ }
512
+ for (const arg of expr.named) {
513
+ const index = spec.params.findIndex((param) => param.name === arg.name);
514
+ if (index < 0) {
515
+ const hint = didYouMean(arg.name, spec.params.map((param) => param.name));
516
+ this.diagnostics.report(arg.pos, `\`${expr.name}\` has no parameter \`${arg.name}\`.${hint} ${sig}`);
517
+ } else if (slots[index] !== undefined) {
518
+ this.diagnostics.report(arg.pos, `\`${arg.name}\` is given twice: ${sig}`);
519
+ } else {
520
+ slots[index] = arg.value;
521
+ }
522
+ }
523
+ spec.params.forEach((param, index) => {
524
+ const slot = slots[index];
525
+ if (slot === undefined) {
526
+ if (!param.optional) this.diagnostics.report(expr.pos, `\`${expr.name}\` is missing \`${param.name}\`: ${sig}`);
527
+ } else {
528
+ this.checkParam(expr.name, param, slot);
529
+ }
530
+ });
531
+ }
532
+
533
+ private checkParam(owner: string, param: ParamSpec, slot: Expr): void {
534
+ const label = `\`${owner}\` ${param.name}`;
535
+ if (owner === "Snippet" && param.name === "path") this.literalPath(slot, "Snippet");
536
+ // A bare state name (`plan` for `$plan`) is already reported with a `$` suggestion.
537
+ const isBareState = slot.type === "ident" && this.states.has(slot.name);
538
+ if (param.kind === "binding" && slot.type !== "state" && !isBareState) {
539
+ this.diagnostics.report(slot.pos, `${label} must be a state variable written as \`$name\``);
540
+ }
541
+ if (param.kind === "binding" && slot.type === "state") {
542
+ const initial = this.states.get(slot.name)?.decl.value;
543
+ const kind = initial === undefined ? undefined : staticKind(initial);
544
+ const mismatch = kind === undefined ? null : stateMismatch(label, param, slot.name, kind, "starts as");
545
+ // A literal the input can read losslessly is only worth a warning.
546
+ const coercible = initial?.type === "literal" && coerceState(param.holds, initial.value) !== undefined;
547
+ if (mismatch !== null) this.diagnostics.report(slot.pos, mismatch, coercible ? "warning" : "error");
548
+ }
549
+ const wanted = LITERAL_KINDS[param.kind];
550
+ const got = staticKind(slot);
551
+ if (wanted !== undefined && got !== undefined && got !== wanted && !(got === "null" && (param.optional || param.nullable))) {
552
+ this.diagnostics.report(slot.pos, `${label} needs ${EXPECTED[param.kind]}, got ${DESCRIPTIONS[got]}`);
553
+ }
554
+ if (param.kind === "enum" && slot.type === "literal" && !(slot.value === null && param.optional)) {
555
+ const values = param.values ?? [];
556
+ if (typeof slot.value !== "string" || !values.includes(slot.value)) {
557
+ this.diagnostics.report(slot.pos, `${label} must be one of ${values.join(", ")}; got ${JSON.stringify(slot.value)}`);
558
+ }
559
+ }
560
+ if (param.kind === "actions") {
561
+ for (const item of slot.type === "list" ? slot.items : [slot]) {
562
+ let got: string | undefined;
563
+ if (item.type === "call" && (COMPONENT_SPECS.has(item.name) || FUNCTION_ARITY.has(item.name))) {
564
+ got = `\`${item.name}(...)\``;
565
+ } else if (item.type === "literal" && item.value !== null) {
566
+ got = JSON.stringify(item.value);
567
+ }
568
+ if (got !== undefined) {
569
+ this.diagnostics.report(item.pos, `${label} takes actions: ${ACTION_LIST}; got ${got}`);
570
+ }
571
+ }
572
+ }
573
+ }
574
+ }