@nodetool-ai/execution 0.8.1

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 (231) hide show
  1. package/README.md +129 -0
  2. package/dist/.nodetool-build-stamp +1 -0
  3. package/dist/app-debug/app-spec.d.ts +94 -0
  4. package/dist/app-debug/app-spec.d.ts.map +1 -0
  5. package/dist/app-debug/app-spec.js +710 -0
  6. package/dist/app-debug/app-spec.js.map +1 -0
  7. package/dist/app-debug/application-target.d.ts +53 -0
  8. package/dist/app-debug/application-target.d.ts.map +1 -0
  9. package/dist/app-debug/application-target.js +126 -0
  10. package/dist/app-debug/application-target.js.map +1 -0
  11. package/dist/app-debug/bundle-target.d.ts +21 -0
  12. package/dist/app-debug/bundle-target.d.ts.map +1 -0
  13. package/dist/app-debug/bundle-target.js +51 -0
  14. package/dist/app-debug/bundle-target.js.map +1 -0
  15. package/dist/app-debug/graph-shape.d.ts +14 -0
  16. package/dist/app-debug/graph-shape.d.ts.map +1 -0
  17. package/dist/app-debug/graph-shape.js +23 -0
  18. package/dist/app-debug/graph-shape.js.map +1 -0
  19. package/dist/app-debug/index.d.ts +26 -0
  20. package/dist/app-debug/index.d.ts.map +1 -0
  21. package/dist/app-debug/index.js +19 -0
  22. package/dist/app-debug/index.js.map +1 -0
  23. package/dist/app-debug/markdown.d.ts +6 -0
  24. package/dist/app-debug/markdown.d.ts.map +1 -0
  25. package/dist/app-debug/markdown.js +133 -0
  26. package/dist/app-debug/markdown.js.map +1 -0
  27. package/dist/app-debug/runtime.d.ts +267 -0
  28. package/dist/app-debug/runtime.d.ts.map +1 -0
  29. package/dist/app-debug/runtime.js +633 -0
  30. package/dist/app-debug/runtime.js.map +1 -0
  31. package/dist/app-debug/script-operation.d.ts +58 -0
  32. package/dist/app-debug/script-operation.d.ts.map +1 -0
  33. package/dist/app-debug/script-operation.js +44 -0
  34. package/dist/app-debug/script-operation.js.map +1 -0
  35. package/dist/app-debug/simulate.d.ts +85 -0
  36. package/dist/app-debug/simulate.d.ts.map +1 -0
  37. package/dist/app-debug/simulate.js +991 -0
  38. package/dist/app-debug/simulate.js.map +1 -0
  39. package/dist/app-debug/summarize.d.ts +64 -0
  40. package/dist/app-debug/summarize.d.ts.map +1 -0
  41. package/dist/app-debug/summarize.js +65 -0
  42. package/dist/app-debug/summarize.js.map +1 -0
  43. package/dist/app-debug/types.d.ts +343 -0
  44. package/dist/app-debug/types.d.ts.map +1 -0
  45. package/dist/app-debug/types.js +2 -0
  46. package/dist/app-debug/types.js.map +1 -0
  47. package/dist/cost-ledger.d.ts +147 -0
  48. package/dist/cost-ledger.d.ts.map +1 -0
  49. package/dist/cost-ledger.js +288 -0
  50. package/dist/cost-ledger.js.map +1 -0
  51. package/dist/debug/collector.d.ts +28 -0
  52. package/dist/debug/collector.d.ts.map +1 -0
  53. package/dist/debug/collector.js +284 -0
  54. package/dist/debug/collector.js.map +1 -0
  55. package/dist/debug/index.d.ts +15 -0
  56. package/dist/debug/index.d.ts.map +1 -0
  57. package/dist/debug/index.js +16 -0
  58. package/dist/debug/index.js.map +1 -0
  59. package/dist/debug/types.d.ts +179 -0
  60. package/dist/debug/types.d.ts.map +1 -0
  61. package/dist/debug/types.js +11 -0
  62. package/dist/debug/types.js.map +1 -0
  63. package/dist/debug/verdict.d.ts +27 -0
  64. package/dist/debug/verdict.d.ts.map +1 -0
  65. package/dist/debug/verdict.js +51 -0
  66. package/dist/debug/verdict.js.map +1 -0
  67. package/dist/executor-resolver.d.ts +13 -0
  68. package/dist/executor-resolver.d.ts.map +1 -0
  69. package/dist/executor-resolver.js +13 -0
  70. package/dist/executor-resolver.js.map +1 -0
  71. package/dist/fal-output-decoder.d.ts +36 -0
  72. package/dist/fal-output-decoder.d.ts.map +1 -0
  73. package/dist/fal-output-decoder.js +129 -0
  74. package/dist/fal-output-decoder.js.map +1 -0
  75. package/dist/generation-lifecycle.d.ts +54 -0
  76. package/dist/generation-lifecycle.d.ts.map +1 -0
  77. package/dist/generation-lifecycle.js +431 -0
  78. package/dist/generation-lifecycle.js.map +1 -0
  79. package/dist/generation-recovery-worker.d.ts +100 -0
  80. package/dist/generation-recovery-worker.d.ts.map +1 -0
  81. package/dist/generation-recovery-worker.js +995 -0
  82. package/dist/generation-recovery-worker.js.map +1 -0
  83. package/dist/generation-tracker.d.ts +83 -0
  84. package/dist/generation-tracker.d.ts.map +1 -0
  85. package/dist/generation-tracker.js +436 -0
  86. package/dist/generation-tracker.js.map +1 -0
  87. package/dist/index.d.ts +28 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +21 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/js-script-debug/index.d.ts +13 -0
  92. package/dist/js-script-debug/index.d.ts.map +1 -0
  93. package/dist/js-script-debug/index.js +12 -0
  94. package/dist/js-script-debug/index.js.map +1 -0
  95. package/dist/js-script-debug/markdown.d.ts +6 -0
  96. package/dist/js-script-debug/markdown.d.ts.map +1 -0
  97. package/dist/js-script-debug/markdown.js +62 -0
  98. package/dist/js-script-debug/markdown.js.map +1 -0
  99. package/dist/js-script-debug/outputs.d.ts +20 -0
  100. package/dist/js-script-debug/outputs.d.ts.map +1 -0
  101. package/dist/js-script-debug/outputs.js +30 -0
  102. package/dist/js-script-debug/outputs.js.map +1 -0
  103. package/dist/js-script-debug/report.d.ts +15 -0
  104. package/dist/js-script-debug/report.d.ts.map +1 -0
  105. package/dist/js-script-debug/report.js +90 -0
  106. package/dist/js-script-debug/report.js.map +1 -0
  107. package/dist/js-script-debug/types.d.ts +63 -0
  108. package/dist/js-script-debug/types.d.ts.map +1 -0
  109. package/dist/js-script-debug/types.js +10 -0
  110. package/dist/js-script-debug/types.js.map +1 -0
  111. package/dist/js-script-debug/validate.d.ts +38 -0
  112. package/dist/js-script-debug/validate.d.ts.map +1 -0
  113. package/dist/js-script-debug/validate.js +137 -0
  114. package/dist/js-script-debug/validate.js.map +1 -0
  115. package/dist/message-stream.d.ts +33 -0
  116. package/dist/message-stream.d.ts.map +1 -0
  117. package/dist/message-stream.js +58 -0
  118. package/dist/message-stream.js.map +1 -0
  119. package/dist/normalize-graph.d.ts +13 -0
  120. package/dist/normalize-graph.d.ts.map +1 -0
  121. package/dist/normalize-graph.js +63 -0
  122. package/dist/normalize-graph.js.map +1 -0
  123. package/dist/output-names.d.ts +13 -0
  124. package/dist/output-names.d.ts.map +1 -0
  125. package/dist/output-names.js +13 -0
  126. package/dist/output-names.js.map +1 -0
  127. package/dist/predicates.d.ts +6 -0
  128. package/dist/predicates.d.ts.map +1 -0
  129. package/dist/predicates.js +6 -0
  130. package/dist/predicates.js.map +1 -0
  131. package/dist/preflight.d.ts +87 -0
  132. package/dist/preflight.d.ts.map +1 -0
  133. package/dist/preflight.js +139 -0
  134. package/dist/preflight.js.map +1 -0
  135. package/dist/provider-secret-key.d.ts +3 -0
  136. package/dist/provider-secret-key.d.ts.map +1 -0
  137. package/dist/provider-secret-key.js +7 -0
  138. package/dist/provider-secret-key.js.map +1 -0
  139. package/dist/service/app-debug-service.d.ts +78 -0
  140. package/dist/service/app-debug-service.d.ts.map +1 -0
  141. package/dist/service/app-debug-service.js +201 -0
  142. package/dist/service/app-debug-service.js.map +1 -0
  143. package/dist/service/app-run-server.d.ts +28 -0
  144. package/dist/service/app-run-server.d.ts.map +1 -0
  145. package/dist/service/app-run-server.js +104 -0
  146. package/dist/service/app-run-server.js.map +1 -0
  147. package/dist/service/debug-sessions.d.ts +158 -0
  148. package/dist/service/debug-sessions.d.ts.map +1 -0
  149. package/dist/service/debug-sessions.js +358 -0
  150. package/dist/service/debug-sessions.js.map +1 -0
  151. package/dist/service/index.d.ts +22 -0
  152. package/dist/service/index.d.ts.map +1 -0
  153. package/dist/service/index.js +17 -0
  154. package/dist/service/index.js.map +1 -0
  155. package/dist/service/job-progress.d.ts +40 -0
  156. package/dist/service/job-progress.d.ts.map +1 -0
  157. package/dist/service/job-progress.js +53 -0
  158. package/dist/service/job-progress.js.map +1 -0
  159. package/dist/service/workflow-run.d.ts +279 -0
  160. package/dist/service/workflow-run.d.ts.map +1 -0
  161. package/dist/service/workflow-run.js +598 -0
  162. package/dist/service/workflow-run.js.map +1 -0
  163. package/dist/service/workflow-workspace.d.ts +89 -0
  164. package/dist/service/workflow-workspace.d.ts.map +1 -0
  165. package/dist/service/workflow-workspace.js +160 -0
  166. package/dist/service/workflow-workspace.js.map +1 -0
  167. package/dist/session.d.ts +62 -0
  168. package/dist/session.d.ts.map +1 -0
  169. package/dist/session.js +353 -0
  170. package/dist/session.js.map +1 -0
  171. package/dist/sketch-debug/index.d.ts +11 -0
  172. package/dist/sketch-debug/index.d.ts.map +1 -0
  173. package/dist/sketch-debug/index.js +10 -0
  174. package/dist/sketch-debug/index.js.map +1 -0
  175. package/dist/sketch-debug/markdown.d.ts +6 -0
  176. package/dist/sketch-debug/markdown.d.ts.map +1 -0
  177. package/dist/sketch-debug/markdown.js +93 -0
  178. package/dist/sketch-debug/markdown.js.map +1 -0
  179. package/dist/sketch-debug/report.d.ts +15 -0
  180. package/dist/sketch-debug/report.d.ts.map +1 -0
  181. package/dist/sketch-debug/report.js +98 -0
  182. package/dist/sketch-debug/report.js.map +1 -0
  183. package/dist/sketch-debug/types.d.ts +62 -0
  184. package/dist/sketch-debug/types.d.ts.map +1 -0
  185. package/dist/sketch-debug/types.js +10 -0
  186. package/dist/sketch-debug/types.js.map +1 -0
  187. package/dist/sketch-debug/validate.d.ts +14 -0
  188. package/dist/sketch-debug/validate.d.ts.map +1 -0
  189. package/dist/sketch-debug/validate.js +478 -0
  190. package/dist/sketch-debug/validate.js.map +1 -0
  191. package/dist/supervisor.d.ts +45 -0
  192. package/dist/supervisor.d.ts.map +1 -0
  193. package/dist/supervisor.js +97 -0
  194. package/dist/supervisor.js.map +1 -0
  195. package/dist/timeline-debug/index.d.ts +11 -0
  196. package/dist/timeline-debug/index.d.ts.map +1 -0
  197. package/dist/timeline-debug/index.js +10 -0
  198. package/dist/timeline-debug/index.js.map +1 -0
  199. package/dist/timeline-debug/legibility.d.ts +23 -0
  200. package/dist/timeline-debug/legibility.d.ts.map +1 -0
  201. package/dist/timeline-debug/legibility.js +240 -0
  202. package/dist/timeline-debug/legibility.js.map +1 -0
  203. package/dist/timeline-debug/markdown.d.ts +6 -0
  204. package/dist/timeline-debug/markdown.d.ts.map +1 -0
  205. package/dist/timeline-debug/markdown.js +103 -0
  206. package/dist/timeline-debug/markdown.js.map +1 -0
  207. package/dist/timeline-debug/motion.d.ts +20 -0
  208. package/dist/timeline-debug/motion.d.ts.map +1 -0
  209. package/dist/timeline-debug/motion.js +362 -0
  210. package/dist/timeline-debug/motion.js.map +1 -0
  211. package/dist/timeline-debug/report.d.ts +16 -0
  212. package/dist/timeline-debug/report.d.ts.map +1 -0
  213. package/dist/timeline-debug/report.js +100 -0
  214. package/dist/timeline-debug/report.js.map +1 -0
  215. package/dist/timeline-debug/showcase.d.ts +7 -0
  216. package/dist/timeline-debug/showcase.d.ts.map +1 -0
  217. package/dist/timeline-debug/showcase.js +347 -0
  218. package/dist/timeline-debug/showcase.js.map +1 -0
  219. package/dist/timeline-debug/types.d.ts +64 -0
  220. package/dist/timeline-debug/types.d.ts.map +1 -0
  221. package/dist/timeline-debug/types.js +10 -0
  222. package/dist/timeline-debug/types.js.map +1 -0
  223. package/dist/timeline-debug/validate.d.ts +24 -0
  224. package/dist/timeline-debug/validate.d.ts.map +1 -0
  225. package/dist/timeline-debug/validate.js +1432 -0
  226. package/dist/timeline-debug/validate.js.map +1 -0
  227. package/dist/types.d.ts +211 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +2 -0
  230. package/dist/types.js.map +1 -0
  231. package/package.json +103 -0
@@ -0,0 +1,991 @@
1
+ import { previewValue } from "../debug/collector.js";
2
+ import { DEFAULT_OPERATION_ID, eventToAction, parseBinding, operationTarget, parseInputStateKey, resolveBinding, stateKey } from "@nodetool-ai/app-runtime";
3
+ import { jsScriptRunMessages, scriptAppIO, scriptOperationInvocation } from "./script-operation.js";
4
+ import { bindingScopeFor, documentOperations, extractAppIO, operationSpec, parseAppSpec, validateApp } from "./app-spec.js";
5
+ import { effectiveTimeoutMs, HeadlessAppRuntime, writeRefusal } from "./runtime.js";
6
+ import { isNonEmptyString, isObjectLike, isString } from "../predicates.js";
7
+ /** Resolve a step's widget reference: exact id, unique type, or unique label. */
8
+ function findWidget(spec, ref) {
9
+ const byId = spec.widgets.find((w) => w.id === ref);
10
+ if (byId)
11
+ return byId;
12
+ const byType = spec.widgets.filter((w) => w.type === ref);
13
+ if (byType.length === 1)
14
+ return byType[0];
15
+ if (byType.length > 1) {
16
+ return `widget reference "${ref}" is ambiguous — ${byType.length} widgets of that type; use the component id.`;
17
+ }
18
+ const byLabel = spec.widgets.filter((w) => w.label === ref);
19
+ if (byLabel.length === 1)
20
+ return byLabel[0];
21
+ if (byLabel.length > 1) {
22
+ return `widget reference "${ref}" is ambiguous — ${byLabel.length} widgets share that label; use the component id.`;
23
+ }
24
+ return `no widget matches "${ref}" (tried id, type, and label). Widgets: ${widgetRoster(spec)}`;
25
+ }
26
+ /**
27
+ * What the caller could have written instead. Naming only the miss sent one
28
+ * agent round after round guessing the step shape, because "no widget matches
29
+ * undefined" says nothing about what a target may be.
30
+ */
31
+ function widgetRoster(spec) {
32
+ if (spec.widgets.length === 0)
33
+ return "none — the app has no widgets.";
34
+ const listed = spec.widgets
35
+ .slice(0, 12)
36
+ .map((w) => w.label
37
+ ? `${w.id} (${w.type}, label "${w.label}")`
38
+ : `${w.id} (${w.type})`)
39
+ .join(", ");
40
+ return spec.widgets.length > 12 ? `${listed}, …` : listed;
41
+ }
42
+ /**
43
+ * The app's natural run trigger, mirroring what a user would do first: click
44
+ * the first `click`→`run` widget (a Run button), else touch the first
45
+ * `change`→`run` write widget.
46
+ */
47
+ export function defaultInteractions(spec) {
48
+ const clicker = spec.widgets.find((w) => w.events.some((e) => e.trigger === "click" && e.kind === "run"));
49
+ if (clicker)
50
+ return [{ click: clicker.id }];
51
+ const changer = spec.widgets.find((w) => w.bindingMode === "write" &&
52
+ w.events.some((e) => e.trigger === "change" && e.kind === "run"));
53
+ if (changer)
54
+ return [{ change: changer.id, value: undefined }];
55
+ return [];
56
+ }
57
+ function describeStep(step) {
58
+ if ("set" in step)
59
+ return `set ${step.set.key}`;
60
+ if ("seedResource" in step)
61
+ return `seedResource ${step.seedResource.id}`;
62
+ if ("click" in step)
63
+ return `click ${step.click}`;
64
+ if ("run" in step)
65
+ return `run ${step.run}`;
66
+ if ("cancel" in step)
67
+ return `cancel ${step.cancel}`;
68
+ return `change ${step.change}`;
69
+ }
70
+ /**
71
+ * Node types that emit on one of several output handles per run. A widget fed
72
+ * from one of their branches is *expected* to stay empty whenever the other
73
+ * branch is taken, so a single run cannot tell "the untaken branch" from "a
74
+ * branch that can never be taken".
75
+ */
76
+ const BRANCHING_NODE_TYPES = new Set(["nodetool.control.If"]);
77
+ /**
78
+ * Node ids reachable only through a branching node. Walks forward from every
79
+ * branch point over the graph's edges.
80
+ */
81
+ function conditionalNodeIds(graph) {
82
+ const downstream = new Map();
83
+ for (const edge of graph.edges) {
84
+ const source = isString(edge.source) ? edge.source : null;
85
+ const target = isString(edge.target) ? edge.target : null;
86
+ if (!source || !target)
87
+ continue;
88
+ const list = downstream.get(source);
89
+ if (list)
90
+ list.push(target);
91
+ else
92
+ downstream.set(source, [target]);
93
+ }
94
+ const conditional = new Set();
95
+ const queue = [];
96
+ for (const node of graph.nodes) {
97
+ const id = isString(node.id) ? node.id : null;
98
+ const type = isString(node.type) ? node.type : null;
99
+ if (id && type && BRANCHING_NODE_TYPES.has(type))
100
+ queue.push(id);
101
+ }
102
+ while (queue.length > 0) {
103
+ const current = queue.pop();
104
+ for (const next of downstream.get(current) ?? []) {
105
+ if (conditional.has(next))
106
+ continue;
107
+ conditional.add(next);
108
+ queue.push(next);
109
+ }
110
+ }
111
+ return conditional;
112
+ }
113
+ /**
114
+ * Overlay the live values of node-property bindings onto the graph, the way the
115
+ * web runtime does before every run (`collectNodePropertyOverlays` →
116
+ * `withNodeProperties` in `web/src/components/appbuilder/nodeBinding.ts`).
117
+ *
118
+ * A widget bound `op:main/prop:node7#strength` writes an input slot keyed by
119
+ * node and property, which no input node reads — the value only reaches the run
120
+ * as a property of the node it names. The web helpers take ReactFlow nodes, so
121
+ * the mapping is repeated here against the kernel shape (`node.properties`);
122
+ * `parseInputStateKey` — the shared inverse of the state key — is what both
123
+ * sides actually agree on.
124
+ *
125
+ * Only the running operation's slots apply: two operations can bind the same
126
+ * node id in different workflows, so an unscoped merge would let one
127
+ * operation's slider drive the other's run.
128
+ */
129
+ export function withNodePropertyOverlays(graph, inputs, operationId) {
130
+ const byNode = new Map();
131
+ for (const [key, slot] of Object.entries(inputs)) {
132
+ if (slot.value === undefined)
133
+ continue;
134
+ const parsed = parseInputStateKey(key);
135
+ if (!parsed?.property)
136
+ continue;
137
+ if (parsed.operationId !== operationId)
138
+ continue;
139
+ const existing = byNode.get(parsed.nodeId);
140
+ if (existing)
141
+ existing[parsed.property] = slot.value;
142
+ else
143
+ byNode.set(parsed.nodeId, { [parsed.property]: slot.value });
144
+ }
145
+ if (byNode.size === 0)
146
+ return graph;
147
+ return {
148
+ nodes: graph.nodes.map((node) => {
149
+ const overlay = isString(node.id) ? byNode.get(node.id) : undefined;
150
+ if (!overlay)
151
+ return node;
152
+ const properties = isObjectLike(node.properties)
153
+ ? node.properties
154
+ : {};
155
+ return { ...node, properties: { ...properties, ...overlay } };
156
+ }),
157
+ edges: graph.edges
158
+ };
159
+ }
160
+ /**
161
+ * Whether a value that did arrive leaves the widget showing nothing. `""`,
162
+ * `[]` and `null` all render blank, so counting them as "received a value"
163
+ * reports an app that computed nothing as one that worked.
164
+ */
165
+ function isEmptyValue(value) {
166
+ if (value === undefined || value === null)
167
+ return true;
168
+ if (isString(value))
169
+ return value.length === 0;
170
+ if (Array.isArray(value))
171
+ return value.length === 0;
172
+ return false;
173
+ }
174
+ /**
175
+ * A workflow can complete while a node hands its own failure downstream as a
176
+ * plain value — a Code node that catches and emits the message, which is how
177
+ * the calculator app that motivated this check reported `'eval' is not
178
+ * defined` while every node reported success. The run carries no error in that
179
+ * case and neither does the invocation, so the value's own shape is the only
180
+ * signal there is. Kept to a stringified `Error` at the very start of the
181
+ * value, where prose that merely discusses an error does not match.
182
+ */
183
+ const ERROR_VALUE = /^(?:[A-Z][A-Za-z]*)?Error:\s+\S/;
184
+ function errorLikeValue(value) {
185
+ if (!isString(value) || !ERROR_VALUE.test(value))
186
+ return null;
187
+ return value.replace(/\s+/g, " ").slice(0, 200);
188
+ }
189
+ /** Node ids keyed by the `name` their data carries, for name-form bindings. */
190
+ function nodeIdsByName(graph) {
191
+ const byName = new Map();
192
+ for (const node of graph.nodes) {
193
+ const id = isString(node.id) ? node.id : null;
194
+ // Runner shape carries node props under `properties`; editor JSON uses
195
+ // `data`, and this runs against both.
196
+ const props = (node.properties ?? node.data);
197
+ const name = isString(props?.name) ? props.name : null;
198
+ if (id && name && !byName.has(name))
199
+ byName.set(name, id);
200
+ }
201
+ return byName;
202
+ }
203
+ /**
204
+ * What a headless run leaves to the browser. Conditions and `format` are
205
+ * simulated, so only what needs a DOM (or a provider the simulator has not
206
+ * grown yet) belongs here.
207
+ */
208
+ const NOT_SIMULATED = [
209
+ "Layout, styling, focus, and scroll — nothing here renders a DOM.",
210
+ "Stored resource collections — a run reads the seeded in-memory provider, never the database, and `openResource` has no editor to open.",
211
+ "Reactive subgraph runs — the browser reruns one input's downstream subgraph, the harness runs the whole workflow."
212
+ ];
213
+ /**
214
+ * Variables a widget writes rather than the graph — a chat composer's
215
+ * conversation. Nothing the run emits fills them, so an empty one after a
216
+ * headless run says nothing about the wiring.
217
+ */
218
+ function uiWrittenVariableIds(spec) {
219
+ const ids = new Set();
220
+ for (const widget of spec?.widgets ?? []) {
221
+ for (const extra of widget.extraBindings) {
222
+ if (extra.ref?.kind === "variable")
223
+ ids.add(extra.ref.variableId);
224
+ }
225
+ }
226
+ return ids;
227
+ }
228
+ /**
229
+ * What every read-bound widget shows after a run that ended clean: an error
230
+ * message a node handed downstream as a plain value, and — for the widgets that
231
+ * stayed empty — whether that emptiness is a defect or expected.
232
+ */
233
+ function widgetValueFindings(report, graph) {
234
+ const issues = [];
235
+ const warnings = [];
236
+ const conditional = conditionalNodeIds(graph);
237
+ const byName = nodeIdsByName(graph);
238
+ const nodeIds = new Set(graph.nodes.map((n) => n.id).filter(isString));
239
+ const uiWrittenVariables = uiWrittenVariableIds(report.spec);
240
+ const ranOperations = new Set(report.invocations.map((i) => i.operationId));
241
+ for (const w of report.widgets) {
242
+ if (w.bindingMode !== "read" || !w.binding)
243
+ continue;
244
+ const message = errorLikeValue(w.display ?? w.value);
245
+ if (message) {
246
+ issues.push(`${w.type} "${w.id}" shows an error message from "${w.binding}": ${message}`);
247
+ }
248
+ }
249
+ for (const w of report.widgets) {
250
+ if (w.bindingMode !== "read" || !w.binding || w.hasValue)
251
+ continue;
252
+ const ref = parseBinding(w.binding);
253
+ if (ref?.kind === "variable" && uiWrittenVariables.has(ref.variableId)) {
254
+ continue;
255
+ }
256
+ if (ref?.kind === "execution") {
257
+ // Execution state is not a value the graph emits — an empty one only
258
+ // means the operation never ran, or reported no activity.
259
+ if (!ranOperations.has(ref.operationId)) {
260
+ issues.push(`${w.type} "${w.id}" shows operation "${ref.operationId}" ${ref.field}, but that operation never ran.`);
261
+ }
262
+ else if (ref.field === "activity") {
263
+ warnings.push(`${w.type} "${w.id}" shows activity, but the run reported none — only streaming agent nodes emit activity labels.`);
264
+ }
265
+ continue;
266
+ }
267
+ // A legacy document binds by node name, and `parseBinding` hands the name
268
+ // back in the `nodeId` slot, so an unrecognised id is retried as a name.
269
+ const parsed = (ref && "nodeId" in ref ? ref.nodeId : null) ?? w.binding;
270
+ const nodeId = nodeIds.has(parsed) ? parsed : (byName.get(parsed) ?? null);
271
+ if (nodeId && conditional.has(nodeId)) {
272
+ // One run takes one branch, so an empty widget here is expected. It is
273
+ // still worth surfacing: a branch that no input can reach looks exactly
274
+ // the same, and only running both branches tells them apart.
275
+ warnings.push(`${w.type} "${w.id}" is bound to "${w.binding}", downstream of a branch that was not taken this run — run the other branch to confirm it can be reached.`);
276
+ continue;
277
+ }
278
+ // The value arrived and is empty ("", [], null). A warning rather than an
279
+ // issue: an output is legitimately empty often enough — no matches, no
280
+ // remainder, nothing to say — that failing the verdict on it would call
281
+ // working apps broken. An app that computed nothing looks the same from
282
+ // here, so the report says what happened and leaves the call to the author.
283
+ // A widget whose `format` template renders nothing keeps the issue below:
284
+ // the value reached it and the template still dropped it, which is wiring,
285
+ // not data.
286
+ if (w.display === null && w.value !== undefined && !w.resourceBindingId) {
287
+ warnings.push(`${w.type} "${w.id}" is bound to "${w.binding}" but received an empty value — confirm the output is meant to be empty.`);
288
+ continue;
289
+ }
290
+ issues.push(`${w.type} "${w.id}" is bound to "${w.binding}" but never received a value — check the output node emits.`);
291
+ }
292
+ return { issues, warnings };
293
+ }
294
+ function verdictHeadline(firstIssue, runCount) {
295
+ if (firstIssue !== undefined)
296
+ return `App has issues — ${firstIssue}`;
297
+ if (runCount > 0) {
298
+ return `App ran clean — ${runCount} run(s), every bound widget on a taken branch received a value.`;
299
+ }
300
+ return "App wiring is valid (static check only — no run executed).";
301
+ }
302
+ function buildAppVerdict(report, ranWorkflow, graph, conditionIssues) {
303
+ const issues = [...report.validation.errors, ...conditionIssues];
304
+ const warnings = [];
305
+ for (const interaction of report.interactions) {
306
+ if (interaction.error) {
307
+ issues.push(`Interaction "${interaction.step}": ${interaction.error}`);
308
+ }
309
+ }
310
+ for (const run of report.runs) {
311
+ if (!run.ok) {
312
+ issues.push(`Run ended ${run.status}${run.error ? `: ${run.error}` : ""}`);
313
+ }
314
+ for (const e of run.summary.errors.slice(0, 5)) {
315
+ const where = e.nodeType ?? e.nodeId ?? "workflow";
316
+ issues.push(`Node ${where}: ${e.message.replace(/\s+/g, " ").slice(0, 200)}`);
317
+ }
318
+ }
319
+ for (const invocation of report.invocations) {
320
+ if (invocation.timedOutMs == null)
321
+ continue;
322
+ issues.push(`Operation "${invocation.operationId}" did not finish within its ${invocation.timedOutMs}ms timeout — the app would show it still running.`);
323
+ }
324
+ // A picker over an unseeded collection renders empty, and no interaction can
325
+ // pick from it — worth saying, but only the script knows what belongs there.
326
+ for (const resource of report.resources) {
327
+ if (resource.seeded)
328
+ continue;
329
+ const shown = report.widgets
330
+ .filter((w) => w.resourceBindingId === resource.id)
331
+ .map((w) => `${w.type} "${w.id}"`);
332
+ if (shown.length === 0)
333
+ continue;
334
+ warnings.push(`${shown.join(", ")} shows resource binding "${resource.id}", which nothing seeded — the collection is empty. Seed it with a seedResource step or a "resource:${resource.id}" param.`);
335
+ }
336
+ const ranClean = ranWorkflow && report.runs.length > 0 && report.runs.every((r) => r.ok);
337
+ if (ranClean) {
338
+ const found = widgetValueFindings(report, graph);
339
+ issues.push(...found.issues);
340
+ warnings.push(...found.warnings);
341
+ }
342
+ const neverRan = ranWorkflow && report.runs.length === 0 && report.invocations.length === 0;
343
+ if (neverRan && (report.spec?.widgets.length ?? 0) > 0) {
344
+ issues.push("No interaction triggered a workflow run — the app was never executed.");
345
+ }
346
+ return {
347
+ ok: issues.length === 0,
348
+ headline: verdictHeadline(issues[0], report.runs.length),
349
+ issues,
350
+ warnings
351
+ };
352
+ }
353
+ /** A finished script run in the report's server-run shape. */
354
+ function scriptRunReport(result, durationMs, scriptName, messagesFile) {
355
+ const status = result.ok ? "completed" : "failed";
356
+ const report = {
357
+ surface: "server",
358
+ ok: result.ok,
359
+ status,
360
+ error: result.error ?? null,
361
+ durationMs,
362
+ summary: {
363
+ status,
364
+ error: result.error ?? null,
365
+ nodes: [],
366
+ logs: result.logs.map((content) => ({
367
+ nodeId: scriptName,
368
+ severity: "info",
369
+ content
370
+ })),
371
+ edges: [],
372
+ llmCalls: [],
373
+ outputs: [],
374
+ interventions: [],
375
+ counts: {
376
+ nodes: 0,
377
+ completed: result.ok ? 1 : 0,
378
+ errored: result.ok ? 0 : 1,
379
+ logs: result.logs.length,
380
+ outputs: 0,
381
+ llmCalls: 0,
382
+ interventions: 0
383
+ },
384
+ errors: result.error
385
+ ? [{ nodeId: scriptName, message: result.error }]
386
+ : []
387
+ },
388
+ trace: null
389
+ };
390
+ if (messagesFile) {
391
+ report.messagesFile = messagesFile;
392
+ }
393
+ return report;
394
+ }
395
+ /**
396
+ * What a script operation runs: the document the target carries under the key
397
+ * its target names, else the one the host loads by id and version.
398
+ */
399
+ async function resolveOperationScript(target, carried, loadScript) {
400
+ const bundled = carried?.get(target.scriptId);
401
+ if (bundled)
402
+ return { script: bundled, unavailable: null };
403
+ if (!loadScript) {
404
+ return {
405
+ script: null,
406
+ unavailable: `runs JS script "${target.scriptId}" v${target.scriptVersion}, which this host cannot resolve.`
407
+ };
408
+ }
409
+ try {
410
+ const script = await loadScript(target.scriptId, target.scriptVersion);
411
+ if (!script) {
412
+ return {
413
+ script: null,
414
+ unavailable: `runs JS script "${target.scriptId}" v${target.scriptVersion}, which does not exist.`
415
+ };
416
+ }
417
+ return { script, unavailable: null };
418
+ }
419
+ catch (error) {
420
+ return {
421
+ script: null,
422
+ unavailable: `runs JS script "${target.scriptId}" v${target.scriptVersion}, which could not be loaded: ${String(error)}`
423
+ };
424
+ }
425
+ }
426
+ /**
427
+ * The graph an operation runs: one the target carries (a bundle's workflows,
428
+ * the host workflow), else the database.
429
+ */
430
+ async function resolveOperationGraph(operation, host, loadFromDb) {
431
+ const target = operation.workflowId;
432
+ const carried = host.graphs.get(target);
433
+ if (carried)
434
+ return { graph: carried, unavailable: null };
435
+ if (!target || target === "self" || target === host.workflowId) {
436
+ return { graph: host.graph, unavailable: null };
437
+ }
438
+ try {
439
+ const workflow = await loadFromDb(target);
440
+ if (!workflow?.graph) {
441
+ return {
442
+ graph: null,
443
+ unavailable: `runs workflow "${target}", which is not in the local database.`
444
+ };
445
+ }
446
+ return { graph: workflow.graph, unavailable: null };
447
+ }
448
+ catch (error) {
449
+ return {
450
+ graph: null,
451
+ unavailable: `runs workflow "${target}", which could not be loaded: ${String(error)}`
452
+ };
453
+ }
454
+ }
455
+ export async function simulateApp(resolved, options, deps) {
456
+ const log = deps.onLog ?? (() => { });
457
+ const allowRuns = options.run ?? true;
458
+ const io = extractAppIO(resolved.graph);
459
+ // The target loader already parsed the document; `parseAppSpec` reports any
460
+ // issue it hit.
461
+ const document = resolved.document;
462
+ // Resolve every declared operation against a real graph before parsing the
463
+ // widgets: a binding into a second operation resolves against that
464
+ // operation's surface, not the host's.
465
+ const graphByOperation = new Map();
466
+ const context = {
467
+ defaultOperationId: DEFAULT_OPERATION_ID,
468
+ operations: [],
469
+ variables: document?.variables ?? [],
470
+ resources: document?.resources.map(({ id, name, kind }) => ({ id, name, kind })) ??
471
+ []
472
+ };
473
+ const bindingByOperation = new Map();
474
+ const scriptByOperation = new Map();
475
+ if (document) {
476
+ for (const binding of documentOperations(document)) {
477
+ const target = operationTarget(binding);
478
+ bindingByOperation.set(binding.id, binding);
479
+ if (target.kind === "script") {
480
+ const { script, unavailable } = await resolveOperationScript(target, resolved.scripts, deps.loadScript);
481
+ if (script)
482
+ scriptByOperation.set(binding.id, script);
483
+ context.operations.push(operationSpec(binding, script ? scriptAppIO(script.document) : null, script && !deps.runScript
484
+ ? `runs JS script "${script.name}", which this host cannot execute.`
485
+ : unavailable));
486
+ continue;
487
+ }
488
+ const { graph, unavailable } = await resolveOperationGraph(binding, {
489
+ workflowId: resolved.info.workflowId,
490
+ graph: resolved.graph,
491
+ graphs: resolved.graphs
492
+ }, deps.loadFromDb);
493
+ const operationIO = graph === resolved.graph ? io : graph ? extractAppIO(graph) : null;
494
+ if (graph)
495
+ graphByOperation.set(binding.id, graph);
496
+ context.operations.push(operationSpec(binding, operationIO, unavailable));
497
+ }
498
+ // The first declared operation, exactly as the web runtime picks it
499
+ // (`operations[0]` in `useAppRuntime`) — so a bare-name binding resolves to
500
+ // the same operation headlessly and in the browser.
501
+ context.defaultOperationId =
502
+ context.operations[0]?.id ?? DEFAULT_OPERATION_ID;
503
+ }
504
+ const { spec, issues: parseIssues, warnings: parseWarnings } = parseAppSpec(resolved.appDoc, io, document ? context : undefined, {
505
+ document,
506
+ issue: resolved.issue
507
+ });
508
+ const validation = spec
509
+ ? validateApp(spec, io, context)
510
+ : { errors: [], warnings: [] };
511
+ validation.errors = [...parseIssues, ...validation.errors];
512
+ validation.warnings = [...parseWarnings, ...validation.warnings];
513
+ const report = {
514
+ generatedAt: new Date().toISOString(),
515
+ target: resolved.info,
516
+ app: spec
517
+ ? {
518
+ version: spec.version,
519
+ // A document keeps its title in the Puck root; an application row
520
+ // keeps it as the row's name.
521
+ title: spec.title ?? resolved.appName,
522
+ widgetCount: spec.widgets.length
523
+ }
524
+ : null,
525
+ spec,
526
+ io: {
527
+ inputs: io.inputs.map((i) => i.name),
528
+ outputs: io.outputs.map((o) => o.name),
529
+ variables: io.variables
530
+ },
531
+ validation,
532
+ interactions: [],
533
+ runs: [],
534
+ values: {},
535
+ variables: {},
536
+ invocations: [],
537
+ activity: [],
538
+ widgets: [],
539
+ resources: [],
540
+ notSimulated: [...NOT_SIMULATED],
541
+ verdict: { ok: false, headline: "", issues: [] },
542
+ bundleDir: null
543
+ };
544
+ /** Verdict issues the condition simulation found, folded in at the end. */
545
+ const conditionIssues = [];
546
+ if (spec) {
547
+ const runs = report.runs;
548
+ const runWorkflow = async (operationId, graph, workflowId, timeoutMs, params) => {
549
+ log(`Running operation "${operationId}"…`);
550
+ const runInput = {
551
+ graph: withNodePropertyOverlays(graph, runtime.state.inputs, operationId),
552
+ workflowId,
553
+ params
554
+ };
555
+ if (timeoutMs != null) {
556
+ runInput.timeoutMs = timeoutMs;
557
+ }
558
+ const outcome = await deps.runOnServer(runInput);
559
+ // The slot is claimed only once the run settles, so a run the harness
560
+ // timed out on never takes an index from the one that follows it.
561
+ const runIndex = runs.length;
562
+ const messagesFile = await deps.onRunMessages?.(runIndex, outcome.rawMessages);
563
+ if (messagesFile)
564
+ outcome.report.messagesFile = messagesFile;
565
+ runs.push(outcome.report);
566
+ log(`Run ${runIndex + 1}: ${outcome.report.status} · ${outcome.report.summary.counts.errored} node error(s)`);
567
+ // SAFETY: the fold reads a message by field name off the wire shape,
568
+ // and a ProcessingMessage is that shape — it is what the runner
569
+ // serializes and what the web runtime folds.
570
+ return {
571
+ messages: outcome.rawMessages,
572
+ runIndex
573
+ };
574
+ };
575
+ /**
576
+ * Run one script operation and hand back the stream the fold expects. The
577
+ * run endpoint answers with plain JSON, so `jsScriptRunMessages` is what
578
+ * turns that into per-emit and final output messages.
579
+ */
580
+ const runScriptOperation = async (operationId, target, script, timeoutMs, inputs) => {
581
+ log(`Running script operation "${operationId}"…`);
582
+ const started = Date.now();
583
+ // A streaming body reads its inputs off the inbox, so the operation's
584
+ // one value per input is staged as a one-item stream instead.
585
+ const invocation = scriptOperationInvocation(script.document, inputs);
586
+ const scriptInput = {
587
+ scriptId: target.scriptId,
588
+ scriptVersion: target.scriptVersion,
589
+ name: script.name,
590
+ document: script.document,
591
+ inputs: invocation.inputs
592
+ };
593
+ if (invocation.inputStreams) {
594
+ scriptInput.inputStreams = invocation.inputStreams;
595
+ }
596
+ if (timeoutMs != null) {
597
+ scriptInput.timeoutMs = timeoutMs;
598
+ }
599
+ const result = await deps.runScript(scriptInput);
600
+ const messages = jsScriptRunMessages(result);
601
+ const runIndex = runs.length;
602
+ // SAFETY: `jsScriptRunMessages` builds the same per-emit and final
603
+ // message shapes the runner streams, and this hook only serializes them
604
+ // to JSONL.
605
+ const messagesFile = await deps.onRunMessages?.(runIndex, messages);
606
+ runs.push(scriptRunReport(result, Date.now() - started, script.name, messagesFile));
607
+ log(`Run ${runIndex + 1}: ${result.ok ? "completed" : "failed"}`);
608
+ return { messages, runIndex };
609
+ };
610
+ const scope = bindingScopeFor(io, context);
611
+ const operations = context.operations.map((op) => {
612
+ const binding = bindingByOperation.get(op.id);
613
+ const script = scriptByOperation.get(op.id) ?? null;
614
+ const graph = graphByOperation.get(op.id) ?? null;
615
+ const operationIO = op.io ?? {
616
+ inputs: [],
617
+ outputs: [],
618
+ variables: [],
619
+ nodeIds: []
620
+ };
621
+ const defaults = {};
622
+ for (const input of operationIO.inputs) {
623
+ if (input.defaultValue === undefined)
624
+ continue;
625
+ defaults[stateKey({ kind: "input", operationId: op.id, nodeId: input.nodeId })] = input.defaultValue;
626
+ }
627
+ const timeoutMs = effectiveTimeoutMs(op.timeoutMs, options.timeoutMs);
628
+ const init = {
629
+ binding,
630
+ outputKeyByNodeId: new Map(operationIO.outputs.map((o) => [
631
+ o.nodeId,
632
+ stateKey({ kind: "output", operationId: op.id, nodeId: o.nodeId })
633
+ ])),
634
+ inputNodeIds: operationIO.inputs.map((i) => i.nodeId),
635
+ inputNameByNodeId: new Map(operationIO.inputs.map((i) => [i.nodeId, i.name])),
636
+ inputNodeTypeByNodeId: new Map(operationIO.inputs.map((i) => [i.nodeId, i.nodeType])),
637
+ defaults
638
+ };
639
+ if (script && deps.runScript) {
640
+ init.runWorkflow = (params) => runScriptOperation(op.id, op.target, script, timeoutMs, params);
641
+ }
642
+ // A workflow graph wins over the script binding, exactly as the later
643
+ // spread did before.
644
+ if (graph) {
645
+ init.runWorkflow = (params) => runWorkflow(op.id, graph,
646
+ // A bundle's operations name bundle-local keys, so there is
647
+ // no workflow id to attribute the run to.
648
+ resolved.operationsReferenceKeys
649
+ ? null
650
+ : graph === resolved.graph
651
+ ? resolved.info.workflowId
652
+ : op.workflowId, timeoutMs, params);
653
+ }
654
+ return init;
655
+ });
656
+ const runtimeInit = {
657
+ operations,
658
+ defaultOperationId: context.defaultOperationId,
659
+ variables: context.variables,
660
+ resources: document?.resources ?? [],
661
+ scope,
662
+ widgets: spec.widgets.map((w) => {
663
+ const widget = { id: w.id };
664
+ if (w.visibleWhen) {
665
+ widget.visibleWhen = w.visibleWhen;
666
+ }
667
+ if (w.disabledWhen) {
668
+ widget.disabledWhen = w.disabledWhen;
669
+ }
670
+ if (w.format) {
671
+ widget.format = w.format;
672
+ }
673
+ return widget;
674
+ })
675
+ };
676
+ if (options.timeoutMs != null) {
677
+ runtimeInit.timeoutMs = options.timeoutMs;
678
+ }
679
+ const runtime = new HeadlessAppRuntime(runtimeInit);
680
+ /**
681
+ * Write a value addressed by name (a param, or an `--interact` set step),
682
+ * returning why it did not land, or null once it did. Reading a name in
683
+ * write mode first and read mode second means an output name resolves — to
684
+ * a slot nothing can write, which is a failed step, not a silent no-op.
685
+ */
686
+ const writeByName = (name, value, operationId = context.defaultOperationId) => {
687
+ const ref = resolveBinding(name, scope, "write", operationId) ??
688
+ resolveBinding(name, scope, "read", operationId);
689
+ if (!ref)
690
+ return `"${name}" matches no input, output, or variable.`;
691
+ const refusal = writeRefusal(ref);
692
+ if (refusal)
693
+ return `"${name}" ${refusal}.`;
694
+ runtime.write(ref, value);
695
+ return null;
696
+ };
697
+ /** Seed one collection, reporting why a bad seed did nothing. */
698
+ const seedResource = (resourceBindingId, items) => {
699
+ if (!Array.isArray(items)) {
700
+ return `Resource seed for "${resourceBindingId}" is not an array of items.`;
701
+ }
702
+ const seeds = [];
703
+ for (const item of items) {
704
+ const seed = item;
705
+ if (!seed || !isNonEmptyString(seed.id)) {
706
+ return `Resource seed for "${resourceBindingId}" has an item without an "id".`;
707
+ }
708
+ seeds.push(seed);
709
+ }
710
+ try {
711
+ runtime.seedResource(resourceBindingId, seeds);
712
+ return null;
713
+ }
714
+ catch (error) {
715
+ return error.message;
716
+ }
717
+ };
718
+ for (const [key, value] of Object.entries({
719
+ ...resolved.fileParams,
720
+ ...(options.params ?? {})
721
+ })) {
722
+ // `--params '{"resource:docs": [...]}'` seeds a collection; every other
723
+ // key writes a reactive value.
724
+ if (key.startsWith("resource:")) {
725
+ const failure = seedResource(key.slice("resource:".length), value);
726
+ if (failure)
727
+ report.validation.warnings.push(failure);
728
+ continue;
729
+ }
730
+ const failure = writeByName(key, value);
731
+ if (failure)
732
+ report.validation.warnings.push(`Param ${failure}`);
733
+ }
734
+ /** Dispatch one action, tracking which run it produced. */
735
+ const dispatch = async (record, action) => {
736
+ const before = runtime.invocations.length;
737
+ try {
738
+ await runtime.dispatch(action);
739
+ }
740
+ catch (error) {
741
+ record.error = record.error ?? error.message;
742
+ return;
743
+ }
744
+ const started = runtime.invocations.slice(before);
745
+ const withRun = started.find((i) => i.runIndex !== null);
746
+ if (withRun)
747
+ record.runIndex = withRun.runIndex;
748
+ if (action.kind === "run") {
749
+ record.error = record.error ?? runtime.errorFor(action.operationId);
750
+ }
751
+ };
752
+ const steps = options.interact ?? defaultInteractions(spec);
753
+ for (const step of steps) {
754
+ const record = {
755
+ step: describeStep(step),
756
+ actions: [],
757
+ runIndex: null,
758
+ error: null
759
+ };
760
+ report.interactions.push(record);
761
+ if ("set" in step) {
762
+ const failure = writeByName(step.set.key, step.set.value, step.set.operationId);
763
+ if (failure)
764
+ record.error = failure;
765
+ else
766
+ record.actions.push(`set ${step.set.key}`);
767
+ continue;
768
+ }
769
+ if ("seedResource" in step) {
770
+ const { id, items } = step.seedResource;
771
+ const failure = seedResource(id, items);
772
+ if (failure)
773
+ record.error = failure;
774
+ else
775
+ record.actions.push(`seedResource ${id} (${items.length} item(s))`);
776
+ continue;
777
+ }
778
+ if ("run" in step) {
779
+ if (!allowRuns) {
780
+ record.actions.push("run (skipped — --no-run)");
781
+ continue;
782
+ }
783
+ record.actions.push(`run ${step.run}`);
784
+ await dispatch(record, { kind: "run", operationId: step.run });
785
+ continue;
786
+ }
787
+ if ("cancel" in step) {
788
+ const cancelled = runtime.cancel(step.cancel);
789
+ record.actions.push(cancelled.length > 0
790
+ ? `cancel ${step.cancel} (${cancelled.join(", ")})`
791
+ : `cancel ${step.cancel} (nothing running)`);
792
+ continue;
793
+ }
794
+ const trigger = "click" in step ? "click" : "change";
795
+ const found = findWidget(spec, "click" in step ? step.click : step.change);
796
+ if (isString(found)) {
797
+ record.error = found;
798
+ continue;
799
+ }
800
+ // A `set` step writes state directly and models the runtime; a click or a
801
+ // change models a user, and a user cannot touch what the app hides or
802
+ // disables.
803
+ const gate = runtime.widgetState(found.id);
804
+ const verb = trigger === "click" ? "clicked" : "changed";
805
+ const named = found.label ?? found.id;
806
+ if (gate && !gate.visible) {
807
+ record.error = `${verb} "${named}" while hidden by \`${gate.visibleWhen ?? "visibleWhen"}\`.`;
808
+ continue;
809
+ }
810
+ if (gate?.disabled) {
811
+ record.error = `${verb} "${named}" while disabled by \`${gate.disabledWhen ?? "disabledWhen"}\`.`;
812
+ continue;
813
+ }
814
+ if ("change" in step && step.value !== undefined) {
815
+ // A change whose value never landed must not go on to fire the widget's
816
+ // events: the run behind them would read the old value.
817
+ if (!found.ref) {
818
+ record.error = found.binding
819
+ ? `changed "${named}" but its binding "${found.binding}" resolves to nothing — nothing was set.`
820
+ : `changed "${named}", which has no binding to write to.`;
821
+ continue;
822
+ }
823
+ const refusal = writeRefusal(found.ref);
824
+ if (refusal) {
825
+ record.error = `changed "${named}", whose binding "${found.binding ?? ""}" ${refusal}.`;
826
+ continue;
827
+ }
828
+ runtime.write(found.ref, step.value);
829
+ record.actions.push(`set ${found.stateKey ?? found.id}`);
830
+ }
831
+ // A run from a bound write widget carries its binding; the web engine
832
+ // would run just that input's downstream subgraph — headless, both paths
833
+ // are a full authoritative run.
834
+ const from = found.bindingMode === "write"
835
+ ? (found.canonicalBinding ?? undefined)
836
+ : undefined;
837
+ const eventCtx = {
838
+ defaultOperationId: context.defaultOperationId,
839
+ resolveVariableId: (key) => {
840
+ const ref = resolveBinding(key, scope, "read");
841
+ return ref?.kind === "variable" ? ref.variableId : null;
842
+ },
843
+ from
844
+ };
845
+ for (const event of found.events) {
846
+ if (event.trigger !== trigger)
847
+ continue;
848
+ const action = eventToAction(event, eventCtx);
849
+ if (!action) {
850
+ record.error =
851
+ record.error ??
852
+ `event "${event.kind}" is incomplete — nothing to dispatch.`;
853
+ continue;
854
+ }
855
+ if (action.kind === "run" && !allowRuns) {
856
+ record.actions.push("run (skipped — --no-run)");
857
+ continue;
858
+ }
859
+ record.actions.push(action.kind === "setVariable" || action.kind === "toggleVariable"
860
+ ? `${action.kind} ${action.variableId}`
861
+ : action.kind === "run" || action.kind === "cancel"
862
+ ? `${action.kind} ${action.operationId ?? context.defaultOperationId}`
863
+ : action.kind);
864
+ await dispatch(record, action);
865
+ }
866
+ if (record.actions.length === 0) {
867
+ record.error = `widget has no "${trigger}" events to fire.`;
868
+ }
869
+ }
870
+ // The report reads better keyed by the names an author recognizes than by
871
+ // the runtime's namespaced state keys. A second operation prefixes its
872
+ // names so two operations over one workflow stay apart.
873
+ const values = {};
874
+ for (const operation of context.operations) {
875
+ const prefix = operation.id === context.defaultOperationId ? "" : `${operation.id}.`;
876
+ for (const input of operation.io?.inputs ?? []) {
877
+ const value = runtime.read({
878
+ kind: "input",
879
+ operationId: operation.id,
880
+ nodeId: input.nodeId
881
+ });
882
+ if (value !== undefined)
883
+ values[`${prefix}${input.name}`] = value;
884
+ }
885
+ for (const output of operation.io?.outputs ?? []) {
886
+ const value = runtime.read({
887
+ kind: "output",
888
+ operationId: operation.id,
889
+ nodeId: output.nodeId
890
+ });
891
+ if (value !== undefined)
892
+ values[`${prefix}${output.name}`] = value;
893
+ }
894
+ }
895
+ const variables = {};
896
+ for (const [variableId, value] of Object.entries(runtime.state.variables)) {
897
+ if (value === undefined)
898
+ continue;
899
+ values[variableId] = value;
900
+ variables[variableId] = value;
901
+ }
902
+ report.values = previewValue(values);
903
+ report.variables = previewValue(variables);
904
+ report.invocations = runtime.invocations.map((invocation) => ({
905
+ id: invocation.id,
906
+ operationId: invocation.operationId,
907
+ status: runtime.state.invocations[invocation.id]?.status ?? "unknown",
908
+ decision: invocation.decision,
909
+ decisionTargets: invocation.decisionTargets,
910
+ runIndex: invocation.runIndex,
911
+ timedOutMs: invocation.timedOutMs,
912
+ error: runtime.state.invocations[invocation.id]?.error ?? null,
913
+ activity: invocation.activity
914
+ }));
915
+ report.activity = [...runtime.activity];
916
+ report.resources = runtime.resourceBindingList.map((binding) => {
917
+ const provider = runtime.resourceProvider(binding.id);
918
+ const items = provider?.list() ?? [];
919
+ return {
920
+ id: binding.id,
921
+ kind: binding.kind,
922
+ seeded: provider !== null,
923
+ items: items.map((item) => {
924
+ const listed = { id: item.ref.id, name: item.name };
925
+ if (item.ref.revision !== undefined) {
926
+ listed.revision = item.ref.revision;
927
+ }
928
+ return listed;
929
+ }),
930
+ selected: provider?.selected()?.id ?? null,
931
+ commands: runtime.resourceCommands
932
+ .filter((c) => c.resourceBindingId === binding.id)
933
+ .map((c) => c.command)
934
+ };
935
+ });
936
+ report.widgets = spec.widgets
937
+ .filter((w) => w.bindingMode !== "layout")
938
+ .map((w) => {
939
+ // A resource widget shows a collection, not a state value — report what
940
+ // its picker or gallery would list.
941
+ if (w.resourceBindingId) {
942
+ const items = runtime.resourceProvider(w.resourceBindingId)?.list() ?? [];
943
+ const state = runtime.widgetState(w.id);
944
+ return {
945
+ id: w.id,
946
+ type: w.type,
947
+ bindingMode: w.bindingMode,
948
+ binding: w.binding,
949
+ stateKey: w.stateKey,
950
+ resourceBindingId: w.resourceBindingId,
951
+ value: previewValue(items.map((item) => ({ id: item.ref.id, name: item.name }))),
952
+ display: null,
953
+ hasValue: items.length > 0,
954
+ visible: state?.visible ?? true,
955
+ disabled: state?.disabled ?? false
956
+ };
957
+ }
958
+ const value = runtime.read(w.ref);
959
+ // A `format` template is what the widget actually shows, so it — not
960
+ // the raw value — decides whether anything reached the screen.
961
+ const display = runtime.display(w.id);
962
+ const state = runtime.widgetState(w.id);
963
+ return {
964
+ id: w.id,
965
+ type: w.type,
966
+ bindingMode: w.bindingMode,
967
+ binding: w.binding,
968
+ stateKey: w.stateKey,
969
+ value: previewValue(value),
970
+ display,
971
+ hasValue: display !== null ? display.length > 0 : !isEmptyValue(value),
972
+ visible: state?.visible ?? true,
973
+ disabled: state?.disabled ?? false
974
+ };
975
+ });
976
+ for (const w of spec.widgets) {
977
+ if (!w.events.some((e) => e.kind === "run"))
978
+ continue;
979
+ const state = runtime.widgetState(w.id);
980
+ if (!state || state.everReachable)
981
+ continue;
982
+ const blocking = !state.visible
983
+ ? `visibleWhen (\`${state.visibleWhen ?? ""}\`) never held`
984
+ : `disabledWhen (\`${state.disabledWhen ?? ""}\`) always held`;
985
+ conditionIssues.push(`${w.type} "${w.label ?? w.id}" runs the app, but its ${blocking} — no interaction could start a run.`);
986
+ }
987
+ }
988
+ report.verdict = buildAppVerdict(report, allowRuns, resolved.graph, conditionIssues);
989
+ return report;
990
+ }
991
+ //# sourceMappingURL=simulate.js.map