@field/cascade 0.8.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/AGENTS.md +36 -3
  2. package/CHANGELOG.md +401 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -26
  5. package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
  6. package/dist/assets/CodePanel-BU-autmq.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  8. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
  9. package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
  10. package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
  11. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
  12. package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
  13. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
  14. package/dist/assets/definitions-GreEMXLV.js +1 -0
  15. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
  16. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
  17. package/dist/assets/geo-u0n_jOsD.js +1 -0
  18. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
  19. package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
  20. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
  21. package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
  22. package/dist/assets/index-5ZtDAuy2.js +220 -0
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-client-DqYmKstU.js +1 -0
  25. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
  26. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
  27. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  28. package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  30. package/dist/assets/math-DioIQOPJ.js +1 -0
  31. package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
  32. package/dist/assets/pop-DC_TQG9t.js +1 -0
  33. package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
  34. package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
  35. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  36. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
  37. package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
  38. package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
  39. package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
  40. package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
  41. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  42. package/dist/cli/chunk-2KXB47FP.js +115 -0
  43. package/dist/cli/chunk-35D343DC.js +1447 -0
  44. package/dist/cli/chunk-36P5K3SD.js +631 -0
  45. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  46. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  47. package/dist/cli/chunk-53NMBJAO.js +35 -0
  48. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  49. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  50. package/dist/cli/chunk-BP7UKF36.js +376 -0
  51. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  52. package/dist/cli/chunk-CO22XHNG.js +593 -0
  53. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  54. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  55. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  56. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  57. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  58. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  59. package/dist/cli/chunk-HFHWYL55.js +139 -0
  60. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  61. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  62. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  63. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  64. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  65. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  66. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  67. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  68. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  69. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  70. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  71. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  72. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  73. package/dist/cli/chunk-SGJAET2L.js +197 -0
  74. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  75. package/dist/cli/chunk-WD62JIP6.js +118 -0
  76. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  77. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  78. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  79. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  80. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  81. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  82. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  83. package/dist/cli/commands-Q23M26QM.js +1292 -0
  84. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  85. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  86. package/dist/cli/edit-JHCPTQFT.js +28 -0
  87. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  88. package/dist/cli/encode-HAUZTYPT.js +17 -0
  89. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  90. package/dist/cli/geo-4RNK3QVN.js +43 -0
  91. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  92. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  93. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  94. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  95. package/dist/cli/index.js +106 -14207
  96. package/dist/cli/math-ZRMLKU2D.js +16 -0
  97. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  98. package/dist/cli/parity-KINARCP2.js +328 -0
  99. package/dist/cli/pop-USDFXB7H.js +39 -0
  100. package/dist/cli/project-K3QIPGBB.js +12 -0
  101. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  102. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  103. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  104. package/dist/cli/render-YURAPPUI.js +436 -0
  105. package/dist/cli/runCommand-PI764LEG.js +198 -0
  106. package/dist/cli/runner-RA6UKEPD.js +37 -0
  107. package/dist/cli/runtime/image.ts +277 -0
  108. package/dist/cli/runtime/stage.ts +74 -11
  109. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  110. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  111. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  112. package/dist/contracts/bundle.d.ts +75 -0
  113. package/dist/contracts/bundle.js +112 -0
  114. package/dist/contracts/definition.d.ts +67 -4
  115. package/dist/contracts/document.d.ts +29 -0
  116. package/dist/contracts/documentFormat.d.ts +18 -0
  117. package/dist/contracts/documentFormat.js +195 -0
  118. package/dist/contracts/editor.d.ts +189 -0
  119. package/dist/contracts/editor.js +2 -0
  120. package/dist/contracts/index.d.ts +3 -0
  121. package/dist/contracts/index.js +3 -0
  122. package/dist/contracts/validate.js +59 -3
  123. package/dist/contracts/values.d.ts +3 -3
  124. package/dist/contracts/values.js +1 -0
  125. package/dist/image.d.ts +141 -0
  126. package/dist/image.js +127 -0
  127. package/dist/index.html +4 -4
  128. package/dist/player/app.js +17 -5
  129. package/dist/player/editor.js +1919 -0
  130. package/dist/player/editorEngine.js +477 -0
  131. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  132. package/dist/runtime/builtins/core/bundle.js +81 -0
  133. package/dist/runtime/builtins/core/index.d.ts +3 -2
  134. package/dist/runtime/builtins/core/index.js +6 -2
  135. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  136. package/dist/runtime/builtins/core/routing.js +28 -0
  137. package/dist/runtime/builtins/headless.d.ts +19 -0
  138. package/dist/runtime/builtins/headless.js +25 -0
  139. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  140. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  141. package/dist/runtime/builtins/image/index.d.ts +9 -0
  142. package/dist/runtime/builtins/image/index.js +9 -0
  143. package/dist/runtime/edit/document.d.ts +94 -0
  144. package/dist/runtime/edit/document.js +194 -0
  145. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  146. package/dist/runtime/edit/graphEditor.js +893 -0
  147. package/dist/runtime/edit/index.d.ts +12 -0
  148. package/dist/runtime/edit/index.js +6 -0
  149. package/dist/runtime/edit/layout.d.ts +50 -0
  150. package/dist/runtime/edit/layout.js +126 -0
  151. package/dist/runtime/edit/operations.d.ts +65 -0
  152. package/dist/runtime/edit/operations.js +57 -0
  153. package/dist/runtime/edit/parameters.d.ts +75 -0
  154. package/dist/runtime/edit/parameters.js +284 -0
  155. package/dist/runtime/edit/read.d.ts +35 -0
  156. package/dist/runtime/edit/read.js +65 -0
  157. package/dist/runtime/editor/index.d.ts +104 -0
  158. package/dist/runtime/editor/index.js +439 -0
  159. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  160. package/dist/runtime/expressions/bundlePaths.js +55 -0
  161. package/dist/runtime/expressions/index.d.ts +2 -0
  162. package/dist/runtime/expressions/index.js +1 -0
  163. package/dist/runtime/index.d.ts +2 -0
  164. package/dist/runtime/index.js +2 -0
  165. package/dist/runtime/params/animator.d.ts +5 -0
  166. package/dist/runtime/params/animator.js +16 -0
  167. package/dist/runtime/runtime.js +238 -12
  168. package/dist/runtime/seed.d.ts +29 -0
  169. package/dist/runtime/seed.js +52 -0
  170. package/dist/runtime/types.d.ts +12 -0
  171. package/dist/stage.js +43 -9
  172. package/dist/studio/panel.d.ts +2 -0
  173. package/doc/CLI_REFERENCE.md +171 -0
  174. package/doc/EDITOR_EXPORT.md +235 -0
  175. package/doc/HEADLESS_GPU.md +27 -0
  176. package/doc/MCP.md +69 -0
  177. package/doc/NODE_REFERENCE.md +208 -0
  178. package/doc/PROJECT_AUTHORING.md +258 -1
  179. package/package.json +29 -8
  180. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  181. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  182. package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
  183. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  184. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  185. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  186. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  187. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  188. package/dist/assets/geo-D2VPjAdt.js +0 -1
  189. package/dist/assets/index-CMiBdEuT.js +0 -212
  190. package/dist/assets/index-DMIuml9a.css +0 -1
  191. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  192. package/dist/assets/legacy-ZA7textG.js +0 -2
  193. package/dist/assets/math-B6hpxPJO.js +0 -1
  194. package/dist/assets/pop-CYUIjT6x.js +0 -1
  195. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,3006 @@
1
+ import {
2
+ builtinNodeRegistration,
3
+ builtinNodeRegistrations,
4
+ headlessBuiltin,
5
+ headlessBuiltinRegistrations
6
+ } from "./chunk-36P5K3SD.js";
7
+ import {
8
+ imagePath,
9
+ sequenceFileName,
10
+ sequenceWidth
11
+ } from "./chunk-ICH7N5PN.js";
12
+ import {
13
+ DEFAULT_GRAPH_SEED,
14
+ bundleFieldType,
15
+ bundleSchemasCompatible,
16
+ bundleValueProblem,
17
+ createGpuHost,
18
+ graphSeed,
19
+ isBundleValue,
20
+ isGpuHost,
21
+ nodeSeed,
22
+ seededRandom,
23
+ selectBundleFields,
24
+ suggestName,
25
+ unknownFieldMessage
26
+ } from "./chunk-GFS6W3C5.js";
27
+ import {
28
+ PropAnimator,
29
+ isBoundProp
30
+ } from "./chunk-HFHWYL55.js";
31
+ import {
32
+ DEFAULT_FPS,
33
+ MIN_FRAME,
34
+ bundleFieldReferences,
35
+ deserializeChannel,
36
+ frameRange,
37
+ isEmptyChannel,
38
+ parseBundleFieldPath,
39
+ remoteBundlePath
40
+ } from "./chunk-CO22XHNG.js";
41
+ import {
42
+ ProjectRoot,
43
+ extractNodeDefinition
44
+ } from "./chunk-RQZ4SX5R.js";
45
+ import {
46
+ CAPABILITIES_BY_ENVIRONMENT,
47
+ validateNodeDefinition
48
+ } from "./chunk-A4OAMOZH.js";
49
+ import {
50
+ canConnectTypes
51
+ } from "./chunk-BIJO5OFS.js";
52
+
53
+ // src/cli/projectRuntime.ts
54
+ import fs4 from "node:fs/promises";
55
+ import path5 from "node:path";
56
+ import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
57
+ import { existsSync } from "node:fs";
58
+ import ts from "typescript";
59
+ import { build } from "esbuild";
60
+
61
+ // packages/runtime/src/error.ts
62
+ var CascadeRuntimeError = class extends Error {
63
+ constructor(code, diagnostics, message = diagnostics[0]?.message ?? code) {
64
+ super(message);
65
+ this.code = code;
66
+ this.diagnostics = diagnostics;
67
+ this.name = "CascadeRuntimeError";
68
+ }
69
+ };
70
+
71
+ // packages/runtime/src/runtime.ts
72
+ var PREFLIGHT_WARNING_CODES = /* @__PURE__ */ new Set([
73
+ "runtime/preflight-environment",
74
+ "runtime/stray-params",
75
+ "runtime/unknown-props"
76
+ ]);
77
+ var RUN_TOLERATED_CODES = /* @__PURE__ */ new Set([
78
+ "runtime/stray-params",
79
+ "runtime/unknown-props"
80
+ ]);
81
+ var MutableAbortSignal = class {
82
+ constructor(report) {
83
+ this.report = report;
84
+ this.aborted = false;
85
+ this.listeners = /* @__PURE__ */ new Set();
86
+ }
87
+ addEventListener(_type, listener, options) {
88
+ if (this.aborted) this.call(listener);
89
+ else if (options?.once) {
90
+ const once = () => {
91
+ this.listeners.delete(once);
92
+ listener();
93
+ };
94
+ this.listeners.add(once);
95
+ } else this.listeners.add(listener);
96
+ }
97
+ removeEventListener(_type, listener) {
98
+ this.listeners.delete(listener);
99
+ }
100
+ abort(reason) {
101
+ if (this.aborted) return;
102
+ this.aborted = true;
103
+ this.reason = reason;
104
+ for (const listener of [...this.listeners]) this.call(listener);
105
+ this.listeners.clear();
106
+ }
107
+ call(listener) {
108
+ try {
109
+ listener();
110
+ } catch (error) {
111
+ this.report({
112
+ phase: "run",
113
+ code: "runtime/abort-listener-failed",
114
+ message: error instanceof Error ? error.message : String(error)
115
+ });
116
+ }
117
+ }
118
+ };
119
+ function createRuntime(options) {
120
+ return new Runtime(options.host, [...builtinNodeRegistrations, ...options.nodes ?? []]);
121
+ }
122
+ var Runtime = class {
123
+ constructor(host, registrations) {
124
+ this.host = host;
125
+ this.registrations = /* @__PURE__ */ new Map();
126
+ this.graphs = /* @__PURE__ */ new Set();
127
+ this.sealed = false;
128
+ this.disposed = false;
129
+ for (const registration of registrations) this.registerNode(registration);
130
+ }
131
+ registerNode(registration) {
132
+ if (this.sealed || this.disposed)
133
+ misuse(
134
+ "runtime/registry-sealed",
135
+ "Node registrations are sealed after the first load"
136
+ );
137
+ if (this.registrations.has(registration.moduleId))
138
+ misuse(
139
+ "runtime/duplicate-registration",
140
+ `Duplicate registration ${registration.moduleId}`
141
+ );
142
+ this.registrations.set(
143
+ registration.moduleId,
144
+ snapshotRegistration(registration)
145
+ );
146
+ }
147
+ async load(document2) {
148
+ if (this.disposed) misuse("runtime/disposed", "Runtime is disposed");
149
+ this.sealed = true;
150
+ const nodes = [];
151
+ const ids = /* @__PURE__ */ new Set();
152
+ for (const authored of document2.nodes) {
153
+ if (!authored.id || ids.has(authored.id))
154
+ misuse(
155
+ "runtime/invalid-node-id",
156
+ `Invalid or duplicate node id ${authored.id}`
157
+ );
158
+ ids.add(authored.id);
159
+ const moduleId2 = authored.module ?? authored.type;
160
+ if (!moduleId2)
161
+ misuse("runtime/missing-module", `Node ${authored.id} has no module`);
162
+ const resolved = this.registrations.get(moduleId2) ?? await this.host.modules.resolve(moduleId2);
163
+ if (!resolved)
164
+ misuse(
165
+ "runtime/module-not-found",
166
+ `Module ${moduleId2} was not registered`
167
+ );
168
+ if (resolved.moduleId !== moduleId2)
169
+ misuse(
170
+ "runtime/module-identity",
171
+ `Resolver returned ${resolved.moduleId} for ${moduleId2}`
172
+ );
173
+ const snapshot2 = snapshotRegistration(resolved);
174
+ if (snapshot2.kind === "legacy-dynamic")
175
+ misuse(
176
+ "runtime/legacy-adapter-required",
177
+ `Legacy module ${moduleId2} requires the explicit legacy adapter`
178
+ );
179
+ const registration = specializeCoreRegistration(
180
+ snapshot2,
181
+ authored,
182
+ document2.nodes
183
+ );
184
+ const diagnostics = validateNodeDefinition(registration.definition);
185
+ if (diagnostics.length)
186
+ throw new CascadeRuntimeError(
187
+ "runtime/definition-invalid",
188
+ diagnostics
189
+ );
190
+ nodes.push(materialize(authored, registration));
191
+ }
192
+ const byId = new Map(nodes.map((node) => [node.id, node]));
193
+ validateHierarchy(nodes, byId);
194
+ const connections = (document2.connections ?? []).map(
195
+ (connection, index) => materializeConnection(connection, index, byId)
196
+ );
197
+ validateCardinality(byId, connections);
198
+ validateTopology(nodes, connections);
199
+ const graph = new Graph(
200
+ this.host,
201
+ nodes,
202
+ connections,
203
+ () => this.graphs.delete(graph),
204
+ document2.annotations,
205
+ graphSeed(document2)
206
+ );
207
+ const fps = document2.timeline?.fps;
208
+ if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) graph.setFps(fps);
209
+ this.graphs.add(graph);
210
+ return graph;
211
+ }
212
+ async dispose() {
213
+ if (this.disposed) return;
214
+ this.disposed = true;
215
+ await Promise.all([...this.graphs].map((graph) => graph.dispose()));
216
+ await this.host.legacyShell?.dispose?.();
217
+ }
218
+ };
219
+ var Graph = class {
220
+ constructor(host, nodes, connections, onDispose, annotations = [], seed = DEFAULT_GRAPH_SEED) {
221
+ this.host = host;
222
+ this.nodes = nodes;
223
+ this.connections = connections;
224
+ this.onDispose = onDispose;
225
+ this.seed = seed;
226
+ this.graphState = "ready";
227
+ this.listeners = /* @__PURE__ */ new Set();
228
+ this.diagnostics = [];
229
+ this.runCounter = 0;
230
+ this.frame = MIN_FRAME;
231
+ this.fps = DEFAULT_FPS;
232
+ /**
233
+ * The state a feedback container is carrying, while it is running.
234
+ *
235
+ * Runtime state on the instance rather than on the node, because it is alive
236
+ * only during one container's loop and a node's `outputs` map is the graph's
237
+ * result — putting a half-finished step in there would make a partially
238
+ * simulated value readable by anything that inspects outputs mid-run.
239
+ *
240
+ * Keyed by container id so nested feedback works: an outer loop's carried
241
+ * state survives an inner loop running to completion inside one of its steps.
242
+ */
243
+ this.feedbackState = /* @__PURE__ */ new Map();
244
+ this.byId = new Map(nodes.map((node) => [node.id, node]));
245
+ const byInput = /* @__PURE__ */ new Map();
246
+ const byNode = /* @__PURE__ */ new Map();
247
+ const connectedInputs = /* @__PURE__ */ new Set();
248
+ const connectedOutputs = /* @__PURE__ */ new Set();
249
+ for (const connection of connections) {
250
+ connectedInputs.add(
251
+ `${connection.target.nodeId}\0${connection.target.inputName}`
252
+ );
253
+ connectedOutputs.add(
254
+ `${connection.source.nodeId}\0${connection.source.outputName}`
255
+ );
256
+ if (connection.kind !== "data") continue;
257
+ const inputKey = `${connection.target.nodeId}\0${connection.target.inputName}`;
258
+ const inputConnections = byInput.get(inputKey) ?? [];
259
+ inputConnections.push(connection);
260
+ byInput.set(inputKey, inputConnections);
261
+ const nodeConnections = byNode.get(connection.target.nodeId) ?? [];
262
+ nodeConnections.push(connection);
263
+ byNode.set(connection.target.nodeId, nodeConnections);
264
+ }
265
+ this.dataConnectionsByInput = new Map(
266
+ [...byInput].map(([key, inputConnections]) => [
267
+ key,
268
+ Object.freeze(
269
+ inputConnections.sort(
270
+ (first, second) => (first.variadicIndex ?? first.order) - (second.variadicIndex ?? second.order)
271
+ )
272
+ )
273
+ ])
274
+ );
275
+ this.dataConnectionsByNode = new Map(
276
+ [...byNode].map(([key, nodeConnections]) => [
277
+ key,
278
+ Object.freeze(
279
+ nodeConnections.sort((first, second) => first.order - second.order)
280
+ )
281
+ ])
282
+ );
283
+ this.connectedInputs = connectedInputs;
284
+ this.connectedOutputs = connectedOutputs;
285
+ this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
286
+ this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
287
+ this.authoredAnnotations = Object.freeze(
288
+ annotations.map((annotation) => snapshot(annotation))
289
+ );
290
+ this.animator = new PropAnimator({
291
+ nodes,
292
+ node: (id) => this.byId.get(id),
293
+ inputNode: (node, index) => {
294
+ const feeding = this.dataConnectionsByNode.get(node.id) ?? [];
295
+ const source = feeding[index]?.source.nodeId;
296
+ return (source ? this.byId.get(source) : void 0) ?? null;
297
+ },
298
+ inputValue: (node, name) => this.dataInputValue(node, name),
299
+ // A failed expression has already fallen back to the stored value, so it
300
+ // is a diagnostic rather than a stopped run — but it is never silent: a
301
+ // parameter that quietly reverts is the hardest fault to see in a
302
+ // hundred rendered frames.
303
+ report: (nodeId, propName, error) => this.reportDiagnostic({
304
+ phase: "run",
305
+ code: "runtime/expression-failed",
306
+ message: `${nodeId}.${propName}: ${error}`,
307
+ path: `${nodeId}.${propName}`
308
+ })
309
+ });
310
+ this.animator.setFps(this.fps);
311
+ this.animator.setFrame(this.frame);
312
+ this.animator.resolveAll();
313
+ }
314
+ get state() {
315
+ return this.graphState;
316
+ }
317
+ /**
318
+ * Everything that would stop this host running the graph, without running
319
+ * it: a node whose environment this host is not, a bridged node that cannot
320
+ * be bridged, a declared capability this host does not install. Empty means
321
+ * the graph clears the same gate `run` puts it through.
322
+ */
323
+ preflight() {
324
+ return this.preflightDiagnostics(this.nodes);
325
+ }
326
+ inspect() {
327
+ const nodes = this.nodes.map((node) => ({
328
+ id: node.id,
329
+ moduleId: node.moduleId,
330
+ runsOn: node.definition.runsOn,
331
+ ...node.parent ? { parent: node.parent } : {},
332
+ inputs: Object.fromEntries(
333
+ Object.entries(node.definition.inputs ?? {}).map(
334
+ ([name, definition]) => [
335
+ name,
336
+ {
337
+ kind: definition.kind,
338
+ type: definition.kind === "trigger" ? "trigger" : definition.type,
339
+ connected: this.connectedInputs.has(`${node.id}\0${name}`),
340
+ ...definition.kind === "data" ? { value: snapshot(node.inputs[name]) } : {}
341
+ }
342
+ ]
343
+ )
344
+ ),
345
+ outputs: Object.fromEntries(
346
+ Object.entries(node.definition.outputs ?? {}).map(
347
+ ([name, definition]) => [
348
+ name,
349
+ {
350
+ kind: definition.kind,
351
+ type: definition.kind === "trigger" ? "trigger" : definition.type,
352
+ connected: this.connectedOutputs.has(`${node.id}\0${name}`),
353
+ ...definition.kind === "data" && node.outputs.has(name) ? { value: snapshot(node.outputs.get(name)) } : {}
354
+ }
355
+ ]
356
+ )
357
+ ),
358
+ props: Object.fromEntries(
359
+ Object.entries(node.definition.props ?? {}).map(
360
+ ([name, definition]) => [
361
+ name,
362
+ { type: definition.type, value: snapshot(node.props[name]) }
363
+ ]
364
+ )
365
+ )
366
+ }));
367
+ return Object.freeze({
368
+ state: this.graphState,
369
+ interface: Object.freeze({
370
+ inputs: boundaryInspection(this.graphInputs, "output"),
371
+ outputs: boundaryInspection(this.graphOutputs, "output")
372
+ }),
373
+ nodes: Object.freeze(nodes),
374
+ connections: Object.freeze(
375
+ this.connections.map(
376
+ (connection) => Object.freeze({
377
+ source: Object.freeze({ ...connection.source }),
378
+ target: Object.freeze({ ...connection.target })
379
+ })
380
+ )
381
+ ),
382
+ diagnostics: Object.freeze([...this.diagnostics])
383
+ });
384
+ }
385
+ async setInput(nodeId, inputName2, value) {
386
+ this.requireReady();
387
+ const node = this.requireNode(nodeId);
388
+ const definition = node.definition.inputs?.[inputName2];
389
+ if (!definition || definition.kind !== "data")
390
+ misuse(
391
+ "runtime/invalid-input",
392
+ `${nodeId}.${inputName2} is not a data input`
393
+ );
394
+ node.inputs[inputName2] = runtimeValue(
395
+ value,
396
+ definition.type,
397
+ !("default" in definition),
398
+ `${nodeId}.${inputName2}`
399
+ );
400
+ }
401
+ setGraphInput(name, value) {
402
+ const node = this.graphInputs.get(name);
403
+ if (!node)
404
+ return Promise.reject(runtimeError("runtime/graph-input-not-found", `Unknown graph input ${name}`));
405
+ return this.setInput(node.id, "value", value);
406
+ }
407
+ async setProp(nodeId, propName, value) {
408
+ this.requireReady();
409
+ const node = this.requireNode(nodeId);
410
+ if (isStructuralProp(node, propName))
411
+ misuse(
412
+ "runtime/structural-prop",
413
+ `${nodeId}.${propName} is structural and requires a graph reload`
414
+ );
415
+ if (!node.definition.props?.[propName])
416
+ misuse("runtime/invalid-prop", `${nodeId}.${propName} is not a prop`);
417
+ const resolved = runtimeValue(
418
+ value,
419
+ node.definition.props[propName].type,
420
+ false,
421
+ `${nodeId}.${propName}`
422
+ );
423
+ node.props[propName] = resolved;
424
+ this.rebind(node, propName, resolved);
425
+ }
426
+ /**
427
+ * Keep a bound prop's STORED value in step with a direct write.
428
+ *
429
+ * A channel or an expression still wins on the next resolve — setting a
430
+ * keyed parameter's value does not delete its keys, in Studio or here — but
431
+ * the value written is the one the parameter falls back to, so dropping it
432
+ * would make a later `clearChannel` reinstate a number nobody chose.
433
+ */
434
+ rebind(node, propName, value) {
435
+ const binding = node.bindings[propName];
436
+ if (binding) node.bindings[propName] = { ...binding, value };
437
+ }
438
+ reportDiagnostic(diagnostic) {
439
+ this.diagnostics.push(diagnostic);
440
+ this.emit({ type: "diagnostic", diagnostic });
441
+ }
442
+ async applyPreset(preset) {
443
+ this.requireReady();
444
+ if (preset.version !== 1)
445
+ misuse("runtime/invalid-preset", "Preset version must be 1");
446
+ const inputDrafts = [];
447
+ const propDrafts = [];
448
+ for (const [nodeId, values] of Object.entries(preset.nodes)) {
449
+ const node = this.requireNode(nodeId);
450
+ for (const [name, value] of Object.entries(values.inputs ?? {})) {
451
+ const definition = node.definition.inputs?.[name];
452
+ if (!definition || definition.kind !== "data")
453
+ misuse(
454
+ "runtime/invalid-input",
455
+ `${nodeId}.${name} is not a data input`
456
+ );
457
+ inputDrafts.push([
458
+ node,
459
+ name,
460
+ runtimeValue(
461
+ value,
462
+ definition.type,
463
+ !("default" in definition),
464
+ `${nodeId}.${name}`
465
+ )
466
+ ]);
467
+ }
468
+ for (const [name, value] of Object.entries(values.props ?? {})) {
469
+ const definition = node.definition.props?.[name];
470
+ if (!definition)
471
+ misuse("runtime/invalid-prop", `${nodeId}.${name} is not a prop`);
472
+ if (isStructuralProp(node, name))
473
+ misuse(
474
+ "runtime/structural-prop",
475
+ `${nodeId}.${name} is structural and cannot be changed by a preset`
476
+ );
477
+ propDrafts.push([
478
+ node,
479
+ name,
480
+ runtimeValue(value, definition.type, false, `${nodeId}.${name}`)
481
+ ]);
482
+ }
483
+ }
484
+ for (const [node, name, value] of inputDrafts) node.inputs[name] = value;
485
+ for (const [node, name, value] of propDrafts) {
486
+ node.props[name] = value;
487
+ this.rebind(node, name, value);
488
+ }
489
+ }
490
+ /**
491
+ * The frame this graph is cooking at, and the rate frames are counted in.
492
+ *
493
+ * Separate from `run` so a caller can scrub without cooking — and so that
494
+ * `setFrame` then `inspect()` reports a keyed parameter at that frame, which
495
+ * is what a panel needs and what `run` alone cannot give it.
496
+ */
497
+ setFrame(frame) {
498
+ if (!Number.isFinite(frame)) return;
499
+ this.frame = Math.max(MIN_FRAME, frame);
500
+ this.animator.setFrame(this.frame);
501
+ this.animator.resolveAll();
502
+ }
503
+ setFps(fps) {
504
+ if (!Number.isFinite(fps)) return;
505
+ this.fps = Math.max(1, fps);
506
+ this.animator.setFps(this.fps);
507
+ this.animator.resolveAll();
508
+ }
509
+ getFrame() {
510
+ return this.frame;
511
+ }
512
+ getFps() {
513
+ return this.fps;
514
+ }
515
+ run(options = {}) {
516
+ if (options.fps !== void 0) this.setFps(options.fps);
517
+ if (options.frame !== void 0) this.setFrame(options.frame);
518
+ else this.animator.resolveAll();
519
+ return this.trackRun(
520
+ options.target ?? { kind: "all" },
521
+ options.signal,
522
+ options.runId,
523
+ void 0,
524
+ options.reuse === true || options.reuse === "strict" ? options.reuse : false
525
+ );
526
+ }
527
+ trigger(nodeId, inputName2, payload, options = {}) {
528
+ const node = this.requireNode(nodeId);
529
+ if (node.definition.inputs?.[inputName2]?.kind !== "trigger")
530
+ return Promise.reject(
531
+ runtimeError(
532
+ "runtime/invalid-trigger",
533
+ `${nodeId}.${inputName2} is not a trigger input`
534
+ )
535
+ );
536
+ return this.trackRun(
537
+ { kind: "node", nodeId },
538
+ options.signal,
539
+ options.runId,
540
+ { nodeId, inputName: inputName2, payload }
541
+ );
542
+ }
543
+ cancel(reason = "Cancelled") {
544
+ if (!this.active || this.active.signal.aborted) return;
545
+ this.graphState = "cancelling";
546
+ this.active.reason = reason;
547
+ this.active.signal.abort(reason);
548
+ this.emit({ type: "run:cancel", runId: this.active.runId, reason });
549
+ }
550
+ subscribe(listener) {
551
+ this.listeners.add(listener);
552
+ return () => this.listeners.delete(listener);
553
+ }
554
+ getOutput(nodeId, outputName2) {
555
+ return snapshot(this.requireNode(nodeId).outputs.get(outputName2));
556
+ }
557
+ getGraphOutput(name) {
558
+ const node = this.graphOutputs.get(name);
559
+ if (!node)
560
+ misuse("runtime/graph-output-not-found", `Unknown graph output ${name}`);
561
+ return this.getOutput(node.id, "output");
562
+ }
563
+ getOutputs(nodeId) {
564
+ return immutableMap(
565
+ [...this.requireNode(nodeId).outputs].map(([name, value]) => [
566
+ name,
567
+ snapshot(value)
568
+ ])
569
+ );
570
+ }
571
+ dispose() {
572
+ this.disposal ??= this.performDispose();
573
+ return this.disposal;
574
+ }
575
+ trackRun(target, signal, runId, injection, reuse = false) {
576
+ const running = this.executeRun(target, signal, runId, injection, reuse);
577
+ this.activeRun = running;
578
+ const clear = () => {
579
+ if (this.activeRun === running) this.activeRun = void 0;
580
+ };
581
+ void running.then(clear, clear);
582
+ return running;
583
+ }
584
+ async performDispose() {
585
+ if (this.graphState === "disposed") return;
586
+ this.cancel("Graph disposed");
587
+ try {
588
+ await this.activeRun;
589
+ } catch {
590
+ }
591
+ this.graphState = "disposed";
592
+ this.listeners.clear();
593
+ this.onDispose();
594
+ }
595
+ async executeRun(target, externalSignal, requestedId, injection, reuse = false) {
596
+ this.requireReady();
597
+ const runId = requestedId ?? `run-${this.runCounter++}`;
598
+ const signal = new MutableAbortSignal(
599
+ (diagnostic) => this.host.report(diagnostic)
600
+ );
601
+ this.active = { runId, signal };
602
+ this.graphState = "running";
603
+ const startedAt = this.host.now();
604
+ const runDiagnostics = [];
605
+ this.emit({ type: "run:start", runId, target });
606
+ const relay = () => this.cancel(
607
+ typeof externalSignal?.reason === "string" ? externalSignal.reason : "Cancelled by external signal"
608
+ );
609
+ if (externalSignal?.aborted) relay();
610
+ else externalSignal?.addEventListener("abort", relay, { once: true });
611
+ let executionStarted = false;
612
+ try {
613
+ const selection = injection ? {
614
+ reachable: this.triggerReachable(injection.nodeId),
615
+ initial: this.triggerDataAncestors(injection.nodeId)
616
+ } : this.runSelection(target);
617
+ const { reachable, initial } = selection;
618
+ this.assertPreflight(reachable);
619
+ if (!signal.aborted)
620
+ await Promise.all(
621
+ reachable.filter((node) => !this.isRemote(node)).map((node) => this.loadExecute(node))
622
+ );
623
+ executionStarted = true;
624
+ const queue = [];
625
+ let sequence = 0;
626
+ const enqueue = (nodeId, inputName2, payload, source) => {
627
+ const event = snapshot({
628
+ id: `${runId}:${sequence}`,
629
+ runId,
630
+ sequence: sequence++,
631
+ ...payload === void 0 ? {} : { payload: snapshot(payload) },
632
+ source
633
+ });
634
+ queue.push({ nodeId, inputName: inputName2, event });
635
+ this.emit({ type: "trigger:queued", runId, nodeId, inputName: inputName2, event });
636
+ };
637
+ const insideFeedback = this.feedbackDescendants();
638
+ const ordered = topological(this.nodes, this.connections).filter(
639
+ (node) => initial.includes(node) && !insideFeedback.has(node.id)
640
+ );
641
+ const cooked = /* @__PURE__ */ new Set();
642
+ for (const node of ordered) {
643
+ if (signal.aborted) break;
644
+ if (reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked))) {
645
+ this.emit({ type: "node:start", runId, nodeId: node.id });
646
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
647
+ continue;
648
+ }
649
+ await this.executeNode(node, runId, signal, {}, enqueue);
650
+ cooked.add(node.id);
651
+ if (!signal.aborted) node.lastCook = this.cookKey(node);
652
+ }
653
+ if (injection && !signal.aborted)
654
+ enqueue(injection.nodeId, injection.inputName, injection.payload, {
655
+ kind: "external"
656
+ });
657
+ while (queue.length && !signal.aborted) {
658
+ const next = queue.shift();
659
+ this.emit({
660
+ type: "trigger:delivered",
661
+ runId,
662
+ nodeId: next.nodeId,
663
+ inputName: next.inputName,
664
+ event: next.event
665
+ });
666
+ await this.executeNode(
667
+ this.requireNode(next.nodeId),
668
+ runId,
669
+ signal,
670
+ { [next.inputName]: next.event },
671
+ enqueue
672
+ );
673
+ }
674
+ return this.finish(
675
+ runId,
676
+ signal.aborted ? "cancelled" : "completed",
677
+ startedAt,
678
+ runDiagnostics
679
+ );
680
+ } catch (error) {
681
+ if (!executionStarted && error instanceof CascadeRuntimeError)
682
+ throw error;
683
+ const diagnostic = {
684
+ phase: "run",
685
+ code: "runtime/execution-failed",
686
+ message: error instanceof Error ? error.message : String(error)
687
+ };
688
+ runDiagnostics.push(diagnostic);
689
+ this.diagnostics.push(diagnostic);
690
+ this.emit({ type: "diagnostic", runId, diagnostic });
691
+ return this.finish(
692
+ runId,
693
+ signal.aborted ? "cancelled" : "failed",
694
+ startedAt,
695
+ runDiagnostics
696
+ );
697
+ } finally {
698
+ externalSignal?.removeEventListener("abort", relay);
699
+ if (!this.isDisposed()) this.graphState = "ready";
700
+ this.active = void 0;
701
+ }
702
+ }
703
+ async executeNode(node, runId, signal, triggers, enqueue) {
704
+ this.emit({ type: "node:start", runId, nodeId: node.id });
705
+ if (node.moduleId === "cascade.core.Previous") {
706
+ const container = this.enclosingFeedback(node);
707
+ const state = container ? this.feedbackState.get(container) : void 0;
708
+ if (state) {
709
+ node.outputs.set("value", state.value);
710
+ node.outputs.set("step", state.step);
711
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
712
+ return;
713
+ }
714
+ }
715
+ const inputs = Object.fromEntries(
716
+ Object.entries(node.definition.inputs ?? {}).map(([name, definition]) => {
717
+ if (definition.kind === "trigger") return [name, triggers[name]];
718
+ if (node.moduleId === "cascade.core.Input" && name === "value" && node.parent) {
719
+ const parent = this.requireNode(node.parent);
720
+ const index = integerProp(node.props.inputIndex);
721
+ return [name, this.dataInputValue(parent, `input_${index}`)];
722
+ }
723
+ const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
724
+ if (definition.variadic) {
725
+ const values = connections.map(
726
+ (connection2) => this.requireNode(connection2.source.nodeId).outputs.get(
727
+ connection2.source.outputName
728
+ )
729
+ );
730
+ if (!values.length && node.inputs[name] !== void 0)
731
+ values.push(node.inputs[name]);
732
+ return [name, Object.freeze(values)];
733
+ }
734
+ const connection = connections[0];
735
+ const value = connection ? this.requireNode(connection.source.nodeId).outputs.get(
736
+ connection.source.outputName
737
+ ) : node.inputs[name];
738
+ return [
739
+ name,
740
+ definition.type === "bundle" && definition.fields ? selectBundleFields(value, definition.fields) : value
741
+ ];
742
+ })
743
+ );
744
+ Object.freeze(inputs);
745
+ if (readsBundleFields(node)) this.animator.resolveNode(node);
746
+ if (this.isRemote(node)) {
747
+ const bridge = this.host.capabilities.serverBridge;
748
+ if (!bridge)
749
+ throw runtimeError(
750
+ "runtime/bridge-missing",
751
+ `No server bridge for ${node.moduleId}`
752
+ );
753
+ const progress2 = {
754
+ report: (event) => this.emit({
755
+ type: "node:progress",
756
+ runId,
757
+ nodeId: node.id,
758
+ ...event
759
+ })
760
+ };
761
+ const response = await bridge.execute(
762
+ {
763
+ moduleId: node.moduleId,
764
+ nodeId: node.id,
765
+ inputs,
766
+ props: Object.freeze({ ...node.props })
767
+ },
768
+ { signal, progress: progress2 }
769
+ );
770
+ for (const diagnostic of response.diagnostics) {
771
+ this.diagnostics.push(diagnostic);
772
+ this.emit({ type: "diagnostic", runId, diagnostic });
773
+ }
774
+ for (const name of Object.keys(node.definition.outputs ?? {}))
775
+ if (name in response.outputs) {
776
+ const definition = node.definition.outputs?.[name];
777
+ if (!definition || definition.kind !== "data") continue;
778
+ const value = outputValue(
779
+ response.outputs[name],
780
+ definition,
781
+ `${node.id}.${name}`
782
+ );
783
+ node.outputs.set(name, value);
784
+ this.emit({
785
+ type: "node:output",
786
+ runId,
787
+ nodeId: node.id,
788
+ outputName: name,
789
+ value
790
+ });
791
+ }
792
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
793
+ return;
794
+ }
795
+ if (!node.execute)
796
+ throw runtimeError(
797
+ "runtime/executor-missing",
798
+ `Executor ${node.moduleId} was not loaded`
799
+ );
800
+ const pendingData = /* @__PURE__ */ new Map();
801
+ const pendingTriggers = [];
802
+ const outputs = Object.fromEntries(
803
+ Object.entries(node.definition.outputs ?? {}).map(
804
+ ([name, definition]) => [
805
+ name,
806
+ definition.kind === "trigger" ? {
807
+ trigger: (payload) => pendingTriggers.push({
808
+ name,
809
+ ...payload === void 0 ? {} : { payload: snapshot(payload) }
810
+ })
811
+ } : {
812
+ set: (value) => pendingData.set(
813
+ name,
814
+ outputValue(value, definition, `${node.id}.${name}`)
815
+ )
816
+ }
817
+ ]
818
+ )
819
+ );
820
+ const capabilityEntries = [];
821
+ for (const name of node.definition.capabilities ?? []) {
822
+ const installed = this.host.capabilities[name];
823
+ if (name === "gpu" && isGpuHost(installed)) {
824
+ await installed.ensure();
825
+ capabilityEntries.push([name, installed.forNode(node.id)]);
826
+ } else {
827
+ capabilityEntries.push([
828
+ name,
829
+ name === "shell" ? bindShellToRun(this.host.capabilities.shell, signal) : installed
830
+ ]);
831
+ }
832
+ }
833
+ const capabilities = Object.freeze(Object.fromEntries(capabilityEntries));
834
+ const props = Object.freeze({ ...node.props });
835
+ const progress = {
836
+ report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event })
837
+ };
838
+ const seed = nodeSeed(this.seed, node.id);
839
+ await node.execute({
840
+ nodeId: node.id,
841
+ inputs,
842
+ outputs,
843
+ props,
844
+ capabilities,
845
+ signal,
846
+ progress,
847
+ seed,
848
+ // Fresh per cook: a node that draws N numbers draws the same N each time.
849
+ random: seededRandom(seed)
850
+ });
851
+ if (signal.aborted) return;
852
+ if (node.definition.container === "feedback") {
853
+ const feedback = await this.runFeedback(node, runId, signal, enqueue);
854
+ if (signal.aborted) return;
855
+ pendingData.set("result", feedback.result);
856
+ pendingData.set("history", feedback.history);
857
+ }
858
+ if (node.moduleId === "cascade.core.Subnet") {
859
+ for (const child of this.nodes) {
860
+ if (child.parent !== node.id || child.moduleId !== "cascade.core.Output") continue;
861
+ const name = `output_${integerProp(child.props.outputIndex)}`;
862
+ if (node.definition.outputs?.[name]?.kind === "data")
863
+ pendingData.set(name, child.outputs.get("output"));
864
+ }
865
+ }
866
+ for (const name of Object.keys(node.definition.outputs ?? {}))
867
+ if (pendingData.has(name)) {
868
+ const value = pendingData.get(name);
869
+ node.outputs.set(name, value);
870
+ this.emit({
871
+ type: "node:output",
872
+ runId,
873
+ nodeId: node.id,
874
+ outputName: name,
875
+ value: snapshot(value)
876
+ });
877
+ }
878
+ for (const emitted of pendingTriggers)
879
+ for (const connection of this.connections) {
880
+ if (connection.kind === "trigger" && connection.source.nodeId === node.id && connection.source.outputName === emitted.name)
881
+ enqueue(
882
+ connection.target.nodeId,
883
+ connection.target.inputName,
884
+ emitted.payload,
885
+ { nodeId: node.id, outputName: emitted.name }
886
+ );
887
+ }
888
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
889
+ }
890
+ /**
891
+ * Every reason this host cannot run these nodes, collected rather than
892
+ * thrown. `assertPreflight` is the run path and raises the first of them;
893
+ * `preflight()` is the static path and hands the whole list to a caller that
894
+ * is not executing anything.
895
+ *
896
+ * There is deliberately one implementation. `validate` and `check` used to
897
+ * answer "will this graph run?" without ever consulting a host, so a node
898
+ * declaring a capability nobody installs passed both and then failed here —
899
+ * the two commands could not see the fault they exist to report. Any second
900
+ * copy of these rules would drift back into that.
901
+ */
902
+ preflightDiagnostics(nodes) {
903
+ const diagnostics = [];
904
+ const push = (code, nodeId, message) => diagnostics.push({ phase: "preflight", code, path: nodeId, message });
905
+ for (const node of nodes) {
906
+ if (node.unknownProps.length)
907
+ push(
908
+ "runtime/unknown-props",
909
+ node.id,
910
+ `${node.moduleId} stores ${node.unknownProps.join(", ")} under "props", and its definition declares no such prop \u2014 ${node.unknownProps.length === 1 ? "that value was" : "those values were"} dropped on load. A prop renamed or removed leaves its value behind; a name that never existed is a typo`
911
+ );
912
+ if (node.strayParams.length)
913
+ push(
914
+ "runtime/stray-params",
915
+ node.id,
916
+ `${node.moduleId} stores ${node.strayParams.join(", ")} under "params", which this runtime does not read \u2014 move ${node.strayParams.length === 1 ? "it" : "them"} to "props" or "inputs", whichever the definition declares, or the ${node.strayParams.length === 1 ? "value is" : "values are"} silently the default`
917
+ );
918
+ const remote = this.isRemote(node);
919
+ const locationWorks = node.definition.runsOn === "portable" || node.definition.runsOn === this.host.environment || remote;
920
+ if (!locationWorks) {
921
+ push(
922
+ "runtime/preflight-environment",
923
+ node.id,
924
+ `${node.moduleId} requires ${node.definition.runsOn}`
925
+ );
926
+ continue;
927
+ }
928
+ if (remote) {
929
+ const hasTriggers = [
930
+ ...Object.values(node.definition.inputs ?? {}),
931
+ ...Object.values(node.definition.outputs ?? {})
932
+ ].some((port) => port.kind === "trigger");
933
+ if (hasTriggers)
934
+ push(
935
+ "runtime/preflight-bridge-trigger",
936
+ node.id,
937
+ `${node.moduleId} cannot send triggers over the server bridge`
938
+ );
939
+ const hasTexture = [
940
+ ...Object.values(node.definition.inputs ?? {}),
941
+ ...Object.values(node.definition.outputs ?? {})
942
+ ].some((port) => port.kind === "data" && port.type === "texture");
943
+ if (hasTexture)
944
+ push(
945
+ "runtime/preflight-bridge-texture",
946
+ node.id,
947
+ `${node.moduleId} cannot send textures over the server bridge`
948
+ );
949
+ } else
950
+ for (const capability of node.definition.capabilities ?? [])
951
+ if (!this.host.capabilities[capability])
952
+ push(
953
+ "runtime/missing-capability",
954
+ node.id,
955
+ `Missing capability ${capability} for ${node.moduleId} (node ${node.id}) \u2014 the ${this.host.environment} host ${this.hostProvides()}`
956
+ );
957
+ }
958
+ diagnostics.push(...this.bundleDiagnostics(nodes));
959
+ return diagnostics;
960
+ }
961
+ /**
962
+ * Bundle fields read by name, checked against what the producer declares:
963
+ * a consumer's declared `fields`, an expression's `ch("format.dpi")`, and a
964
+ * Pick's `field`. A producer that declares no fields (Pack, a subnet Input,
965
+ * a project node that left them out) cannot be checked and is not. Reading
966
+ * another node's bundle by absolute path is refused with what to do instead.
967
+ */
968
+ bundleDiagnostics(nodes) {
969
+ const diagnostics = [];
970
+ const push = (nodeId, code, message, at = nodeId) => diagnostics.push({ phase: "preflight", code, path: at, message });
971
+ const producerFields = (node, input) => {
972
+ const connection = this.dataConnectionsByInput.get(`${node.id}\0${input}`)?.[0];
973
+ if (!connection) return null;
974
+ const output = this.byId.get(connection.source.nodeId)?.definition.outputs?.[connection.source.outputName];
975
+ if (output?.kind !== "data" || output.type !== "bundle" || !output.fields) return null;
976
+ return Object.keys(output.fields);
977
+ };
978
+ for (const node of nodes) {
979
+ for (const [name, input] of Object.entries(node.definition.inputs ?? {})) {
980
+ if (input.kind !== "data" || input.type !== "bundle" || !input.fields) continue;
981
+ const known = producerFields(node, name);
982
+ if (!known) continue;
983
+ for (const field of input.fields)
984
+ if (!known.includes(field))
985
+ push(
986
+ node.id,
987
+ "runtime/bundle-unknown-field",
988
+ unknownFieldMessage(field, known, `${node.id}.${name} (declared fields)`),
989
+ `${node.id}.${name}`
990
+ );
991
+ }
992
+ if (node.moduleId === "cascade.core.Pick") {
993
+ const field = String(node.props.field ?? "").trim();
994
+ const known = producerFields(node, "bundle");
995
+ if (field && known && !known.includes(field))
996
+ push(
997
+ node.id,
998
+ "runtime/bundle-unknown-field",
999
+ unknownFieldMessage(field, known, `${node.id}.field`),
1000
+ `${node.id}.field`
1001
+ );
1002
+ }
1003
+ for (const [prop, binding] of Object.entries(node.bindings)) {
1004
+ if (!binding.expression) continue;
1005
+ for (const path6 of expressionPaths(binding.expression)) {
1006
+ const remote = remoteBundlePath(path6);
1007
+ if (remote) {
1008
+ const owner = this.byId.get(remote.node);
1009
+ const output = owner?.definition.outputs?.[remote.port];
1010
+ if (output?.kind === "data" && output.type === "bundle")
1011
+ push(
1012
+ node.id,
1013
+ "runtime/bundle-remote-read",
1014
+ `${node.id}.${prop} reads "${path6}", another node's bundle, without a wire. Wire ${remote.node}.${remote.port} into a bundle input on ${node.id} and read "${remote.port}.${remote.field}" instead`,
1015
+ `${node.id}.${prop}`
1016
+ );
1017
+ continue;
1018
+ }
1019
+ const reference = parseBundleFieldPath(path6);
1020
+ if (!reference) continue;
1021
+ const input = node.definition.inputs?.[reference.port];
1022
+ if (!input) {
1023
+ const ports = Object.entries(node.definition.inputs ?? {}).filter(([, item]) => item.kind === "data" && item.type === "bundle").map(([portName]) => portName);
1024
+ const suggestion = suggestName(reference.port, ports);
1025
+ push(
1026
+ node.id,
1027
+ "runtime/bundle-unknown-port",
1028
+ `${node.id}.${prop} reads "${path6}", but ${node.moduleId} has no input "${reference.port}"` + (suggestion ? ` \u2014 did you mean "${suggestion}.${reference.field}"?` : ""),
1029
+ `${node.id}.${prop}`
1030
+ );
1031
+ continue;
1032
+ }
1033
+ if (input.kind !== "data" || input.type !== "bundle") continue;
1034
+ const known = producerFields(node, reference.port);
1035
+ if (known && !known.includes(reference.field))
1036
+ push(
1037
+ node.id,
1038
+ "runtime/bundle-unknown-field",
1039
+ unknownFieldMessage(reference.field, known, `${node.id}.${prop}`),
1040
+ `${node.id}.${prop}`
1041
+ );
1042
+ }
1043
+ }
1044
+ }
1045
+ return diagnostics;
1046
+ }
1047
+ /**
1048
+ * What the host actually installed, asked of the host rather than written
1049
+ * down. `serverBridge` is left out: it is how a node runs somewhere else,
1050
+ * not a capability a definition can declare.
1051
+ */
1052
+ hostProvides() {
1053
+ const supplied = Object.keys(this.host.capabilities).filter((name) => name !== "serverBridge" && !!this.host.capabilities[name]).sort();
1054
+ return supplied.length ? `provides ${supplied.join(", ")}` : `provides no capabilities`;
1055
+ }
1056
+ assertPreflight(nodes) {
1057
+ const [first] = this.preflightDiagnostics(nodes).filter(
1058
+ (diagnostic) => !RUN_TOLERATED_CODES.has(diagnostic.code)
1059
+ );
1060
+ if (first) throw new CascadeRuntimeError(first.code, [first]);
1061
+ }
1062
+ isRemote(node) {
1063
+ return this.host.environment === "browser" && node.definition.runsOn === "server" && !!this.host.capabilities.serverBridge;
1064
+ }
1065
+ async loadExecute(node) {
1066
+ node.execute ??= await node.registration.loadExecute(
1067
+ this.host.environment
1068
+ );
1069
+ }
1070
+ runSelection(target) {
1071
+ if (target.kind === "all") {
1072
+ const gatedDataDescendants = this.gatedDataDescendants();
1073
+ const initial = this.nodes.filter(
1074
+ (node) => !isGated(node) && !gatedDataDescendants.has(node.id)
1075
+ );
1076
+ const ids = new Set(initial.map((node) => node.id));
1077
+ let changed = true;
1078
+ while (changed) {
1079
+ changed = false;
1080
+ for (const connection of this.connections) {
1081
+ if (connection.kind === "trigger" && ids.has(connection.source.nodeId) && !ids.has(connection.target.nodeId)) {
1082
+ ids.add(connection.target.nodeId);
1083
+ for (const dependency of this.dataClosure(connection.target.nodeId))
1084
+ ids.add(dependency.id);
1085
+ changed = true;
1086
+ }
1087
+ }
1088
+ }
1089
+ return {
1090
+ initial,
1091
+ reachable: this.nodes.filter((node) => ids.has(node.id))
1092
+ };
1093
+ }
1094
+ const root = this.requireNode(target.nodeId);
1095
+ if (target.kind === "output" && !root.definition.outputs?.[target.outputName])
1096
+ misuse(
1097
+ "runtime/invalid-target",
1098
+ `Unknown output ${target.nodeId}.${target.outputName}`
1099
+ );
1100
+ const reachable = this.dataClosure(root.id);
1101
+ if (reachable.some(isGated))
1102
+ misuse(
1103
+ "runtime/invalid-target",
1104
+ `Target ${root.id} depends on a trigger event`
1105
+ );
1106
+ return { reachable, initial: reachable };
1107
+ }
1108
+ dataClosure(nodeId) {
1109
+ const ids = /* @__PURE__ */ new Set([nodeId]);
1110
+ const dependencies = dataDependencies(this.nodes, this.connections);
1111
+ let changed = true;
1112
+ while (changed) {
1113
+ changed = false;
1114
+ for (const dependency of dependencies)
1115
+ if (ids.has(dependency.targetId) && !ids.has(dependency.sourceId)) {
1116
+ ids.add(dependency.sourceId);
1117
+ changed = true;
1118
+ }
1119
+ }
1120
+ return this.nodes.filter((node) => ids.has(node.id));
1121
+ }
1122
+ gatedDataDescendants() {
1123
+ const outgoing = /* @__PURE__ */ new Map();
1124
+ for (const dependency of dataDependencies(this.nodes, this.connections)) {
1125
+ const targets = outgoing.get(dependency.sourceId) ?? [];
1126
+ targets.push(dependency.targetId);
1127
+ outgoing.set(dependency.sourceId, targets);
1128
+ }
1129
+ const queue = this.nodes.filter(isGated).map((node) => node.id);
1130
+ const descendants = new Set(queue);
1131
+ let queueIndex = 0;
1132
+ while (queueIndex < queue.length) {
1133
+ for (const targetId of outgoing.get(queue[queueIndex++]) ?? []) {
1134
+ if (descendants.has(targetId)) continue;
1135
+ descendants.add(targetId);
1136
+ queue.push(targetId);
1137
+ }
1138
+ }
1139
+ return descendants;
1140
+ }
1141
+ triggerDataAncestors(nodeId) {
1142
+ const dependencies = this.dataClosure(nodeId).filter(
1143
+ (node) => node.id !== nodeId
1144
+ );
1145
+ if (dependencies.some(isGated))
1146
+ misuse(
1147
+ "runtime/invalid-target",
1148
+ `Trigger target ${nodeId} depends on another trigger event`
1149
+ );
1150
+ return dependencies;
1151
+ }
1152
+ triggerReachable(nodeId) {
1153
+ const ids = /* @__PURE__ */ new Set([this.requireNode(nodeId).id]);
1154
+ const dependencies = dataDependencies(this.nodes, this.connections);
1155
+ let changed = true;
1156
+ while (changed) {
1157
+ changed = false;
1158
+ for (const connection of this.connections) {
1159
+ if (connection.kind === "trigger" && ids.has(connection.source.nodeId) && !ids.has(connection.target.nodeId)) {
1160
+ ids.add(connection.target.nodeId);
1161
+ changed = true;
1162
+ }
1163
+ }
1164
+ for (const dependency of dependencies)
1165
+ if (ids.has(dependency.targetId) && !ids.has(dependency.sourceId)) {
1166
+ ids.add(dependency.sourceId);
1167
+ changed = true;
1168
+ }
1169
+ }
1170
+ return this.nodes.filter((node) => ids.has(node.id));
1171
+ }
1172
+ finish(runId, status, startedAt, diagnostics) {
1173
+ const finishedAt = this.host.now();
1174
+ const outputs = immutableMap(
1175
+ this.nodes.map((node) => [node.id, this.getOutputs(node.id)])
1176
+ );
1177
+ const result = Object.freeze({
1178
+ runId,
1179
+ status,
1180
+ outputs,
1181
+ diagnostics: Object.freeze([...diagnostics]),
1182
+ timings: Object.freeze({
1183
+ startedAt,
1184
+ finishedAt,
1185
+ durationMs: finishedAt - startedAt
1186
+ })
1187
+ });
1188
+ this.emit({ type: "run:complete", result });
1189
+ return result;
1190
+ }
1191
+ emit(event) {
1192
+ for (const listener of [...this.listeners])
1193
+ try {
1194
+ listener(event);
1195
+ } catch (error) {
1196
+ this.host.report({
1197
+ phase: "event",
1198
+ code: "runtime/listener-failed",
1199
+ message: error instanceof Error ? error.message : String(error)
1200
+ });
1201
+ }
1202
+ }
1203
+ requireReady() {
1204
+ if (this.graphState !== "ready")
1205
+ misuse("runtime/invalid-state", `Graph is ${this.graphState}`);
1206
+ }
1207
+ isDisposed() {
1208
+ return this.graphState === "disposed";
1209
+ }
1210
+ /**
1211
+ * Every node under a feedback container, transitively.
1212
+ *
1213
+ * These are excluded from the main cook order — the container runs them, once
1214
+ * per step. Computed once per run rather than per step: the hierarchy cannot
1215
+ * change mid-run, and this is walked for every node in the order.
1216
+ */
1217
+ /**
1218
+ * Early cutoff for `run({ reuse: true })`: a node whose wired inputs are the
1219
+ * very same values and whose resolved props are equal keeps its outputs.
1220
+ *
1221
+ * Sound because a definition-v1 node may observe only its inputs, its props
1222
+ * and declared capabilities, and time reaches it through props. Upstream
1223
+ * reuse keeps output identity, so one unchanged stage skips everything below
1224
+ * it that is unchanged too. Trigger-driven, feedback and remote nodes always
1225
+ * cook. Off by default: a host opts in when it re-runs a graph on every edit.
1226
+ */
1227
+ /** Whether any data input of `node` (or, for a subnet, any inner output)
1228
+ * comes from a node in `cooked`. */
1229
+ feedsFrom(node, cooked) {
1230
+ for (const name of Object.keys(node.definition.inputs ?? {})) {
1231
+ for (const connection of this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [])
1232
+ if (cooked.has(connection.source.nodeId)) return true;
1233
+ }
1234
+ if (node.moduleId === "cascade.core.Input" && node.parent && cooked.size) return true;
1235
+ if (node.moduleId === "cascade.core.Subnet") {
1236
+ for (const child of this.nodes) if (child.parent === node.id && cooked.has(child.id)) return true;
1237
+ }
1238
+ return false;
1239
+ }
1240
+ reusable(node) {
1241
+ const last = node.lastCook;
1242
+ if (!last || this.isRemote(node) || node.definition.container === "feedback") return false;
1243
+ if (Object.values(node.definition.inputs ?? {}).some((input) => input.kind === "trigger")) return false;
1244
+ if (Object.values(node.definition.outputs ?? {}).some((output) => output.kind === "trigger")) return false;
1245
+ const next = this.cookKey(node);
1246
+ return sameValues(last.inputs, next.inputs, Object.is) && sameValues(last.props, next.props, sameJson);
1247
+ }
1248
+ cookKey(node) {
1249
+ const inputs = [];
1250
+ for (const [name, definition] of Object.entries(node.definition.inputs ?? {})) {
1251
+ if (definition.kind !== "data") continue;
1252
+ if (node.moduleId === "cascade.core.Input" && name === "value" && node.parent) {
1253
+ const parent = this.requireNode(node.parent);
1254
+ inputs.push(this.dataInputValue(parent, `input_${integerProp(node.props.inputIndex)}`));
1255
+ continue;
1256
+ }
1257
+ const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
1258
+ if (!connections.length) inputs.push(node.inputs[name]);
1259
+ for (const connection of connections)
1260
+ inputs.push(this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
1261
+ }
1262
+ if (node.moduleId === "cascade.core.Subnet") {
1263
+ for (const child of this.nodes)
1264
+ if (child.parent === node.id && child.moduleId === "cascade.core.Output") inputs.push(child.outputs.get("output"));
1265
+ }
1266
+ return { inputs, props: Object.keys(node.definition.props ?? {}).map((name) => node.props[name]) };
1267
+ }
1268
+ feedbackDescendants() {
1269
+ const containers = new Set(
1270
+ this.nodes.filter((node) => node.definition.container === "feedback").map((node) => node.id)
1271
+ );
1272
+ if (!containers.size) return /* @__PURE__ */ new Set();
1273
+ const inside = /* @__PURE__ */ new Set();
1274
+ for (const node of this.nodes) {
1275
+ let parent = node.parent;
1276
+ const seen = /* @__PURE__ */ new Set();
1277
+ while (parent && !seen.has(parent)) {
1278
+ seen.add(parent);
1279
+ if (containers.has(parent)) {
1280
+ inside.add(node.id);
1281
+ break;
1282
+ }
1283
+ parent = this.byId.get(parent)?.parent;
1284
+ }
1285
+ }
1286
+ return inside;
1287
+ }
1288
+ /**
1289
+ * Run a feedback container's contents `steps` times and return the last
1290
+ * result.
1291
+ *
1292
+ * The order the children run in is the graph's own topological order
1293
+ * restricted to this container's descendants, which is what makes the forces
1294
+ * inside behave like nodes rather than like a list: wiring one into another
1295
+ * changes the order they apply in.
1296
+ */
1297
+ async runFeedback(node, runId, signal, enqueue) {
1298
+ const initial = this.dataInputValue(node, "initial");
1299
+ const requested = this.dataInputValue(node, "steps");
1300
+ const steps = Math.max(0, Math.trunc(Number(requested ?? 0)));
1301
+ if (!Number.isFinite(steps))
1302
+ throw runtimeError(
1303
+ "runtime/feedback-steps",
1304
+ `${node.id}.steps is not a finite number`
1305
+ );
1306
+ const children = topological(this.nodes, this.connections).filter(
1307
+ (child) => this.enclosingFeedback(child) === node.id
1308
+ );
1309
+ const sink = children.find(
1310
+ (child) => child.parent === node.id && child.moduleId === "cascade.core.Output" && integerProp(child.props.outputIndex) === 0
1311
+ );
1312
+ if (steps > 0 && !sink)
1313
+ throw runtimeError(
1314
+ "runtime/feedback-no-output",
1315
+ `${node.id} has no cascade.core.Output inside it, so a step has no result to carry. Add one and wire the step's result into it.`
1316
+ );
1317
+ const keep = Math.max(0, integerProp(node.props.history));
1318
+ const history = [];
1319
+ let carried = initial;
1320
+ try {
1321
+ for (let step = 0; step < steps; step += 1) {
1322
+ if (signal.aborted) break;
1323
+ this.feedbackState.set(node.id, { value: carried, step });
1324
+ for (const child of children) {
1325
+ if (signal.aborted) break;
1326
+ await this.executeNode(child, runId, signal, {}, enqueue);
1327
+ }
1328
+ if (signal.aborted) break;
1329
+ carried = sink.outputs.get("output");
1330
+ if (keep > 0) {
1331
+ history.push(carried);
1332
+ if (history.length > keep) history.shift();
1333
+ }
1334
+ }
1335
+ return Object.freeze({
1336
+ result: carried,
1337
+ history: Object.freeze(history)
1338
+ });
1339
+ } finally {
1340
+ this.feedbackState.delete(node.id);
1341
+ }
1342
+ }
1343
+ /** The nearest ancestor that is a feedback container, or undefined. */
1344
+ enclosingFeedback(node) {
1345
+ let parent = node.parent;
1346
+ const seen = /* @__PURE__ */ new Set();
1347
+ while (parent && !seen.has(parent)) {
1348
+ seen.add(parent);
1349
+ const candidate = this.byId.get(parent);
1350
+ if (candidate?.definition.container === "feedback") return candidate.id;
1351
+ parent = candidate?.parent;
1352
+ }
1353
+ return void 0;
1354
+ }
1355
+ requireNode(id) {
1356
+ const node = this.byId.get(id);
1357
+ if (!node) misuse("runtime/node-not-found", `Unknown node ${id}`);
1358
+ return node;
1359
+ }
1360
+ dataInputValue(node, inputName2) {
1361
+ const connection = this.connections.find(
1362
+ (item) => item.kind === "data" && item.target.nodeId === node.id && item.target.inputName === inputName2
1363
+ );
1364
+ return connection ? this.requireNode(connection.source.nodeId).outputs.get(
1365
+ connection.source.outputName
1366
+ ) : node.inputs[inputName2];
1367
+ }
1368
+ };
1369
+ function bindShellToRun(shell, runSignal) {
1370
+ if (!shell) return void 0;
1371
+ const options = (value) => ({
1372
+ ...value,
1373
+ signal: combineAbortSignals(runSignal, value?.signal)
1374
+ });
1375
+ return Object.freeze({
1376
+ run: (command, args, value) => shell.run(command, args, options(value)),
1377
+ runJson: (command, args, value) => shell.runJson(command, args, options(value))
1378
+ });
1379
+ }
1380
+ function combineAbortSignals(first, second) {
1381
+ if (!second || first === second) return first;
1382
+ const wrappers = /* @__PURE__ */ new Map();
1383
+ return {
1384
+ get aborted() {
1385
+ return first.aborted || second.aborted;
1386
+ },
1387
+ get reason() {
1388
+ return first.aborted ? first.reason : second.reason;
1389
+ },
1390
+ addEventListener(_type, listener, options) {
1391
+ if (first.aborted || second.aborted) {
1392
+ listener();
1393
+ return;
1394
+ }
1395
+ const wrapped = () => {
1396
+ first.removeEventListener("abort", wrapped);
1397
+ second.removeEventListener("abort", wrapped);
1398
+ wrappers.delete(listener);
1399
+ listener();
1400
+ };
1401
+ wrappers.set(listener, wrapped);
1402
+ first.addEventListener("abort", wrapped, options);
1403
+ second.addEventListener("abort", wrapped, options);
1404
+ },
1405
+ removeEventListener(_type, listener) {
1406
+ const wrapped = wrappers.get(listener);
1407
+ if (!wrapped) return;
1408
+ first.removeEventListener("abort", wrapped);
1409
+ second.removeEventListener("abort", wrapped);
1410
+ wrappers.delete(listener);
1411
+ }
1412
+ };
1413
+ }
1414
+ function materialize(authored, registration) {
1415
+ const inputs = {};
1416
+ const props = {};
1417
+ for (const [name, definition] of Object.entries(
1418
+ registration.definition.inputs ?? {}
1419
+ ))
1420
+ if (definition.kind === "data" && "default" in definition)
1421
+ inputs[name] = runtimeValue(
1422
+ definition.default,
1423
+ definition.type,
1424
+ false,
1425
+ `${authored.id}.${name}`
1426
+ );
1427
+ if (Array.isArray(authored.inputs))
1428
+ for (const saved of authored.inputs) {
1429
+ const definition = registration.definition.inputs?.[saved.name];
1430
+ if (definition?.kind === "data") {
1431
+ inputs[saved.name] = runtimeValue(
1432
+ "defaultValue" in saved ? saved.defaultValue : saved.value,
1433
+ definition.type,
1434
+ !("default" in definition),
1435
+ `${authored.id}.${saved.name}`
1436
+ );
1437
+ }
1438
+ }
1439
+ else if (authored.inputs)
1440
+ for (const [name, saved] of Object.entries(authored.inputs)) {
1441
+ const definition = registration.definition.inputs?.[name];
1442
+ if (definition?.kind === "data")
1443
+ inputs[name] = runtimeValue(
1444
+ saved,
1445
+ definition.type,
1446
+ !("default" in definition),
1447
+ `${authored.id}.${name}`
1448
+ );
1449
+ }
1450
+ for (const [name, definition] of Object.entries(
1451
+ registration.definition.props ?? {}
1452
+ ))
1453
+ props[name] = runtimeValue(
1454
+ definition.default,
1455
+ definition.type,
1456
+ false,
1457
+ `${authored.id}.${name}`
1458
+ );
1459
+ if (Array.isArray(authored.props))
1460
+ misuse(
1461
+ "runtime/props-not-a-map",
1462
+ `Node ${authored.id} stores props as a list. "props" is a map of name to value; "params" was the list. Every value in it is being dropped, silently, and the graph would run on its defaults`
1463
+ );
1464
+ const bindings = {};
1465
+ const storedProps = authored.props ?? {};
1466
+ for (const [name, definition] of Object.entries(
1467
+ registration.definition.props ?? {}
1468
+ )) {
1469
+ if (!definition.expression || name in storedProps) continue;
1470
+ bindings[name] = {
1471
+ value: props[name],
1472
+ expression: definition.expression
1473
+ };
1474
+ }
1475
+ for (const [name, saved] of Object.entries(storedProps)) {
1476
+ const definition = registration.definition.props?.[name];
1477
+ if (!definition) continue;
1478
+ const object = saved && typeof saved === "object" && "value" in saved ? saved : null;
1479
+ const value = runtimeValue(
1480
+ object ? object.value : saved,
1481
+ definition.type,
1482
+ false,
1483
+ `${authored.id}.${name}`
1484
+ );
1485
+ props[name] = value;
1486
+ if (!object) {
1487
+ delete bindings[name];
1488
+ continue;
1489
+ }
1490
+ const channel = deserializeChannel(object.channel);
1491
+ const expression = typeof object.expression === "string" && object.expression ? object.expression : void 0;
1492
+ const binding = {
1493
+ value,
1494
+ ...expression ? { expression } : {},
1495
+ ...isEmptyChannel(channel) ? {} : { channel }
1496
+ };
1497
+ if (isBoundProp(binding)) bindings[name] = binding;
1498
+ }
1499
+ return {
1500
+ id: authored.id,
1501
+ moduleId: registration.moduleId,
1502
+ ...authored.parent ? { parent: authored.parent } : {},
1503
+ registration,
1504
+ definition: registration.definition,
1505
+ inputs,
1506
+ props,
1507
+ bindings,
1508
+ strayParams: strayParams(authored, registration),
1509
+ unknownProps: unknownProps(authored, registration),
1510
+ outputs: /* @__PURE__ */ new Map()
1511
+ };
1512
+ }
1513
+ function unknownProps(authored, registration) {
1514
+ const props = authored.props;
1515
+ if (!props || Array.isArray(props)) return [];
1516
+ const declared = registration.definition.props ?? {};
1517
+ return Object.keys(props).filter((name) => !(name in declared));
1518
+ }
1519
+ function strayParams(authored, registration) {
1520
+ const definition = registration.definition;
1521
+ const declared = /* @__PURE__ */ new Set([
1522
+ ...Object.keys(definition.props ?? {}),
1523
+ ...Object.keys(definition.inputs ?? {})
1524
+ ]);
1525
+ const names = Array.isArray(authored.params) ? authored.params.map((saved) => saved?.name) : Object.keys(authored.params ?? {});
1526
+ return names.filter(
1527
+ (name) => typeof name === "string" && declared.has(name)
1528
+ );
1529
+ }
1530
+ function specializeCoreRegistration(registration, authored, nodes) {
1531
+ if (registration.moduleId === "cascade.core.Input" || registration.moduleId === "cascade.core.Output") {
1532
+ const type = savedDataType(authored.props?.dataType);
1533
+ const inputName2 = registration.moduleId === "cascade.core.Input" ? "value" : "input";
1534
+ return Object.freeze({
1535
+ ...registration,
1536
+ definition: Object.freeze({
1537
+ ...registration.definition,
1538
+ inputs: Object.freeze({
1539
+ [inputName2]: dataInputDefinition(type)
1540
+ }),
1541
+ outputs: Object.freeze({
1542
+ output: { kind: "data", type }
1543
+ })
1544
+ })
1545
+ });
1546
+ }
1547
+ if (registration.moduleId !== "cascade.core.Subnet") return registration;
1548
+ const children = nodes.filter((candidate) => candidate.parent === authored.id);
1549
+ const inputs = indexedBoundaryPorts(children, "cascade.core.Input", "inputIndex", "input");
1550
+ const outputs = indexedBoundaryPorts(children, "cascade.core.Output", "outputIndex", "output");
1551
+ return Object.freeze({
1552
+ ...registration,
1553
+ definition: Object.freeze({
1554
+ ...registration.definition,
1555
+ ...Object.keys(inputs).length ? { inputs: Object.freeze(inputs) } : {},
1556
+ ...Object.keys(outputs).length ? { outputs: Object.freeze(outputs) } : {}
1557
+ })
1558
+ });
1559
+ }
1560
+ function indexedBoundaryPorts(nodes, moduleId2, propName, prefix) {
1561
+ const boundaries = nodes.filter((node) => (node.module ?? node.type) === moduleId2).map((node) => ({
1562
+ index: savedIntegerProp(node.props?.[propName]),
1563
+ type: savedDataType(node.props?.dataType)
1564
+ })).filter((boundary) => boundary.index >= 0);
1565
+ const max = boundaries.length ? Math.max(...boundaries.map(({ index }) => index)) : -1;
1566
+ const types = new Map(boundaries.map(({ index, type }) => [index, type]));
1567
+ return Object.fromEntries(
1568
+ Array.from({ length: max + 1 }, (_, index) => [
1569
+ `${prefix}_${index}`,
1570
+ prefix === "input" ? dataInputDefinition(types.get(index) ?? "any") : { kind: "data", type: types.get(index) ?? "any" }
1571
+ ])
1572
+ );
1573
+ }
1574
+ function dataInputDefinition(type) {
1575
+ return type === "any" ? { kind: "data", type, default: null } : { kind: "data", type };
1576
+ }
1577
+ function savedDataType(value) {
1578
+ const type = savedStringProp(value);
1579
+ return type || "any";
1580
+ }
1581
+ function savedStringProp(value) {
1582
+ const unwrapped = value && typeof value === "object" && "value" in value ? value.value : value;
1583
+ return typeof unwrapped === "string" ? unwrapped : "";
1584
+ }
1585
+ function savedIntegerProp(value) {
1586
+ const unwrapped = value && typeof value === "object" && "value" in value ? value.value : value;
1587
+ return typeof unwrapped === "number" && Number.isInteger(unwrapped) ? unwrapped : 0;
1588
+ }
1589
+ function integerProp(value) {
1590
+ return typeof value === "number" && Number.isInteger(value) ? value : 0;
1591
+ }
1592
+ function graphBoundaries(nodes, moduleId2, nameProp) {
1593
+ const boundaries = /* @__PURE__ */ new Map();
1594
+ for (const node of nodes) {
1595
+ if (node.parent || node.moduleId !== moduleId2) continue;
1596
+ const name = savedStringProp(node.props[nameProp]).trim() || node.id;
1597
+ if (boundaries.has(name))
1598
+ misuse("runtime/duplicate-graph-boundary", `Duplicate graph boundary name ${name}`);
1599
+ boundaries.set(name, node);
1600
+ }
1601
+ return boundaries;
1602
+ }
1603
+ function boundaryInspection(boundaries, portName) {
1604
+ return Object.freeze(Object.fromEntries(
1605
+ [...boundaries].map(([name, node]) => [
1606
+ name,
1607
+ Object.freeze({
1608
+ nodeId: node.id,
1609
+ type: node.definition.outputs?.[portName]?.kind === "data" ? node.definition.outputs[portName].type : "any"
1610
+ })
1611
+ ])
1612
+ ));
1613
+ }
1614
+ function isStructuralProp(node, name) {
1615
+ if (node.moduleId === "cascade.core.Input")
1616
+ return name === "inputIndex" || name === "inputName" || name === "dataType";
1617
+ if (node.moduleId === "cascade.core.Output")
1618
+ return name === "outputIndex" || name === "outputName" || name === "dataType";
1619
+ return false;
1620
+ }
1621
+ function materializeConnection(connection, index, nodes) {
1622
+ const endpoints = Array.isArray(connection) ? {
1623
+ source: {
1624
+ nodeId: connection[0][0],
1625
+ outputName: connection[0][2] ?? outputName(nodes.get(connection[0][0]), connection[0][1])
1626
+ },
1627
+ target: {
1628
+ nodeId: connection[1][0],
1629
+ inputName: connection[1][2] ?? inputName(nodes.get(connection[1][0]), connection[1][1])
1630
+ }
1631
+ } : {
1632
+ source: {
1633
+ nodeId: connection.source.nodeId,
1634
+ outputName: connection.source.outputName
1635
+ },
1636
+ target: {
1637
+ nodeId: connection.target.nodeId,
1638
+ inputName: connection.target.inputName
1639
+ }
1640
+ };
1641
+ const source = nodes.get(endpoints.source.nodeId);
1642
+ const target = nodes.get(endpoints.target.nodeId);
1643
+ if (!source || !target)
1644
+ misuse(
1645
+ "runtime/invalid-connection",
1646
+ `Connection ${index} references an unknown node`
1647
+ );
1648
+ const output = source.definition.outputs?.[endpoints.source.outputName];
1649
+ let input = target.definition.inputs?.[endpoints.target.inputName];
1650
+ let variadicIndex;
1651
+ if (!input && /^input_\d+$/.test(endpoints.target.inputName)) {
1652
+ const variadic = variadicInputName(target);
1653
+ if (variadic !== void 0) {
1654
+ variadicIndex = Number(endpoints.target.inputName.slice("input_".length));
1655
+ endpoints.target.inputName = variadic;
1656
+ input = target.definition.inputs?.[variadic];
1657
+ }
1658
+ }
1659
+ if (!output || !input)
1660
+ misuse(
1661
+ "runtime/invalid-connection",
1662
+ `Connection ${index} references an unknown port`
1663
+ );
1664
+ if (output.kind !== input.kind)
1665
+ misuse(
1666
+ "runtime/connection-kind",
1667
+ `Connection ${index} mixes ${output.kind} and ${input.kind}`
1668
+ );
1669
+ if (output.kind === "data" && input.kind === "data" && !canConnectTypes(output.type, input.type))
1670
+ misuse(
1671
+ "runtime/connection-type",
1672
+ `Connection ${index} has incompatible types ${output.type} and ${input.type}`
1673
+ );
1674
+ if (output.kind === "data" && input.kind === "data" && output.type === "bundle" && input.type === "bundle" && !bundleSchemasCompatible(output.schema, input.schema))
1675
+ misuse(
1676
+ "runtime/connection-schema",
1677
+ `Connection ${index} wires a ${output.schema} bundle into an input that takes ${input.schema}`
1678
+ );
1679
+ return Object.freeze({
1680
+ source: Object.freeze(endpoints.source),
1681
+ target: Object.freeze(endpoints.target),
1682
+ kind: output.kind,
1683
+ order: index,
1684
+ ...variadicIndex === void 0 ? {} : { variadicIndex }
1685
+ });
1686
+ }
1687
+ function validateCardinality(nodes, connections) {
1688
+ const counts = /* @__PURE__ */ new Map();
1689
+ for (const connection of connections) {
1690
+ if (connection.kind !== "data") continue;
1691
+ const key = `${connection.target.nodeId}\0${connection.target.inputName}`;
1692
+ const count = (counts.get(key) ?? 0) + 1;
1693
+ counts.set(key, count);
1694
+ const input = nodes.get(connection.target.nodeId)?.definition.inputs?.[connection.target.inputName];
1695
+ if (count > 1 && input?.kind === "data" && !input.variadic)
1696
+ misuse(
1697
+ "runtime/input-cardinality",
1698
+ `${connection.target.nodeId}.${connection.target.inputName} accepts one connection`
1699
+ );
1700
+ }
1701
+ }
1702
+ function validateHierarchy(nodes, byId) {
1703
+ for (const node of nodes) {
1704
+ if (!node.parent) continue;
1705
+ const parent = byId.get(node.parent);
1706
+ if (!parent)
1707
+ misuse(
1708
+ "runtime/parent-not-found",
1709
+ `Node ${node.id} references missing parent ${node.parent}`
1710
+ );
1711
+ if (parent.definition.container !== "subnet" && parent.definition.container !== "feedback")
1712
+ misuse(
1713
+ "runtime/parent-not-container",
1714
+ `Node ${node.id} parent ${node.parent} is not a container`
1715
+ );
1716
+ }
1717
+ for (const node of nodes) {
1718
+ const visited = /* @__PURE__ */ new Set([node.id]);
1719
+ let parentId = node.parent;
1720
+ while (parentId) {
1721
+ if (visited.has(parentId))
1722
+ misuse(
1723
+ "runtime/parent-cycle",
1724
+ `Node hierarchy contains a cycle through ${parentId}`
1725
+ );
1726
+ visited.add(parentId);
1727
+ parentId = byId.get(parentId)?.parent;
1728
+ }
1729
+ }
1730
+ }
1731
+ function snapshotRegistration(registration) {
1732
+ if (registration.kind === "legacy-dynamic")
1733
+ return Object.freeze({ ...registration });
1734
+ const definition = snapshot(registration.definition);
1735
+ return Object.freeze({ ...registration, definition });
1736
+ }
1737
+ function isGated(node) {
1738
+ return Object.values(node.definition.inputs ?? {}).some(
1739
+ (input) => input.kind === "trigger"
1740
+ );
1741
+ }
1742
+ function outputName(node, index) {
1743
+ const name = Object.keys(node?.definition.outputs ?? {})[index];
1744
+ if (!name)
1745
+ misuse("runtime/invalid-connection", `Unknown output index ${index}`);
1746
+ return name;
1747
+ }
1748
+ function variadicInputName(node) {
1749
+ const names = Object.entries(node.definition.inputs ?? {}).filter(([, input]) => input.kind === "data" && input.variadic === true).map(([name]) => name);
1750
+ return names.length === 1 ? names[0] : void 0;
1751
+ }
1752
+ function inputName(node, index) {
1753
+ const name = Object.keys(node?.definition.inputs ?? {})[index];
1754
+ if (!name)
1755
+ misuse("runtime/invalid-connection", `Unknown input index ${index}`);
1756
+ return name;
1757
+ }
1758
+ function validateTopology(nodes, connections) {
1759
+ topological(nodes, connections);
1760
+ }
1761
+ function topological(nodes, connections) {
1762
+ const byId = new Map(nodes.map((node) => [node.id, node]));
1763
+ const indegree = new Map(nodes.map((node) => [node.id, 0]));
1764
+ const outgoing = new Map(nodes.map((node) => [node.id, []]));
1765
+ for (const dependency of graphDependencies(nodes, connections)) {
1766
+ outgoing.get(dependency.sourceId).push(dependency.targetId);
1767
+ indegree.set(
1768
+ dependency.targetId,
1769
+ indegree.get(dependency.targetId) + 1
1770
+ );
1771
+ }
1772
+ const queue = nodes.filter((node) => indegree.get(node.id) === 0);
1773
+ const result = [];
1774
+ let queueIndex = 0;
1775
+ while (queueIndex < queue.length) {
1776
+ const node = queue[queueIndex++];
1777
+ result.push(node);
1778
+ for (const id of outgoing.get(node.id)) {
1779
+ indegree.set(id, indegree.get(id) - 1);
1780
+ if (indegree.get(id) === 0)
1781
+ queue.push(byId.get(id));
1782
+ }
1783
+ }
1784
+ if (result.length !== nodes.length)
1785
+ misuse("runtime/cycle", "Graph contains a data or trigger cycle");
1786
+ return result;
1787
+ }
1788
+ function graphDependencies(nodes, connections) {
1789
+ const dependencies = /* @__PURE__ */ new Map();
1790
+ for (const connection of connections) {
1791
+ const dependency = {
1792
+ sourceId: connection.source.nodeId,
1793
+ targetId: connection.target.nodeId
1794
+ };
1795
+ dependencies.set(`${dependency.sourceId}\0${dependency.targetId}`, dependency);
1796
+ }
1797
+ for (const dependency of dataDependencies(nodes, connections))
1798
+ dependencies.set(`${dependency.sourceId}\0${dependency.targetId}`, dependency);
1799
+ return [...dependencies.values()];
1800
+ }
1801
+ function dataDependencies(nodes, connections) {
1802
+ const dependencies = /* @__PURE__ */ new Map();
1803
+ for (const connection of connections) {
1804
+ if (connection.kind !== "data") continue;
1805
+ const dependency = {
1806
+ sourceId: connection.source.nodeId,
1807
+ targetId: connection.target.nodeId
1808
+ };
1809
+ dependencies.set(`${dependency.sourceId}\0${dependency.targetId}`, dependency);
1810
+ }
1811
+ for (const node of nodes) {
1812
+ if (!node.parent) continue;
1813
+ if (node.moduleId === "cascade.core.Input") {
1814
+ const inputName2 = `input_${integerProp(node.props.inputIndex)}`;
1815
+ const source = connections.find(
1816
+ (connection) => connection.kind === "data" && connection.target.nodeId === node.parent && connection.target.inputName === inputName2
1817
+ );
1818
+ if (source) {
1819
+ const dependency = {
1820
+ sourceId: source.source.nodeId,
1821
+ targetId: node.id
1822
+ };
1823
+ dependencies.set(`${dependency.sourceId}\0${dependency.targetId}`, dependency);
1824
+ }
1825
+ } else if (node.moduleId === "cascade.core.Output") {
1826
+ const dependency = { sourceId: node.id, targetId: node.parent };
1827
+ dependencies.set(`${dependency.sourceId}\0${dependency.targetId}`, dependency);
1828
+ }
1829
+ }
1830
+ const parents = new Map(nodes.map((node) => [node.id, node.parent]));
1831
+ const feedbackContainers = new Set(
1832
+ nodes.filter((node) => node.definition.container === "feedback").map((node) => node.id)
1833
+ );
1834
+ if (feedbackContainers.size) {
1835
+ const feedbackLineage = (id) => {
1836
+ const lineage = [];
1837
+ const seen = /* @__PURE__ */ new Set();
1838
+ let current = id;
1839
+ while (current && !seen.has(current)) {
1840
+ seen.add(current);
1841
+ if (feedbackContainers.has(current)) lineage.push(current);
1842
+ current = parents.get(current);
1843
+ }
1844
+ return lineage;
1845
+ };
1846
+ const add = (sourceId, targetId) => {
1847
+ if (sourceId === targetId) return;
1848
+ const dependency = { sourceId, targetId };
1849
+ dependencies.set(`${sourceId}\0${targetId}`, dependency);
1850
+ };
1851
+ for (const connection of connections) {
1852
+ if (connection.kind !== "data") continue;
1853
+ const sourceLineage = feedbackLineage(connection.source.nodeId);
1854
+ const targetLineage = feedbackLineage(connection.target.nodeId);
1855
+ const sourceSet = new Set(sourceLineage);
1856
+ const targetSet = new Set(targetLineage);
1857
+ const crossedFrom = sourceLineage.filter((id) => !targetSet.has(id));
1858
+ const crossedInto = targetLineage.filter((id) => !sourceSet.has(id));
1859
+ if (crossedFrom.length > 0 && crossedInto.length > 0) {
1860
+ for (const sourceId of crossedFrom)
1861
+ for (const targetId of crossedInto) add(sourceId, targetId);
1862
+ } else if (crossedFrom.length > 0) {
1863
+ for (const sourceId of crossedFrom)
1864
+ add(sourceId, connection.target.nodeId);
1865
+ } else {
1866
+ for (const targetId of crossedInto)
1867
+ add(connection.source.nodeId, targetId);
1868
+ }
1869
+ }
1870
+ }
1871
+ return [...dependencies.values()];
1872
+ }
1873
+ function runtimeError(code, message) {
1874
+ return new CascadeRuntimeError(code, [{ phase: "runtime", code, message }]);
1875
+ }
1876
+ function misuse(code, message) {
1877
+ throw runtimeError(code, message);
1878
+ }
1879
+ function snapshot(value) {
1880
+ return cloneValue(value, /* @__PURE__ */ new WeakSet());
1881
+ }
1882
+ function immutableMap(entries) {
1883
+ const source = new Map(entries);
1884
+ let view;
1885
+ view = Object.freeze({
1886
+ get size() {
1887
+ return source.size;
1888
+ },
1889
+ has(key) {
1890
+ return source.has(key);
1891
+ },
1892
+ get(key) {
1893
+ return source.get(key);
1894
+ },
1895
+ entries() {
1896
+ return source.entries();
1897
+ },
1898
+ keys() {
1899
+ return source.keys();
1900
+ },
1901
+ values() {
1902
+ return source.values();
1903
+ },
1904
+ [Symbol.iterator]() {
1905
+ return source[Symbol.iterator]();
1906
+ },
1907
+ forEach(callback, thisArg) {
1908
+ source.forEach((value, key) => callback.call(thisArg, value, key, view));
1909
+ }
1910
+ });
1911
+ return view;
1912
+ }
1913
+ function cloneValue(value, ancestors) {
1914
+ if (typeof value === "number" && !Number.isFinite(value))
1915
+ misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
1916
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint")
1917
+ misuse(
1918
+ "runtime/non-serializable-value",
1919
+ `Runtime values cannot contain ${typeof value}`
1920
+ );
1921
+ if (value === void 0 || value === null || typeof value !== "object")
1922
+ return value;
1923
+ const prototype = Object.getPrototypeOf(value);
1924
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
1925
+ return value;
1926
+ if (ancestors.has(value))
1927
+ misuse(
1928
+ "runtime/non-serializable-value",
1929
+ "Runtime values cannot contain cycles"
1930
+ );
1931
+ ancestors.add(value);
1932
+ const clone = Array.isArray(value) ? value.map((item) => cloneValue(item, ancestors)) : Object.fromEntries(
1933
+ Object.entries(value).map(([key, item]) => [
1934
+ key,
1935
+ cloneValue(item, ancestors)
1936
+ ])
1937
+ );
1938
+ ancestors.delete(value);
1939
+ return Object.freeze(clone);
1940
+ }
1941
+ var TUPLE_LENGTHS = {
1942
+ vec2: 2,
1943
+ vec3: 3,
1944
+ vec4: 4,
1945
+ vec2i: 2,
1946
+ vec3i: 3,
1947
+ vec4i: 4,
1948
+ mat2: 4,
1949
+ mat3: 9,
1950
+ mat4: 16,
1951
+ color: 4
1952
+ };
1953
+ function outputValue(value, definition, path6) {
1954
+ if (definition.type !== "bundle" || !definition.fields)
1955
+ return runtimeValue(value, definition.type, false, path6);
1956
+ const fields = definition.fields;
1957
+ const problem = bundleValueProblem(value, fields);
1958
+ if (problem) misuse("runtime/value-type", `${path6} ${problem}`);
1959
+ if (isBundleValue(value))
1960
+ for (const [field, item] of Object.entries(value))
1961
+ runtimeValue(item, bundleFieldType(fields[field]), true, `${path6}.${field}`);
1962
+ return runtimeValue(value, "bundle", false, path6);
1963
+ }
1964
+ function expressionPaths(expression) {
1965
+ const paths = [];
1966
+ const pattern = /\b(?:ch|chs|chv)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
1967
+ let match;
1968
+ while ((match = pattern.exec(expression)) !== null) paths.push(match[1]);
1969
+ return paths;
1970
+ }
1971
+ function readsBundleFields(node) {
1972
+ for (const binding of Object.values(node.bindings))
1973
+ if (binding.expression)
1974
+ for (const reference of bundleFieldReferences(binding.expression)) {
1975
+ const input = node.definition.inputs?.[reference.port];
1976
+ if (input?.kind === "data" && input.type === "bundle") return true;
1977
+ }
1978
+ return false;
1979
+ }
1980
+ function runtimeValue(value, type, allowUndefined, path6) {
1981
+ if (value === void 0) {
1982
+ if (allowUndefined) return void 0;
1983
+ misuse("runtime/value-type", `${path6} requires a ${type} value`);
1984
+ }
1985
+ const tupleLength = TUPLE_LENGTHS[type];
1986
+ const valid = type === "float" && typeof value === "number" && Number.isFinite(value) || type === "int" && typeof value === "number" && Number.isInteger(value) || type === "bool" && typeof value === "boolean" || type === "string" && typeof value === "string" || tupleLength !== void 0 && Array.isArray(value) && value.length === tupleLength && value.every(
1987
+ (item) => type.endsWith("i") ? typeof item === "number" && Number.isInteger(item) : typeof item === "number" && Number.isFinite(item)
1988
+ ) || type === "bundle" && isBundleValue(value) || !tupleLength && type !== "bundle" && type !== "float" && type !== "int" && type !== "bool" && type !== "string";
1989
+ if (!valid) misuse("runtime/value-type", `${path6} is not a valid ${type}`);
1990
+ return snapshot(value);
1991
+ }
1992
+ function sameValues(a, b, equal) {
1993
+ return a.length === b.length && a.every((value, index) => equal(value, b[index]));
1994
+ }
1995
+ function sameJson(a, b) {
1996
+ if (Object.is(a, b)) return true;
1997
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
1998
+ try {
1999
+ return JSON.stringify(a) === JSON.stringify(b);
2000
+ } catch {
2001
+ return false;
2002
+ }
2003
+ }
2004
+
2005
+ // packages/runtime/src/node.ts
2006
+ function createNodeRuntimeHost(options) {
2007
+ const {
2008
+ modules,
2009
+ legacyShell,
2010
+ now = () => Date.now(),
2011
+ report = () => {
2012
+ },
2013
+ ...capabilities
2014
+ } = options;
2015
+ return {
2016
+ environment: "server",
2017
+ modules,
2018
+ legacyShell,
2019
+ capabilities,
2020
+ now,
2021
+ report
2022
+ };
2023
+ }
2024
+
2025
+ // packages/runtime/src/definition/architecture.ts
2026
+ var AMBIENT_STATE = /* @__PURE__ */ new Set([
2027
+ "globalThis",
2028
+ "window",
2029
+ "document",
2030
+ "localStorage",
2031
+ "sessionStorage",
2032
+ "indexedDB",
2033
+ "process",
2034
+ "Bun",
2035
+ "Deno"
2036
+ ]);
2037
+ function validateNodeModuleArchitecture(source, fileName, ts2) {
2038
+ const file = ts2.createSourceFile(
2039
+ fileName,
2040
+ source,
2041
+ ts2.ScriptTarget.Latest,
2042
+ true,
2043
+ ts2.ScriptKind.TS
2044
+ );
2045
+ const diagnostics = [];
2046
+ const exportsDefinition = file.statements.some(
2047
+ (statement) => ts2.isVariableStatement(statement) && isDefinition(statement)
2048
+ );
2049
+ for (const statement of file.statements) {
2050
+ if (ts2.isVariableStatement(statement) && !isDefinition(statement)) {
2051
+ const superseded = supersededByDefinition(statement);
2052
+ add(
2053
+ statement,
2054
+ "module-state",
2055
+ superseded ? `\`${superseded}\` belongs inside the definition, not beside it \u2014 a definition-v1 node declares it as \`${superseded}: ...\` in \`export const definition\`, and a module-level copy is the module state this rule forbids. A dynamic node without a definition still declares it here.` : "Node modules may not declare module-level values; use a pure function or pass state through ports, props, and declared capabilities"
2056
+ );
2057
+ } else if (ts2.isClassDeclaration(statement) || ts2.isEnumDeclaration(statement) || ts2.isModuleDeclaration(statement)) {
2058
+ add(
2059
+ statement,
2060
+ "module-state",
2061
+ "Node modules may not declare classes, enums, or namespaces because they can retain module-global state"
2062
+ );
2063
+ } else if (ts2.isExpressionStatement(statement)) {
2064
+ add(
2065
+ statement,
2066
+ "top-level-effect",
2067
+ "Node modules may not run top-level effects"
2068
+ );
2069
+ } else if (ts2.isImportDeclaration(statement) && !statement.importClause) {
2070
+ add(
2071
+ statement,
2072
+ "side-effect-import",
2073
+ "Side-effect imports are not allowed in node modules"
2074
+ );
2075
+ } else if (ts2.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts2.isStringLiteral(statement.moduleSpecifier) && importsSiblingNode(fileName, statement.moduleSpecifier.text)) {
2076
+ add(
2077
+ statement.moduleSpecifier,
2078
+ "node-import",
2079
+ "A node may not import another node implementation; connect the nodes with a graph wire"
2080
+ );
2081
+ }
2082
+ }
2083
+ visit(file);
2084
+ return diagnostics;
2085
+ function visit(node) {
2086
+ if (ts2.isTypeNode(node) || ts2.isImportDeclaration(node)) return;
2087
+ if (ts2.isIdentifier(node) && AMBIENT_STATE.has(node.text) && isReference(node)) {
2088
+ add(
2089
+ node,
2090
+ "ambient-state",
2091
+ `${node.text} is ambient state; receive data through ports, props, or a declared capability`
2092
+ );
2093
+ }
2094
+ if (ts2.isCallExpression(node) && node.expression.kind === ts2.SyntaxKind.ImportKeyword) {
2095
+ add(
2096
+ node,
2097
+ "dynamic-import",
2098
+ "Dynamic imports are not allowed in deterministic node execution"
2099
+ );
2100
+ }
2101
+ ts2.forEachChild(node, visit);
2102
+ }
2103
+ function isReference(node) {
2104
+ const parent = node.parent;
2105
+ return !(ts2.isPropertyAccessExpression(parent) && parent.name === node || ts2.isPropertyAssignment(parent) && parent.name === node || ts2.isMethodDeclaration(parent) && parent.name === node || ts2.isPropertyDeclaration(parent) && parent.name === node);
2106
+ }
2107
+ function supersededByDefinition(statement) {
2108
+ if (!exportsDefinition) return null;
2109
+ const declaration = statement.declarationList.declarations[0];
2110
+ if (statement.declarationList.declarations.length !== 1) return null;
2111
+ if (!declaration || !ts2.isIdentifier(declaration.name)) return null;
2112
+ return declaration.name.text === "icon" || declaration.name.text === "runsOn" ? declaration.name.text : null;
2113
+ }
2114
+ function isDefinition(statement) {
2115
+ return Boolean(statement.declarationList.flags & ts2.NodeFlags.Const) && Boolean(statement.modifiers?.some((item) => item.kind === ts2.SyntaxKind.ExportKeyword)) && statement.declarationList.declarations.length === 1 && ts2.isIdentifier(statement.declarationList.declarations[0].name) && statement.declarationList.declarations[0].name.text === "definition";
2116
+ }
2117
+ function add(node, code, message) {
2118
+ const point = file.getLineAndCharacterOfPosition(node.getStart(file));
2119
+ diagnostics.push({
2120
+ phase: "architecture",
2121
+ code: `architecture/${code}`,
2122
+ message,
2123
+ file: fileName,
2124
+ line: point.line + 1,
2125
+ column: point.character + 1
2126
+ });
2127
+ }
2128
+ }
2129
+ function importsSiblingNode(fileName, specifier) {
2130
+ if (!specifier.startsWith(".")) return false;
2131
+ const file = normalize(fileName).split("/");
2132
+ const nodesIndex = file.lastIndexOf("nodes");
2133
+ if (nodesIndex < 0 || !file[nodesIndex + 1]) return false;
2134
+ const sourceNode = file[nodesIndex + 1];
2135
+ const resolved = normalize([...file.slice(0, -1), ...specifier.split("/")].join("/")).split("/");
2136
+ const resolvedNodesIndex = resolved.lastIndexOf("nodes");
2137
+ return resolvedNodesIndex >= 0 && Boolean(resolved[resolvedNodesIndex + 1]) && resolved[resolvedNodesIndex + 1] !== sourceNode;
2138
+ }
2139
+ function normalize(value) {
2140
+ const output = [];
2141
+ for (const part of value.split("\\").join("/").split("/")) {
2142
+ if (!part || part === ".") continue;
2143
+ if (part === "..") output.pop();
2144
+ else output.push(part);
2145
+ }
2146
+ return output.join("/");
2147
+ }
2148
+
2149
+ // src/cli/nodeFiles.ts
2150
+ import fs from "node:fs/promises";
2151
+ import path from "node:path";
2152
+ function createNodeFileCapability(project) {
2153
+ const within2 = (relative, what) => {
2154
+ try {
2155
+ return project.resolve(relative.replace(/^\.?\//, ""));
2156
+ } catch (error) {
2157
+ throw new Error(
2158
+ `files: ${what} outside the project is refused \u2014 ${relative} (${error instanceof Error ? error.message : String(error)})`
2159
+ );
2160
+ }
2161
+ };
2162
+ const cacheOnly = (relative) => {
2163
+ const clean = relative.replace(/^\.?\//, "");
2164
+ if (!clean.startsWith(".cascade-cache/"))
2165
+ throw new Error(
2166
+ `files: writes are confined to .cascade-cache/ \u2014 got ${clean}. A node's output is derived data, and the same rule applies in the browser.`
2167
+ );
2168
+ return within2(clean, "writing");
2169
+ };
2170
+ const live = (options) => {
2171
+ if (options.signal.aborted) throw new Error("files: cancelled");
2172
+ };
2173
+ return {
2174
+ async read(relative, options) {
2175
+ live(options);
2176
+ return new Uint8Array(await fs.readFile(within2(relative, "reading")));
2177
+ },
2178
+ async write(relative, data, options) {
2179
+ live(options);
2180
+ const full = cacheOnly(relative);
2181
+ await fs.mkdir(path.dirname(full), { recursive: true });
2182
+ await fs.writeFile(full, data);
2183
+ },
2184
+ async list(relative, options) {
2185
+ live(options);
2186
+ const full = within2(relative, "listing");
2187
+ try {
2188
+ const entries = await fs.readdir(full, { withFileTypes: true });
2189
+ return entries.map((entry) => entry.isDirectory() ? `${entry.name}/` : entry.name).sort();
2190
+ } catch (error) {
2191
+ if (error.code === "ENOENT") return [];
2192
+ throw error;
2193
+ }
2194
+ },
2195
+ async stat(relative, options) {
2196
+ live(options);
2197
+ try {
2198
+ const stats = await fs.stat(within2(relative, "reading"));
2199
+ return { size: stats.size, modifiedAt: stats.mtimeMs };
2200
+ } catch (error) {
2201
+ if (error.code === "ENOENT") return null;
2202
+ throw error;
2203
+ }
2204
+ }
2205
+ };
2206
+ }
2207
+
2208
+ // src/cli/nodeAssets.ts
2209
+ import path3 from "node:path";
2210
+ import { pathToFileURL } from "node:url";
2211
+
2212
+ // src/cli/headlessIo.ts
2213
+ import crypto from "node:crypto";
2214
+ import fs2 from "node:fs/promises";
2215
+ import path2 from "node:path";
2216
+
2217
+ // server/src/runtime/io.ts
2218
+ var IO_BRIDGE_KEY = "__cascadeIoBridge";
2219
+ function installIoBridge(bridge) {
2220
+ const globals = globalThis;
2221
+ const previous = globals[IO_BRIDGE_KEY];
2222
+ globals[IO_BRIDGE_KEY] = bridge ?? void 0;
2223
+ return () => {
2224
+ globals[IO_BRIDGE_KEY] = previous;
2225
+ };
2226
+ }
2227
+
2228
+ // src/cli/headlessIo.ts
2229
+ var CACHE_DIR = ".cascade-cache";
2230
+ function contentAddressed(relative, body) {
2231
+ const hash = crypto.createHash("sha1").update(body).digest("hex").slice(0, 12);
2232
+ const directory = path2.posix.dirname(relative);
2233
+ const name = path2.posix.basename(relative);
2234
+ const [stem, ...rest] = name.split(".");
2235
+ const tail = rest.length ? `.${rest.join(".")}` : "";
2236
+ return path2.posix.join(directory, `${stem}.${hash}${tail}`);
2237
+ }
2238
+ function createIoBridge(project) {
2239
+ return {
2240
+ async read(relative) {
2241
+ const full = project.resolveMedia(relative.replace(/^\.?\//, ""));
2242
+ try {
2243
+ return await fs2.readFile(full);
2244
+ } catch (error) {
2245
+ throw new Error(`cascade/io: cannot load ${relative} \u2014 ${error instanceof Error ? error.message : error}`);
2246
+ }
2247
+ },
2248
+ async write(relative, data) {
2249
+ const clean = relative.replace(/^\.?\//, "");
2250
+ if (!clean.startsWith(`${CACHE_DIR}/`)) {
2251
+ throw new Error(`cascade/io: writes are confined to ${CACHE_DIR}/ \u2014 got ${clean}`);
2252
+ }
2253
+ if (!data.byteLength) throw new Error(`cascade/io: refusing to write an empty file to ${clean}`);
2254
+ const target = contentAddressed(clean, data);
2255
+ const full = project.resolve(target);
2256
+ await fs2.mkdir(path2.dirname(full), { recursive: true });
2257
+ await fs2.writeFile(full, data);
2258
+ return target;
2259
+ }
2260
+ };
2261
+ }
2262
+ function installProjectIo(project) {
2263
+ return installIoBridge(createIoBridge(project));
2264
+ }
2265
+
2266
+ // src/cli/nodeAssets.ts
2267
+ var CACHE_DIR2 = ".cascade-cache";
2268
+ function assetFileName(suggested) {
2269
+ const base = path3.posix.basename(String(suggested ?? "").split("\\").join("/").trim());
2270
+ const safe2 = base.replace(/[^A-Za-z0-9._-]/g, "_").replace(/^\.+/, "");
2271
+ return safe2 || "asset.bin";
2272
+ }
2273
+ function createNodeAssetCapability(project) {
2274
+ const io = createIoBridge(project);
2275
+ return {
2276
+ async read(ref) {
2277
+ return io.read(ref.path);
2278
+ },
2279
+ async write(data, metadata) {
2280
+ const written = await io.write(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, data);
2281
+ return { path: written, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
2282
+ },
2283
+ async resolveUrl(ref) {
2284
+ const full = project.resolveMedia(String(ref.path).replace(/^\.?\//, ""));
2285
+ return { value: pathToFileURL(full).href, release: () => {
2286
+ } };
2287
+ }
2288
+ };
2289
+ }
2290
+
2291
+ // src/cli/imageOutputs.ts
2292
+ import fs3 from "node:fs/promises";
2293
+ import path4 from "node:path";
2294
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".ppm", ".pgm", ".tif", ".tiff", ".exr", ".bmp", ".avif", ".hdr"]);
2295
+ function looksLikeImage(value) {
2296
+ const file = imagePath(value);
2297
+ if (!file) return false;
2298
+ const mediaType = value && typeof value === "object" ? value.mediaType : void 0;
2299
+ if (typeof mediaType === "string") return mediaType.startsWith("image/");
2300
+ return IMAGE_EXTENSIONS.has(path4.extname(file).toLowerCase());
2301
+ }
2302
+ function collectImageOutputs(graph, options = {}) {
2303
+ const wanted = new Set(options.nodes ?? []);
2304
+ const extra = new Set(options.extra ?? []);
2305
+ const found = [];
2306
+ const inspection = graph.inspect();
2307
+ const isAssertion = (moduleId2) => moduleId2.startsWith("cascade.assert.");
2308
+ const assertions = new Set(inspection.nodes.filter((node) => isAssertion(node.moduleId)).map((node) => node.id));
2309
+ const consumed = new Set(inspection.connections.filter((connection) => !assertions.has(connection.target.nodeId)).map((connection) => `${connection.source.nodeId}\0${connection.source.outputName}`));
2310
+ for (const node of inspection.nodes) {
2311
+ const named = wanted.has(node.id) || extra.has(node.id);
2312
+ if (wanted.size && !named) continue;
2313
+ if (!named && assertions.has(node.id)) continue;
2314
+ const ports = Object.entries(node.outputs).filter(([, port]) => port.value !== void 0 && (port.type === "image" || port.type === "any" && looksLikeImage(port.value)));
2315
+ for (const [name, port] of ports) {
2316
+ if (!named && port.connected && consumed.has(`${node.id}\0${name}`)) continue;
2317
+ found.push({ nodeId: node.id, port: name, value: port.value, shared: ports.length > 1 });
2318
+ }
2319
+ }
2320
+ return found;
2321
+ }
2322
+ function unknownNodes(graph, ids) {
2323
+ const known = new Set(graph.inspect().nodes.map((node) => node.id));
2324
+ return ids.filter((id) => !known.has(id));
2325
+ }
2326
+ var safe = (text) => text.replace(/[^a-zA-Z0-9_-]/g, "_");
2327
+ function outputStem(output) {
2328
+ return output.shared ? `${safe(output.nodeId)}.${safe(output.port)}` : safe(output.nodeId);
2329
+ }
2330
+ function stillFileName(output, source) {
2331
+ return `${outputStem(output)}${path4.extname(source) || ".png"}`;
2332
+ }
2333
+ async function saveStillImages(graph, project, options) {
2334
+ const missing = unknownNodes(graph, [...options.nodes ?? [], ...options.extra ?? []]);
2335
+ if (missing.length) throw new Error(`No node ${missing.map((id) => `"${id}"`).join(", ")} in this graph (--node/--out-node take node ids; cascade graph read lists them).`);
2336
+ const outputs = collectImageOutputs(graph, options);
2337
+ const out = options.out ? outDirectory(options.out, (relative) => project.resolve(relative)) : void 0;
2338
+ if (out) await fs3.mkdir(out.directory, { recursive: true });
2339
+ const saved = [];
2340
+ for (const output of outputs) {
2341
+ const relative = imagePath(output.value)?.replace(/^\.?\//, "");
2342
+ if (!relative) continue;
2343
+ const source = path4.isAbsolute(relative) ? relative : project.resolveMedia(relative);
2344
+ if (!out) {
2345
+ saved.push({ node: output.nodeId, port: output.port, source });
2346
+ continue;
2347
+ }
2348
+ const name = stillFileName(output, relative);
2349
+ await fs3.copyFile(source, path4.join(out.directory, name));
2350
+ saved.push({ node: output.nodeId, port: output.port, source, file: out.spell(name) });
2351
+ }
2352
+ if (out && !saved.length) {
2353
+ throw new Error("--out: no image output to save. The graph has no unconsumed image port; name one with --node, or a mid-graph one with --out-node.");
2354
+ }
2355
+ return saved;
2356
+ }
2357
+ function outDirectory(out, resolveInProject) {
2358
+ if (path4.isAbsolute(out)) {
2359
+ const directory = path4.resolve(out);
2360
+ return { directory, spell: (name) => path4.join(directory, name) };
2361
+ }
2362
+ return { directory: resolveInProject(out), spell: (name) => path4.posix.join(out.split(path4.sep).join("/"), name) };
2363
+ }
2364
+
2365
+ // src/cli/headlessGpu.ts
2366
+ var MISSING_GPU_MESSAGE = 'Cascade cannot load the optional Dawn WebGPU renderer. Install "webgpu@0.6.0" on a supported platform and run again.';
2367
+ function createDawnGpuHost() {
2368
+ return createGpuHost({
2369
+ async acquire() {
2370
+ let dawn;
2371
+ try {
2372
+ dawn = await import("webgpu");
2373
+ } catch (cause) {
2374
+ throw Object.assign(new Error(MISSING_GPU_MESSAGE), { cause });
2375
+ }
2376
+ let gpu = dawn.create([]);
2377
+ let device;
2378
+ try {
2379
+ const adapter = await gpu.requestAdapter();
2380
+ if (!adapter) throw new Error("Dawn found no WebGPU adapter. Check GPU drivers and backend availability; headless rendering still needs a hardware or explicitly configured software adapter.");
2381
+ device = await adapter.requestDevice();
2382
+ const info = adapter.info;
2383
+ return { device, info: {
2384
+ vendor: info.vendor,
2385
+ architecture: info.architecture,
2386
+ device: info.device,
2387
+ description: info.description
2388
+ }, limits: device.limits, release: () => {
2389
+ gpu = void 0;
2390
+ } };
2391
+ } catch (error) {
2392
+ device?.destroy();
2393
+ gpu = void 0;
2394
+ throw error;
2395
+ }
2396
+ }
2397
+ });
2398
+ }
2399
+
2400
+ // src/cli/projectRuntime.ts
2401
+ function createProjectHost(file, registrations) {
2402
+ const project = new ProjectRoot(path5.dirname(path5.resolve(file)));
2403
+ return createNodeRuntimeHost({
2404
+ modules: resolver(registrations),
2405
+ shell: project.shell,
2406
+ // `assets` is the browser's alone until it is given here, and
2407
+ // `cascade.geo.SvgExport` declares it — so an export graph passed both
2408
+ // `validate` and `check` and then refused to run. See ./nodeAssets.ts
2409
+ // for where the file lands and why.
2410
+ assets: createNodeAssetCapability(project),
2411
+ // Same reason as `assets` above, found the same way: `files` was valid in
2412
+ // the contract and installed by nobody, so a node declaring it ran with
2413
+ // `undefined` — and the workaround that worked was an undeclared `node:fs`
2414
+ // import, which is the invisible version of the thing declarations exist
2415
+ // to make visible. See ./nodeFiles.ts for the containment rules.
2416
+ files: createNodeFileCapability(project),
2417
+ gpu: createDawnGpuHost()
2418
+ });
2419
+ }
2420
+ async function preflightAgainstRunHost(file, registrations, document2) {
2421
+ let host;
2422
+ let checkable = true;
2423
+ try {
2424
+ host = createProjectHost(file, registrations);
2425
+ } catch (error) {
2426
+ console.warn(`Capability check skipped: ${error instanceof Error ? error.message : error}`);
2427
+ host = createNodeRuntimeHost({ modules: resolver(registrations) });
2428
+ checkable = false;
2429
+ }
2430
+ const runtime = createRuntime({ host, nodes: registrations });
2431
+ let diagnostics = [];
2432
+ try {
2433
+ const graph = await runtime.load(document2);
2434
+ if (checkable) diagnostics = graph.preflight();
2435
+ } finally {
2436
+ await runtime.dispose();
2437
+ await disposeProjectGpu(host);
2438
+ }
2439
+ if (!diagnostics.length) return;
2440
+ const { errors, warnings } = classifyPreflight(diagnostics);
2441
+ reportPreflightWarnings(host, warnings);
2442
+ if (!errors.length) return;
2443
+ const bundleOnly = errors.every((item) => item.code.startsWith("runtime/bundle-"));
2444
+ throw new Error([
2445
+ bundleOnly ? "Bundle fields do not match what their producers declare:" : `This graph cannot run on the ${host.environment} host that "cascade run" uses:`,
2446
+ ...errors.map(line),
2447
+ ...capabilityAdvice(host, errors)
2448
+ ].join("\n"));
2449
+ }
2450
+ function reportPreflightWarnings(host, warnings) {
2451
+ const elsewhere = warnings.filter((item) => item.code === "runtime/preflight-environment");
2452
+ const lossy = warnings.filter((item) => item.code === "runtime/stray-params" || item.code === "runtime/unknown-props");
2453
+ if (elsewhere.length) {
2454
+ console.warn([
2455
+ `Not checkable here \u2014 ${elsewhere.length === 1 ? "one node" : `${elsewhere.length} nodes`} in this graph target${elsewhere.length === 1 ? "s" : ""} another host:`,
2456
+ ...elsewhere.map(line),
2457
+ ` The CLI runs the ${host.environment} host, so "cascade run" cannot run this graph. Nothing above says the graph is wrong.`
2458
+ ].join("\n"));
2459
+ }
2460
+ if (lossy.length) {
2461
+ console.warn([
2462
+ `Stored values were dropped on load \u2014 ${lossy.length === 1 ? "one node" : `${lossy.length} nodes`}:`,
2463
+ ...lossy.map(line),
2464
+ ` This graph will run, on the defaults, and look like it worked.`
2465
+ ].join("\n"));
2466
+ }
2467
+ }
2468
+ function line(item) {
2469
+ return ` ${item.code}${item.path ? ` [${item.path}]` : ""}: ${item.message}`;
2470
+ }
2471
+ function classifyPreflight(diagnostics) {
2472
+ return {
2473
+ errors: diagnostics.filter((item) => !PREFLIGHT_WARNING_CODES.has(item.code)),
2474
+ warnings: diagnostics.filter((item) => PREFLIGHT_WARNING_CODES.has(item.code))
2475
+ };
2476
+ }
2477
+ function capabilityAdvice(host, diagnostics) {
2478
+ const missing = [...new Set(diagnostics.filter((item) => item.code === "runtime/missing-capability").map((item) => /Missing capability (\w+)/.exec(item.message)?.[1] ?? "").filter(Boolean))];
2479
+ if (!missing.length) return [];
2480
+ const hostCapabilities = CAPABILITIES_BY_ENVIRONMENT[host.environment] ?? [];
2481
+ return missing.map((name) => hostCapabilities.includes(name) ? ` ${name}: declared in @cascade/contracts but not installed by the host \u2014 drop it from the definition, or install it where the host is built (createProjectHost, src/cli/projectRuntime.ts).` : ` ${name}: a ${environmentOwning(name)} capability. The CLI runs the ${host.environment} host, so no CLI run can supply it.`);
2482
+ }
2483
+ function environmentOwning(capability) {
2484
+ const owners = Object.entries(CAPABILITIES_BY_ENVIRONMENT).filter(([, names]) => names.includes(capability)).map(([environment]) => environment);
2485
+ return owners.length ? owners.join("/") : "unknown";
2486
+ }
2487
+ function reportUnresolved(prepared, strict = false) {
2488
+ if (!prepared.unresolved.length) return 0;
2489
+ const count = prepared.unresolved.length;
2490
+ const summary = [
2491
+ `${count === 1 ? "One module has no file" : `${count} modules have no file`}:`,
2492
+ ...prepared.unresolved.map(line)
2493
+ ].join("\n");
2494
+ if (strict) throw new Error(summary);
2495
+ console.warn(summary);
2496
+ return count;
2497
+ }
2498
+ function usesDeterministicHost(document2, prepared) {
2499
+ const nodes = Array.isArray(document2?.nodes) ? document2.nodes : [];
2500
+ const deterministicCount = nodes.filter((node) => prepared.deterministic.has(moduleId(node))).length;
2501
+ if (!nodes.length || deterministicCount === 0) return false;
2502
+ if (deterministicCount !== nodes.length) {
2503
+ throw new Error("Mixed deterministic and dynamic graphs are not executable until the bounded legacy adapter is implemented");
2504
+ }
2505
+ return true;
2506
+ }
2507
+ async function validateProjectGraph(file, document2, strict = false) {
2508
+ const prepared = await prepare(file, document2, false);
2509
+ const unresolved = reportUnresolved(prepared, strict);
2510
+ const nodes = document2.nodes;
2511
+ if (nodes.some((node) => !prepared.deterministic.has(moduleId(node)))) {
2512
+ validateDynamicDocument(document2);
2513
+ return unresolved;
2514
+ }
2515
+ await preflightAgainstRunHost(file, prepared.registrations, document2);
2516
+ return unresolved;
2517
+ }
2518
+ async function checkProjectGraph(file, document2, strict = false) {
2519
+ const prepared = await prepare(file, document2, false);
2520
+ const unresolved = reportUnresolved(prepared, strict);
2521
+ const nodes = document2.nodes;
2522
+ const dynamic = [...new Set(nodes.map(moduleId).filter((id) => id && !prepared.deterministic.has(id)))];
2523
+ if (dynamic.length) {
2524
+ throw new Error(`Static check requires definition-v1 modules; dynamic modules: ${dynamic.join(", ")}`);
2525
+ }
2526
+ await preflightAgainstRunHost(file, prepared.registrations, document2);
2527
+ return unresolved;
2528
+ }
2529
+ async function runDeterministicProjectGraph(file, document2, entryNode, observe, afterRun) {
2530
+ const prepared = await prepare(file, document2, true);
2531
+ reportUnresolved(prepared);
2532
+ if (!usesDeterministicHost(document2, prepared)) return false;
2533
+ const host = createProjectHost(file, prepared.registrations);
2534
+ const runtime = createRuntime({
2535
+ host,
2536
+ nodes: prepared.registrations
2537
+ });
2538
+ let graph;
2539
+ try {
2540
+ graph = await runtime.load(document2);
2541
+ observe?.(graph);
2542
+ reportPreflightWarnings(
2543
+ host,
2544
+ classifyPreflight(graph.preflight()).warnings.filter((item) => item.code === "runtime/stray-params" || item.code === "runtime/unknown-props")
2545
+ );
2546
+ const result = await graph.run(entryNode ? { target: { kind: "node", nodeId: entryNode } } : {});
2547
+ if (result.status !== "completed") {
2548
+ throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
2549
+ }
2550
+ await afterRun?.(graph);
2551
+ return true;
2552
+ } finally {
2553
+ try {
2554
+ await runtime.dispose();
2555
+ } finally {
2556
+ await disposeProjectGpu(host);
2557
+ }
2558
+ }
2559
+ }
2560
+ async function cookProjectGraph(file, document2, target, observe) {
2561
+ const prepared = await prepare(file, document2, true);
2562
+ reportUnresolved(prepared);
2563
+ if (!usesDeterministicHost(document2, prepared))
2564
+ throw new Error("This graph mixes execution styles or uses dynamic modules; only definition-v1 graphs can be cooked for their values");
2565
+ const host = createProjectHost(file, prepared.registrations);
2566
+ const runtime = createRuntime({ host, nodes: prepared.registrations });
2567
+ try {
2568
+ const graph = await runtime.load(document2);
2569
+ observe?.(graph);
2570
+ const result = await graph.run(target ? { target: { kind: "node", nodeId: target } } : {});
2571
+ if (result.status !== "completed")
2572
+ throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
2573
+ const outputs = /* @__PURE__ */ new Map();
2574
+ for (const node of graph.inspect().nodes) outputs.set(node.id, new Map(graph.getOutputs(node.id)));
2575
+ return { outputs, assets: host.capabilities?.assets };
2576
+ } finally {
2577
+ try {
2578
+ await runtime.dispose();
2579
+ } finally {
2580
+ await disposeProjectGpu(host);
2581
+ }
2582
+ }
2583
+ }
2584
+ async function renderDeterministicProjectFrames(file, document2, options) {
2585
+ const prepared = await prepare(file, document2, true);
2586
+ reportUnresolved(prepared);
2587
+ if (!usesDeterministicHost(document2, prepared)) return null;
2588
+ const project = new ProjectRoot(path5.dirname(path5.resolve(file)));
2589
+ const host = createProjectHost(file, prepared.registrations);
2590
+ const runtime = createRuntime({
2591
+ host,
2592
+ nodes: prepared.registrations
2593
+ });
2594
+ let graph;
2595
+ const width = sequenceWidth(options.end);
2596
+ const frames = [];
2597
+ const files = [];
2598
+ let aborted = false;
2599
+ let gpu;
2600
+ const started = performance.now();
2601
+ try {
2602
+ graph = await runtime.load(document2);
2603
+ options.observe?.(graph);
2604
+ reportPreflightWarnings(host, classifyPreflight(graph.preflight()).warnings);
2605
+ const targets = options.nodes?.length ? options.nodes : options.entryNode ? [options.entryNode] : [];
2606
+ const extra = options.outNodes ?? [];
2607
+ const out = outDirectory(options.out, (relative) => project.resolve(relative));
2608
+ await fs4.mkdir(out.directory, { recursive: true });
2609
+ const missing = unknownNodes(graph, [...targets, ...extra]);
2610
+ if (missing.length) throw new Error(`No node ${missing.map((id) => `"${id}"`).join(", ")} in this graph (--node/--out-node take node ids; cascade graph read lists them).`);
2611
+ for await (const frame of options.frameSource ?? frameRange(options.start, options.end, options.step ?? 1)) {
2612
+ const frameStarted = performance.now();
2613
+ if (options.signal?.aborted) {
2614
+ aborted = true;
2615
+ break;
2616
+ }
2617
+ const result = await graph.run({
2618
+ frame,
2619
+ /**
2620
+ * Only what this frame changed cooks: a node whose props (time
2621
+ * reaches nodes through props) and inputs are unchanged, and that
2622
+ * nothing which cooked this frame feeds, keeps its outputs from the
2623
+ * last frame. Strict, so an upstream that re-ran is never trusted on
2624
+ * equal values. The morph's Moments and Match cook once per film.
2625
+ */
2626
+ ...options.fresh ? {} : { reuse: "strict" },
2627
+ ...options.fps === void 0 ? {} : { fps: options.fps },
2628
+ /**
2629
+ * One named node narrows the COOK as well as the output, because the
2630
+ * runtime can walk back from a single target. Several cannot be
2631
+ * expressed as one target, so the graph cooks whole and only the saving
2632
+ * narrows — which is still the bulk of the win when the outputs are
2633
+ * 18 MB PNGs.
2634
+ */
2635
+ ...targets.length === 1 && !extra.length ? { target: { kind: "node", nodeId: targets[0] } } : {}
2636
+ });
2637
+ if (result.status !== "completed") {
2638
+ throw new Error(
2639
+ `frame ${frame} ${result.status} \u2014 ${result.diagnostics.map((item) => item.message).join("; ") || "no diagnostics"}`
2640
+ );
2641
+ }
2642
+ const outputs = collectImageOutputs(graph, { nodes: targets, extra });
2643
+ if (!outputs.length) {
2644
+ throw new Error(
2645
+ targets.length || extra.length ? `No image output on ${[...targets, ...extra].join(", ")} \u2014 check that the node has an image output (or a Null passing one).` : "No image output to save: the graph has no unconsumed image port. Name the node to render with --node, or a mid-graph one to save as well with --out-node."
2646
+ );
2647
+ }
2648
+ const written = [];
2649
+ for (const output of outputs) {
2650
+ const source = imagePath(output.value);
2651
+ if (!source) continue;
2652
+ const name = sequenceFileName(output.nodeId, frame, source, width, output.shared ? output.port : void 0);
2653
+ const relative = source.replace(/^\.?\//, "");
2654
+ await fs4.copyFile(project.resolveMedia(relative), path5.join(out.directory, name));
2655
+ files.push(out.spell(name));
2656
+ written.push({ nodeId: output.nodeId, port: output.port, source: relative, file: out.spell(name) });
2657
+ }
2658
+ frames.push(frame);
2659
+ await options.onFrame?.({ frame, outputs: written, durationMs: performance.now() - frameStarted });
2660
+ if (options.verbose) console.log(` frame ${frame}`);
2661
+ }
2662
+ try {
2663
+ gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
2664
+ } catch {
2665
+ }
2666
+ } finally {
2667
+ try {
2668
+ await runtime.dispose();
2669
+ } finally {
2670
+ await disposeProjectGpu(host);
2671
+ }
2672
+ }
2673
+ return { frames, files, aborted, fps: graph.getFps(), durationMs: performance.now() - started, ...gpu ? { gpu } : {} };
2674
+ }
2675
+ async function disposeProjectGpu(host) {
2676
+ await host.capabilities.gpu?.dispose();
2677
+ }
2678
+ async function inspectProjectGraph(file, document2) {
2679
+ const prepared = await prepare(file, document2, false);
2680
+ reportUnresolved(prepared);
2681
+ const registrations = new Map(
2682
+ [...headlessBuiltinRegistrations, ...prepared.registrations].map((item) => [item.moduleId, item])
2683
+ );
2684
+ const nodes = document2.nodes.map((node) => {
2685
+ const id = moduleId(node);
2686
+ return {
2687
+ id: node.id,
2688
+ module: id,
2689
+ classification: prepared.deterministic.has(id) ? "definition-v1" : "dynamic",
2690
+ ...prepared.deterministic.has(id) ? { definition: registrations.get(id)?.definition } : {}
2691
+ };
2692
+ });
2693
+ return {
2694
+ file: path5.resolve(file),
2695
+ version: document2.version ?? null,
2696
+ deterministic: nodes.length > 0 && nodes.every((node) => node.classification === "definition-v1"),
2697
+ nodes,
2698
+ connections: Array.isArray(document2.connections) ? document2.connections.length : 0
2699
+ };
2700
+ }
2701
+ async function prepare(file, document2, compileExecutors) {
2702
+ if (!document2 || typeof document2 !== "object" || !Array.isArray(document2.nodes)) {
2703
+ throw new Error("Graph document must contain a nodes array");
2704
+ }
2705
+ const projectRoot = path5.dirname(path5.resolve(file));
2706
+ const registrations = [];
2707
+ const deterministic = /* @__PURE__ */ new Set();
2708
+ const seen = /* @__PURE__ */ new Set();
2709
+ const unresolved = [];
2710
+ for (const node of document2.nodes) {
2711
+ const id = moduleId(node);
2712
+ if (!id || seen.has(id)) continue;
2713
+ seen.add(id);
2714
+ if (builtinNodeRegistration(id)) {
2715
+ deterministic.add(id);
2716
+ continue;
2717
+ }
2718
+ const headless = headlessBuiltin(id);
2719
+ if (headless) {
2720
+ deterministic.add(id);
2721
+ registrations.push(headless);
2722
+ continue;
2723
+ }
2724
+ const resolved = await projectModuleFile(projectRoot, node, id);
2725
+ const moduleFile = resolved.file;
2726
+ if (!moduleFile) {
2727
+ unresolved.push(unresolvedModule(id, node, resolved.tried, projectRoot));
2728
+ registrations.push(savedRegistration(id, node));
2729
+ continue;
2730
+ }
2731
+ const source = await fs4.readFile(moduleFile, "utf8");
2732
+ const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
2733
+ const extracted = extractNodeDefinition(source, moduleFile, ts);
2734
+ if (!extracted.ok) {
2735
+ const reExport = reExportedDefinition(source);
2736
+ if (reExport) throw new Error(formatDiagnostic(reExport(moduleFile)));
2737
+ if (!mentionsDefinition && extracted.diagnostics.every((item) => item.code === "definition/missing-export")) {
2738
+ registrations.push(savedRegistration(id, node));
2739
+ continue;
2740
+ }
2741
+ throw new Error(extracted.diagnostics.map(formatDiagnostic).join("\n"));
2742
+ }
2743
+ const architectureDiagnostics = validateNodeModuleArchitecture(source, moduleFile, ts);
2744
+ if (architectureDiagnostics.length) {
2745
+ throw new Error(architectureDiagnostics.map(formatDiagnostic).join("\n"));
2746
+ }
2747
+ let executor;
2748
+ deterministic.add(id);
2749
+ registrations.push({
2750
+ kind: "definition-v1",
2751
+ moduleId: id,
2752
+ definition: extracted.definition,
2753
+ loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile) : async () => {
2754
+ throw new Error("Static validation never loads execute");
2755
+ }
2756
+ });
2757
+ }
2758
+ return { registrations, deterministic, unresolved };
2759
+ }
2760
+ async function projectModuleFile(root, node, id) {
2761
+ const tried = [];
2762
+ const source = node.source;
2763
+ if (source && typeof source === "object" && source.type === "project" && typeof source.file === "string") {
2764
+ const candidate2 = path5.resolve(root, source.file);
2765
+ if (within(root, candidate2)) {
2766
+ tried.push(candidate2);
2767
+ if (await exists(candidate2)) return { file: candidate2, tried };
2768
+ }
2769
+ }
2770
+ if (!id.startsWith("project.")) return { file: null, tried };
2771
+ const name = id.slice("project.".length);
2772
+ const candidate = path5.resolve(root, "nodes", name, "index.ts");
2773
+ if (!within(root, candidate)) return { file: null, tried };
2774
+ tried.push(candidate);
2775
+ return { file: await exists(candidate) ? candidate : null, tried };
2776
+ }
2777
+ function reExportedDefinition(source) {
2778
+ const named = /\bexport\s*\{[^}]*\bdefinition\b[^}]*\}\s*from\s*['"]/.test(source);
2779
+ const star = /\bexport\s*\*\s*from\s*['"]/.test(source);
2780
+ if (!named && !star) return null;
2781
+ const what = named ? "this module re-exports `definition` from another file" : "this module has a star re-export and no `definition` of its own";
2782
+ return (file) => ({
2783
+ phase: "prepare",
2784
+ code: "definition/re-exported",
2785
+ file,
2786
+ message: `${what}, and a definition cannot arrive that way \u2014 it is read statically from this file. Write the literal here, or keep the stub for \`execute\` only and leave this module dynamic. Without this the node loads as dynamic and every command reports success.`
2787
+ });
2788
+ }
2789
+ function unresolvedModule(id, node, tried, root) {
2790
+ const relative = tried.map((candidate) => path5.relative(root, candidate) || candidate);
2791
+ const where = relative.length ? `looked for ${relative.join(" and ")}` : `no project file is possible for this module id \u2014 a project module is named "project.<dir>" or carries a source file`;
2792
+ return {
2793
+ phase: "prepare",
2794
+ code: "project/module-not-found",
2795
+ path: node.id,
2796
+ message: `${id} has no module file \u2014 ${where}. Only known legacy modules can use saved-port compatibility, for inspection only; unknown and explicit project modules are rejected`
2797
+ };
2798
+ }
2799
+ async function compileExecute(projectRoot, moduleFile) {
2800
+ const cache = path5.join(projectRoot, ".cascade-cache", "nodes");
2801
+ await fs4.mkdir(cache, { recursive: true });
2802
+ const output = path5.join(cache, `${path5.basename(path5.dirname(moduleFile))}.mjs`);
2803
+ const staging = `${output}.${process.pid}.${Date.now()}.tmp`;
2804
+ await build({
2805
+ entryPoints: [moduleFile],
2806
+ outfile: staging,
2807
+ bundle: true,
2808
+ packages: "external",
2809
+ platform: "node",
2810
+ target: "node20",
2811
+ format: "esm",
2812
+ sourcemap: "inline",
2813
+ plugins: [cliImageModule()]
2814
+ });
2815
+ await fs4.rename(staging, output);
2816
+ const loaded = await import(`${pathToFileURL2(output).href}?v=${Date.now()}`);
2817
+ if (typeof loaded.execute !== "function") throw new Error(`${moduleFile} does not export execute`);
2818
+ return loaded.execute;
2819
+ }
2820
+ function cliImageModule() {
2821
+ const here = path5.dirname(fileURLToPath(import.meta.url));
2822
+ const source = [
2823
+ path5.join(here, "runtime", "image.ts"),
2824
+ // dist/cli/runtime, copied by build-cli
2825
+ path5.join(here, "..", "..", "server", "src", "runtime", "image.ts")
2826
+ // running from src
2827
+ ].find((candidate) => existsSync(candidate));
2828
+ return {
2829
+ name: "cascade-cli-image",
2830
+ setup(builder) {
2831
+ if (!source) return;
2832
+ builder.onResolve({ filter: /^(?:@field\/)?cascade\/image$/ }, () => ({ path: source }));
2833
+ }
2834
+ };
2835
+ }
2836
+ function savedRegistration(moduleId2, node) {
2837
+ return {
2838
+ kind: "definition-v1",
2839
+ moduleId: moduleId2,
2840
+ definition: savedDefinition(node),
2841
+ loadExecute: async () => {
2842
+ throw new Error(`Legacy module ${moduleId2} cannot run through the deterministic runtime`);
2843
+ }
2844
+ };
2845
+ }
2846
+ function savedDefinition(node) {
2847
+ const inputs = portDefinitions(node.inputs, "input");
2848
+ const outputs = portDefinitions(node.outputs, "output");
2849
+ const used = new Set(Object.keys(inputs));
2850
+ const props = Object.fromEntries(Object.entries(node.props ?? {}).filter(([name]) => !used.has(name)).map(([name, value]) => [name, {
2851
+ type: "any",
2852
+ default: jsonValue(value && typeof value === "object" && "value" in value ? value.value : value)
2853
+ }]));
2854
+ return {
2855
+ apiVersion: 1,
2856
+ runsOn: "portable",
2857
+ inputs,
2858
+ outputs,
2859
+ props
2860
+ };
2861
+ }
2862
+ function validateDynamicDocument(document2) {
2863
+ if (!Array.isArray(document2.nodes)) throw new Error("Graph document must contain a nodes array");
2864
+ const ids = /* @__PURE__ */ new Set();
2865
+ for (const node of document2.nodes) {
2866
+ if (!node || typeof node.id !== "string" || !node.id || !moduleId(node)) {
2867
+ throw new Error("Every graph node must have a non-empty id and module");
2868
+ }
2869
+ if (ids.has(node.id)) throw new Error(`Duplicate node id: ${node.id}`);
2870
+ ids.add(node.id);
2871
+ }
2872
+ if (document2.connections !== void 0 && !Array.isArray(document2.connections)) {
2873
+ throw new Error("Graph connections must be an array");
2874
+ }
2875
+ for (const [index, connection] of (document2.connections ?? []).entries()) {
2876
+ const endpoints = Array.isArray(connection) ? connection : [connection?.source, connection?.target];
2877
+ if (endpoints.length !== 2) throw new Error(`Connection ${index} must have two endpoints`);
2878
+ for (const endpoint of endpoints) {
2879
+ const nodeId = Array.isArray(endpoint) ? endpoint[0] : endpoint?.nodeId;
2880
+ if (typeof nodeId !== "string" || !ids.has(nodeId)) {
2881
+ throw new Error(`Connection ${index} references an unknown node`);
2882
+ }
2883
+ }
2884
+ }
2885
+ }
2886
+ function portDefinitions(value, direction) {
2887
+ if (!Array.isArray(value)) return {};
2888
+ return Object.fromEntries(value.flatMap((port, index) => {
2889
+ const name = typeof port?.name === "string" && /^[A-Za-z][A-Za-z0-9_]*$/.test(port.name) ? port.name : `${direction}_${index}`;
2890
+ if (port?.portType === "trigger" || port?.dataType === "trigger") return [[name, { kind: "trigger" }]];
2891
+ const definition = { kind: "data", type: cascadeType(port?.dataType) };
2892
+ if (direction === "input" && ("defaultValue" in (port ?? {}) || "value" in (port ?? {}))) {
2893
+ definition.default = jsonValue("defaultValue" in port ? port.defaultValue : port.value);
2894
+ }
2895
+ return [[name, definition]];
2896
+ }));
2897
+ }
2898
+ function moduleId(node) {
2899
+ return typeof node?.module === "string" ? node.module : typeof node?.type === "string" ? node.type : "";
2900
+ }
2901
+ function cascadeType(value) {
2902
+ if (value === "number") return "float";
2903
+ if (value === "boolean") return "bool";
2904
+ return typeof value === "string" && value !== "trigger" ? value : "any";
2905
+ }
2906
+ function jsonValue(value) {
2907
+ try {
2908
+ return value === void 0 ? null : JSON.parse(JSON.stringify(value));
2909
+ } catch {
2910
+ return null;
2911
+ }
2912
+ }
2913
+ function resolver(registrations) {
2914
+ const modules = new Map(registrations.map((registration) => [registration.moduleId, registration]));
2915
+ return { resolve: async (id) => modules.get(id) ?? null };
2916
+ }
2917
+ function formatDiagnostic(diagnostic) {
2918
+ const location = diagnostic.file ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}:${diagnostic.column}` : ""}: ` : "";
2919
+ return `${location}${diagnostic.code}: ${diagnostic.message}`;
2920
+ }
2921
+ function within(root, candidate) {
2922
+ const relative = path5.relative(path5.resolve(root), candidate);
2923
+ return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
2924
+ }
2925
+ async function exists(file) {
2926
+ try {
2927
+ await fs4.access(file);
2928
+ return true;
2929
+ } catch {
2930
+ return false;
2931
+ }
2932
+ }
2933
+ async function moduleDefinitions(file, ids) {
2934
+ const definitions = /* @__PURE__ */ new Map();
2935
+ for (const id of new Set(ids)) {
2936
+ const builtin = headlessBuiltin(id);
2937
+ if (builtin) {
2938
+ definitions.set(id, builtin.definition);
2939
+ continue;
2940
+ }
2941
+ try {
2942
+ const prepared = await prepare(file, { nodes: [{ id: "probe", module: id }] }, false);
2943
+ definitions.set(id, prepared.deterministic.has(id) ? prepared.registrations[0]?.definition : void 0);
2944
+ } catch {
2945
+ definitions.set(id, void 0);
2946
+ }
2947
+ }
2948
+ return definitions;
2949
+ }
2950
+ function builtinModuleIds() {
2951
+ return headlessBuiltinRegistrations.map((registration) => registration.moduleId);
2952
+ }
2953
+ async function diagnoseGraph(file, document2) {
2954
+ const errors = [];
2955
+ const warnings = [];
2956
+ try {
2957
+ const prepared = await prepare(file, document2, false);
2958
+ for (const item of prepared.unresolved) warnings.push(line(item).trim());
2959
+ const nodes = document2.nodes;
2960
+ if (nodes.some((node) => !prepared.deterministic.has(moduleId(node)))) {
2961
+ validateDynamicDocument(document2);
2962
+ return { errors, warnings };
2963
+ }
2964
+ let host;
2965
+ try {
2966
+ host = createProjectHost(file, prepared.registrations);
2967
+ } catch {
2968
+ host = createNodeRuntimeHost({ modules: resolver(prepared.registrations) });
2969
+ }
2970
+ const runtime = createRuntime({ host, nodes: prepared.registrations });
2971
+ try {
2972
+ const graph = await runtime.load(document2);
2973
+ const classified = classifyPreflight(graph.preflight());
2974
+ errors.push(...classified.errors.map((item) => line(item).trim()));
2975
+ warnings.push(...classified.warnings.map((item) => line(item).trim()));
2976
+ } finally {
2977
+ await runtime.dispose();
2978
+ await disposeProjectGpu(host);
2979
+ }
2980
+ } catch (error) {
2981
+ errors.push(error instanceof Error ? error.message : String(error));
2982
+ }
2983
+ return { errors, warnings };
2984
+ }
2985
+
2986
+ export {
2987
+ PREFLIGHT_WARNING_CODES,
2988
+ createRuntime,
2989
+ validateNodeModuleArchitecture,
2990
+ installProjectIo,
2991
+ saveStillImages,
2992
+ createProjectHost,
2993
+ classifyPreflight,
2994
+ validateProjectGraph,
2995
+ checkProjectGraph,
2996
+ runDeterministicProjectGraph,
2997
+ cookProjectGraph,
2998
+ renderDeterministicProjectFrames,
2999
+ inspectProjectGraph,
3000
+ projectModuleFile,
3001
+ moduleId,
3002
+ moduleDefinitions,
3003
+ builtinModuleIds,
3004
+ diagnoseGraph
3005
+ };
3006
+ //# sourceMappingURL=chunk-EUIAC3U3.js.map