@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,63 @@
1
+ import { useAui, useAuiState, useToolArgsStatus } from "@ulearning/ai-sdk";
2
+ import { useIsomorphicLayoutEffect } from "@ulearning/ai-store/internal";
3
+ import { computed, defineComponent, h } from "vue";
4
+ import type { ModelContextSource } from "../../assistant-ui/modelContext";
5
+ import type { WidgetToolkitExtension } from "../../assistant-ui/extension";
6
+ import type { Catalog } from "../catalog";
7
+ import { SpecRenderer } from "../vue/SpecRenderer";
8
+ import { writeClipboard } from "./thread";
9
+ import { specToolkitFor, type SpecToolkitOptions } from "./toolkit";
10
+ function useMessages() {
11
+ return useAuiState((state) => state.thread.messages);
12
+ }
13
+ function useArgsComplete() {
14
+ const status = useToolArgsStatus();
15
+ return computed(() => status.value.argsStatus === "complete");
16
+ }
17
+ const Frame = defineComponent(
18
+ (props: { title: string; streaming: boolean }, { slots }) =>
19
+ () =>
20
+ h(
21
+ "div",
22
+ {
23
+ "data-gf-spec": props.title || undefined,
24
+ "data-gf-streaming": props.streaming || undefined,
25
+ },
26
+ slots.default?.(),
27
+ ),
28
+ { props: ["title", "streaming"] },
29
+ );
30
+ function useSendMessage() {
31
+ const aui = useAui();
32
+ return (text: string) => aui.thread.append(text);
33
+ }
34
+ function useModelContext(source: ModelContextSource) {
35
+ const aui = useAui();
36
+ useIsomorphicLayoutEffect(
37
+ () =>
38
+ aui.modelContext.register({
39
+ getModelContext() {
40
+ const system = source.read();
41
+ return system ? { system } : {};
42
+ },
43
+ subscribe: source.subscribe,
44
+ }),
45
+ [],
46
+ );
47
+ }
48
+ const build = specToolkitFor({
49
+ useMessages,
50
+ useArgsComplete,
51
+ SpecRenderer,
52
+ Frame,
53
+ useSendMessage,
54
+ useModelContext,
55
+ copyText: writeClipboard,
56
+ });
57
+ /** Spec mode for createWidgetToolkit: streams render_spec patches and supplies the spec read_me module. */
58
+ export function createSpecToolkit(
59
+ catalog: Catalog,
60
+ options: SpecToolkitOptions,
61
+ ): WidgetToolkitExtension {
62
+ return build(catalog, options);
63
+ }
@@ -0,0 +1,2 @@
1
+ export { createSpecToolkit } from "./assistant-ui/web";
2
+ export type { SpecToolkitOptions } from "./assistant-ui/toolkit";
@@ -0,0 +1,598 @@
1
+ import type { ExpressionContext } from "./expressions";
2
+ import { getAtPointer, parsePointer } from "./pointer";
3
+
4
+ type Node =
5
+ | { kind: "literal"; value: unknown }
6
+ | { kind: "reference"; path: string }
7
+ | { kind: "unary"; operator: string; operand: Node }
8
+ | { kind: "binary"; operator: string; left: Node; right: Node }
9
+ | { kind: "conditional"; condition: Node; then: Node; otherwise: Node }
10
+ | { kind: "call"; name: string; args: Node[] };
11
+
12
+ type Token =
13
+ | { kind: "literal"; value: unknown }
14
+ | { kind: "reference" | "name" | "symbol"; text: string }
15
+ | { kind: "end" };
16
+
17
+ type FormulaFunction = {
18
+ min: number;
19
+ max: number;
20
+ run: (args: unknown[], step: () => void) => unknown;
21
+ };
22
+
23
+ const isNumber = (value: unknown): value is number =>
24
+ typeof value === "number" && Number.isFinite(value);
25
+
26
+ const normalize = (value: unknown): unknown =>
27
+ value === undefined || (typeof value === "number" && !Number.isFinite(value))
28
+ ? null
29
+ : value;
30
+
31
+ const pointerTokens = (path: string) => {
32
+ try {
33
+ return parsePointer(path && !path.startsWith("/") ? `/${path}` : path);
34
+ } catch {
35
+ return [];
36
+ }
37
+ };
38
+
39
+ const readPath = (base: unknown, path: string) =>
40
+ getAtPointer(base, pointerTokens(path));
41
+
42
+ const readReference = (path: string, ctx: ExpressionContext) => {
43
+ if (path === "$index") return ctx.index;
44
+ if (path === "$item") return ctx.item;
45
+ if (path.startsWith("$item/")) return readPath(ctx.item, path.slice(5));
46
+ if (path === "$event") return ctx.event;
47
+ if (path.startsWith("$event/")) return readPath(ctx.event, path.slice(6));
48
+ return readPath(ctx.state, path);
49
+ };
50
+
51
+ const eachChild = (
52
+ value: object,
53
+ visit: (child: unknown, key: string) => boolean,
54
+ ) => {
55
+ if (Array.isArray(value)) {
56
+ for (let index = 0; index < value.length; index++) {
57
+ if (Object.hasOwn(value, index) && !visit(value[index], String(index))) {
58
+ return false;
59
+ }
60
+ }
61
+ return true;
62
+ }
63
+ for (const key in value) {
64
+ if (
65
+ Object.hasOwn(value, key) &&
66
+ !visit((value as Record<string, unknown>)[key], key)
67
+ ) {
68
+ return false;
69
+ }
70
+ }
71
+ return true;
72
+ };
73
+
74
+ const visitAll = (value: unknown, step: () => void): void => {
75
+ step();
76
+ if (typeof value === "object" && value !== null) {
77
+ eachChild(value, (child) => {
78
+ visitAll(child, step);
79
+ return true;
80
+ });
81
+ }
82
+ };
83
+
84
+ const formatText = (value: unknown, step: () => void) => {
85
+ if (value === null || value === undefined) return "";
86
+ if (typeof value !== "object") return String(value);
87
+ visitAll(value, step);
88
+ return JSON.stringify(value);
89
+ };
90
+
91
+ const fractionDigits = (value: number) =>
92
+ Math.max(0, Math.min(20, Math.trunc(value)));
93
+
94
+ const numeric =
95
+ (fn: (...args: number[]) => number): FormulaFunction["run"] =>
96
+ (args) =>
97
+ args.every(isNumber) ? fn(...args) : null;
98
+
99
+ const aggregate = (
100
+ operation: "sum" | "avg" | "min" | "max",
101
+ args: unknown[],
102
+ step: () => void,
103
+ ) => {
104
+ const [list, field] = args;
105
+ if (!Array.isArray(list) || args.length > 2) return null;
106
+ if (args.length === 2 && typeof field !== "string") return null;
107
+ if (typeof field === "string" && field.length > 500) return null;
108
+ const tokens = typeof field === "string" ? pointerTokens(field) : undefined;
109
+ if (tokens && tokens.length > 32) return null;
110
+ let total = 0;
111
+ let count = 0;
112
+ let extreme: number | null = null;
113
+ for (const item of list) {
114
+ step();
115
+ const value = tokens ? getAtPointer(item, tokens) : item;
116
+ if (!isNumber(value)) continue;
117
+ total += value;
118
+ count++;
119
+ if (extreme === null) extreme = value;
120
+ else if (operation === "min") extreme = Math.min(extreme, value);
121
+ else if (operation === "max") extreme = Math.max(extreme, value);
122
+ }
123
+ if (operation === "sum") return total;
124
+ if (operation === "avg") return count ? total / count : null;
125
+ return extreme;
126
+ };
127
+
128
+ const formatters = new Map<string, Intl.NumberFormat>();
129
+
130
+ const formatNumber = (value: unknown, options: Intl.NumberFormatOptions) => {
131
+ if (!isNumber(value)) return null;
132
+ const key = JSON.stringify(options);
133
+ let formatter = formatters.get(key);
134
+ if (!formatter) {
135
+ try {
136
+ formatter = new Intl.NumberFormat("en-US", options);
137
+ } catch {
138
+ return null;
139
+ }
140
+ if (formatters.size >= 64) formatters.clear();
141
+ formatters.set(key, formatter);
142
+ }
143
+ return formatter.format(value);
144
+ };
145
+
146
+ const decimalFormat = (args: unknown[], style: "decimal" | "percent") => {
147
+ const [value, digits] = args;
148
+ if (args.length === 1) return formatNumber(value, { style });
149
+ if (!isNumber(digits)) return null;
150
+ const precision = fractionDigits(digits);
151
+ return formatNumber(value, {
152
+ style,
153
+ minimumFractionDigits: precision,
154
+ maximumFractionDigits: precision,
155
+ });
156
+ };
157
+
158
+ const functions = new Map<string, FormulaFunction>([
159
+ [
160
+ "round",
161
+ {
162
+ min: 1,
163
+ max: 2,
164
+ run: numeric((x, digits = 0) => {
165
+ const scale = 10 ** fractionDigits(digits);
166
+ return Math.round(x * scale) / scale;
167
+ }),
168
+ },
169
+ ],
170
+ ["floor", { min: 1, max: 1, run: numeric(Math.floor) }],
171
+ ["ceil", { min: 1, max: 1, run: numeric(Math.ceil) }],
172
+ ["abs", { min: 1, max: 1, run: numeric(Math.abs) }],
173
+ [
174
+ "clamp",
175
+ {
176
+ min: 3,
177
+ max: 3,
178
+ run: numeric((x, low, high) => Math.min(Math.max(x, low), high)),
179
+ },
180
+ ],
181
+ ...(["min", "max"] as const).map((name): [string, FormulaFunction] => [
182
+ name,
183
+ {
184
+ min: 1,
185
+ max: Infinity,
186
+ run: (args, step) =>
187
+ Array.isArray(args[0])
188
+ ? aggregate(name, args, step)
189
+ : numeric(name === "min" ? Math.min : Math.max)(args, step),
190
+ },
191
+ ]),
192
+ [
193
+ "count",
194
+ {
195
+ min: 1,
196
+ max: 1,
197
+ run: ([list]) => (Array.isArray(list) ? list.length : null),
198
+ },
199
+ ],
200
+ ...(["sum", "avg"] as const).map((name): [string, FormulaFunction] => [
201
+ name,
202
+ {
203
+ min: 1,
204
+ max: 2,
205
+ run: (args, step) => aggregate(name, args, step),
206
+ },
207
+ ]),
208
+ ["number", { min: 1, max: 2, run: (args) => decimalFormat(args, "decimal") }],
209
+ [
210
+ "currency",
211
+ {
212
+ min: 2,
213
+ max: 2,
214
+ run: ([value, code]) =>
215
+ typeof code === "string"
216
+ ? formatNumber(value, { style: "currency", currency: code })
217
+ : null,
218
+ },
219
+ ],
220
+ [
221
+ "percent",
222
+ { min: 1, max: 2, run: (args) => decimalFormat(args, "percent") },
223
+ ],
224
+ [
225
+ "time",
226
+ {
227
+ min: 1,
228
+ max: 1,
229
+ run: ([value]) => {
230
+ if (!isNumber(value)) return null;
231
+ const minutes = ((Math.round(value) % 1440) + 1440) % 1440;
232
+ const hour = Math.floor(minutes / 60);
233
+ return `${hour % 12 || 12}:${String(minutes % 60).padStart(2, "0")} ${hour < 12 ? "AM" : "PM"}`;
234
+ },
235
+ },
236
+ ],
237
+ [
238
+ "plural",
239
+ { min: 3, max: 3, run: ([n, one, other]) => (n === 1 ? one : other) },
240
+ ],
241
+ ]);
242
+
243
+ const tokenize = (formula: string): Token[] => {
244
+ const tokens: Token[] = [];
245
+ let offset = 0;
246
+ while (offset < formula.length) {
247
+ const char = formula[offset]!;
248
+ if (/\s/.test(char)) {
249
+ offset++;
250
+ } else if (char === "'" || char === '"') {
251
+ offset++;
252
+ let value = "";
253
+ while (offset < formula.length && formula[offset] !== char) {
254
+ let next = formula[offset++]!;
255
+ if (next === "\\") {
256
+ next = formula[offset++] ?? "";
257
+ if (next !== "\\" && next !== "'" && next !== '"') {
258
+ throw new Error("Invalid string escape");
259
+ }
260
+ }
261
+ value += next;
262
+ }
263
+ if (offset >= formula.length) throw new Error("Unterminated string");
264
+ offset++;
265
+ tokens.push({ kind: "literal", value });
266
+ } else if (char === "$" && formula[offset + 1] === "{") {
267
+ const end = formula.indexOf("}", offset + 2);
268
+ if (end === -1) throw new Error("Unterminated reference");
269
+ tokens.push({
270
+ kind: "reference",
271
+ text: formula.slice(offset + 2, end).trim(),
272
+ });
273
+ offset = end + 1;
274
+ } else if (/[0-9]/.test(char)) {
275
+ const start = offset++;
276
+ while (/[0-9]/.test(formula[offset] ?? "")) offset++;
277
+ if (formula[offset] === "." && /[0-9]/.test(formula[offset + 1] ?? "")) {
278
+ offset++;
279
+ while (/[0-9]/.test(formula[offset] ?? "")) offset++;
280
+ }
281
+ tokens.push({
282
+ kind: "literal",
283
+ value: Number(formula.slice(start, offset)),
284
+ });
285
+ } else if (/[a-zA-Z_]/.test(char)) {
286
+ const start = offset++;
287
+ while (/[a-zA-Z0-9_]/.test(formula[offset] ?? "")) offset++;
288
+ const name = formula.slice(start, offset);
289
+ if (name === "true" || name === "false" || name === "null") {
290
+ tokens.push({
291
+ kind: "literal",
292
+ value: name === "null" ? null : name === "true",
293
+ });
294
+ } else tokens.push({ kind: "name", text: name });
295
+ } else {
296
+ const pair = formula.slice(offset, offset + 2);
297
+ if (["||", "&&", "==", "!=", "<=", ">="].includes(pair)) {
298
+ tokens.push({ kind: "symbol", text: pair });
299
+ offset += 2;
300
+ } else if ("?:<>+-*/%!(),".includes(char)) {
301
+ tokens.push({ kind: "symbol", text: char });
302
+ offset++;
303
+ } else throw new Error(`Unexpected token "${char}"`);
304
+ }
305
+ }
306
+ tokens.push({ kind: "end" });
307
+ return tokens;
308
+ };
309
+
310
+ const precedence = [
311
+ ["||"],
312
+ ["&&"],
313
+ ["==", "!="],
314
+ ["<", "<=", ">", ">="],
315
+ ["+", "-"],
316
+ ["*", "/", "%"],
317
+ ];
318
+
319
+ const parse = (formula: string): Node => {
320
+ const tokens = tokenize(formula);
321
+ let position = 0;
322
+ const current = () => tokens[position]!;
323
+ const match = (symbol: string) => {
324
+ const token = current();
325
+ if (token.kind !== "symbol" || token.text !== symbol) return false;
326
+ position++;
327
+ return true;
328
+ };
329
+ const expect = (symbol: string) => {
330
+ if (!match(symbol)) throw new Error(`Expected "${symbol}"`);
331
+ };
332
+ const checkDepth = (depth: number) => {
333
+ if (depth > 32) throw new Error("Formula exceeds 32 levels of nesting");
334
+ };
335
+ const primary = (depth: number): Node => {
336
+ checkDepth(depth);
337
+ const token = current();
338
+ position++;
339
+ if (token.kind === "literal") return token;
340
+ if (token.kind === "reference")
341
+ return { kind: "reference", path: token.text };
342
+ if (token.kind === "symbol" && token.text === "(") {
343
+ const node = conditional(depth + 1);
344
+ expect(")");
345
+ return node;
346
+ }
347
+ if (token.kind === "name") {
348
+ const fn = functions.get(token.text);
349
+ if (!fn) {
350
+ throw new Error(
351
+ `Unknown function "${token.text}"; known: ${[...functions.keys()].join(", ")}`,
352
+ );
353
+ }
354
+ checkDepth(depth + 1);
355
+ expect("(");
356
+ const args: Node[] = [];
357
+ if (!match(")")) {
358
+ do {
359
+ args.push(conditional(depth + 1));
360
+ } while (match(","));
361
+ expect(")");
362
+ }
363
+ if (args.length < fn.min || args.length > fn.max) {
364
+ throw new Error(`Wrong argument count for "${token.text}"`);
365
+ }
366
+ return { kind: "call", name: token.text, args };
367
+ }
368
+ throw new Error("Expected a value");
369
+ };
370
+ const unary = (depth: number): Node => {
371
+ checkDepth(depth);
372
+ const token = current();
373
+ if (token.kind === "symbol" && (token.text === "-" || token.text === "!")) {
374
+ position++;
375
+ return { kind: "unary", operator: token.text, operand: unary(depth + 1) };
376
+ }
377
+ return primary(depth);
378
+ };
379
+ const binary = (level: number, depth: number): Node => {
380
+ const operators = precedence[level];
381
+ if (!operators) return unary(depth);
382
+ let left = binary(level + 1, depth);
383
+ for (;;) {
384
+ const token = current();
385
+ if (token.kind !== "symbol" || !operators.includes(token.text))
386
+ return left;
387
+ position++;
388
+ left = {
389
+ kind: "binary",
390
+ operator: token.text,
391
+ left,
392
+ right: binary(level + 1, depth),
393
+ };
394
+ }
395
+ };
396
+ const conditional = (depth: number): Node => {
397
+ const condition = binary(0, depth);
398
+ if (!match("?")) return condition;
399
+ const then = conditional(depth + 1);
400
+ expect(":");
401
+ return {
402
+ kind: "conditional",
403
+ condition,
404
+ then,
405
+ otherwise: conditional(depth + 1),
406
+ };
407
+ };
408
+ const node = conditional(0);
409
+ if (current().kind !== "end") throw new Error("Unexpected trailing token");
410
+ return node;
411
+ };
412
+
413
+ const parsedFormulas = new Map<string, Node | string>();
414
+
415
+ const parsedFormula = (formula: string): Node | string => {
416
+ if (formula.length > 500) return "Formula exceeds 500 characters";
417
+ const cached = parsedFormulas.get(formula);
418
+ if (cached !== undefined) return cached;
419
+ let result: Node | string;
420
+ try {
421
+ result = parse(formula);
422
+ } catch (error) {
423
+ result = error instanceof Error ? error.message : "Invalid formula";
424
+ }
425
+ if (parsedFormulas.size >= 500) parsedFormulas.clear();
426
+ parsedFormulas.set(formula, result);
427
+ return result;
428
+ };
429
+
430
+ const childCount = (value: object, step: () => void) => {
431
+ let count = 0;
432
+ eachChild(value, () => {
433
+ step();
434
+ count++;
435
+ return true;
436
+ });
437
+ return count;
438
+ };
439
+
440
+ const equal = (a: unknown, b: unknown, step: () => void): boolean => {
441
+ step();
442
+ if (a === b) return true;
443
+ if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
444
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
445
+ if (childCount(a, step) !== childCount(b, step)) return false;
446
+ return eachChild(a, (child, key) =>
447
+ equal(child, (b as Record<string, unknown>)[key], step),
448
+ );
449
+ };
450
+
451
+ const calculate = (
452
+ operator: string,
453
+ left: unknown,
454
+ right: unknown,
455
+ step: () => void,
456
+ ): unknown => {
457
+ if (operator === "==") return equal(left, right, step);
458
+ if (operator === "!=") return !equal(left, right, step);
459
+ if (
460
+ operator === "+" &&
461
+ (typeof left === "string" || typeof right === "string")
462
+ ) {
463
+ return formatText(left, step) + formatText(right, step);
464
+ }
465
+ if (["<", "<=", ">", ">="].includes(operator)) {
466
+ if (
467
+ !(typeof left === "number" && typeof right === "number") &&
468
+ !(typeof left === "string" && typeof right === "string")
469
+ )
470
+ return false;
471
+ if (operator === "<") return left < right;
472
+ if (operator === "<=") return left <= right;
473
+ if (operator === ">") return left > right;
474
+ return left >= right;
475
+ }
476
+ if (!isNumber(left) || !isNumber(right)) return null;
477
+ switch (operator) {
478
+ case "+":
479
+ return left + right;
480
+ case "-":
481
+ return left - right;
482
+ case "*":
483
+ return left * right;
484
+ case "/":
485
+ return right === 0 ? null : left / right;
486
+ case "%":
487
+ return right === 0 ? null : left % right;
488
+ default:
489
+ return null;
490
+ }
491
+ };
492
+
493
+ const evaluate = (
494
+ node: Node,
495
+ ctx: ExpressionContext,
496
+ step: () => void,
497
+ ): unknown => {
498
+ step();
499
+ const visit = (child: Node) => normalize(evaluate(child, ctx, step));
500
+ switch (node.kind) {
501
+ case "literal":
502
+ return node.value;
503
+ case "reference":
504
+ return readReference(node.path, ctx);
505
+ case "unary": {
506
+ const value = visit(node.operand);
507
+ return node.operator === "!" ? !value : isNumber(value) ? -value : null;
508
+ }
509
+ case "conditional":
510
+ return visit(node.condition) ? visit(node.then) : visit(node.otherwise);
511
+ case "call":
512
+ return functions.get(node.name)!.run(node.args.map(visit), step);
513
+ case "binary": {
514
+ const left = visit(node.left);
515
+ if (node.operator === "&&")
516
+ return Boolean(left) && Boolean(visit(node.right));
517
+ if (node.operator === "||")
518
+ return Boolean(left) || Boolean(visit(node.right));
519
+ return calculate(node.operator, left, visit(node.right), step);
520
+ }
521
+ }
522
+ };
523
+
524
+ /** Evaluates a bounded formula, returning null when it cannot run. */
525
+ export function evaluateFormula(
526
+ formula: string,
527
+ ctx: ExpressionContext,
528
+ ): unknown {
529
+ try {
530
+ const node = parsedFormula(formula);
531
+ if (typeof node === "string") return null;
532
+ let steps = 0;
533
+ return normalize(
534
+ evaluate(node, ctx, () => {
535
+ if (++steps > 10_000)
536
+ throw new Error("Formula exceeds 10000 evaluation steps");
537
+ }),
538
+ );
539
+ } catch {
540
+ return null;
541
+ }
542
+ }
543
+
544
+ /** The state paths a formula shows as values, leaving out reads under a condition and reads in the `?` branch of a condition on that same path, which only runs while it has a value. */
545
+ export function formulaValueReads(formula: string): string[] {
546
+ const node = parsedFormula(formula);
547
+ if (typeof node === "string") return [];
548
+ const reads = new Set<string>();
549
+ const visit = (
550
+ current: Node,
551
+ condition: boolean,
552
+ guarded: ReadonlySet<string>,
553
+ ): void => {
554
+ switch (current.kind) {
555
+ case "literal":
556
+ return;
557
+ case "reference":
558
+ if (
559
+ !condition &&
560
+ !current.path.startsWith("$") &&
561
+ !guarded.has(current.path)
562
+ )
563
+ reads.add(current.path);
564
+ return;
565
+ case "unary":
566
+ visit(current.operand, condition || current.operator === "!", guarded);
567
+ return;
568
+ case "binary": {
569
+ const logical = current.operator === "&&" || current.operator === "||";
570
+ visit(current.left, condition || logical, guarded);
571
+ visit(current.right, condition || logical, guarded);
572
+ return;
573
+ }
574
+ case "conditional":
575
+ visit(current.condition, true, guarded);
576
+ visit(
577
+ current.then,
578
+ condition,
579
+ current.condition.kind === "reference"
580
+ ? new Set([...guarded, current.condition.path])
581
+ : guarded,
582
+ );
583
+ visit(current.otherwise, condition, guarded);
584
+ return;
585
+ case "call":
586
+ for (const arg of current.args) visit(arg, condition, guarded);
587
+ return;
588
+ }
589
+ };
590
+ visit(node, false, new Set());
591
+ return [...reads];
592
+ }
593
+
594
+ /** Why a formula cannot run, or undefined when it parses within the bounds. */
595
+ export function formulaIssue(formula: string): string | undefined {
596
+ const result = parsedFormula(formula);
597
+ return typeof result === "string" ? result : undefined;
598
+ }