@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,710 @@
1
+ /**
2
+ * Pure app-spec layer: parse a workflow's app document into a flat widget
3
+ * list, extract the graph's bindable surface, and statically validate the
4
+ * wiring between the two.
5
+ *
6
+ * The semantics — widget catalog, document parsing, binding resolution — come
7
+ * from `@nodetool-ai/app-runtime`, so this harness checks the same rules the
8
+ * web runtime enforces instead of a copy that drifts away from it.
9
+ */
10
+ import { DEFAULT_OPERATION_ID, encodeBinding, operationTarget, parseApplicationDocument, parseBinding, parseCondition, resolveBinding, stateKey, widgetBindingProps, widgetMode, WIDGET_CATALOG } from "@nodetool-ai/app-runtime";
11
+ import { isNonEmptyString, isString } from "../predicates.js";
12
+ const SET_VARIABLE_NODE_TYPE = "nodetool.variable.SetVariable";
13
+ const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
14
+ const str = (v) => (isString(v) ? v : null);
15
+ const isPuckNode = (v) => isRecord(v) && typeof v.type === "string" && isRecord(v.props);
16
+ /**
17
+ * A widget's stored `visibleWhen`/`disabledWhen` props. Undefined when the
18
+ * widget carries none, so an unconditional widget's spec is unchanged.
19
+ */
20
+ const parseConditionProps = (raw) => {
21
+ if (!isRecord(raw))
22
+ return undefined;
23
+ const binding = str(raw.binding);
24
+ if (!binding)
25
+ return undefined;
26
+ const op = str(raw.op);
27
+ const value = str(raw.value);
28
+ const props = { binding };
29
+ if (op) {
30
+ props.op = op;
31
+ }
32
+ if (value !== null) {
33
+ props.value = value;
34
+ }
35
+ return props;
36
+ };
37
+ const parseEvents = (raw) => {
38
+ if (!Array.isArray(raw))
39
+ return [];
40
+ const events = [];
41
+ for (const item of raw) {
42
+ if (!isRecord(item))
43
+ continue;
44
+ if (item.trigger !== "click" && item.trigger !== "change")
45
+ continue;
46
+ events.push({
47
+ trigger: item.trigger,
48
+ kind: str(item.kind) ?? "run",
49
+ // The builder stores "" for unset key/value — normalize to undefined.
50
+ key: str(item.key) || undefined,
51
+ value: str(item.value) || undefined,
52
+ operationId: str(item.operationId) || undefined,
53
+ invocationId: str(item.invocationId) || undefined,
54
+ resourceBindingId: str(item.resourceBindingId) || undefined,
55
+ command: str(item.command) || undefined
56
+ });
57
+ }
58
+ return events;
59
+ };
60
+ /**
61
+ * The binding scope an app resolves against: every declared operation with the
62
+ * surface of the workflow it runs, plus the declared variables. Without a
63
+ * context this is the single-operation shape a legacy `app_doc` has.
64
+ */
65
+ export function bindingScopeFor(io, context) {
66
+ const operationIO = (id, own) => ({
67
+ operationId: id,
68
+ inputs: (own ?? io).inputs.map(({ nodeId, name }) => ({ nodeId, name })),
69
+ outputs: (own ?? io).outputs.map(({ nodeId, name }) => ({ nodeId, name })),
70
+ nodeIds: (own ?? io).nodeIds,
71
+ variableNames: (own ?? io).variables
72
+ });
73
+ if (!context) {
74
+ return {
75
+ defaultOperationId: DEFAULT_OPERATION_ID,
76
+ operations: [operationIO(DEFAULT_OPERATION_ID, io)],
77
+ variables: []
78
+ };
79
+ }
80
+ return {
81
+ defaultOperationId: context.defaultOperationId,
82
+ operations: context.operations.map((op) => operationIO(op.id, op.io)),
83
+ variables: context.variables
84
+ };
85
+ }
86
+ /**
87
+ * True for the widgets the catalog gives a `resourceBindingId` field — the ones
88
+ * that read a document collection through the resource provider.
89
+ */
90
+ const usesResourceBinding = (type) => WIDGET_CATALOG[type]?.fields.resourceBindingId !== undefined;
91
+ /**
92
+ * A widget that renders one operation's whole input surface rather than one
93
+ * slot. It carries `operationId` where the state widgets carry `binding`, so
94
+ * the unbound-write warning does not apply to it.
95
+ */
96
+ const rendersOperationInputs = (type) => WIDGET_CATALOG[type]?.fields.operationId !== undefined;
97
+ const modeToBindingMode = (mode) => mode === "write" ? "write" : mode === "read" ? "read" : "none";
98
+ /**
99
+ * Widgets whose value is a file the user has to supply on the spot. None of
100
+ * them starts with a value — a Sketch Pad publishes nothing until the first
101
+ * stroke, a recorder nothing until the first take — so a run they feed can
102
+ * start with the input unset. A Workflow Form is deliberately not one of
103
+ * these: it renders every input of its operation rather than binding one, so
104
+ * there is no single binding a run trigger could be guarded on.
105
+ */
106
+ const MEDIA_INPUT_WIDGETS = new Set([
107
+ "ImageInput",
108
+ "SketchPad",
109
+ "CameraCapture",
110
+ "AudioInput",
111
+ "AudioRecorder",
112
+ "VideoInput",
113
+ "DocumentInput"
114
+ ]);
115
+ /** True when an input node ships a media value of its own to fall back on. */
116
+ const hasMediaDefault = (value) => {
117
+ if (isNonEmptyString(value))
118
+ return true;
119
+ if (!isRecord(value))
120
+ return false;
121
+ return isNonEmptyString(value.uri) || isNonEmptyString(value.asset_id);
122
+ };
123
+ /** Record that some widget displays `field` of `operationId`. */
124
+ const addExecField = (fields, operationId, field) => {
125
+ const set = fields.get(operationId) ?? new Set();
126
+ set.add(field);
127
+ fields.set(operationId, set);
128
+ };
129
+ /** What a second click does under each policy, for the re-entry warning. */
130
+ const REENTRY_CONSEQUENCE = {
131
+ replace: "cancels the running job and starts it again",
132
+ queue: "queues another run behind the one in flight",
133
+ parallel: "starts a second run alongside the one in flight"
134
+ };
135
+ /**
136
+ * The operations a parsed document declares, as report specs. A document with
137
+ * none gets the implicit single operation a legacy `app_doc` runs.
138
+ */
139
+ export function documentOperations(document) {
140
+ if (document.operations.length > 0)
141
+ return document.operations;
142
+ return [
143
+ {
144
+ id: DEFAULT_OPERATION_ID,
145
+ name: "Run",
146
+ workflowId: "",
147
+ inputs: {},
148
+ outputs: {},
149
+ policy: "replace"
150
+ }
151
+ ];
152
+ }
153
+ /** Parse an app document (object or JSON string), or explain why it will not. */
154
+ export function parseAppDocument(appDoc) {
155
+ if (appDoc == null) {
156
+ return {
157
+ document: null,
158
+ issue: "Workflow has no app_doc — build the app in the App Builder first."
159
+ };
160
+ }
161
+ let raw = appDoc;
162
+ if (isString(raw)) {
163
+ try {
164
+ raw = JSON.parse(raw);
165
+ }
166
+ catch {
167
+ return {
168
+ document: null,
169
+ issue: "app_doc is a string but not valid JSON."
170
+ };
171
+ }
172
+ }
173
+ const document = parseApplicationDocument(raw, { hostWorkflowId: "self" });
174
+ return document
175
+ ? { document, issue: null }
176
+ : {
177
+ document: null,
178
+ issue: "app_doc is not a valid application document (no `ui`/`data`)."
179
+ };
180
+ }
181
+ /**
182
+ * Variables the document declared as persisted but that the parser downgraded:
183
+ * only user-scoped variables may persist, and the parser applies that silently.
184
+ */
185
+ function persistDowngrades(appDoc) {
186
+ const raw = isString(appDoc) ? safeParse(appDoc) : appDoc;
187
+ if (!isRecord(raw) || !Array.isArray(raw.variables))
188
+ return [];
189
+ const names = [];
190
+ for (const entry of raw.variables) {
191
+ if (!isRecord(entry) || entry.persist !== true)
192
+ continue;
193
+ if (entry.scope === "user")
194
+ continue;
195
+ names.push(str(entry.name) ?? str(entry.id) ?? "(unnamed)");
196
+ }
197
+ return names;
198
+ }
199
+ const safeParse = (value) => {
200
+ try {
201
+ return JSON.parse(value);
202
+ }
203
+ catch {
204
+ return null;
205
+ }
206
+ };
207
+ /**
208
+ * Parse an app document (object or JSON string) into an {@link AppSpec},
209
+ * resolving every widget binding to a node-ID reference against the live
210
+ * graph. Slot children are discovered from the widget catalog, falling back to
211
+ * a structural scan so a new layout widget keeps working before the catalog
212
+ * knows about it.
213
+ */
214
+ export function parseAppSpec(appDoc, io, context,
215
+ /**
216
+ * The document the target loader already parsed. Passed in so an application
217
+ * or bundle target is not re-parsed as if it were a `workflow.app_doc` —
218
+ * `appDoc` stays the raw stored value, which is what the persist-downgrade
219
+ * check needs to see.
220
+ */
221
+ preparsed) {
222
+ const { document, issue } = preparsed ?? parseAppDocument(appDoc);
223
+ if (!document)
224
+ return { spec: null, issues: [issue ?? ""], warnings: [] };
225
+ const issues = [];
226
+ const warnings = [];
227
+ for (const name of persistDowngrades(appDoc)) {
228
+ warnings.push(`Variable "${name}" declares persist: true with scope: "instance" — only user-scoped variables persist, so it was downgraded to in-memory.`);
229
+ }
230
+ const scope = bindingScopeFor(io, context);
231
+ const rootProps = isRecord(document.ui.root.props)
232
+ ? document.ui.root.props
233
+ : {};
234
+ const title = str(rootProps.title);
235
+ const widgets = [];
236
+ const walk = (items, parentId, slot) => {
237
+ for (const item of items) {
238
+ if (!isPuckNode(item))
239
+ continue;
240
+ const id = str(item.props.id) ?? `${item.type}-${widgets.length}`;
241
+ const bindingMode = widgetMode(item.type);
242
+ const binding = str(item.props.binding) || null;
243
+ // A resource widget addresses a collection, not app state, so it carries
244
+ // `resourceBindingId` where the state widgets carry `binding`.
245
+ const resourceBindingId = usesResourceBinding(item.type)
246
+ ? str(item.props.resourceBindingId) || null
247
+ : null;
248
+ const visibleWhen = parseConditionProps(item.props.visibleWhen);
249
+ const disabledWhen = parseConditionProps(item.props.disabledWhen);
250
+ const format = str(item.props.format) || null;
251
+ const ref = binding
252
+ ? resolveBinding(binding, scope, modeToBindingMode(bindingMode))
253
+ : bindingMode === "write"
254
+ ? // An unbound write widget still holds its own value — as widget-local
255
+ // view state, which cannot collide with a workflow input.
256
+ { kind: "view", componentId: id, prop: "value" }
257
+ : null;
258
+ const widget = {
259
+ id,
260
+ type: item.type,
261
+ bindingMode,
262
+ binding,
263
+ ref,
264
+ stateKey: ref ? stateKey(ref) : null,
265
+ canonicalBinding: ref ? encodeBinding(ref) : null,
266
+ resourceBindingId,
267
+ extraBindings: widgetBindingProps(item.type).flatMap(({ prop, mode }) => {
268
+ const value = str(item.props[prop]);
269
+ if (!value)
270
+ return [];
271
+ return [
272
+ { prop, binding: value, ref: resolveBinding(value, scope, mode) }
273
+ ];
274
+ }),
275
+ label: str(item.props.label) ?? str(item.props.text) ?? null,
276
+ operationId: rendersOperationInputs(item.type)
277
+ ? str(item.props.operationId) || null
278
+ : null,
279
+ events: parseEvents(item.props.events),
280
+ parentId,
281
+ slot
282
+ };
283
+ if (visibleWhen) {
284
+ widget.visibleWhen = visibleWhen;
285
+ }
286
+ if (disabledWhen) {
287
+ widget.disabledWhen = disabledWhen;
288
+ }
289
+ if (format) {
290
+ widget.format = format;
291
+ }
292
+ widgets.push(widget);
293
+ const slots = WIDGET_CATALOG[item.type]?.slots;
294
+ for (const [prop, value] of Object.entries(item.props)) {
295
+ if (slots && !slots.includes(prop))
296
+ continue;
297
+ if (Array.isArray(value) && value.some(isPuckNode)) {
298
+ walk(value, id, prop);
299
+ }
300
+ }
301
+ }
302
+ };
303
+ walk(document.ui.content, null, null);
304
+ if (widgets.length === 0) {
305
+ issues.push("The app document has no widgets — nothing to render or run.");
306
+ }
307
+ const operations = context?.operations ??
308
+ documentOperations(document).map((binding) => operationSpec(binding, null, null));
309
+ return {
310
+ spec: {
311
+ version: document.schemaVersion,
312
+ title,
313
+ widgets,
314
+ operations,
315
+ variables: context?.variables ?? document.variables,
316
+ resources: document.resources.map(({ id, name, kind }) => ({
317
+ id,
318
+ name,
319
+ kind
320
+ }))
321
+ },
322
+ issues,
323
+ warnings
324
+ };
325
+ }
326
+ /** An operation binding plus the surface it resolved against, for the report. */
327
+ export function operationSpec(binding, io, unavailable) {
328
+ return {
329
+ id: binding.id,
330
+ name: binding.name,
331
+ workflowId: binding.workflowId,
332
+ target: operationTarget(binding),
333
+ policy: binding.policy,
334
+ timeoutMs: binding.timeoutMs ?? null,
335
+ inputs: binding.inputs,
336
+ outputs: binding.outputs,
337
+ io,
338
+ unavailable
339
+ };
340
+ }
341
+ /** Read a node property from the kernel graph shape (`properties`, not `data`). */
342
+ const nodeProps = (node) => isRecord(node.properties) ? node.properties : {};
343
+ const nodeName = (node) => {
344
+ const name = nodeProps(node).name;
345
+ if (isNonEmptyString(name))
346
+ return name;
347
+ return str(node.id) ?? "";
348
+ };
349
+ const isOutputNodeType = (type) => type.includes(".output.") || type === "nodetool.workflows.base_node.Preview";
350
+ /**
351
+ * Extract the app-bindable surface from a kernel-shape graph: input nodes,
352
+ * output/Preview nodes, and SetVariable channel names — the same surface the
353
+ * web builder offers in its binding pickers.
354
+ */
355
+ export function extractAppIO(graph) {
356
+ const io = { inputs: [], outputs: [], variables: [], nodeIds: [] };
357
+ const variables = new Set();
358
+ for (const node of graph.nodes) {
359
+ const type = str(node.type);
360
+ const id = str(node.id);
361
+ if (!type || !id)
362
+ continue;
363
+ io.nodeIds.push(id);
364
+ if (type.startsWith("nodetool.input.")) {
365
+ io.inputs.push({
366
+ nodeId: id,
367
+ nodeType: type,
368
+ name: nodeName(node),
369
+ defaultValue: nodeProps(node).value
370
+ });
371
+ }
372
+ else if (isOutputNodeType(type)) {
373
+ io.outputs.push({ nodeId: id, nodeType: type, name: nodeName(node) });
374
+ }
375
+ else if (type === SET_VARIABLE_NODE_TYPE) {
376
+ const name = str(nodeProps(node).name)?.trim();
377
+ if (name)
378
+ variables.add(name);
379
+ }
380
+ }
381
+ io.variables = [...variables].sort((a, b) => a.localeCompare(b));
382
+ return io;
383
+ }
384
+ const VARIABLE_EVENT_KINDS = new Set([
385
+ "setVariable",
386
+ "toggleVariable",
387
+ "setState",
388
+ "toggleState"
389
+ ]);
390
+ /** Every id a variable binding may name: declared ids/names + graph channels. */
391
+ function knownVariableIds(io, context) {
392
+ const known = new Set(io.variables);
393
+ for (const variable of context?.variables ?? []) {
394
+ known.add(variable.id);
395
+ known.add(variable.name);
396
+ }
397
+ for (const operation of context?.operations ?? []) {
398
+ for (const name of operation.io?.variables ?? [])
399
+ known.add(name);
400
+ }
401
+ return known;
402
+ }
403
+ /** Statically check the app's wiring against the workflow's bindable surface. */
404
+ export function validateApp(spec, io, context) {
405
+ const errors = [];
406
+ const warnings = [];
407
+ const scope = bindingScopeFor(io, context);
408
+ const defaultOperationId = context?.defaultOperationId ?? DEFAULT_OPERATION_ID;
409
+ // Without a context there is one operation over the host workflow, so the
410
+ // host surface is its surface.
411
+ const operations = spec.operations.map((op) => op.io || op.id !== defaultOperationId ? op : { ...op, io });
412
+ const operationIds = new Set(operations.map((op) => op.id));
413
+ // Node ids per operation, for the operations whose workflow resolved. An
414
+ // operation with no `io` has no graph to check against, so its bindings are
415
+ // left alone rather than reported as missing nodes.
416
+ const nodeIdsByOperation = new Map(operations
417
+ .filter((op) => op.io !== null)
418
+ .map((op) => [op.id, new Set(op.io?.nodeIds ?? [])]));
419
+ /**
420
+ * The node an ID-form binding names, when the operation's graph does not have
421
+ * it. `parseBinding` accepts any well-formed token, so a widget bound to a
422
+ * node that was deleted (or never existed) resolves to a ref addressing a
423
+ * state slot no run ever fills — a silent blank widget without this check.
424
+ */
425
+ const missingNode = (binding, ref) => {
426
+ if (!parseBinding(binding) || !ref || !("nodeId" in ref))
427
+ return null;
428
+ const known = nodeIdsByOperation.get(ref.operationId);
429
+ if (!known || known.has(ref.nodeId))
430
+ return null;
431
+ return { operationId: ref.operationId, nodeId: ref.nodeId };
432
+ };
433
+ const variableIds = knownVariableIds(io, context);
434
+ const resourceIds = new Set(spec.resources.map((r) => r.id));
435
+ let hasRunTrigger = false;
436
+ /** `opId:nodeId` of every output a widget displays. */
437
+ const displayedOutputs = new Set();
438
+ /** Operations some widget event can run. */
439
+ const runnableOperations = new Set();
440
+ /** Operation ids whose execution state a widget shows. */
441
+ const execBindings = new Map();
442
+ /** Execution fields a widget actually displays, per operation. */
443
+ const execFields = new Map();
444
+ /** Widgets whose click runs an operation — the re-entry surface. */
445
+ const runTriggers = [];
446
+ /** Media input widgets — the ones a run can start without a value for. */
447
+ const mediaWidgets = [];
448
+ for (const w of spec.widgets) {
449
+ const where = `${w.type} "${w.id}"`;
450
+ if (w.bindingMode === "unknown") {
451
+ errors.push(`${where}: unknown widget type — not in the app-builder catalog.`);
452
+ continue;
453
+ }
454
+ if (w.binding && !w.ref) {
455
+ errors.push(w.bindingMode === "write"
456
+ ? `${where}: bound to "${w.binding}" but the workflow has no input node or node property that resolves it.`
457
+ : `${where}: bound to "${w.binding}" but the workflow has no output or variable with that name.`);
458
+ }
459
+ else if (!w.binding &&
460
+ w.bindingMode === "write" &&
461
+ !usesResourceBinding(w.type) &&
462
+ !rendersOperationInputs(w.type)) {
463
+ warnings.push(`${where}: not bound to an input — its value stays local UI state.`);
464
+ }
465
+ // A resource widget with no collection behind it renders an empty picker
466
+ // the user can never choose from, which a run-only check never surfaces.
467
+ if (usesResourceBinding(w.type)) {
468
+ if (!w.resourceBindingId) {
469
+ errors.push(`${where}: no resource binding selected — the widget has no collection to show. Add one with a resource binding.`);
470
+ }
471
+ else if (!resourceIds.has(w.resourceBindingId)) {
472
+ errors.push(`${where}: bound to resource "${w.resourceBindingId}" but the app declares no such resource binding.`);
473
+ }
474
+ }
475
+ // A form renders the inputs of the operation it names; a stale name renders
476
+ // an empty form, which a run-only check reads as an app that asks for
477
+ // nothing rather than one that lost its inputs.
478
+ if (w.operationId && !operationIds.has(w.operationId)) {
479
+ errors.push(`${where}: renders the inputs of operation "${w.operationId}" but the app declares no such operation.`);
480
+ }
481
+ // An explicit `op:<id>/…` token resolves on its own syntax, so a token
482
+ // naming an operation the document never declared has to be caught here.
483
+ const explicit = parseBinding(w.binding);
484
+ if (explicit && "operationId" in explicit && explicit.operationId) {
485
+ if (!operationIds.has(explicit.operationId)) {
486
+ errors.push(`${where}: bound to "${w.binding}" but the app declares no operation "${explicit.operationId}".`);
487
+ }
488
+ }
489
+ const missing = missingNode(w.binding, w.ref);
490
+ if (missing) {
491
+ errors.push(`${where}: bound to "${w.binding}" but operation "${missing.operationId}" runs a workflow with no node "${missing.nodeId}".`);
492
+ }
493
+ // An unresolvable condition binding is not enforced at runtime — the widget
494
+ // shows and stays enabled — so the author sees a condition that quietly
495
+ // does nothing.
496
+ for (const [prop, props] of [
497
+ ["visibleWhen", w.visibleWhen],
498
+ ["disabledWhen", w.disabledWhen]
499
+ ]) {
500
+ if (!props?.binding || parseCondition(props, scope))
501
+ continue;
502
+ warnings.push(`${where}: ${prop} reads "${props.binding}", which resolves to nothing — the condition never applies, so the widget stays visible and enabled.`);
503
+ }
504
+ for (const extra of w.extraBindings) {
505
+ const extraMissing = missingNode(extra.binding, extra.ref);
506
+ if (!extra.ref) {
507
+ errors.push(`${where}: ${extra.prop} is bound to "${extra.binding}" but the workflow has no output or variable with that name.`);
508
+ }
509
+ else if (extraMissing) {
510
+ errors.push(`${where}: ${extra.prop} is bound to "${extra.binding}" but operation "${extraMissing.operationId}" runs a workflow with no node "${extraMissing.nodeId}".`);
511
+ }
512
+ else if (extra.ref.kind === "output") {
513
+ displayedOutputs.add(`${extra.ref.operationId}:${extra.ref.nodeId}`);
514
+ }
515
+ else if (extra.ref.kind === "execution") {
516
+ addExecField(execFields, extra.ref.operationId, extra.ref.field);
517
+ }
518
+ }
519
+ if (w.ref?.kind === "output") {
520
+ displayedOutputs.add(`${w.ref.operationId}:${w.ref.nodeId}`);
521
+ }
522
+ if (w.ref?.kind === "execution") {
523
+ const list = execBindings.get(w.ref.operationId) ?? [];
524
+ list.push(`${where} (${w.ref.field})`);
525
+ execBindings.set(w.ref.operationId, list);
526
+ addExecField(execFields, w.ref.operationId, w.ref.field);
527
+ }
528
+ if (MEDIA_INPUT_WIDGETS.has(w.type) && w.ref) {
529
+ mediaWidgets.push(w);
530
+ }
531
+ for (const event of w.events) {
532
+ const target = event.operationId ?? defaultOperationId;
533
+ if (event.kind === "run") {
534
+ hasRunTrigger = true;
535
+ runnableOperations.add(target);
536
+ if (event.trigger === "click") {
537
+ runTriggers.push({ widget: w, operationId: target });
538
+ }
539
+ }
540
+ if ((event.kind === "run" || event.kind === "cancel") &&
541
+ !operationIds.has(target)) {
542
+ errors.push(`${where}: ${event.kind} targets operation "${target}" but the app declares no such operation.`);
543
+ }
544
+ if (VARIABLE_EVENT_KINDS.has(event.kind) &&
545
+ !resolveBinding(event.key, scope, "read")) {
546
+ errors.push(`${where}: ${event.kind} targets variable "${event.key ?? ""}" but the workflow has no SetVariable node with that name.`);
547
+ }
548
+ if (event.kind === "resourceCommand" || event.kind === "openResource") {
549
+ if (!event.resourceBindingId ||
550
+ !resourceIds.has(event.resourceBindingId)) {
551
+ errors.push(`${where}: ${event.kind} targets resource "${event.resourceBindingId ?? ""}" but the app declares no such resource binding.`);
552
+ }
553
+ }
554
+ }
555
+ }
556
+ if (spec.widgets.length > 0 && !hasRunTrigger) {
557
+ errors.push('No widget has a "run" event — the app can never execute the workflow. Add a Run button or an on-change run.');
558
+ }
559
+ // The Button primitive disables itself while its click action runs an
560
+ // operation. Other run-capable widgets still need an explicit guard because
561
+ // they do not inherit that browser behavior.
562
+ for (const trigger of runTriggers) {
563
+ const guard = trigger.widget.disabledWhen?.binding
564
+ ? resolveBinding(trigger.widget.disabledWhen.binding, scope)
565
+ : null;
566
+ if (guard?.kind === "execution" &&
567
+ guard.operationId === trigger.operationId &&
568
+ guard.field === "running") {
569
+ continue;
570
+ }
571
+ if (trigger.widget.type === "Button")
572
+ continue;
573
+ const policy = operations.find((op) => op.id === trigger.operationId)?.policy ??
574
+ "parallel";
575
+ warnings.push(`${trigger.widget.type} "${trigger.widget.id}" runs operation "${trigger.operationId}" but has no disabledWhen on its running state — a second click ${REENTRY_CONSEQUENCE[policy]}. Guard it with disabledWhen on "op:${trigger.operationId}/exec#running".`);
576
+ }
577
+ // A media input starts empty by construction — no file is picked and a Sketch
578
+ // Pad publishes nothing until the first stroke — so an unguarded run sends
579
+ // nothing for it. The model answers from the prompt alone or the provider
580
+ // rejects the call, and either way the run is paid for.
581
+ const declaredDefaults = new Map((context?.variables ?? spec.variables).map((v) => [v.id, v.default]));
582
+ for (const widget of mediaWidgets) {
583
+ for (const { operationId, input } of operationInputsFilledBy(widget, operations)) {
584
+ if (!runnableOperations.has(operationId))
585
+ continue;
586
+ if (hasMediaDefault(input.defaultValue))
587
+ continue;
588
+ if (widget.ref?.kind === "variable" &&
589
+ hasMediaDefault(declaredDefaults.get(widget.ref.variableId))) {
590
+ continue;
591
+ }
592
+ const guarded = runTriggers.some((trigger) => trigger.operationId === operationId &&
593
+ [trigger.widget.disabledWhen, trigger.widget.visibleWhen].some((props) => {
594
+ if (props?.binding == null || widget.stateKey == null) {
595
+ return false;
596
+ }
597
+ const ref = resolveBinding(props.binding, scope);
598
+ return ref != null && stateKey(ref) === widget.stateKey;
599
+ }));
600
+ if (guarded)
601
+ continue;
602
+ warnings.push(`${widget.type} "${widget.id}" fills input "${input.name}" of operation "${operationId}", which has no default — nothing stops a run before the user supplies it. Guard the run trigger with disabledWhen on "${widget.binding ?? widget.canonicalBinding ?? ""}".`);
603
+ }
604
+ }
605
+ for (const operationId of runnableOperations) {
606
+ const shown = execFields.get(operationId) ?? new Set();
607
+ if (!shown.has("error")) {
608
+ warnings.push(`No widget shows the error state of operation "${operationId}" — a failed run leaves the app looking idle with nothing explaining why. Add an Alert bound to "op:${operationId}/exec#error".`);
609
+ }
610
+ if (!shown.has("running") &&
611
+ !shown.has("progress") &&
612
+ !shown.has("activity")) {
613
+ warnings.push(`No widget shows the progress of operation "${operationId}" — the user gets no sign a run is underway. Add a Progress bound to "op:${operationId}/exec#progress".`);
614
+ }
615
+ }
616
+ for (const operation of operations) {
617
+ const label = `Operation "${operation.id}"`;
618
+ if (operation.unavailable) {
619
+ errors.push(`${label}: ${operation.unavailable}`);
620
+ }
621
+ validateMappings(operation, variableIds, resourceIds, errors, warnings);
622
+ const mappedToVariable = new Set(Object.entries(operation.outputs)
623
+ .filter(([, mapping]) => mapping.to === "variable")
624
+ .map(([nodeId]) => nodeId));
625
+ for (const output of operation.io?.outputs ?? []) {
626
+ if (displayedOutputs.has(`${operation.id}:${output.nodeId}`))
627
+ continue;
628
+ if (mappedToVariable.has(output.nodeId))
629
+ continue;
630
+ warnings.push(`Workflow output "${output.name}" is not displayed by any widget.`);
631
+ }
632
+ if (spec.widgets.length > 0 && !runnableOperations.has(operation.id)) {
633
+ const shown = execBindings.get(operation.id);
634
+ // A display of an operation nothing can start shows nothing, ever.
635
+ if (shown) {
636
+ errors.push(`${label} is never run by any widget, but ${shown.join(", ")} shows its execution state.`);
637
+ }
638
+ else {
639
+ warnings.push(`${label} is declared but no widget event runs it.`);
640
+ }
641
+ }
642
+ }
643
+ return { errors, warnings };
644
+ }
645
+ /**
646
+ * The operation inputs a widget's value reaches: the one it binds directly, and
647
+ * any input mapped `{from: "variable"}` from the variable it writes — the shape
648
+ * the shipped apps use, where one picker feeds several operations.
649
+ */
650
+ function operationInputsFilledBy(widget, operations) {
651
+ const filled = [];
652
+ for (const operation of operations) {
653
+ const inputs = operation.io?.inputs ?? [];
654
+ for (const input of inputs) {
655
+ const direct = widget.ref?.kind === "input" &&
656
+ widget.ref.operationId === operation.id &&
657
+ widget.ref.nodeId === input.nodeId;
658
+ const mapping = operation.inputs[input.nodeId];
659
+ const viaVariable = widget.ref?.kind === "variable" &&
660
+ mapping?.from === "variable" &&
661
+ mapping.variableId === widget.ref.variableId;
662
+ if (direct || viaVariable) {
663
+ filled.push({ operationId: operation.id, input });
664
+ }
665
+ }
666
+ }
667
+ return filled;
668
+ }
669
+ /** Check one operation's input/output mappings against what the app declares. */
670
+ function validateMappings(operation, variableIds, resourceIds, errors, warnings) {
671
+ const label = `Operation "${operation.id}"`;
672
+ const inputNodeIds = new Set((operation.io?.inputs ?? []).map((i) => i.nodeId));
673
+ const outputNodeIds = new Set((operation.io?.outputs ?? []).map((o) => o.nodeId));
674
+ for (const [nodeId, mapping] of Object.entries(operation.inputs)) {
675
+ if (operation.io && !inputNodeIds.has(nodeId)) {
676
+ errors.push(`${label}: input mapping for node "${nodeId}", which the workflow has no input node for.`);
677
+ }
678
+ switch (mapping.from) {
679
+ case "variable":
680
+ if (!variableIds.has(mapping.variableId)) {
681
+ errors.push(`${label}: input "${nodeId}" reads variable "${mapping.variableId}", which the app does not declare.`);
682
+ }
683
+ break;
684
+ case "constant":
685
+ if (mapping.value === undefined) {
686
+ warnings.push(`${label}: input "${nodeId}" is mapped to a constant with no value — the run sends nothing for it.`);
687
+ }
688
+ break;
689
+ case "resource":
690
+ if (!resourceIds.has(mapping.resourceBindingId)) {
691
+ errors.push(`${label}: input "${nodeId}" reads resource binding "${mapping.resourceBindingId}", which the app does not declare.`);
692
+ }
693
+ else {
694
+ warnings.push(`${label}: input "${nodeId}" comes from resource binding "${mapping.resourceBindingId}" — a headless run only sees a seeded collection, so seed it with a seedResource step or a "resource:${mapping.resourceBindingId}" param before running.`);
695
+ }
696
+ break;
697
+ default:
698
+ break;
699
+ }
700
+ }
701
+ for (const [nodeId, mapping] of Object.entries(operation.outputs)) {
702
+ if (operation.io && !outputNodeIds.has(nodeId)) {
703
+ errors.push(`${label}: output mapping for node "${nodeId}", which the workflow has no output node for.`);
704
+ }
705
+ if (mapping.to === "variable" && !variableIds.has(mapping.variableId)) {
706
+ errors.push(`${label}: output "${nodeId}" writes variable "${mapping.variableId}", which the app does not declare.`);
707
+ }
708
+ }
709
+ }
710
+ //# sourceMappingURL=app-spec.js.map