@tinkerlandai/indiana-iam 0.2.0 → 0.4.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 (55) hide show
  1. package/README.md +127 -5
  2. package/assets/document-controller.js +18 -1
  3. package/assets/document-runtime.js +189 -1
  4. package/assets/viewer.js +74 -7
  5. package/package.json +6 -13
  6. package/src/analyze.mjs +897 -123
  7. package/src/anchors.mjs +5 -0
  8. package/src/build-options.d.ts +6 -1
  9. package/src/build-runtime.mjs +4 -1
  10. package/src/build-styles.mjs +49 -6
  11. package/src/build.mjs +13 -7
  12. package/src/cli.mjs +10 -4
  13. package/src/component-registry.mjs +229 -0
  14. package/src/dependencies.mjs +2 -2
  15. package/src/dom-payload-browser.mjs +28 -0
  16. package/src/dom-payload-json.mjs +12 -0
  17. package/src/dom-payload.mjs +50 -0
  18. package/src/framework-routes.mjs +44 -89
  19. package/src/generate.mjs +20 -7
  20. package/src/graph-sources.mjs +54 -0
  21. package/src/index.d.ts +13 -1
  22. package/src/index.mjs +1 -1
  23. package/src/integrity.mjs +4 -2
  24. package/src/library-entry.mjs +5 -0
  25. package/src/lucide-source.mjs +18 -0
  26. package/src/next-loader.cjs +7 -1
  27. package/src/next.cjs +5 -0
  28. package/src/next.mjs +4 -3
  29. package/src/node-hashes.mjs +13 -9
  30. package/src/render-index.mjs +6 -4
  31. package/src/route-lazy.mjs +80 -0
  32. package/src/route-tree.mjs +15 -3
  33. package/src/routing.mjs +1 -1
  34. package/src/schema-definition.mjs +8 -1
  35. package/src/source-attributes.mjs +24 -0
  36. package/src/source-charts.mjs +38 -0
  37. package/src/source-collections.mjs +241 -0
  38. package/src/source-entry.mjs +50 -0
  39. package/src/source-environment.mjs +26 -0
  40. package/src/source-media.mjs +21 -0
  41. package/src/source-query-evidence.mjs +64 -0
  42. package/src/source-redirects.mjs +388 -0
  43. package/src/source-resolution.mjs +268 -0
  44. package/src/static-primitives.mjs +48 -0
  45. package/src/static-query.mjs +39 -11
  46. package/src/tab-scenes.mjs +214 -0
  47. package/src/tanstack-routes.mjs +143 -0
  48. package/src/transform.mjs +2 -2
  49. package/src/ui-preview.mjs +225 -0
  50. package/src/ui-state-machine.mjs +76 -0
  51. package/src/ui-structure.mjs +645 -0
  52. package/src/upload.mjs +28 -6
  53. package/src/validate.mjs +19 -4
  54. package/src/vite.d.ts +4 -1
  55. package/src/vite.mjs +25 -10
@@ -0,0 +1,143 @@
1
+ /** Generated file-route graphs only: read the registered tree, never execute a router. */
2
+ export function tanstackRoutes({ discoveryModules, ts, resolution, gap, component, add, externalImport }) {
3
+ let routers = 0;
4
+ const unwrap = node => {
5
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
6
+ return node;
7
+ };
8
+ const fail = message => { throw Error("TanStack route adapter: " + message); };
9
+ function ref(node, module) {
10
+ const resolved = resolution.reference(node, module);
11
+ if (!resolved) fail("unresolved or cyclic source reference");
12
+ return resolved;
13
+ }
14
+ function fields(node, module) {
15
+ const resolved = ref(node, module);
16
+ node = resolved.node;
17
+ if (!ts.isObjectLiteralExpression(node) || node.properties.some(p => !(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p) || ts.isMethodDeclaration(p)) || ts.isComputedPropertyName(p.name))) fail("configuration must be a literal record without spreads or getters");
18
+ const values = new Map();
19
+ for (const property of node.properties) {
20
+ if (values.has(property.name.text)) fail("duplicate configuration field");
21
+ values.set(property.name.text, ts.isShorthandPropertyAssignment(property) ? property.name : ts.isMethodDeclaration(property) ? property : property.initializer);
22
+ }
23
+ return { module: resolved.module, node, values };
24
+ }
25
+ function returned(node) {
26
+ node = unwrap(node);
27
+ if (!node || !(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) || node.parameters.length) fail("parent must be a parameterless source callback");
28
+ if (!ts.isBlock(node.body)) return node.body;
29
+ if (node.body.statements.length === 1 && ts.isReturnStatement(node.body.statements[0])) return node.body.statements[0].expression;
30
+ fail("parent callback is not a direct return");
31
+ }
32
+ function identity(node, module, depth = 0) {
33
+ if (depth > 32) fail("route reference budget exceeded");
34
+ const resolved = ref(node, module);
35
+ node = resolved.node; module = resolved.module;
36
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["update", "_addFileChildren", "_addFileTypes"].includes(node.expression.name.text)) return identity(node.expression.expression, module, depth + 1);
37
+ return module.file + ":" + node.pos + ":" + node.end;
38
+ }
39
+ function graph(node, module, seen = new Set()) {
40
+ const resolved = ref(node, module);
41
+ node = resolved.node; module = resolved.module;
42
+ const key = module.file + ":" + node.pos + ":" + node.end;
43
+ if (seen.has(key) || seen.size > 32) fail("cyclic or excessive route graph");
44
+ seen = new Set([...seen, key]);
45
+ if (!ts.isCallExpression(node)) fail("route graph is not a supported source declaration");
46
+ if (ts.isPropertyAccessExpression(node.expression)) {
47
+ const method = node.expression.name.text;
48
+ if (method === "_addFileTypes" && !node.arguments.length) return graph(node.expression.expression, module, seen);
49
+ if (method === "_addFileChildren" && node.arguments.length === 1) {
50
+ const parent = graph(node.expression.expression, module, seen), children = fields(node.arguments[0], module);
51
+ if (parent.children.length) fail("route children registered more than once");
52
+ parent.children = [...children.values.values()].map(child => graph(child, children.module, seen));
53
+ return parent;
54
+ }
55
+ if (method === "update" && node.arguments.length === 1) {
56
+ const original = graph(node.expression.expression, module, seen), config = fields(node.arguments[0], module);
57
+ if (original.root || original.updated || original.children.length) fail("unexpected route update");
58
+ if ([...config.values.keys()].some(key => !["id", "path", "getParentRoute"].includes(key))) fail("unsupported generated update field");
59
+ const routeId = resolution.string(config.values.get("id"), config.module);
60
+ if (!routeId || !config.values.has("getParentRoute")) fail("route update needs a static id and parent");
61
+ original.parent = identity(returned(config.values.get("getParentRoute")), config.module);
62
+ original.path = config.values.has("path") ? resolution.string(config.values.get("path"), config.module) : undefined;
63
+ if (config.values.has("path") && original.path === undefined) fail("dynamic generated path");
64
+ original.updated = true;
65
+ original.id = routeId;
66
+ original.routeModule = module;
67
+ original.routeNode = node;
68
+ return original;
69
+ }
70
+ fail("unsupported graph method " + method);
71
+ }
72
+ let factory = node.expression, options = node.arguments[0], root = false, sourceId = "$root";
73
+ if (ts.isCallExpression(factory)) {
74
+ const creator = externalImport(module, factory.expression.getText(module.source));
75
+ if (creator?.specifier !== "@tanstack/react-router") fail("unrecognized route factory");
76
+ if (creator.name === "createFileRoute") {
77
+ if (factory.arguments.length !== 1 || resolution.string(factory.arguments[0], module) === undefined) fail("file route identity must be static");
78
+ sourceId = resolution.string(factory.arguments[0], module);
79
+ } else if (creator.name === "createRootRouteWithContext" && !factory.arguments.length) root = true;
80
+ else fail("unsupported route factory");
81
+ } else {
82
+ const creator = externalImport(module, factory.getText(module.source));
83
+ if (creator?.specifier !== "@tanstack/react-router" || creator.name !== "createRootRoute") fail("unrecognized root factory");
84
+ root = true;
85
+ }
86
+ if (node.arguments.length !== 1) fail("route factory needs one source configuration");
87
+ const config = fields(options, module);
88
+ if (["id", "path", "getParentRoute"].some(key => config.values.has(key))) fail("file-route URL configuration must come from the generated update");
89
+ if ([...config.values.keys()].some(key => ["loader", "beforeLoad", "validateSearch", "errorComponent", "notFoundComponent", "pendingComponent"].includes(key))) gap("ROUTE_DATA_UNOBSERVED", module.file, "TanStack loaders, guards, search and error states are not executed");
90
+ return { key: identity(node, module), root, sourceId, config, children: [], routeModule: module, routeNode: node };
91
+ }
92
+ function pathOf(parent, child) {
93
+ if (child === undefined) return parent;
94
+ if (/[?#\\\s]/.test(child) || child.split("/").some(part => part === "." || part === "..")) fail("invalid generated path");
95
+ const segments = child.split("/").filter(Boolean).map(part => {
96
+ if (part === "$") return "*";
97
+ if (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(part)) return ":" + part.slice(1);
98
+ if (/[$\[\]{}()*:]/.test(part)) fail("unsupported parameter pattern");
99
+ return part;
100
+ });
101
+ return (parent.replace(/\/$/, "") + "/" + segments.join("/")).replace(/\/$/, "") || "/";
102
+ }
103
+ function project(root) {
104
+ const registered = new Set();
105
+ function walk(route, parent, path, ancestors, depth = 0) {
106
+ if (depth > 16 || registered.size >= 200 || registered.has(route.key)) fail("duplicate, cyclic or excessive registered route");
107
+ registered.add(route.key);
108
+ if (parent ? route.root || !route.updated || route.parent !== parent.key : !route.root) fail("registered parent does not match generated parent");
109
+ const publicPath = pathOf(path, route.path);
110
+ // Use source route identity, not the public URL, for inline layout symbols.
111
+ const target = component(route.config.values.get("component"), route.config.module, "$tanstack:" + route.config.module.file + ":" + route.sourceId);
112
+ let indexAtPath = false;
113
+ const childIds = new Set();
114
+ for (const child of route.children) {
115
+ if (childIds.has(child.id)) fail("duplicate generated child id");
116
+ childIds.add(child.id);
117
+ }
118
+ for (const child of route.children) indexAtPath = walk(child, route, publicPath, target ? [...ancestors, target] : ancestors, depth + 1) || indexAtPath;
119
+ const ownsPath = route.root || route.path !== undefined;
120
+ if (ownsPath && !indexAtPath && target) add(route.routeModule, route.routeNode, publicPath, target, ancestors);
121
+ if (ownsPath && !target && !route.children.length) gap("ROUTE_COMPONENT_UNSUPPORTED", route.config.module.file, "TanStack route without an inspectable component");
122
+ return ownsPath && publicPath === path || indexAtPath && publicPath === path;
123
+ }
124
+ walk(root, null, "/", []);
125
+ }
126
+ for (const module of discoveryModules.values()) {
127
+ function visit(node) {
128
+ if (ts.isCallExpression(node)) {
129
+ const imported = externalImport(module, node.expression.getText(module.source));
130
+ if (imported?.specifier === "@tanstack/react-router" && imported.name === "createRouter") {
131
+ if (++routers > 1) fail("multiple router authorities require one explicit entry");
132
+ const options = fields(node.arguments[0], module);
133
+ if (node.arguments.length !== 1 || !options.values.has("routeTree") || ["basepath", "history", "rewrite", "trailingSlash"].some(key => options.values.has(key))) fail("custom URL routing requires a separate adapter");
134
+ project(graph(options.values.get("routeTree"), options.module));
135
+ return;
136
+ }
137
+ }
138
+ ts.forEachChild(node, visit);
139
+ }
140
+ visit(module.source);
141
+ }
142
+ return routers;
143
+ }
package/src/transform.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import MagicString from "magic-string";
2
2
  import { dependency } from "./dependencies.mjs";
3
- import { automaticSourceAnchors, instrumentableSource } from "./anchors.mjs";
3
+ import { automaticSourceAnchors, instrumentableSource, sourceScriptKind } from "./anchors.mjs";
4
4
 
5
5
  /** Adds build-only anchors without editing merchant source or evaluating any expression. */
6
6
  export function transformSourceForIam(code, file) {
@@ -12,7 +12,7 @@ export function transformSourceForIam(code, file) {
12
12
  code,
13
13
  ts.ScriptTarget.Latest,
14
14
  true,
15
- /\.jsx?$/.test(file) ? ts.ScriptKind.JSX : ts.ScriptKind.TSX,
15
+ sourceScriptKind(file, ts),
16
16
  );
17
17
  if (source.parseDiagnostics.length)
18
18
  throw new Error("IAM cannot instrument invalid source: " + file);
@@ -0,0 +1,225 @@
1
+ import { JSDOM, VirtualConsole } from "jsdom";
2
+ import { documentDom } from "./dom-payload.mjs";
3
+ import { createStateMachine } from "./ui-state-machine.mjs";
4
+ import { applySourceAttributes } from "./source-attributes.mjs";
5
+ import {
6
+ applySourceCollections,
7
+ markUnobservedData,
8
+ } from "./source-collections.mjs";
9
+
10
+ /** Inert source alternatives for UI inspection. This never changes the admitted render or SDK targets. */
11
+ export function previewUiDocument(manifest, document, options = {}) {
12
+ const graph = manifest.routeTree,
13
+ render = graph.renders.find((r) => r.documentKey === document.documentKey);
14
+ if (!render) throw Error("Unknown UI preview render");
15
+ const dom = new JSDOM(documentDom(document).html, {
16
+ virtualConsole: new VirtualConsole(),
17
+ });
18
+ try {
19
+ const doc = dom.window.document;
20
+ const nodes = new Map(
21
+ [...doc.querySelectorAll("[data-iam-node]")].map((n) => [
22
+ n.getAttribute("data-iam-node"),
23
+ n,
24
+ ]),
25
+ );
26
+ const sourceConditions = new Map(
27
+ (graph.ui?.conditions ?? []).map((c) => [c.conditionKey, c]),
28
+ );
29
+ const conditions = [
30
+ ...doc.querySelectorAll(
31
+ '[data-iam-condition][data-iam-alternative="true"]',
32
+ ),
33
+ ].map((node) => {
34
+ const source = sourceConditions.get(
35
+ node.getAttribute("data-iam-condition"),
36
+ );
37
+ if (!source) throw Error("Unknown source condition");
38
+ const other = node.nextElementSibling;
39
+ return {
40
+ ...source,
41
+ defaultChoice:
42
+ node.getAttribute("data-iam-condition-default") === "false"
43
+ ? false
44
+ : undefined,
45
+ groupKey: node.getAttribute("data-iam-node"),
46
+ truthyNodeId: node.getAttribute("data-iam-node"),
47
+ falsyNodeId:
48
+ other?.getAttribute("data-iam-condition") === source.conditionKey &&
49
+ other.getAttribute("data-iam-alternative") === "false"
50
+ ? other.getAttribute("data-iam-node")
51
+ : null,
52
+ };
53
+ });
54
+ const selections = new Map();
55
+ const groups = new Map();
56
+ for (const row of conditions) {
57
+ if (!groups.has(row.groupKey)) groups.set(row.groupKey, []);
58
+ groups.get(row.groupKey).push(row);
59
+ }
60
+ if (options.strategy !== undefined && options.strategy !== "content")
61
+ throw Error("Unknown UI preview strategy");
62
+ for (const [key, rows] of groups) {
63
+ const supplied = options.conditions?.[key];
64
+ if (supplied !== undefined && typeof supplied !== "boolean")
65
+ throw Error("Invalid UI condition choice");
66
+ if (supplied !== undefined) selections.set(key, supplied);
67
+ else if (options.strategy === "content") {
68
+ if (rows[0].observation) continue;
69
+ if (rows[0].defaultChoice !== undefined) {
70
+ selections.set(key, rows[0].defaultChoice);
71
+ continue;
72
+ }
73
+ const score = (id) => {
74
+ const node = nodes.get(id);
75
+ return node
76
+ ? node.textContent.trim().length +
77
+ node.querySelectorAll("input,button,a,textarea,select").length *
78
+ 24
79
+ : 0;
80
+ };
81
+ selections.set(
82
+ key,
83
+ rows.reduce(
84
+ (n, r) => n + score(r.truthyNodeId) - score(r.falsyNodeId),
85
+ 0,
86
+ ) >= 0,
87
+ );
88
+ }
89
+ }
90
+ for (const key of Object.keys(options.conditions ?? {}))
91
+ if (!groups.has(key)) throw Error("Unknown UI condition");
92
+ for (const row of conditions)
93
+ if (selections.has(row.groupKey)) {
94
+ nodes
95
+ .get(row.truthyNodeId)
96
+ ?.toggleAttribute("hidden", !selections.get(row.groupKey));
97
+ if (row.falsyNodeId)
98
+ nodes
99
+ .get(row.falsyNodeId)
100
+ ?.toggleAttribute("hidden", selections.get(row.groupKey));
101
+ }
102
+ const machine = createStateMachine(
103
+ graph,
104
+ render.routeKey,
105
+ render.renderKey,
106
+ );
107
+ if (options.values) machine.preview(options.values);
108
+ for (const binding of machine.bindings()) {
109
+ const node = nodes.get(binding.nodeId);
110
+ if (!node) continue;
111
+ if (binding.property === "visible")
112
+ node.toggleAttribute("hidden", !binding.value);
113
+ else if (binding.property === "text")
114
+ node.textContent = String(binding.value);
115
+ else if (binding.property === "className")
116
+ node.setAttribute("class", String(binding.value));
117
+ else if (["disabled", "checked"].includes(binding.property))
118
+ node.toggleAttribute(binding.property, Boolean(binding.value));
119
+ else if (
120
+ binding.property === "value" &&
121
+ node.getAttribute("type") !== "password"
122
+ )
123
+ node.setAttribute("value", String(binding.value));
124
+ }
125
+ const values = machine.snapshot();
126
+ for (const tab of graph.ui?.tabs ?? [])
127
+ if (tab.renderKey === render.renderKey) {
128
+ const selected = tab.variableKey
129
+ ? values[tab.variableKey]
130
+ : tab.initialValue;
131
+ for (const item of tab.items)
132
+ nodes
133
+ .get(item.nodeId)
134
+ ?.setAttribute("aria-selected", String(item.value === selected));
135
+ }
136
+ const regions = (graph.ui?.regions ?? []).filter(
137
+ (r) => r.renderKey === render.renderKey,
138
+ ),
139
+ componentChoices = [];
140
+ for (const key of Object.keys(options.regions ?? {}))
141
+ if (!regions.some((r) => r.regionKey === key))
142
+ throw Error("Unknown component region");
143
+ for (const region of regions) {
144
+ const requested = options.regions?.[region.regionKey],
145
+ stateValue = region.variableKey ? values[region.variableKey] : null;
146
+ const selected =
147
+ requested ??
148
+ (options.strategy === "content"
149
+ ? region.options.some((o) => o.value === stateValue)
150
+ ? stateValue
151
+ : (region.initialValue ?? region.options[0].value)
152
+ : null);
153
+ if (
154
+ selected !== null &&
155
+ !region.options.some((o) => o.value === selected)
156
+ )
157
+ throw Error("Unknown source component alternative");
158
+ if (selected !== null) {
159
+ for (const option of region.options)
160
+ nodes
161
+ .get(option.nodeId)
162
+ ?.toggleAttribute("hidden", option.value !== selected);
163
+ componentChoices.push({
164
+ regionKey: region.regionKey,
165
+ value: selected,
166
+ source: region.source,
167
+ expression: region.expression,
168
+ });
169
+ }
170
+ }
171
+ applySourceAttributes(doc, graph.ui, render.renderKey, values);
172
+ const focus = options.focusNodeId ? nodes.get(options.focusNodeId) : null;
173
+ const collectionUi = focus
174
+ ? {
175
+ ...graph.ui,
176
+ collections: (graph.ui?.collections ?? []).filter((row) => {
177
+ const node = nodes.get(row.nodeId);
178
+ return node && focus.contains(node);
179
+ }),
180
+ }
181
+ : graph.ui;
182
+ const collectionChoices = applySourceCollections(
183
+ doc,
184
+ collectionUi,
185
+ render.renderKey,
186
+ values,
187
+ options.strategy === "content" || Boolean(options.values),
188
+ );
189
+ const unobserved = markUnobservedData(
190
+ doc,
191
+ graph.ui,
192
+ options.strategy === "content",
193
+ selections,
194
+ );
195
+ const assumptions = [...groups].flatMap(([key, rows]) => {
196
+ if (!selections.has(key)) return [];
197
+ const row = rows.find((r) => {
198
+ const node = nodes.get(
199
+ selections.get(key) ? r.truthyNodeId : r.falsyNodeId,
200
+ );
201
+ return node && !node.parentElement?.closest("[hidden]");
202
+ });
203
+ return row
204
+ ? [
205
+ {
206
+ groupKey: key,
207
+ expression: row.expression,
208
+ selected: selections.get(key),
209
+ source: row.source,
210
+ },
211
+ ]
212
+ : [];
213
+ });
214
+ return {
215
+ html: doc.documentElement.outerHTML,
216
+ assumptions,
217
+ values,
218
+ componentChoices,
219
+ collectionChoices,
220
+ unobserved,
221
+ };
222
+ } finally {
223
+ dom.window.close();
224
+ }
225
+ }
@@ -0,0 +1,76 @@
1
+ /* Generated from assets/state-machine.js by build-runtime.mjs. */
2
+ /* Indiana declarative UI state machine. Pure computation; no merchant callbacks. */
3
+ export function createStateMachine(graph, routeKey, renderKey) {
4
+ {
5
+ renderKey ??= graph.routes.find(route => route.routeKey === routeKey)?.initialRenderKey;
6
+ if (!graph.renders.some(render => render.renderKey === renderKey && render.routeKey === routeKey)) throw new Error("Unknown UI render");
7
+ }
8
+ const owns = row => row.routeKey === routeKey && row.renderKey === renderKey;
9
+ const variables = new Map(graph.variables.filter(owns).map(v => [v.variableKey, v]));
10
+ const transitions = graph.transitions.filter(t => owns(t) && t.simulation === "implemented");
11
+ let values = Object.fromEntries([...variables].map(([key, v]) => [key, v.initialValue]));
12
+ const read = (expression, payload, snapshot, depth = 0) => {
13
+ if (depth > 24) throw new Error("Expression budget exceeded");
14
+ const next = e => read(e, payload, snapshot, depth + 1);
15
+ switch (expression.op) {
16
+ case "const": return expression.value;
17
+ case "var": {
18
+ if (!variables.has(expression.key)) throw new Error("Unknown variable");
19
+ return snapshot[expression.key];
20
+ }
21
+ case "event": {
22
+ if (!Object.hasOwn(payload, expression.key)) throw new Error("Missing event input");
23
+ return payload[expression.key];
24
+ }
25
+ case "not": return !next(expression.arg);
26
+ case "eq": return next(expression.left) === next(expression.right);
27
+ case "ne": return next(expression.left) !== next(expression.right);
28
+ case "add":
29
+ case "subtract": {
30
+ const left = next(expression.left), right = next(expression.right);
31
+ if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Invalid numeric expression");
32
+ const value = expression.op === "add" ? left + right : left - right;
33
+ if (!Number.isFinite(value)) throw new Error("Invalid numeric result");
34
+ return value;
35
+ }
36
+ case "all": return expression.args.every(e => next(e) === true);
37
+ case "any": return expression.args.some(e => next(e) === true);
38
+ case "classes": return expression.parts.filter(part=>next(part.when)===true).map(part=>part.value).join(' ');
39
+ default: throw new Error("Unsupported expression");
40
+ }
41
+ };
42
+ return {
43
+ snapshot: () => ({ ...values }),
44
+ preview(input) {
45
+ if (!input || Array.isArray(input) || typeof input !== "object" || Object.keys(input).length > variables.size) throw new Error("Invalid preview values");
46
+ const writes = Object.create(null);
47
+ for (const [key, value] of Object.entries(input)) {
48
+ const variable = variables.get(key);
49
+ if (!variable || typeof value !== variable.valueType || typeof value === "string" && value.length > 4000 || typeof value === "number" && !Number.isFinite(value)) throw new Error("Invalid preview value");
50
+ writes[key] = value;
51
+ }
52
+ values = { ...values, ...writes };
53
+ return { values: { ...values }, evidence: "manual-ui-preview" };
54
+ },
55
+ reset: () => { values = Object.fromEntries([...variables].map(([key, v]) => [key, v.initialValue])); },
56
+ bindings: () => graph.bindings.filter(owns).map(b => ({ ...b, value: read(b.value, {}, values) })),
57
+ dispatch(nodeId, event, payload = {}) {
58
+ const candidates = transitions.filter(t => t.trigger.nodeId === nodeId && t.trigger.event === event);
59
+ for (const t of candidates) {
60
+ const expected = Object.keys(t.trigger.payload);
61
+ if (Object.keys(payload).some(key => !expected.includes(key)) || expected.some(key => typeof payload[key] !== t.trigger.payload[key] || typeof payload[key] === "string" && payload[key].length > 4000)) throw new Error("Invalid event input");
62
+ }
63
+ const eligible = candidates.filter(t => read(t.guard, payload, values) === true);
64
+ if (eligible.length > 1) throw new Error("Ambiguous UI transition");
65
+ if (!eligible.length) return null;
66
+ const t = eligible[0], writes = Object.create(null);
67
+ for (const w of t.stateWrites) {
68
+ const value = read(w.value, payload, values), variable = variables.get(w.variableKey);
69
+ if (!variable || typeof value !== variable.valueType || typeof value === "string" && value.length > 4000 || typeof value === "number" && !Number.isFinite(value)) throw new Error("Invalid state write");
70
+ writes[w.variableKey] = value;
71
+ }
72
+ values = { ...values, ...writes };
73
+ return { transitionKey: t.transitionKey, values: { ...values } };
74
+ },
75
+ };
76
+ }