@ulearning/ai-generative-frame 0.0.0-stage → 1.0.0-next.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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -2
  3. package/dist/assistant-ui/entry.d.ts +34 -0
  4. package/dist/assistant-ui/entry.d.ts.map +1 -0
  5. package/dist/assistant-ui/entry.js +50 -0
  6. package/dist/assistant-ui/entry.js.map +1 -0
  7. package/dist/assistant-ui/extension.d.ts +18 -0
  8. package/dist/assistant-ui/extension.d.ts.map +1 -0
  9. package/dist/assistant-ui/extension.js +0 -0
  10. package/dist/assistant-ui/history.d.ts +15 -0
  11. package/dist/assistant-ui/history.d.ts.map +1 -0
  12. package/dist/assistant-ui/history.js +39 -0
  13. package/dist/assistant-ui/history.js.map +1 -0
  14. package/dist/assistant-ui/modelContext.d.ts +45 -0
  15. package/dist/assistant-ui/modelContext.d.ts.map +1 -0
  16. package/dist/assistant-ui/modelContext.js +88 -0
  17. package/dist/assistant-ui/modelContext.js.map +1 -0
  18. package/dist/assistant-ui/snapshotSaver.d.ts +11 -0
  19. package/dist/assistant-ui/snapshotSaver.d.ts.map +1 -0
  20. package/dist/assistant-ui/snapshotSaver.js +83 -0
  21. package/dist/assistant-ui/snapshotSaver.js.map +1 -0
  22. package/dist/assistant-ui/tool-calls.d.ts +21 -0
  23. package/dist/assistant-ui/tool-calls.d.ts.map +1 -0
  24. package/dist/assistant-ui/tool-calls.js +65 -0
  25. package/dist/assistant-ui/tool-calls.js.map +1 -0
  26. package/dist/assistant-ui/toolkit.d.ts +96 -0
  27. package/dist/assistant-ui/toolkit.d.ts.map +1 -0
  28. package/dist/assistant-ui/toolkit.js +456 -0
  29. package/dist/assistant-ui/toolkit.js.map +1 -0
  30. package/dist/assistant-ui/useSharedContext.d.ts +14 -0
  31. package/dist/assistant-ui/useSharedContext.d.ts.map +1 -0
  32. package/dist/assistant-ui/useSharedContext.js +27 -0
  33. package/dist/assistant-ui/useSharedContext.js.map +1 -0
  34. package/dist/assistant-ui.d.ts +3 -0
  35. package/dist/assistant-ui.d.ts.map +1 -0
  36. package/dist/assistant-ui.js +1 -0
  37. package/dist/bootstrap.d.ts +23 -0
  38. package/dist/bootstrap.d.ts.map +1 -0
  39. package/dist/bootstrap.js +78 -0
  40. package/dist/bootstrap.js.map +1 -0
  41. package/dist/csp.d.ts +25 -0
  42. package/dist/csp.d.ts.map +1 -0
  43. package/dist/csp.js +78 -0
  44. package/dist/csp.js.map +1 -0
  45. package/dist/index.d.ts +6 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +5 -0
  48. package/dist/json-schema.d.ts +70 -0
  49. package/dist/json-schema.d.ts.map +1 -0
  50. package/dist/json-schema.js +163 -0
  51. package/dist/json-schema.js.map +1 -0
  52. package/dist/preview.d.ts +30 -0
  53. package/dist/preview.d.ts.map +1 -0
  54. package/dist/preview.js +60 -0
  55. package/dist/preview.js.map +1 -0
  56. package/dist/prompts/guidance.d.ts +41 -0
  57. package/dist/prompts/guidance.d.ts.map +1 -0
  58. package/dist/prompts/guidance.js +268 -0
  59. package/dist/prompts/guidance.js.map +1 -0
  60. package/dist/prompts.d.ts +2 -0
  61. package/dist/prompts.d.ts.map +1 -0
  62. package/dist/prompts.js +1 -0
  63. package/dist/protocol.d.ts +114 -0
  64. package/dist/protocol.d.ts.map +1 -0
  65. package/dist/protocol.js +49 -0
  66. package/dist/protocol.js.map +1 -0
  67. package/dist/repair/repair.d.ts +72 -0
  68. package/dist/repair/repair.d.ts.map +1 -0
  69. package/dist/repair/repair.js +103 -0
  70. package/dist/repair/repair.js.map +1 -0
  71. package/dist/repair.d.ts +2 -0
  72. package/dist/repair.d.ts.map +1 -0
  73. package/dist/repair.js +1 -0
  74. package/dist/rpc.d.ts +60 -0
  75. package/dist/rpc.d.ts.map +1 -0
  76. package/dist/rpc.js +127 -0
  77. package/dist/rpc.js.map +1 -0
  78. package/dist/runtime/diagnostics.d.ts +17 -0
  79. package/dist/runtime/diagnostics.d.ts.map +1 -0
  80. package/dist/runtime/diagnostics.js +128 -0
  81. package/dist/runtime/diagnostics.js.map +1 -0
  82. package/dist/runtime/generated.d.ts +2 -0
  83. package/dist/runtime/generated.d.ts.map +1 -0
  84. package/dist/runtime/generated.js +5 -0
  85. package/dist/runtime/generated.js.map +1 -0
  86. package/dist/runtime/index.d.ts +7 -0
  87. package/dist/runtime/index.d.ts.map +1 -0
  88. package/dist/runtime/index.js +264 -0
  89. package/dist/runtime/index.js.map +1 -0
  90. package/dist/runtime/morph.d.ts +51 -0
  91. package/dist/runtime/morph.d.ts.map +1 -0
  92. package/dist/runtime/morph.js +251 -0
  93. package/dist/runtime/morph.js.map +1 -0
  94. package/dist/runtime/screenshot.d.ts +23 -0
  95. package/dist/runtime/screenshot.d.ts.map +1 -0
  96. package/dist/runtime/screenshot.js +188 -0
  97. package/dist/runtime/screenshot.js.map +1 -0
  98. package/dist/runtime/size.d.ts +10 -0
  99. package/dist/runtime/size.d.ts.map +1 -0
  100. package/dist/runtime/size.js +21 -0
  101. package/dist/runtime/size.js.map +1 -0
  102. package/dist/runtime/storage.d.ts +4 -0
  103. package/dist/runtime/storage.d.ts.map +1 -0
  104. package/dist/runtime/storage.js +38 -0
  105. package/dist/runtime/storage.js.map +1 -0
  106. package/dist/spec/actions.d.ts +40 -0
  107. package/dist/spec/actions.d.ts.map +1 -0
  108. package/dist/spec/actions.js +53 -0
  109. package/dist/spec/actions.js.map +1 -0
  110. package/dist/spec/assistant-ui/thread.d.ts +66 -0
  111. package/dist/spec/assistant-ui/thread.d.ts.map +1 -0
  112. package/dist/spec/assistant-ui/thread.js +252 -0
  113. package/dist/spec/assistant-ui/thread.js.map +1 -0
  114. package/dist/spec/assistant-ui/toolkit.d.ts +33 -0
  115. package/dist/spec/assistant-ui/toolkit.d.ts.map +1 -0
  116. package/dist/spec/assistant-ui/toolkit.js +142 -0
  117. package/dist/spec/assistant-ui/toolkit.js.map +1 -0
  118. package/dist/spec/assistant-ui/web.d.ts +6 -0
  119. package/dist/spec/assistant-ui/web.d.ts.map +1 -0
  120. package/dist/spec/assistant-ui/web.js +48 -0
  121. package/dist/spec/assistant-ui/web.js.map +1 -0
  122. package/dist/spec/assistant-ui.d.ts +3 -0
  123. package/dist/spec/assistant-ui.d.ts.map +1 -0
  124. package/dist/spec/assistant-ui.js +1 -0
  125. package/dist/spec/calc.d.ts +8 -0
  126. package/dist/spec/calc.d.ts.map +1 -0
  127. package/dist/spec/calc.js +498 -0
  128. package/dist/spec/calc.js.map +1 -0
  129. package/dist/spec/catalog.d.ts +52 -0
  130. package/dist/spec/catalog.d.ts.map +1 -0
  131. package/dist/spec/catalog.js +104 -0
  132. package/dist/spec/catalog.js.map +1 -0
  133. package/dist/spec/expressions.d.ts +45 -0
  134. package/dist/spec/expressions.d.ts.map +1 -0
  135. package/dist/spec/expressions.js +199 -0
  136. package/dist/spec/expressions.js.map +1 -0
  137. package/dist/spec/patch.d.ts +20 -0
  138. package/dist/spec/patch.d.ts.map +1 -0
  139. package/dist/spec/patch.js +77 -0
  140. package/dist/spec/patch.js.map +1 -0
  141. package/dist/spec/pending.d.ts +15 -0
  142. package/dist/spec/pending.d.ts.map +1 -0
  143. package/dist/spec/pending.js +33 -0
  144. package/dist/spec/pending.js.map +1 -0
  145. package/dist/spec/pointer.d.ts +17 -0
  146. package/dist/spec/pointer.d.ts.map +1 -0
  147. package/dist/spec/pointer.js +88 -0
  148. package/dist/spec/pointer.js.map +1 -0
  149. package/dist/spec/prompt.d.ts +29 -0
  150. package/dist/spec/prompt.d.ts.map +1 -0
  151. package/dist/spec/prompt.js +145 -0
  152. package/dist/spec/prompt.js.map +1 -0
  153. package/dist/spec/render-spec.d.ts +50 -0
  154. package/dist/spec/render-spec.d.ts.map +1 -0
  155. package/dist/spec/render-spec.js +104 -0
  156. package/dist/spec/render-spec.js.map +1 -0
  157. package/dist/spec/resolve.d.ts +39 -0
  158. package/dist/spec/resolve.d.ts.map +1 -0
  159. package/dist/spec/resolve.js +51 -0
  160. package/dist/spec/resolve.js.map +1 -0
  161. package/dist/spec/resolved.d.ts +3 -0
  162. package/dist/spec/resolved.d.ts.map +1 -0
  163. package/dist/spec/resolved.js +36 -0
  164. package/dist/spec/resolved.js.map +1 -0
  165. package/dist/spec/state.d.ts +34 -0
  166. package/dist/spec/state.d.ts.map +1 -0
  167. package/dist/spec/state.js +111 -0
  168. package/dist/spec/state.js.map +1 -0
  169. package/dist/spec/stream.d.ts +50 -0
  170. package/dist/spec/stream.d.ts.map +1 -0
  171. package/dist/spec/stream.js +168 -0
  172. package/dist/spec/stream.js.map +1 -0
  173. package/dist/spec/tools.d.ts +3 -0
  174. package/dist/spec/tools.d.ts.map +1 -0
  175. package/dist/spec/tools.js +3 -0
  176. package/dist/spec/tree.d.ts +21 -0
  177. package/dist/spec/tree.d.ts.map +1 -0
  178. package/dist/spec/tree.js +27 -0
  179. package/dist/spec/tree.js.map +1 -0
  180. package/dist/spec/types.d.ts +66 -0
  181. package/dist/spec/types.d.ts.map +1 -0
  182. package/dist/spec/types.js +9 -0
  183. package/dist/spec/types.js.map +1 -0
  184. package/dist/spec/validate.d.ts +32 -0
  185. package/dist/spec/validate.d.ts.map +1 -0
  186. package/dist/spec/validate.js +264 -0
  187. package/dist/spec/validate.js.map +1 -0
  188. package/dist/spec/vue/SpecRenderer.d.ts +43 -0
  189. package/dist/spec/vue/SpecRenderer.d.ts.map +1 -0
  190. package/dist/spec/vue/SpecRenderer.js +357 -0
  191. package/dist/spec/vue/SpecRenderer.js.map +1 -0
  192. package/dist/spec/vue/useSpecStream.d.ts +19 -0
  193. package/dist/spec/vue/useSpecStream.d.ts.map +1 -0
  194. package/dist/spec/vue/useSpecStream.js +115 -0
  195. package/dist/spec/vue/useSpecStream.js.map +1 -0
  196. package/dist/spec/vue.d.ts +3 -0
  197. package/dist/spec/vue.d.ts.map +1 -0
  198. package/dist/spec/vue.js +3 -0
  199. package/dist/spec.d.ts +12 -0
  200. package/dist/spec.d.ts.map +1 -0
  201. package/dist/spec.js +8 -0
  202. package/dist/theme.d.ts +38 -0
  203. package/dist/theme.d.ts.map +1 -0
  204. package/dist/theme.js +370 -0
  205. package/dist/theme.js.map +1 -0
  206. package/dist/tools/define.d.ts +43 -0
  207. package/dist/tools/define.d.ts.map +1 -0
  208. package/dist/tools/define.js +31 -0
  209. package/dist/tools/define.js.map +1 -0
  210. package/dist/tools/edits.d.ts +19 -0
  211. package/dist/tools/edits.d.ts.map +1 -0
  212. package/dist/tools/edits.js +55 -0
  213. package/dist/tools/edits.js.map +1 -0
  214. package/dist/tools/registry.d.ts +19 -0
  215. package/dist/tools/registry.d.ts.map +1 -0
  216. package/dist/tools/registry.js +33 -0
  217. package/dist/tools/registry.js.map +1 -0
  218. package/dist/tools/tools.d.ts +88 -0
  219. package/dist/tools/tools.d.ts.map +1 -0
  220. package/dist/tools/tools.js +235 -0
  221. package/dist/tools/tools.js.map +1 -0
  222. package/dist/tools.d.ts +5 -0
  223. package/dist/tools.d.ts.map +1 -0
  224. package/dist/tools.js +5 -0
  225. package/dist/version.d.ts +2 -0
  226. package/dist/version.d.ts.map +1 -0
  227. package/dist/version.js +5 -0
  228. package/dist/version.js.map +1 -0
  229. package/dist/vue/Widget.d.ts +16 -0
  230. package/dist/vue/Widget.d.ts.map +1 -0
  231. package/dist/vue/Widget.js +58 -0
  232. package/dist/vue/Widget.js.map +1 -0
  233. package/dist/vue/sync.d.ts +19 -0
  234. package/dist/vue/sync.d.ts.map +1 -0
  235. package/dist/vue/sync.js +31 -0
  236. package/dist/vue/sync.js.map +1 -0
  237. package/dist/vue/themeStore.d.ts +23 -0
  238. package/dist/vue/themeStore.d.ts.map +1 -0
  239. package/dist/vue/themeStore.js +115 -0
  240. package/dist/vue/themeStore.js.map +1 -0
  241. package/dist/vue/useThemeTokens.d.ts +6 -0
  242. package/dist/vue/useThemeTokens.d.ts.map +1 -0
  243. package/dist/vue/useThemeTokens.js +49 -0
  244. package/dist/vue/useThemeTokens.js.map +1 -0
  245. package/dist/vue/useWidget.d.ts +10 -0
  246. package/dist/vue/useWidget.d.ts.map +1 -0
  247. package/dist/vue/useWidget.js +81 -0
  248. package/dist/vue/useWidget.js.map +1 -0
  249. package/dist/vue.d.ts +5 -0
  250. package/dist/vue.d.ts.map +1 -0
  251. package/dist/vue.js +4 -0
  252. package/dist/widget.d.ts +129 -0
  253. package/dist/widget.d.ts.map +1 -0
  254. package/dist/widget.js +655 -0
  255. package/dist/widget.js.map +1 -0
  256. package/package.json +125 -3
  257. package/src/assistant-ui/entry.ts +124 -0
  258. package/src/assistant-ui/extension.ts +19 -0
  259. package/src/assistant-ui/history.ts +63 -0
  260. package/src/assistant-ui/modelContext.ts +162 -0
  261. package/src/assistant-ui/snapshotSaver.ts +116 -0
  262. package/src/assistant-ui/tool-calls.ts +115 -0
  263. package/src/assistant-ui/toolkit.tsx +747 -0
  264. package/src/assistant-ui/useSharedContext.ts +40 -0
  265. package/src/assistant-ui.ts +14 -0
  266. package/src/bootstrap.ts +112 -0
  267. package/src/csp.ts +96 -0
  268. package/src/index.ts +44 -0
  269. package/src/json-schema.ts +353 -0
  270. package/src/preview.ts +120 -0
  271. package/src/prompts/guidance.ts +369 -0
  272. package/src/prompts.ts +8 -0
  273. package/src/protocol.ts +141 -0
  274. package/src/repair/repair.ts +202 -0
  275. package/src/repair.ts +11 -0
  276. package/src/rpc.ts +228 -0
  277. package/src/runtime/diagnostics.ts +165 -0
  278. package/src/runtime/generated.ts +2 -0
  279. package/src/runtime/index.ts +343 -0
  280. package/src/runtime/morph.ts +352 -0
  281. package/src/runtime/screenshot.ts +288 -0
  282. package/src/runtime/size.ts +17 -0
  283. package/src/runtime/storage.ts +49 -0
  284. package/src/spec/actions.ts +111 -0
  285. package/src/spec/assistant-ui/thread.ts +381 -0
  286. package/src/spec/assistant-ui/toolkit.tsx +295 -0
  287. package/src/spec/assistant-ui/web.tsx +63 -0
  288. package/src/spec/assistant-ui.ts +2 -0
  289. package/src/spec/calc.ts +598 -0
  290. package/src/spec/catalog.ts +196 -0
  291. package/src/spec/expressions.ts +301 -0
  292. package/src/spec/patch.ts +112 -0
  293. package/src/spec/pending.ts +36 -0
  294. package/src/spec/pointer.ts +170 -0
  295. package/src/spec/prompt.ts +243 -0
  296. package/src/spec/render-spec.ts +144 -0
  297. package/src/spec/resolve.ts +123 -0
  298. package/src/spec/resolved.ts +53 -0
  299. package/src/spec/state.ts +193 -0
  300. package/src/spec/stream.ts +230 -0
  301. package/src/spec/tools.ts +17 -0
  302. package/src/spec/tree.ts +37 -0
  303. package/src/spec/types.ts +51 -0
  304. package/src/spec/validate.ts +429 -0
  305. package/src/spec/vue/SpecRenderer.tsx +544 -0
  306. package/src/spec/vue/__tests__/harness.ts +50 -0
  307. package/src/spec/vue/useSpecStream.ts +162 -0
  308. package/src/spec/vue.ts +13 -0
  309. package/src/spec.ts +55 -0
  310. package/src/theme.ts +423 -0
  311. package/src/tools/define.ts +65 -0
  312. package/src/tools/edits.ts +73 -0
  313. package/src/tools/registry.ts +44 -0
  314. package/src/tools/tools.ts +361 -0
  315. package/src/tools.ts +32 -0
  316. package/src/version.ts +4 -0
  317. package/src/vue/Widget.tsx +74 -0
  318. package/src/vue/sync.ts +36 -0
  319. package/src/vue/themeStore.ts +157 -0
  320. package/src/vue/useThemeTokens.ts +71 -0
  321. package/src/vue/useWidget.ts +117 -0
  322. package/src/vue.ts +8 -0
  323. package/src/widget.ts +1080 -0
@@ -0,0 +1,429 @@
1
+ import { formulaIssue } from "./calc";
2
+ import type { Catalog } from "./catalog";
3
+ import { isExpressionKey } from "./expressions";
4
+ import { escapePointerToken } from "./pointer";
5
+ import type { ActionBinding, Spec, SpecElement } from "./types";
6
+
7
+ export type SpecIssueCode =
8
+ | "missing-root"
9
+ | "unknown-root"
10
+ | "invalid-element"
11
+ | "unknown-type"
12
+ | "invalid-props"
13
+ | "missing-child"
14
+ | "children-not-allowed"
15
+ | "unknown-slot"
16
+ | "cycle"
17
+ | "unknown-event"
18
+ | "unknown-action"
19
+ | "invalid-params"
20
+ | "invalid-formula"
21
+ | "unfilled-placeholder"
22
+ | "invalid-repeat"
23
+ | "unreachable"
24
+ | "invalid-patch"
25
+ | "invalid-state"
26
+ | "missing-tool-result";
27
+
28
+ export type SpecIssue = {
29
+ code: SpecIssueCode;
30
+ severity: "error" | "warning";
31
+ message: string;
32
+ /** JSON Pointer into the spec where the issue is. */
33
+ path: string;
34
+ elementId?: string;
35
+ };
36
+
37
+ export type SpecValidation = { ok: boolean; issues: SpecIssue[] };
38
+
39
+ export type ValidateSpecOptions = {
40
+ /**
41
+ * The spec is still streaming: children that are not added yet are not
42
+ * errors, and unreachable elements are not reported.
43
+ */
44
+ partial?: boolean;
45
+ };
46
+
47
+ const elementPath = (id: string) => `/elements/${escapePointerToken(id)}`;
48
+
49
+ const PLACEHOLDER = /\$\{\s*(?:\/|\$item\b|\$index\b)[^}]*\}/;
50
+
51
+ const bindingsOf = (
52
+ value: ActionBinding | ActionBinding[] | undefined,
53
+ ): ActionBinding[] =>
54
+ value === undefined ? [] : Array.isArray(value) ? value : [value];
55
+
56
+ /**
57
+ * Checks a spec against a catalog: the root exists, every element has a
58
+ * known type and valid props, children resolve and form no cycle, slots and
59
+ * events are declared, and actions are known with valid params. Issues carry
60
+ * JSON Pointers so a repair prompt can point at the exact place.
61
+ */
62
+ export function validateSpec(
63
+ spec: Spec,
64
+ catalog: Catalog,
65
+ options: ValidateSpecOptions = {},
66
+ ): SpecValidation {
67
+ const issues: SpecIssue[] = [];
68
+ const error = (issue: Omit<SpecIssue, "severity">) =>
69
+ issues.push({ severity: "error", ...issue });
70
+ const elements: Record<string, SpecElement> =
71
+ spec && typeof spec.elements === "object" && spec.elements
72
+ ? spec.elements
73
+ : {};
74
+
75
+ if (!spec?.root) {
76
+ error({
77
+ code: "missing-root",
78
+ message: "The spec has no root element id.",
79
+ path: "/root",
80
+ });
81
+ } else if (!Object.hasOwn(elements, spec.root) && !options.partial) {
82
+ error({
83
+ code: "unknown-root",
84
+ message: `The root "${spec.root}" is not an element.`,
85
+ path: "/root",
86
+ });
87
+ }
88
+ const specState: unknown = spec?.state;
89
+ if (
90
+ specState !== undefined &&
91
+ (typeof specState !== "object" ||
92
+ specState === null ||
93
+ Array.isArray(specState))
94
+ ) {
95
+ error({
96
+ code: "invalid-state",
97
+ message: "The spec's state is not an object.",
98
+ path: "/state",
99
+ });
100
+ }
101
+
102
+ const childLists = (id: string, element: SpecElement) => {
103
+ const lists: { slot: string; path: string; ids: unknown }[] = [];
104
+ if (element.children !== undefined) {
105
+ lists.push({
106
+ slot: "default",
107
+ path: `${elementPath(id)}/children`,
108
+ ids: element.children,
109
+ });
110
+ }
111
+ for (const [slot, ids] of Object.entries(element.slots ?? {})) {
112
+ lists.push({
113
+ slot,
114
+ path: `${elementPath(id)}/slots/${escapePointerToken(slot)}`,
115
+ ids,
116
+ });
117
+ }
118
+ return lists;
119
+ };
120
+
121
+ const checkBindings = (
122
+ id: string,
123
+ path: string,
124
+ bindings: ActionBinding[],
125
+ ) => {
126
+ bindings.forEach((binding, index) => {
127
+ const at = bindings.length > 1 ? `${path}/${index}` : path;
128
+ if (!binding || typeof binding.action !== "string") {
129
+ error({
130
+ code: "unknown-action",
131
+ elementId: id,
132
+ path: at,
133
+ message: "An action binding needs an `action` name.",
134
+ });
135
+ return;
136
+ }
137
+ if (!catalog.action(binding.action)) {
138
+ error({
139
+ code: "unknown-action",
140
+ elementId: id,
141
+ path: `${at}/action`,
142
+ message: `Unknown action "${binding.action}". Known: ${[
143
+ "setState",
144
+ "sendMessage",
145
+ "updateModelContext",
146
+ "copyText",
147
+ ...Object.keys(catalog.actions),
148
+ ].join(", ")}.`,
149
+ });
150
+ return;
151
+ }
152
+ for (const issue of catalog.validateParams(
153
+ binding.action,
154
+ binding.params,
155
+ )) {
156
+ error({
157
+ code: "invalid-params",
158
+ elementId: id,
159
+ path: `${at}/params${issue.path === "/" ? "" : issue.path}`,
160
+ message: `${binding.action} params ${issue.path === "/" ? "" : `${issue.path} `}${issue.message}`,
161
+ });
162
+ }
163
+ });
164
+ };
165
+
166
+ const checkExpressions = (id: string, path: string, value: unknown) => {
167
+ const seen = new Set<object>();
168
+ const pending: [string, unknown][] = [[path, value]];
169
+ for (let next = pending.pop(); next; next = pending.pop()) {
170
+ const [at, current] = next;
171
+ if (typeof current !== "object" || current === null) continue;
172
+ if (seen.has(current)) continue;
173
+ seen.add(current);
174
+ const children: [string, unknown][] = [];
175
+ for (const [key, child] of Object.entries(current)) {
176
+ const childPath = `${at}/${escapePointerToken(key)}`;
177
+ if (key !== "$calc" || Array.isArray(current)) {
178
+ const plain =
179
+ typeof child === "string" &&
180
+ (key !== "$template" || Array.isArray(current))
181
+ ? child
182
+ : undefined;
183
+ // Ending the scan at the last `}` keeps it linear on a long string whose placeholders never close.
184
+ const placeholder =
185
+ plain === undefined
186
+ ? undefined
187
+ : PLACEHOLDER.exec(
188
+ plain.slice(0, plain.lastIndexOf("}") + 1),
189
+ )?.[0];
190
+ if (placeholder) {
191
+ error({
192
+ code: "unfilled-placeholder",
193
+ elementId: id,
194
+ path: childPath,
195
+ message: `"${id}" shows \`${placeholder}\` as written, since only \`$template\` and \`$calc\` fill placeholders. Write the string as \`{ "$template": "…" }\`, or compute the value with \`$calc\`.`,
196
+ });
197
+ }
198
+ if (plain !== undefined && isExpressionKey(plain)) {
199
+ const name = plain.trim();
200
+ error({
201
+ code: "unfilled-placeholder",
202
+ elementId: id,
203
+ path: childPath,
204
+ message: `"${id}" shows \`"${name}"\` as written, since an expression is an object, not a string. Write it as \`{ "${name}": … }\`.`,
205
+ });
206
+ }
207
+ children.push([childPath, child]);
208
+ continue;
209
+ }
210
+ const issue =
211
+ typeof child === "string"
212
+ ? formulaIssue(child)
213
+ : "`$calc` needs a formula string";
214
+ if (issue) {
215
+ error({
216
+ code: "invalid-formula",
217
+ elementId: id,
218
+ path: childPath,
219
+ message: `Formula in "${id}": ${issue}.`,
220
+ });
221
+ }
222
+ }
223
+ for (let i = children.length - 1; i >= 0; i--) pending.push(children[i]!);
224
+ }
225
+ };
226
+
227
+ for (const [id, element] of Object.entries(elements)) {
228
+ const path = elementPath(id);
229
+ if (
230
+ !element ||
231
+ typeof element !== "object" ||
232
+ typeof element.type !== "string"
233
+ ) {
234
+ error({
235
+ code: "invalid-element",
236
+ elementId: id,
237
+ path,
238
+ message: `Element "${id}" needs a string \`type\`.`,
239
+ });
240
+ continue;
241
+ }
242
+ const definition = catalog.component(element.type);
243
+ if (!definition) {
244
+ error({
245
+ code: "unknown-type",
246
+ elementId: id,
247
+ path: `${path}/type`,
248
+ message: `Element "${id}" has unknown type "${element.type}". Known: ${Object.keys(catalog.components).join(", ")}.`,
249
+ });
250
+ } else {
251
+ for (const issue of catalog.validateProps(element.type, element.props)) {
252
+ error({
253
+ code: "invalid-props",
254
+ elementId: id,
255
+ path: `${path}/props${issue.path === "/" ? "" : issue.path}`,
256
+ message: `${element.type} "${id}" prop ${issue.path === "/" ? "" : `${issue.path.slice(1)} `}${issue.message}.`,
257
+ });
258
+ }
259
+ const slots = definition.slots ?? [];
260
+ for (const list of childLists(id, element)) {
261
+ if (Array.isArray(list.ids) && list.ids.length === 0) continue;
262
+ if (!slots.includes(list.slot)) {
263
+ error({
264
+ code:
265
+ list.slot === "default" ? "children-not-allowed" : "unknown-slot",
266
+ elementId: id,
267
+ path: list.path,
268
+ message:
269
+ list.slot === "default"
270
+ ? `${element.type} "${id}" takes no children.`
271
+ : `${element.type} "${id}" has no slot "${list.slot}"${slots.length ? `; slots: ${slots.join(", ")}` : ""}.`,
272
+ });
273
+ }
274
+ }
275
+ for (const event of Object.keys(element.on ?? {})) {
276
+ if (!(definition.events ?? []).includes(event)) {
277
+ error({
278
+ code: "unknown-event",
279
+ elementId: id,
280
+ path: `${path}/on/${escapePointerToken(event)}`,
281
+ message: `${element.type} "${id}" does not emit "${event}"${definition.events?.length ? `; events: ${definition.events.join(", ")}` : ""}.`,
282
+ });
283
+ }
284
+ }
285
+ }
286
+
287
+ for (const list of childLists(id, element)) {
288
+ if (!Array.isArray(list.ids)) {
289
+ error({
290
+ code: "invalid-element",
291
+ elementId: id,
292
+ path: list.path,
293
+ message: `Children of "${id}" must be an array of ids.`,
294
+ });
295
+ continue;
296
+ }
297
+ list.ids.forEach((childId, index) => {
298
+ if (typeof childId !== "string") {
299
+ error({
300
+ code: "invalid-element",
301
+ elementId: id,
302
+ path: `${list.path}/${index}`,
303
+ message: `Child ${index} of "${id}" must be an element id string.`,
304
+ });
305
+ } else if (!Object.hasOwn(elements, childId) && !options.partial) {
306
+ error({
307
+ code: "missing-child",
308
+ elementId: id,
309
+ path: `${list.path}/${index}`,
310
+ message: `"${id}" lists child "${childId}", which is not an element.`,
311
+ });
312
+ }
313
+ });
314
+ }
315
+
316
+ for (const [event, bindings] of Object.entries(element.on ?? {})) {
317
+ checkBindings(
318
+ id,
319
+ `${path}/on/${escapePointerToken(event)}`,
320
+ bindingsOf(bindings),
321
+ );
322
+ }
323
+ for (const [watched, bindings] of Object.entries(element.watch ?? {})) {
324
+ checkBindings(
325
+ id,
326
+ `${path}/watch/${escapePointerToken(watched)}`,
327
+ bindingsOf(bindings),
328
+ );
329
+ }
330
+ checkExpressions(id, `${path}/props`, element.props);
331
+ checkExpressions(id, `${path}/visible`, element.visible);
332
+ for (const key of ["on", "watch"] as const) {
333
+ for (const [name, bindings] of Object.entries(element[key] ?? {})) {
334
+ const at = `${path}/${key}/${escapePointerToken(name)}`;
335
+ if (Array.isArray(bindings)) {
336
+ bindings.forEach((binding, index) =>
337
+ checkExpressions(id, `${at}/${index}/params`, binding?.params),
338
+ );
339
+ } else {
340
+ checkExpressions(id, `${at}/params`, bindings?.params);
341
+ }
342
+ }
343
+ }
344
+ if (
345
+ element.repeat !== undefined &&
346
+ typeof element.repeat?.path !== "string"
347
+ ) {
348
+ error({
349
+ code: "invalid-repeat",
350
+ elementId: id,
351
+ path: `${path}/repeat`,
352
+ message: `"repeat" of "${id}" needs a state \`path\`.`,
353
+ });
354
+ }
355
+ }
356
+
357
+ const childIds = (element: SpecElement) =>
358
+ childLists("", element).flatMap((list) =>
359
+ Array.isArray(list.ids)
360
+ ? list.ids.filter((c): c is string => typeof c === "string")
361
+ : [],
362
+ );
363
+
364
+ // Depth-first search for cycles and reachability from the root.
365
+ const state = new Map<string, "visiting" | "done">();
366
+ const reported = new Set<string>();
367
+ const visit = (id: string, trail: string[]) => {
368
+ const element = elements[id];
369
+ if (!element || typeof element !== "object") return;
370
+ state.set(id, "visiting");
371
+ for (const child of childIds(element)) {
372
+ if (state.get(child) === "visiting") {
373
+ const stack = [...trail, id];
374
+ const loop = [...stack.slice(stack.indexOf(child)), child];
375
+ const key = [...loop].sort().join("|");
376
+ if (!reported.has(key)) {
377
+ reported.add(key);
378
+ error({
379
+ code: "cycle",
380
+ elementId: id,
381
+ path: elementPath(id),
382
+ message: `Children form a cycle: ${loop.join(" → ")}.`,
383
+ });
384
+ }
385
+ } else if (!state.has(child)) {
386
+ visit(child, [...trail, id]);
387
+ }
388
+ }
389
+ state.set(id, "done");
390
+ };
391
+ if (spec?.root) visit(spec.root, []);
392
+ for (const id of Object.keys(elements)) {
393
+ if (state.has(id)) continue;
394
+ if (!options.partial) {
395
+ issues.push({
396
+ code: "unreachable",
397
+ severity: "warning",
398
+ elementId: id,
399
+ path: elementPath(id),
400
+ message: `"${id}" is not reachable from the root.`,
401
+ });
402
+ }
403
+ visit(id, []);
404
+ }
405
+
406
+ return { ok: !issues.some((issue) => issue.severity === "error"), issues };
407
+ }
408
+
409
+ /** Formats issues as model-readable repair feedback. */
410
+ export function formatSpecIssues(issues: readonly SpecIssue[]): string {
411
+ if (issues.length === 0) return "The spec is valid.";
412
+ const errors = issues.filter((issue) => issue.severity === "error");
413
+ const warnings = issues.filter((issue) => issue.severity === "warning");
414
+ const lines: string[] = [];
415
+ if (errors.length) {
416
+ lines.push(
417
+ `The spec has ${errors.length} error${errors.length === 1 ? "" : "s"}. Fix them with patches on the paths shown:`,
418
+ ...errors.map((issue) => `- ${issue.path}: ${issue.message}`),
419
+ );
420
+ }
421
+ if (warnings.length) {
422
+ lines.push(
423
+ ...(lines.length ? [""] : []),
424
+ "Warnings:",
425
+ ...warnings.map((issue) => `- ${issue.path}: ${issue.message}`),
426
+ );
427
+ }
428
+ return lines.join("\n");
429
+ }