@field/cascade 0.11.0 → 0.13.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 (220) hide show
  1. package/AGENTS.md +8 -5
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +245 -0
  4. package/README.md +1 -1
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-4O7FQ__R.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-Bz8JxISH.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-D5Z0nJ5-.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-Yqi0Ni61.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-CxUzc4vp.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-BNFb25CX.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-0ZDnWxeM.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-Cy18MoaD.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-B-2Dx7nq.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-DUAc9B_d.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-DeQsDqP5.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-BiFwJVSk.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-BLoHH68a.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-CNFgm1Kv.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-CFXQVNfs.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-Cf7S7p2E.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-CMHBcG-g.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BEih0GDj.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-COEKYD_a.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-BNhAhMNn.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-CPiMpnPl.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-Bgp1ITLR.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/{Graph-ZJJTRQTM.js → Graph-OB6EWSGL.js} +12 -12
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/{chunk-3JTJHF7V.js → chunk-2L34ERS2.js} +4 -4
  73. package/dist/cli/chunk-53LGFPWX.js +138 -0
  74. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  75. package/dist/cli/{chunk-YAT5Y25U.js → chunk-7QE2ZNSN.js} +3 -3
  76. package/dist/cli/{chunk-SGJAET2L.js → chunk-BPODRH52.js} +8 -4
  77. package/dist/cli/{chunk-XWDSKG7N.js → chunk-BZ63XQ3I.js} +2 -2
  78. package/dist/cli/{chunk-FCDXJ6EK.js → chunk-CTPLRSWF.js} +2 -13
  79. package/dist/cli/{chunk-WIH4TEIS.js → chunk-EZRWJ4G2.js} +1 -1
  80. package/dist/cli/{chunk-BIJO5OFS.js → chunk-FWMSGKEH.js} +1 -1
  81. package/dist/cli/{chunk-POMMNKZZ.js → chunk-G23A2BZZ.js} +38 -5
  82. package/dist/cli/{chunk-35D343DC.js → chunk-GRIN2DDP.js} +19 -1
  83. package/dist/cli/{chunk-WKURDVYP.js → chunk-JEW5MPGI.js} +28 -331
  84. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  85. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  86. package/dist/cli/{chunk-GFS6W3C5.js → chunk-L6B6FUQP.js} +54 -32
  87. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  88. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  89. package/dist/cli/{chunk-A4OAMOZH.js → chunk-NJFMDXHR.js} +13 -3
  90. package/dist/cli/{chunk-DUIUFVU2.js → chunk-OBZAP3GG.js} +49 -23
  91. package/dist/cli/chunk-QAGATJGE.js +336 -0
  92. package/dist/cli/{chunk-EUIAC3U3.js → chunk-QKL7CCRS.js} +966 -132
  93. package/dist/cli/{chunk-RLCAJBVR.js → chunk-TJW67QSI.js} +8 -7
  94. package/dist/cli/{chunk-RQZ4SX5R.js → chunk-W5PJ5RMP.js} +156 -84
  95. package/dist/cli/chunk-W7OUAT7J.js +122 -0
  96. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  97. package/dist/cli/{chunk-D6BUOHUG.js → chunk-X4WKAOQP.js} +4 -4
  98. package/dist/cli/{chunk-VVGSLVSF.js → chunk-X566FWBV.js} +6 -1
  99. package/dist/cli/chunk-YH637PDL.js +317 -0
  100. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  101. package/dist/cli/{cliCommands-UQSFGEHF.js → cliCommands-XVFRD7CF.js} +23 -17
  102. package/dist/cli/{commands-Q23M26QM.js → commands-M2XLTPTZ.js} +129 -47
  103. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  104. package/dist/cli/{definitions-HF2FTHYU.js → definitions-BPOPW52L.js} +7 -7
  105. package/dist/cli/{doctorCommand-B7BPQMN6.js → doctorCommand-RAEI7GVY.js} +36 -6
  106. package/dist/cli/{edit-JHCPTQFT.js → edit-S5KEQ5RA.js} +3 -3
  107. package/dist/cli/editorCommands-5BBG4S6V.js +41 -0
  108. package/dist/cli/{encode-HAUZTYPT.js → encode-ICPEPMH5.js} +2 -2
  109. package/dist/cli/file-3HTQHPYQ.js +41 -0
  110. package/dist/cli/filters-6ODFWYK4.js +379 -0
  111. package/dist/cli/generators-YU7GB4MD.js +210 -0
  112. package/dist/cli/{geo-4RNK3QVN.js → geo-6ZPGKZRU.js} +7 -7
  113. package/dist/cli/{graphCommands-6BMAKGQE.js → graphCommands-MULQ7VD2.js} +4 -4
  114. package/dist/cli/{graphFile-VPSQ6OF7.js → graphFile-CITF36ZM.js} +25 -19
  115. package/dist/cli/{headlessCanvas-5XLDZT7M.js → headlessCanvas-ENRHC7GS.js} +3 -2
  116. package/dist/cli/{helpCommand-3VRE3KVO.js → helpCommand-X2QDANM3.js} +9 -7
  117. package/dist/cli/image-TXELZZK5.js +55 -0
  118. package/dist/cli/index.js +14 -14
  119. package/dist/cli/{math-ZRMLKU2D.js → math-RSYRQ3C6.js} +5 -5
  120. package/dist/cli/{mcp-ZLEEMYGF.js → mcp-YRIGRLYQ.js} +4 -4
  121. package/dist/cli/{parity-KINARCP2.js → parity-3N3HTKWX.js} +42 -36
  122. package/dist/cli/{pop-USDFXB7H.js → pop-B7SIIYKN.js} +5 -5
  123. package/dist/cli/project-SJF5ZF3F.js +12 -0
  124. package/dist/cli/{projectDefinition-DMTZN2NE.js → projectDefinition-F3D7UIHJ.js} +8 -8
  125. package/dist/cli/{projectRuntime-Z5PJOH47.js → projectRuntime-NY4XMDFH.js} +19 -12
  126. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  127. package/dist/cli/{render-YURAPPUI.js → render-Y23W6MY6.js} +29 -22
  128. package/dist/cli/{runCommand-PI764LEG.js → runCommand-XOG7AVC5.js} +3 -3
  129. package/dist/cli/runner-DNJO5WO2.js +43 -0
  130. package/dist/cli/runtime/image.ts +27 -2
  131. package/dist/cli/runtime/io.ts +78 -0
  132. package/dist/cli/share-B7HTCOJX.js +166 -0
  133. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  134. package/dist/cli/{stageRunner-KUNIMQKJ.js → stageRunner-KY2EJP7X.js} +2 -2
  135. package/dist/cli/text-MB3EKR4H.js +126 -0
  136. package/dist/cli/{verbs-RCE3K4VQ.js → verbs-PI2JH5FI.js} +2 -2
  137. package/dist/cli/webBuild-2K4COCGY.js +44 -0
  138. package/dist/contracts/capabilities.d.ts +39 -1
  139. package/dist/contracts/color.d.ts +8 -0
  140. package/dist/contracts/color.js +23 -0
  141. package/dist/contracts/definition.d.ts +10 -0
  142. package/dist/contracts/schema.d.ts +1 -1
  143. package/dist/contracts/schema.js +1 -0
  144. package/dist/contracts/validate.js +11 -1
  145. package/dist/contracts/values.d.ts +24 -0
  146. package/dist/image.d.ts +15 -0
  147. package/dist/image.js +36 -2
  148. package/dist/index.html +8 -2
  149. package/dist/io.d.ts +29 -0
  150. package/dist/io.js +40 -0
  151. package/dist/player/app.js +11 -7
  152. package/dist/player/editor.js +3 -2
  153. package/dist/player/editorEngine.js +3 -2
  154. package/dist/player/embed.js +2 -0
  155. package/dist/player/types.d.ts +6 -0
  156. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  157. package/dist/runtime/builtins/assert/checks.js +207 -0
  158. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  159. package/dist/runtime/builtins/assert/definitions.js +54 -0
  160. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  161. package/dist/runtime/builtins/assert/index.js +36 -0
  162. package/dist/runtime/builtins/image/contactSheet.d.ts +3 -165
  163. package/dist/runtime/builtins/image/contactSheet.js +0 -49
  164. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  165. package/dist/runtime/builtins/image/definitions.js +343 -0
  166. package/dist/runtime/builtins/image/file.d.ts +8 -0
  167. package/dist/runtime/builtins/image/file.js +37 -0
  168. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  169. package/dist/runtime/builtins/image/filters.js +394 -0
  170. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  171. package/dist/runtime/builtins/image/generators.js +144 -0
  172. package/dist/runtime/builtins/image/index.d.ts +947 -4
  173. package/dist/runtime/builtins/image/index.js +41 -6
  174. package/dist/runtime/builtins/image/io.d.ts +100 -0
  175. package/dist/runtime/builtins/image/io.js +142 -0
  176. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  177. package/dist/runtime/builtins/image/planar.js +274 -0
  178. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  179. package/dist/runtime/builtins/image/simplex.js +92 -0
  180. package/dist/runtime/builtins/image/text.d.ts +4 -0
  181. package/dist/runtime/builtins/image/text.js +141 -0
  182. package/dist/runtime/builtins/index.d.ts +3 -0
  183. package/dist/runtime/builtins/index.js +7 -0
  184. package/dist/runtime/definition/architecture.js +89 -0
  185. package/dist/runtime/gpu.d.ts +25 -0
  186. package/dist/runtime/gpu.js +40 -0
  187. package/dist/runtime/runtime.d.ts +2 -0
  188. package/dist/runtime/runtime.js +268 -44
  189. package/dist/runtime/types.d.ts +30 -0
  190. package/doc/CLI_REFERENCE.md +21 -3
  191. package/doc/EDITOR_EXPORT.md +26 -0
  192. package/doc/MCP.md +2 -1
  193. package/doc/NODE_REFERENCE.md +286 -14
  194. package/doc/PROJECT_AUTHORING.md +158 -4
  195. package/doc/WEB_PLAYER.md +9 -1
  196. package/package.json +4 -1
  197. package/dist/assets/CodePanel-BU-autmq.js +0 -2
  198. package/dist/assets/CustomNodeDialog-BRpDfOV5.js +0 -1
  199. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +0 -1
  200. package/dist/assets/ProjectSettings-DLOXLxcA.js +0 -2
  201. package/dist/assets/definitions-GreEMXLV.js +0 -1
  202. package/dist/assets/geo-u0n_jOsD.js +0 -1
  203. package/dist/assets/image-cAa-Nu9O.js +0 -2062
  204. package/dist/assets/index-5ZtDAuy2.js +0 -220
  205. package/dist/assets/index-7vae6YFC.css +0 -1
  206. package/dist/assets/math-DioIQOPJ.js +0 -1
  207. package/dist/assets/pop-DC_TQG9t.js +0 -1
  208. package/dist/cli/chunk-36P5K3SD.js +0 -631
  209. package/dist/cli/chunk-4UDFYF5G.js +0 -132
  210. package/dist/cli/chunk-53NMBJAO.js +0 -35
  211. package/dist/cli/chunk-MIIEFUGG.js +0 -623
  212. package/dist/cli/chunk-QBBKRNGY.js +0 -90
  213. package/dist/cli/chunk-WPUDGEFY.js +0 -20
  214. package/dist/cli/editorCommands-UIF5YLUZ.js +0 -33
  215. package/dist/cli/project-K3QIPGBB.js +0 -12
  216. package/dist/cli/registerStandardNodes-S2VLMF6J.js +0 -3081
  217. package/dist/cli/runner-RA6UKEPD.js +0 -37
  218. package/dist/cli/webBuild-GT4PZT5D.js +0 -37
  219. package/dist/runtime/builtins/headless.d.ts +0 -19
  220. package/dist/runtime/builtins/headless.js +0 -25
@@ -3,31 +3,39 @@ import {
3
3
  builtinNodeRegistrations,
4
4
  headlessBuiltin,
5
5
  headlessBuiltinRegistrations
6
- } from "./chunk-36P5K3SD.js";
6
+ } from "./chunk-MI5JLQYA.js";
7
7
  import {
8
- imagePath,
9
- sequenceFileName,
10
- sequenceWidth
11
- } from "./chunk-ICH7N5PN.js";
8
+ Color
9
+ } from "./chunk-GRIN2DDP.js";
12
10
  import {
13
11
  DEFAULT_GRAPH_SEED,
14
12
  bundleFieldType,
15
13
  bundleSchemasCompatible,
16
14
  bundleValueProblem,
17
15
  createGpuHost,
16
+ gpuLimitShortfall,
18
17
  graphSeed,
19
18
  isBundleValue,
20
19
  isGpuHost,
21
20
  nodeSeed,
21
+ requestDeviceWithAdapterLimits,
22
22
  seededRandom,
23
23
  selectBundleFields,
24
24
  suggestName,
25
25
  unknownFieldMessage
26
- } from "./chunk-GFS6W3C5.js";
26
+ } from "./chunk-L6B6FUQP.js";
27
27
  import {
28
28
  PropAnimator,
29
29
  isBoundProp
30
30
  } from "./chunk-HFHWYL55.js";
31
+ import {
32
+ installIoBridge
33
+ } from "./chunk-KBGUO27Q.js";
34
+ import {
35
+ imagePath,
36
+ sequenceFileName,
37
+ sequenceWidth
38
+ } from "./chunk-ICH7N5PN.js";
31
39
  import {
32
40
  DEFAULT_FPS,
33
41
  MIN_FRAME,
@@ -38,23 +46,36 @@ import {
38
46
  parseBundleFieldPath,
39
47
  remoteBundlePath
40
48
  } from "./chunk-CO22XHNG.js";
49
+ import {
50
+ PythonWorker,
51
+ PythonWorkerPool,
52
+ defaultStageWorkerCount
53
+ } from "./chunk-YH637PDL.js";
54
+ import {
55
+ readStageConfig,
56
+ runProjectStage
57
+ } from "./chunk-X566FWBV.js";
58
+ import {
59
+ callStage,
60
+ stageSources
61
+ } from "./chunk-5ORUQ6ES.js";
41
62
  import {
42
63
  ProjectRoot,
43
64
  extractNodeDefinition
44
- } from "./chunk-RQZ4SX5R.js";
65
+ } from "./chunk-W5PJ5RMP.js";
45
66
  import {
46
67
  CAPABILITIES_BY_ENVIRONMENT,
47
68
  validateNodeDefinition
48
- } from "./chunk-A4OAMOZH.js";
69
+ } from "./chunk-NJFMDXHR.js";
49
70
  import {
50
71
  canConnectTypes
51
- } from "./chunk-BIJO5OFS.js";
72
+ } from "./chunk-FWMSGKEH.js";
52
73
 
53
74
  // src/cli/projectRuntime.ts
54
- import fs4 from "node:fs/promises";
55
- import path5 from "node:path";
75
+ import fs5 from "node:fs/promises";
76
+ import path6 from "node:path";
56
77
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
57
- import { existsSync } from "node:fs";
78
+ import { existsSync as existsSync2 } from "node:fs";
58
79
  import ts from "typescript";
59
80
  import { build } from "esbuild";
60
81
 
@@ -117,11 +138,21 @@ var MutableAbortSignal = class {
117
138
  }
118
139
  };
119
140
  function createRuntime(options) {
120
- return new Runtime(options.host, [...builtinNodeRegistrations, ...options.nodes ?? []]);
141
+ return new Runtime(
142
+ options.host,
143
+ [...builtinNodeRegistrations, ...options.nodes ?? []],
144
+ {
145
+ values: options.values === "trust" ? "trust" : "freeze",
146
+ concurrency: Math.max(1, Math.floor(Number(options.concurrency ?? 1)) || 1),
147
+ ...options.nodeScope ? { nodeScope: options.nodeScope } : {}
148
+ }
149
+ );
121
150
  }
151
+ var SERIAL_EXECUTION = { values: "freeze", concurrency: 1 };
122
152
  var Runtime = class {
123
- constructor(host, registrations) {
153
+ constructor(host, registrations, execution = SERIAL_EXECUTION) {
124
154
  this.host = host;
155
+ this.execution = execution;
125
156
  this.registrations = /* @__PURE__ */ new Map();
126
157
  this.graphs = /* @__PURE__ */ new Set();
127
158
  this.sealed = false;
@@ -202,7 +233,8 @@ var Runtime = class {
202
233
  connections,
203
234
  () => this.graphs.delete(graph),
204
235
  document2.annotations,
205
- graphSeed(document2)
236
+ graphSeed(document2),
237
+ this.execution
206
238
  );
207
239
  const fps = document2.timeline?.fps;
208
240
  if (typeof fps === "number" && Number.isFinite(fps) && fps > 0) graph.setFps(fps);
@@ -217,12 +249,13 @@ var Runtime = class {
217
249
  }
218
250
  };
219
251
  var Graph = class {
220
- constructor(host, nodes, connections, onDispose, annotations = [], seed = DEFAULT_GRAPH_SEED) {
252
+ constructor(host, nodes, connections, onDispose, annotations = [], seed = DEFAULT_GRAPH_SEED, execution = SERIAL_EXECUTION) {
221
253
  this.host = host;
222
254
  this.nodes = nodes;
223
255
  this.connections = connections;
224
256
  this.onDispose = onDispose;
225
257
  this.seed = seed;
258
+ this.execution = execution;
226
259
  this.graphState = "ready";
227
260
  this.listeners = /* @__PURE__ */ new Set();
228
261
  this.diagnostics = [];
@@ -241,6 +274,7 @@ var Graph = class {
241
274
  * state survives an inner loop running to completion inside one of its steps.
242
275
  */
243
276
  this.feedbackState = /* @__PURE__ */ new Map();
277
+ this.values = execution.values;
244
278
  this.byId = new Map(nodes.map((node) => [node.id, node]));
245
279
  const byInput = /* @__PURE__ */ new Map();
246
280
  const byNode = /* @__PURE__ */ new Map();
@@ -350,7 +384,7 @@ var Graph = class {
350
384
  kind: definition.kind,
351
385
  type: definition.kind === "trigger" ? "trigger" : definition.type,
352
386
  connected: this.connectedOutputs.has(`${node.id}\0${name}`),
353
- ...definition.kind === "data" && node.outputs.has(name) ? { value: snapshot(node.outputs.get(name)) } : {}
387
+ ...definition.kind === "data" && node.outputs.has(name) ? { value: this.expose(node.outputs.get(name)) } : {}
354
388
  }
355
389
  ]
356
390
  )
@@ -552,7 +586,15 @@ var Graph = class {
552
586
  return () => this.listeners.delete(listener);
553
587
  }
554
588
  getOutput(nodeId, outputName2) {
555
- return snapshot(this.requireNode(nodeId).outputs.get(outputName2));
589
+ return this.expose(this.requireNode(nodeId).outputs.get(outputName2));
590
+ }
591
+ /**
592
+ * An output value as callers see it. Under `"freeze"` every output was
593
+ * deep-frozen when it was set, so this is the value itself (the snapshot is
594
+ * a lookup); under `"trust"` it is handed out as the producer made it.
595
+ */
596
+ expose(value) {
597
+ return this.values === "trust" ? value : snapshot(value);
556
598
  }
557
599
  getGraphOutput(name) {
558
600
  const node = this.graphOutputs.get(name);
@@ -564,7 +606,7 @@ var Graph = class {
564
606
  return immutableMap(
565
607
  [...this.requireNode(nodeId).outputs].map(([name, value]) => [
566
608
  name,
567
- snapshot(value)
609
+ this.expose(value)
568
610
  ])
569
611
  );
570
612
  }
@@ -639,17 +681,21 @@ var Graph = class {
639
681
  (node) => initial.includes(node) && !insideFeedback.has(node.id)
640
682
  );
641
683
  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;
684
+ const skip = (node) => !!reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked));
685
+ if (this.execution.concurrency > 1)
686
+ await this.cookConcurrently(ordered, runId, signal, enqueue, skip, cooked);
687
+ else
688
+ for (const node of ordered) {
689
+ if (signal.aborted) break;
690
+ if (skip(node)) {
691
+ this.emit({ type: "node:start", runId, nodeId: node.id });
692
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
693
+ continue;
694
+ }
695
+ await this.executeNode(node, runId, signal, {}, enqueue);
696
+ cooked.add(node.id);
697
+ if (!signal.aborted) node.lastCook = this.cookKey(node);
648
698
  }
649
- await this.executeNode(node, runId, signal, {}, enqueue);
650
- cooked.add(node.id);
651
- if (!signal.aborted) node.lastCook = this.cookKey(node);
652
- }
653
699
  if (injection && !signal.aborted)
654
700
  enqueue(injection.nodeId, injection.inputName, injection.payload, {
655
701
  kind: "external"
@@ -700,7 +746,79 @@ var Graph = class {
700
746
  this.active = void 0;
701
747
  }
702
748
  }
749
+ /**
750
+ * The run's main order with up to `concurrency` nodes cooking at once.
751
+ *
752
+ * Nodes START strictly in topological order: the next node starts once
753
+ * everything it reads has completed and a slot is free, and nothing later
754
+ * in the order overtakes it. So the `node:start` events — and a run trace
755
+ * built from them — come in the same order as a serial run on every run;
756
+ * only completions interleave. A Kahn order puts siblings side by side (the
757
+ * morph's four renders are adjacent), which is where the overlap is.
758
+ *
759
+ * A feedback container, a node with trigger ports and a GPU node cook
760
+ * alone: their state (the carried step, the trigger queue's sequence, one
761
+ * device) is ordered by the serial loop and not worth sharing.
762
+ *
763
+ * On a failure nothing new starts, the nodes already cooking finish, and
764
+ * the failure earliest in the order is the run's, so the message does not
765
+ * depend on which stage happened to answer first.
766
+ */
767
+ async cookConcurrently(ordered, runId, signal, enqueue, skip, cooked) {
768
+ const position = new Map(ordered.map((node, index) => [node.id, index]));
769
+ const needs = /* @__PURE__ */ new Map();
770
+ for (const dependency of graphDependencies(this.nodes, this.connections)) {
771
+ if (!position.has(dependency.sourceId) || !position.has(dependency.targetId)) continue;
772
+ const list = needs.get(dependency.targetId) ?? [];
773
+ list.push(dependency.sourceId);
774
+ needs.set(dependency.targetId, list);
775
+ }
776
+ const done = /* @__PURE__ */ new Set();
777
+ const running = /* @__PURE__ */ new Map();
778
+ let exclusiveRunning = false;
779
+ let failure;
780
+ let next = 0;
781
+ for (; ; ) {
782
+ while (!failure && !signal.aborted && !exclusiveRunning && next < ordered.length && running.size < this.execution.concurrency) {
783
+ const node = ordered[next];
784
+ if (!(needs.get(node.id) ?? []).every((id) => done.has(id))) break;
785
+ const alone = cooksAlone(node);
786
+ if (alone && running.size) break;
787
+ next++;
788
+ if (skip(node)) {
789
+ this.emit({ type: "node:start", runId, nodeId: node.id });
790
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
791
+ done.add(node.id);
792
+ continue;
793
+ }
794
+ if (alone) exclusiveRunning = true;
795
+ const index = position.get(node.id);
796
+ const task = this.executeNode(node, runId, signal, {}, enqueue).then(
797
+ () => {
798
+ cooked.add(node.id);
799
+ if (!signal.aborted) node.lastCook = this.cookKey(node);
800
+ done.add(node.id);
801
+ },
802
+ (error) => {
803
+ if (!failure || index < failure.index) failure = { index, error };
804
+ }
805
+ ).finally(() => {
806
+ running.delete(node.id);
807
+ if (alone) exclusiveRunning = false;
808
+ });
809
+ running.set(node.id, task);
810
+ }
811
+ if (!running.size) break;
812
+ await Promise.race(running.values());
813
+ }
814
+ if (failure) throw failure.error;
815
+ }
703
816
  async executeNode(node, runId, signal, triggers, enqueue) {
817
+ const scope = this.execution.nodeScope;
818
+ if (!scope) return this.cookNode(node, runId, signal, triggers, enqueue);
819
+ return scope(node.id, () => this.cookNode(node, runId, signal, triggers, enqueue));
820
+ }
821
+ async cookNode(node, runId, signal, triggers, enqueue) {
704
822
  this.emit({ type: "node:start", runId, nodeId: node.id });
705
823
  if (node.moduleId === "cascade.core.Previous") {
706
824
  const container = this.enclosingFeedback(node);
@@ -778,7 +896,8 @@ var Graph = class {
778
896
  const value = outputValue(
779
897
  response.outputs[name],
780
898
  definition,
781
- `${node.id}.${name}`
899
+ `${node.id}.${name}`,
900
+ this.values
782
901
  );
783
902
  node.outputs.set(name, value);
784
903
  this.emit({
@@ -811,7 +930,7 @@ var Graph = class {
811
930
  } : {
812
931
  set: (value) => pendingData.set(
813
932
  name,
814
- outputValue(value, definition, `${node.id}.${name}`)
933
+ outputValue(value, definition, `${node.id}.${name}`, this.values)
815
934
  )
816
935
  }
817
936
  ]
@@ -822,7 +941,10 @@ var Graph = class {
822
941
  const installed = this.host.capabilities[name];
823
942
  if (name === "gpu" && isGpuHost(installed)) {
824
943
  await installed.ensure();
825
- capabilityEntries.push([name, installed.forNode(node.id)]);
944
+ const gpu = installed.forNode(node.id);
945
+ const shortfall = gpuLimitShortfall(node.id, node.definition.gpuLimits, gpu);
946
+ if (shortfall) throw new Error(shortfall);
947
+ capabilityEntries.push([name, gpu]);
826
948
  } else {
827
949
  capabilityEntries.push([
828
950
  name,
@@ -872,7 +994,7 @@ var Graph = class {
872
994
  runId,
873
995
  nodeId: node.id,
874
996
  outputName: name,
875
- value: snapshot(value)
997
+ value: this.expose(value)
876
998
  });
877
999
  }
878
1000
  for (const emitted of pendingTriggers)
@@ -1002,8 +1124,8 @@ var Graph = class {
1002
1124
  }
1003
1125
  for (const [prop, binding] of Object.entries(node.bindings)) {
1004
1126
  if (!binding.expression) continue;
1005
- for (const path6 of expressionPaths(binding.expression)) {
1006
- const remote = remoteBundlePath(path6);
1127
+ for (const path7 of expressionPaths(binding.expression)) {
1128
+ const remote = remoteBundlePath(path7);
1007
1129
  if (remote) {
1008
1130
  const owner = this.byId.get(remote.node);
1009
1131
  const output = owner?.definition.outputs?.[remote.port];
@@ -1011,12 +1133,12 @@ var Graph = class {
1011
1133
  push(
1012
1134
  node.id,
1013
1135
  "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`,
1136
+ `${node.id}.${prop} reads "${path7}", 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
1137
  `${node.id}.${prop}`
1016
1138
  );
1017
1139
  continue;
1018
1140
  }
1019
- const reference = parseBundleFieldPath(path6);
1141
+ const reference = parseBundleFieldPath(path7);
1020
1142
  if (!reference) continue;
1021
1143
  const input = node.definition.inputs?.[reference.port];
1022
1144
  if (!input) {
@@ -1025,7 +1147,7 @@ var Graph = class {
1025
1147
  push(
1026
1148
  node.id,
1027
1149
  "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}"?` : ""),
1150
+ `${node.id}.${prop} reads "${path7}", but ${node.moduleId} has no input "${reference.port}"` + (suggestion ? ` \u2014 did you mean "${suggestion}.${reference.field}"?` : ""),
1029
1151
  `${node.id}.${prop}`
1030
1152
  );
1031
1153
  continue;
@@ -1334,7 +1456,8 @@ var Graph = class {
1334
1456
  }
1335
1457
  return Object.freeze({
1336
1458
  result: carried,
1337
- history: Object.freeze(history)
1459
+ // Recorded as settled, so reading `history` back is not a copy.
1460
+ history: settle(history)
1338
1461
  });
1339
1462
  } finally {
1340
1463
  this.feedbackState.delete(node.id);
@@ -1476,8 +1599,10 @@ function materialize(authored, registration) {
1476
1599
  const definition = registration.definition.props?.[name];
1477
1600
  if (!definition) continue;
1478
1601
  const object = saved && typeof saved === "object" && "value" in saved ? saved : null;
1602
+ const stored = object ? object.value : saved;
1479
1603
  const value = runtimeValue(
1480
- object ? object.value : saved,
1604
+ // A colour saved as `{ r, g, b }` by the class image nodes reads as one.
1605
+ definition.type === "color" ? Color.upgrade(stored) : stored,
1481
1606
  definition.type,
1482
1607
  false,
1483
1608
  `${authored.id}.${name}`
@@ -1734,6 +1859,15 @@ function snapshotRegistration(registration) {
1734
1859
  const definition = snapshot(registration.definition);
1735
1860
  return Object.freeze({ ...registration, definition });
1736
1861
  }
1862
+ function cooksAlone(node) {
1863
+ if (node.definition.container === "feedback") return true;
1864
+ if (node.definition.capabilities?.includes("gpu")) return true;
1865
+ const ports = [
1866
+ ...Object.values(node.definition.inputs ?? {}),
1867
+ ...Object.values(node.definition.outputs ?? {})
1868
+ ];
1869
+ return ports.some((port) => port.kind === "trigger");
1870
+ }
1737
1871
  function isGated(node) {
1738
1872
  return Object.values(node.definition.inputs ?? {}).some(
1739
1873
  (input) => input.kind === "trigger"
@@ -1877,7 +2011,69 @@ function misuse(code, message) {
1877
2011
  throw runtimeError(code, message);
1878
2012
  }
1879
2013
  function snapshot(value) {
1880
- return cloneValue(value, /* @__PURE__ */ new WeakSet());
2014
+ const clone = cloneValue(value, /* @__PURE__ */ new WeakSet());
2015
+ if (clone !== null && typeof clone === "object") settled.add(clone);
2016
+ return clone;
2017
+ }
2018
+ var settled = /* @__PURE__ */ new WeakSet();
2019
+ var copiedContainers = 0;
2020
+ function isPlainContainer(value) {
2021
+ if (Array.isArray(value)) return true;
2022
+ const prototype = Object.getPrototypeOf(value);
2023
+ return prototype === Object.prototype || prototype === null;
2024
+ }
2025
+ function checkScalar(value) {
2026
+ if (typeof value === "number" && !Number.isFinite(value))
2027
+ misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
2028
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint")
2029
+ misuse(
2030
+ "runtime/non-serializable-value",
2031
+ `Runtime values cannot contain ${typeof value}`
2032
+ );
2033
+ }
2034
+ function settle(value) {
2035
+ if (value === null || typeof value !== "object") {
2036
+ checkScalar(value);
2037
+ return value;
2038
+ }
2039
+ if (!settled.has(value) && isPlainContainer(value)) {
2040
+ settleContainer(value, /* @__PURE__ */ new WeakSet());
2041
+ settled.add(value);
2042
+ }
2043
+ return value;
2044
+ }
2045
+ function settleContainer(value, ancestors) {
2046
+ let nested = false;
2047
+ const visit = (item) => {
2048
+ if (item === null || typeof item !== "object") {
2049
+ checkScalar(item);
2050
+ return;
2051
+ }
2052
+ if (settled.has(item) || !isPlainContainer(item)) return;
2053
+ if (ancestors.has(item))
2054
+ misuse(
2055
+ "runtime/non-serializable-value",
2056
+ "Runtime values cannot contain cycles"
2057
+ );
2058
+ if (!nested) {
2059
+ nested = true;
2060
+ ancestors.add(value);
2061
+ }
2062
+ if (settleContainer(item, ancestors)) settled.add(item);
2063
+ };
2064
+ if (Array.isArray(value)) for (let index = 0; index < value.length; index++) visit(value[index]);
2065
+ else for (const key of Object.keys(value)) visit(value[key]);
2066
+ if (nested) ancestors.delete(value);
2067
+ Object.freeze(value);
2068
+ return nested;
2069
+ }
2070
+ function trust(value) {
2071
+ if (value === null || typeof value !== "object") {
2072
+ checkScalar(value);
2073
+ return value;
2074
+ }
2075
+ if (isPlainContainer(value)) Object.freeze(value);
2076
+ return value;
1881
2077
  }
1882
2078
  function immutableMap(entries) {
1883
2079
  const source = new Map(entries);
@@ -1911,24 +2107,17 @@ function immutableMap(entries) {
1911
2107
  return view;
1912
2108
  }
1913
2109
  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
- );
2110
+ checkScalar(value);
1921
2111
  if (value === void 0 || value === null || typeof value !== "object")
1922
2112
  return value;
1923
- const prototype = Object.getPrototypeOf(value);
1924
- if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
1925
- return value;
2113
+ if (settled.has(value) || !isPlainContainer(value)) return value;
1926
2114
  if (ancestors.has(value))
1927
2115
  misuse(
1928
2116
  "runtime/non-serializable-value",
1929
2117
  "Runtime values cannot contain cycles"
1930
2118
  );
1931
2119
  ancestors.add(value);
2120
+ copiedContainers++;
1932
2121
  const clone = Array.isArray(value) ? value.map((item) => cloneValue(item, ancestors)) : Object.fromEntries(
1933
2122
  Object.entries(value).map(([key, item]) => [
1934
2123
  key,
@@ -1950,16 +2139,17 @@ var TUPLE_LENGTHS = {
1950
2139
  mat4: 16,
1951
2140
  color: 4
1952
2141
  };
1953
- function outputValue(value, definition, path6) {
2142
+ function outputValue(value, definition, path7, policy) {
2143
+ const adopt = policy === "trust" ? trust : settle;
1954
2144
  if (definition.type !== "bundle" || !definition.fields)
1955
- return runtimeValue(value, definition.type, false, path6);
2145
+ return runtimeValue(value, definition.type, false, path7, adopt);
1956
2146
  const fields = definition.fields;
1957
2147
  const problem = bundleValueProblem(value, fields);
1958
- if (problem) misuse("runtime/value-type", `${path6} ${problem}`);
2148
+ if (problem) misuse("runtime/value-type", `${path7} ${problem}`);
1959
2149
  if (isBundleValue(value))
1960
2150
  for (const [field, item] of Object.entries(value))
1961
- runtimeValue(item, bundleFieldType(fields[field]), true, `${path6}.${field}`);
1962
- return runtimeValue(value, "bundle", false, path6);
2151
+ runtimeValue(item, bundleFieldType(fields[field]), true, `${path7}.${field}`, (checked) => checked);
2152
+ return runtimeValue(value, "bundle", false, path7, adopt);
1963
2153
  }
1964
2154
  function expressionPaths(expression) {
1965
2155
  const paths = [];
@@ -1977,17 +2167,17 @@ function readsBundleFields(node) {
1977
2167
  }
1978
2168
  return false;
1979
2169
  }
1980
- function runtimeValue(value, type, allowUndefined, path6) {
2170
+ function runtimeValue(value, type, allowUndefined, path7, keep = snapshot) {
1981
2171
  if (value === void 0) {
1982
2172
  if (allowUndefined) return void 0;
1983
- misuse("runtime/value-type", `${path6} requires a ${type} value`);
2173
+ misuse("runtime/value-type", `${path7} requires a ${type} value`);
1984
2174
  }
1985
2175
  const tupleLength = TUPLE_LENGTHS[type];
1986
2176
  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
2177
  (item) => type.endsWith("i") ? typeof item === "number" && Number.isInteger(item) : typeof item === "number" && Number.isFinite(item)
1988
2178
  ) || 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);
2179
+ if (!valid) misuse("runtime/value-type", `${path7} is not a valid ${type}`);
2180
+ return keep(value);
1991
2181
  }
1992
2182
  function sameValues(a, b, equal) {
1993
2183
  return a.length === b.length && a.every((value, index) => equal(value, b[index]));
@@ -2047,6 +2237,9 @@ function validateNodeModuleArchitecture(source, fileName, ts2) {
2047
2237
  (statement) => ts2.isVariableStatement(statement) && isDefinition(statement)
2048
2238
  );
2049
2239
  for (const statement of file.statements) {
2240
+ if (ts2.isVariableStatement(statement) && !isDefinition(statement) && !supersededByDefinition(statement) && isPureConstant(statement)) {
2241
+ continue;
2242
+ }
2050
2243
  if (ts2.isVariableStatement(statement) && !isDefinition(statement)) {
2051
2244
  const superseded = supersededByDefinition(statement);
2052
2245
  add(
@@ -2111,6 +2304,48 @@ function validateNodeModuleArchitecture(source, fileName, ts2) {
2111
2304
  if (!declaration || !ts2.isIdentifier(declaration.name)) return null;
2112
2305
  return declaration.name.text === "icon" || declaration.name.text === "runsOn" ? declaration.name.text : null;
2113
2306
  }
2307
+ function isPureConstant(statement) {
2308
+ if (!(statement.declarationList.flags & ts2.NodeFlags.Const)) return false;
2309
+ return statement.declarationList.declarations.every(
2310
+ (declaration) => ts2.isIdentifier(declaration.name) && declaration.initializer !== void 0 && isPure(declaration.initializer, false)
2311
+ );
2312
+ }
2313
+ function isPure(node, frozen) {
2314
+ if (ts2.isNumericLiteral(node) || ts2.isStringLiteral(node) || ts2.isBigIntLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node) || node.kind === ts2.SyntaxKind.TrueKeyword || node.kind === ts2.SyntaxKind.FalseKeyword || node.kind === ts2.SyntaxKind.NullKeyword) return true;
2315
+ if (ts2.isIdentifier(node)) return true;
2316
+ if (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node)) return true;
2317
+ if (ts2.isParenthesizedExpression(node)) return isPure(node.expression, frozen);
2318
+ if (ts2.isAsExpression(node))
2319
+ return isPure(node.expression, frozen || isConstAssertion(node.type));
2320
+ if (ts2.isSatisfiesExpression(node) || ts2.isNonNullExpression(node) || ts2.isTypeAssertionExpression(node))
2321
+ return isPure(node.expression, frozen);
2322
+ if (ts2.isPrefixUnaryExpression(node))
2323
+ return node.operator !== ts2.SyntaxKind.PlusPlusToken && node.operator !== ts2.SyntaxKind.MinusMinusToken && isPure(node.operand, false);
2324
+ if (ts2.isBinaryExpression(node))
2325
+ return !isAssignment(node.operatorToken.kind) && isPure(node.left, false) && isPure(node.right, false);
2326
+ if (ts2.isConditionalExpression(node))
2327
+ return isPure(node.condition, false) && isPure(node.whenTrue, frozen) && isPure(node.whenFalse, frozen);
2328
+ if (ts2.isTemplateExpression(node))
2329
+ return node.templateSpans.every((span) => isPure(span.expression, false));
2330
+ if (ts2.isPropertyAccessExpression(node)) return isPure(node.expression, false);
2331
+ if (ts2.isElementAccessExpression(node))
2332
+ return isPure(node.expression, false) && isPure(node.argumentExpression, false);
2333
+ if (ts2.isArrayLiteralExpression(node))
2334
+ return frozen && node.elements.every((element) => !ts2.isSpreadElement(element) && isPure(element, true));
2335
+ if (ts2.isObjectLiteralExpression(node))
2336
+ return frozen && node.properties.every((property) => ts2.isPropertyAssignment(property) ? !ts2.isComputedPropertyName(property.name) && isPure(property.initializer, true) : ts2.isShorthandPropertyAssignment(property));
2337
+ if (ts2.isCallExpression(node) && ts2.isPropertyAccessExpression(node.expression) && ts2.isIdentifier(node.expression.expression) && node.expression.expression.text === "Object" && node.expression.name.text === "freeze" && node.arguments.length === 1) {
2338
+ const [argument] = node.arguments;
2339
+ return ts2.isArrayLiteralExpression(argument) ? argument.elements.every((element) => !ts2.isSpreadElement(element) && isPure(element, false)) : ts2.isObjectLiteralExpression(argument) ? argument.properties.every((property) => ts2.isPropertyAssignment(property) && !ts2.isComputedPropertyName(property.name) && isPure(property.initializer, false)) : false;
2340
+ }
2341
+ return false;
2342
+ }
2343
+ function isConstAssertion(type) {
2344
+ return ts2.isTypeReferenceNode(type) && ts2.isIdentifier(type.typeName) && type.typeName.text === "const";
2345
+ }
2346
+ function isAssignment(kind) {
2347
+ return kind >= ts2.SyntaxKind.FirstAssignment && kind <= ts2.SyntaxKind.LastAssignment;
2348
+ }
2114
2349
  function isDefinition(statement) {
2115
2350
  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
2351
  }
@@ -2147,25 +2382,41 @@ function normalize(value) {
2147
2382
  }
2148
2383
 
2149
2384
  // src/cli/nodeFiles.ts
2385
+ import { randomBytes } from "node:crypto";
2150
2386
  import fs from "node:fs/promises";
2151
2387
  import path from "node:path";
2388
+ var KEYED_DIR = ".cascade-cache/keyed";
2389
+ var KEY = /^[A-Za-z0-9][A-Za-z0-9._-]*(?:\/[A-Za-z0-9][A-Za-z0-9._-]*)*$/;
2152
2390
  function createNodeFileCapability(project) {
2153
2391
  const within2 = (relative, what) => {
2154
2392
  try {
2155
- return project.resolve(relative.replace(/^\.?\//, ""));
2393
+ const clean = relative.replace(/^\.?\//, "");
2394
+ return what === "writing" ? project.resolve(clean) : project.resolveMedia(clean);
2156
2395
  } catch (error) {
2157
2396
  throw new Error(
2158
2397
  `files: ${what} outside the project is refused \u2014 ${relative} (${error instanceof Error ? error.message : String(error)})`
2159
2398
  );
2160
2399
  }
2161
2400
  };
2162
- const cacheOnly = (relative) => {
2401
+ const writable = (relative) => {
2163
2402
  const clean = relative.replace(/^\.?\//, "");
2164
- if (!clean.startsWith(".cascade-cache/"))
2403
+ const full = within2(clean, "writing");
2404
+ if (!project.writeFolderOf(clean))
2165
2405
  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.`
2406
+ `files: writes are confined to .cascade-cache/ and the folders cascade.json "writes" declares \u2014 got ${clean}. A node's output is derived data; declare an export folder such as "writes": ["renders"] to write named files.`
2167
2407
  );
2168
- return within2(clean, "writing");
2408
+ return full;
2409
+ };
2410
+ const writeWhole2 = async (full, data) => {
2411
+ await fs.mkdir(path.dirname(full), { recursive: true });
2412
+ const staging = `${full}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
2413
+ await fs.writeFile(staging, data);
2414
+ await fs.rename(staging, full);
2415
+ };
2416
+ const keyed = (key) => {
2417
+ if (typeof key !== "string" || key.length > 512 || !KEY.test(key))
2418
+ throw new Error(`files.cache: key ${JSON.stringify(key)} must be letters, digits, ".", "_" and "-" in "/"-separated parts, each starting with a letter or digit`);
2419
+ return within2(`${KEYED_DIR}/${key}`, "writing");
2169
2420
  };
2170
2421
  const live = (options) => {
2171
2422
  if (options.signal.aborted) throw new Error("files: cancelled");
@@ -2177,9 +2428,22 @@ function createNodeFileCapability(project) {
2177
2428
  },
2178
2429
  async write(relative, data, options) {
2179
2430
  live(options);
2180
- const full = cacheOnly(relative);
2181
- await fs.mkdir(path.dirname(full), { recursive: true });
2182
- await fs.writeFile(full, data);
2431
+ await writeWhole2(writable(relative), data);
2432
+ },
2433
+ cache: {
2434
+ async get(key, options) {
2435
+ live(options);
2436
+ try {
2437
+ return new Uint8Array(await fs.readFile(keyed(key)));
2438
+ } catch (error) {
2439
+ if (error.code === "ENOENT") return null;
2440
+ throw error;
2441
+ }
2442
+ },
2443
+ async put(key, data, options) {
2444
+ live(options);
2445
+ await writeWhole2(keyed(key), data);
2446
+ }
2183
2447
  },
2184
2448
  async list(relative, options) {
2185
2449
  live(options);
@@ -2211,56 +2475,198 @@ import { pathToFileURL } from "node:url";
2211
2475
 
2212
2476
  // src/cli/headlessIo.ts
2213
2477
  import crypto from "node:crypto";
2478
+ import { existsSync } from "node:fs";
2214
2479
  import fs2 from "node:fs/promises";
2215
2480
  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
2481
  var CACHE_DIR = ".cascade-cache";
2230
2482
  function contentAddressed(relative, body) {
2231
- const hash = crypto.createHash("sha1").update(body).digest("hex").slice(0, 12);
2483
+ return addressed(relative, crypto.createHash("sha1").update(body).digest("hex").slice(0, 12));
2484
+ }
2485
+ function pixelAddressed(relative, pixels) {
2486
+ const hash = crypto.createHash("sha1").update(`${pixels.width}x${pixels.height}\0`).update(pixels.data).digest("hex").slice(0, 12);
2487
+ return addressed(relative, hash);
2488
+ }
2489
+ function addressed(relative, hash) {
2232
2490
  const directory = path2.posix.dirname(relative);
2233
2491
  const name = path2.posix.basename(relative);
2234
2492
  const [stem, ...rest] = name.split(".");
2235
2493
  const tail = rest.length ? `.${rest.join(".")}` : "";
2236
2494
  return path2.posix.join(directory, `${stem}.${hash}${tail}`);
2237
2495
  }
2496
+ var INTERMEDIATE_PNG = { level: 1, adaptiveFiltering: false };
2497
+ var FINAL_PNG = { level: 6, adaptiveFiltering: true };
2498
+ var sharpLoad;
2499
+ function loadSharp() {
2500
+ sharpLoad ??= import("sharp").then((module) => module.default, () => null);
2501
+ return sharpLoad;
2502
+ }
2503
+ async function encodePixelsPng(pixels, options = INTERMEDIATE_PNG) {
2504
+ const sharp = await loadSharp();
2505
+ if (sharp) {
2506
+ const raw = Buffer.from(pixels.data.buffer, pixels.data.byteOffset, pixels.data.byteLength);
2507
+ return sharp(raw, { raw: { width: pixels.width, height: pixels.height, channels: 4 } }).png({ compressionLevel: options.level, adaptiveFiltering: options.adaptiveFiltering }).toBuffer();
2508
+ }
2509
+ const Canvas = globalThis.OffscreenCanvas;
2510
+ const Data = globalThis.ImageData;
2511
+ if (!Canvas || !Data) throw new Error("cascade/io: cannot encode a PNG here (neither sharp nor a canvas is installed)");
2512
+ const canvas = new Canvas(pixels.width, pixels.height);
2513
+ canvas.getContext("2d").putImageData(new Data(pixels.data, pixels.width, pixels.height), 0, 0);
2514
+ return new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer());
2515
+ }
2516
+ async function writeWhole(full, data) {
2517
+ await fs2.mkdir(path2.dirname(full), { recursive: true });
2518
+ const staging = `${full}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
2519
+ await fs2.writeFile(staging, data);
2520
+ await fs2.rename(staging, full);
2521
+ }
2522
+ var RESIDENT_BUDGET_BYTES = 512 * 1024 * 1024;
2523
+ var ResidentImages = class {
2524
+ constructor(project, budget = RESIDENT_BUDGET_BYTES) {
2525
+ this.project = project;
2526
+ this.budget = budget;
2527
+ this.held = /* @__PURE__ */ new Map();
2528
+ this.bytes = 0;
2529
+ /** Files written behind, for a test or a trace. */
2530
+ this.encodes = 0;
2531
+ }
2532
+ /** Hold the pixels, start the write behind, return the path at once. */
2533
+ keep(relative, pixels) {
2534
+ const target = pixelAddressed(relative, pixels);
2535
+ const known = this.held.get(target);
2536
+ if (known) {
2537
+ this.touch(target, known);
2538
+ return target;
2539
+ }
2540
+ const full = this.project.resolve(target);
2541
+ const entry = {
2542
+ pixels,
2543
+ done: false,
2544
+ written: (async () => {
2545
+ if (existsSync(full)) return;
2546
+ this.encodes += 1;
2547
+ await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
2548
+ })()
2549
+ };
2550
+ entry.written.then(() => {
2551
+ entry.done = true;
2552
+ this.trim();
2553
+ }, () => {
2554
+ entry.done = true;
2555
+ });
2556
+ this.held.set(target, entry);
2557
+ this.bytes += pixels.data.byteLength;
2558
+ this.trim();
2559
+ return target;
2560
+ }
2561
+ /** The pixels behind a path, while held. */
2562
+ pixels(relative) {
2563
+ const entry = this.held.get(relative);
2564
+ if (!entry) return void 0;
2565
+ this.touch(relative, entry);
2566
+ return entry.pixels;
2567
+ }
2568
+ /** Resolve when `relative` (or every held image) is a whole file. */
2569
+ async settle(relative) {
2570
+ if (relative !== void 0) {
2571
+ await this.held.get(relative)?.written;
2572
+ return;
2573
+ }
2574
+ await Promise.all([...this.held.values()].filter((entry) => !entry.done).map((entry) => entry.written));
2575
+ }
2576
+ touch(key, entry) {
2577
+ this.held.delete(key);
2578
+ this.held.set(key, entry);
2579
+ }
2580
+ /** Let the oldest written images go until under budget; a pending one stays. */
2581
+ trim() {
2582
+ if (this.bytes <= this.budget) return;
2583
+ for (const [key, entry] of this.held) {
2584
+ if (this.bytes <= this.budget) break;
2585
+ if (!entry.done) continue;
2586
+ this.held.delete(key);
2587
+ this.bytes -= entry.pixels.data.byteLength;
2588
+ }
2589
+ }
2590
+ };
2591
+ var residentStores = /* @__PURE__ */ new Map();
2592
+ function residentImages(project) {
2593
+ return residentStores.get(project.root);
2594
+ }
2595
+ async function settleProjectImages(project) {
2596
+ await residentImages(project)?.settle();
2597
+ }
2598
+ async function copyImageFile(project, relative, destination) {
2599
+ const clean = relative.replace(/^\.?\//, "");
2600
+ const store = residentImages(project);
2601
+ const pixels = /\.png$/i.test(clean) ? store?.pixels(clean) : void 0;
2602
+ if (pixels) {
2603
+ await writeWhole(destination, await encodePixelsPng(pixels, FINAL_PNG));
2604
+ return;
2605
+ }
2606
+ await store?.settle(clean);
2607
+ await fs2.copyFile(project.resolveMedia(clean), destination);
2608
+ }
2238
2609
  function createIoBridge(project) {
2239
- return {
2610
+ const confined = (relative) => {
2611
+ const clean = relative.replace(/^\.?\//, "");
2612
+ if (!clean.startsWith(`${CACHE_DIR}/`)) {
2613
+ throw new Error(`cascade/io: writes are confined to ${CACHE_DIR}/ \u2014 got ${clean}`);
2614
+ }
2615
+ return clean;
2616
+ };
2617
+ const bridge = {
2618
+ /** An intermediate image: held in memory when the run keeps pixels, else a fast PNG now. */
2619
+ async keep(relative, pixels) {
2620
+ const clean = confined(relative);
2621
+ if (!pixels.width || !pixels.height) throw new Error(`cascade/io: refusing to store an empty image to ${clean}`);
2622
+ const store = residentImages(project);
2623
+ if (store) return store.keep(clean, pixels);
2624
+ const target = pixelAddressed(clean, pixels);
2625
+ const full = project.resolve(target);
2626
+ if (!existsSync(full)) await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
2627
+ return target;
2628
+ },
2629
+ pixels(relative) {
2630
+ return residentImages(project)?.pixels(relative.replace(/^\.?\//, ""));
2631
+ },
2240
2632
  async read(relative) {
2241
- const full = project.resolveMedia(relative.replace(/^\.?\//, ""));
2633
+ const clean = relative.replace(/^\.?\//, "");
2634
+ const full = project.resolveMedia(clean);
2242
2635
  try {
2636
+ await residentImages(project)?.settle(clean);
2243
2637
  return await fs2.readFile(full);
2244
2638
  } catch (error) {
2245
2639
  throw new Error(`cascade/io: cannot load ${relative} \u2014 ${error instanceof Error ? error.message : error}`);
2246
2640
  }
2247
2641
  },
2248
2642
  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
- }
2643
+ const clean = confined(relative);
2253
2644
  if (!data.byteLength) throw new Error(`cascade/io: refusing to write an empty file to ${clean}`);
2254
2645
  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);
2646
+ await writeWhole(project.resolve(target), data);
2258
2647
  return target;
2259
2648
  }
2260
2649
  };
2650
+ return bridge;
2261
2651
  }
2262
- function installProjectIo(project) {
2263
- return installIoBridge(createIoBridge(project));
2652
+ function installProjectIo(project, options = {}) {
2653
+ let removeStore = () => {
2654
+ };
2655
+ if (options.resident) {
2656
+ const store = new ResidentImages(project);
2657
+ const previous = residentStores.get(project.root);
2658
+ residentStores.set(project.root, store);
2659
+ removeStore = () => {
2660
+ if (residentStores.get(project.root) !== store) return;
2661
+ if (previous) residentStores.set(project.root, previous);
2662
+ else residentStores.delete(project.root);
2663
+ };
2664
+ }
2665
+ const removeBridge = installIoBridge(createIoBridge(project));
2666
+ return () => {
2667
+ removeBridge();
2668
+ removeStore();
2669
+ };
2264
2670
  }
2265
2671
 
2266
2672
  // src/cli/nodeAssets.ts
@@ -2280,7 +2686,18 @@ function createNodeAssetCapability(project) {
2280
2686
  const written = await io.write(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, data);
2281
2687
  return { path: written, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
2282
2688
  },
2689
+ // `cascade/image`'s `writeImage` hands an intermediate over as pixels:
2690
+ // the same file policy as `write`, through the bridge's fast PNG.
2691
+ async keepImage(pixels, metadata) {
2692
+ const written = await io.keep(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, pixels);
2693
+ return { path: written, mediaType: "image/png" };
2694
+ },
2695
+ // And reads them back from memory while the run holds them.
2696
+ imagePixels(ref) {
2697
+ return io.pixels(String(ref.path));
2698
+ },
2283
2699
  async resolveUrl(ref) {
2700
+ await settleProjectImages(project);
2284
2701
  const full = project.resolveMedia(String(ref.path).replace(/^\.?\//, ""));
2285
2702
  return { value: pathToFileURL(full).href, release: () => {
2286
2703
  } };
@@ -2300,24 +2717,43 @@ function looksLikeImage(value) {
2300
2717
  return IMAGE_EXTENSIONS.has(path4.extname(file).toLowerCase());
2301
2718
  }
2302
2719
  function collectImageOutputs(graph, options = {}) {
2720
+ return planImageOutputs(graph, options).collect();
2721
+ }
2722
+ function planImageOutputs(graph, options = {}) {
2303
2723
  const wanted = new Set(options.nodes ?? []);
2304
2724
  const extra = new Set(options.extra ?? []);
2305
- const found = [];
2306
2725
  const inspection = graph.inspect();
2307
2726
  const isAssertion = (moduleId2) => moduleId2.startsWith("cascade.assert.");
2308
2727
  const assertions = new Set(inspection.nodes.filter((node) => isAssertion(node.moduleId)).map((node) => node.id));
2309
2728
  const consumed = new Set(inspection.connections.filter((connection) => !assertions.has(connection.target.nodeId)).map((connection) => `${connection.source.nodeId}\0${connection.source.outputName}`));
2729
+ const candidates = [];
2310
2730
  for (const node of inspection.nodes) {
2311
2731
  const named = wanted.has(node.id) || extra.has(node.id);
2312
2732
  if (wanted.size && !named) continue;
2313
2733
  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
- }
2734
+ const ports = Object.entries(node.outputs).filter(([, port]) => port.type === "image" || port.type === "any").map(([name, port]) => ({
2735
+ name,
2736
+ type: port.type,
2737
+ // Consumed downstream: not a final image, though it still counts
2738
+ // towards `shared` when it carries one.
2739
+ skip: !named && port.connected && consumed.has(`${node.id}\0${name}`)
2740
+ }));
2741
+ if (ports.length) candidates.push({ nodeId: node.id, ports });
2319
2742
  }
2320
- return found;
2743
+ return {
2744
+ collect() {
2745
+ const found = [];
2746
+ for (const candidate of candidates) {
2747
+ const values = graph.getOutputs(candidate.nodeId);
2748
+ const ports = candidate.ports.map((port) => ({ ...port, value: values.get(port.name) })).filter((port) => port.value !== void 0 && (port.type === "image" || looksLikeImage(port.value)));
2749
+ for (const port of ports) {
2750
+ if (port.skip) continue;
2751
+ found.push({ nodeId: candidate.nodeId, port: port.name, value: port.value, shared: ports.length > 1 });
2752
+ }
2753
+ }
2754
+ return found;
2755
+ }
2756
+ };
2321
2757
  }
2322
2758
  function unknownNodes(graph, ids) {
2323
2759
  const known = new Set(graph.inspect().nodes.map((node) => node.id));
@@ -2346,7 +2782,8 @@ async function saveStillImages(graph, project, options) {
2346
2782
  continue;
2347
2783
  }
2348
2784
  const name = stillFileName(output, relative);
2349
- await fs3.copyFile(source, path4.join(out.directory, name));
2785
+ if (path4.isAbsolute(relative)) await fs3.copyFile(source, path4.join(out.directory, name));
2786
+ else await copyImageFile(project, relative, path4.join(out.directory, name));
2350
2787
  saved.push({ node: output.nodeId, port: output.port, source, file: out.spell(name) });
2351
2788
  }
2352
2789
  if (out && !saved.length) {
@@ -2378,7 +2815,7 @@ function createDawnGpuHost() {
2378
2815
  try {
2379
2816
  const adapter = await gpu.requestAdapter();
2380
2817
  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();
2818
+ device = await requestDeviceWithAdapterLimits(adapter);
2382
2819
  const info = adapter.info;
2383
2820
  return { device, info: {
2384
2821
  vendor: info.vendor,
@@ -2397,9 +2834,376 @@ function createDawnGpuHost() {
2397
2834
  });
2398
2835
  }
2399
2836
 
2837
+ // src/cli/nodeShape.ts
2838
+ var LONG_EXECUTE_LINES = 150;
2839
+ var MIXED_JOB_COUNT = 4;
2840
+ var JOB_ORDER = ["load", "generate", "colour", "draw", "export"];
2841
+ var BY_NAME = {
2842
+ load: /^(readFile(Sync)?|loadImage|loadFont|loadJson|readJson|fetch|decodeImage)$/,
2843
+ generate: /^(random|rng|hash\d*|noise\w*|\w*[nN]oise\d*|fbm|simplex\w*|perlin\w*|worley\w*|poisson\w*|scatter\w*)$/,
2844
+ colour: /^(oklch|oklab|\w*[oO]klab\w*|\w*[oO]klch\w*|okMix|hsl\w*|palette\w*|mixColou?r|lerpColou?r|\w*Palette)$/,
2845
+ draw: /^(getContext|beginPath|fillRect|strokeRect|drawImage|putImageData|createImageData|arc|lineTo|createRenderPipeline|createComputePipeline|draw|drawIndexed|dispatchWorkgroups)$/,
2846
+ export: /^(writeFile(Sync)?|savePng|saveSvg|saveJpe?g|toBuffer|toBlob|toDataURL|encodePng|encodeJpe?g|encodeImage)$/
2847
+ };
2848
+ var CAPABILITY_RECEIVER = /(^|\.)(files|assets)$/;
2849
+ function measureNodeShape(source, fileName, ts2) {
2850
+ const file = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
2851
+ let executeLines = null;
2852
+ let executeLine = null;
2853
+ const jobs = /* @__PURE__ */ new Set();
2854
+ for (const statement of file.statements) {
2855
+ const body = executeBody(statement);
2856
+ if (!body) continue;
2857
+ const start = file.getLineAndCharacterOfPosition(body.getStart(file)).line;
2858
+ const end = file.getLineAndCharacterOfPosition(body.getEnd()).line;
2859
+ executeLines = end - start + 1;
2860
+ executeLine = file.getLineAndCharacterOfPosition(statement.getStart(file)).line + 1;
2861
+ }
2862
+ visit(file);
2863
+ return { executeLines, executeLine, jobs: JOB_ORDER.filter((job) => jobs.has(job)) };
2864
+ function executeBody(statement) {
2865
+ if (ts2.isFunctionDeclaration(statement) && statement.name?.text === "execute") return statement.body;
2866
+ if (ts2.isVariableStatement(statement)) {
2867
+ for (const declaration of statement.declarationList.declarations) {
2868
+ if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "execute") continue;
2869
+ const init = declaration.initializer;
2870
+ if (init && (ts2.isArrowFunction(init) || ts2.isFunctionExpression(init))) return init.body;
2871
+ }
2872
+ }
2873
+ return void 0;
2874
+ }
2875
+ function visit(node) {
2876
+ if (ts2.isCallExpression(node)) classify(node.expression);
2877
+ ts2.forEachChild(node, visit);
2878
+ }
2879
+ function classify(callee) {
2880
+ let name;
2881
+ if (ts2.isIdentifier(callee)) name = callee.text;
2882
+ else if (ts2.isPropertyAccessExpression(callee)) {
2883
+ name = callee.name.text;
2884
+ const receiver = callee.expression.getText(file);
2885
+ if (CAPABILITY_RECEIVER.test(receiver)) {
2886
+ if (/^(read|list|stat|resolveUrl)$/.test(name)) jobs.add("load");
2887
+ if (name === "write") jobs.add("export");
2888
+ }
2889
+ if (receiver === "Math" && name === "random") {
2890
+ jobs.add("generate");
2891
+ return;
2892
+ }
2893
+ }
2894
+ if (!name) return;
2895
+ for (const job of JOB_ORDER) if (BY_NAME[job].test(name)) jobs.add(job);
2896
+ }
2897
+ }
2898
+ function nodeShapeWarnings(source, fileName, ts2, moduleId2) {
2899
+ const shape = measureNodeShape(source, fileName, ts2);
2900
+ const where = moduleId2 ?? fileName;
2901
+ const out = [];
2902
+ if (shape.executeLines !== null && shape.executeLines > LONG_EXECUTE_LINES) {
2903
+ out.push({
2904
+ phase: "shape",
2905
+ code: "shape/long-execute",
2906
+ message: `${where}: execute is ${shape.executeLines} lines (over ${LONG_EXECUTE_LINES}). Split it into single-job nodes wired in the graph.`,
2907
+ file: fileName,
2908
+ line: shape.executeLine ?? void 0
2909
+ });
2910
+ }
2911
+ const loadsAndExports = shape.jobs.includes("load") && shape.jobs.includes("export") && shape.jobs.length > 2;
2912
+ if (shape.jobs.length >= MIXED_JOB_COUNT || loadsAndExports) {
2913
+ out.push({
2914
+ phase: "shape",
2915
+ code: "shape/mixed-jobs",
2916
+ message: `${where}: one node does ${shape.jobs.join(", ")} (judged from the calls it makes). Give each job its own node.`,
2917
+ file: fileName
2918
+ });
2919
+ }
2920
+ return out;
2921
+ }
2922
+ function reportNodeShape(warnings) {
2923
+ if (!warnings.length) return;
2924
+ console.warn([
2925
+ `Node shape \u2014 ${warnings.length === 1 ? "one warning" : `${warnings.length} warnings`} (advice only, the check still passes):`,
2926
+ ...warnings.map((item) => ` ${item.code}: ${item.message}`),
2927
+ ' See "Design the graph" in doc/PROJECT_AUTHORING.md.'
2928
+ ].join("\n"));
2929
+ }
2930
+
2931
+ // server/src/runtime/stage.ts
2932
+ var BRIDGE_KEY = "__cascadeStageBridge";
2933
+ function installStageBridge(bridge) {
2934
+ const globals = globalThis;
2935
+ const previous = globals[BRIDGE_KEY];
2936
+ globals[BRIDGE_KEY] = bridge ?? void 0;
2937
+ return () => {
2938
+ globals[BRIDGE_KEY] = previous;
2939
+ };
2940
+ }
2941
+
2942
+ // src/cli/projectStages.ts
2943
+ function stageWorkerCount(config, poolSize) {
2944
+ if (poolSize !== void 0) return Math.max(1, Math.floor(poolSize));
2945
+ const fromEnv = Number(process.env.CASCADE_STAGE_WORKERS);
2946
+ if (Number.isInteger(fromEnv) && fromEnv >= 1) return fromEnv;
2947
+ return config.workers ?? defaultStageWorkerCount();
2948
+ }
2949
+ var LANES_KEY = "__cascadeStageLanes";
2950
+ function installedStageLanes() {
2951
+ const lanes = globalThis[LANES_KEY];
2952
+ return typeof lanes === "number" && lanes > 1 ? lanes : 1;
2953
+ }
2954
+ function installProjectStages(project, options = {}) {
2955
+ let worker = null;
2956
+ const warmWorker = (config) => {
2957
+ if (options.worker === false || !config.worker) return null;
2958
+ const entrypoint = config.worker;
2959
+ const sources = stageSources(project, config);
2960
+ const size = stageWorkerCount(config, options.poolSize);
2961
+ const key = `${config.python}\0${entrypoint}\0${sources.roots.join("\0")}\0${size}`;
2962
+ if (worker?.key !== key) {
2963
+ worker?.process.shutdown();
2964
+ worker = {
2965
+ key,
2966
+ process: new PythonWorkerPool(size, () => new PythonWorker(project, entrypoint, config.python, process.env, {
2967
+ sources,
2968
+ // The one-shot path's bound, not Studio's two minutes: an offline
2969
+ // render may run a stage for several.
2970
+ requestTimeoutMs: 10 * 60 * 1e3,
2971
+ // What a stage prints is shown with --verbose only, as before.
2972
+ onStderr: options.onOutput ? (text) => {
2973
+ for (const line2 of text.split("\n")) options.onOutput(null, "stderr", line2);
2974
+ } : options.verbose ? (text) => {
2975
+ for (const line2 of text.split("\n")) console.warn(`[stage-worker] ${line2}`);
2976
+ } : () => {
2977
+ }
2978
+ }))
2979
+ };
2980
+ }
2981
+ return worker.process;
2982
+ };
2983
+ const globals = globalThis;
2984
+ const previousLanes = globals[LANES_KEY];
2985
+ try {
2986
+ const config = readStageConfig(project);
2987
+ globals[LANES_KEY] = options.worker === false || !config.worker ? 1 : stageWorkerCount(config, options.poolSize);
2988
+ } catch {
2989
+ globals[LANES_KEY] = 1;
2990
+ }
2991
+ if (options.prewarm) {
2992
+ try {
2993
+ warmWorker(readStageConfig(project))?.warm();
2994
+ } catch {
2995
+ }
2996
+ }
2997
+ const dispose = installStageBridge(async (stage, args, callOptions) => {
2998
+ await settleProjectImages(project);
2999
+ const config = readStageConfig(project);
3000
+ const warm = warmWorker(config);
3001
+ if (warm) {
3002
+ const { result: result2 } = await callStage(project, config, stage, args, (envelope) => warm.callWithMeta(stage, args, { signal: callOptions?.signal, cascade: envelope }), { onTrace: options.onTrace });
3003
+ return result2;
3004
+ }
3005
+ const { result } = await callStage(project, config, stage, args, async (envelope) => ({
3006
+ result: await runProjectStage(project, stage, args, {
3007
+ // A superseded cook kills its subprocess rather than waiting for it.
3008
+ signal: callOptions?.signal,
3009
+ cascade: envelope,
3010
+ codeHash: envelope.code,
3011
+ /**
3012
+ * A stage can log, and until 0.8 nobody could read it: `stderr` was
3013
+ * consulted only on failure and `stdout` only for its last line. Behind
3014
+ * `--verbose`, because a stage that prints per mark would bury the
3015
+ * render it is part of; prefixed with the stage name, because a graph
3016
+ * runs several.
3017
+ */
3018
+ onOutput: options.onOutput ? (stream, text) => {
3019
+ for (const line2 of text.split("\n")) options.onOutput(stage, stream, line2);
3020
+ } : options.verbose ? (stream, text) => {
3021
+ const write = stream === "stderr" ? console.warn : console.log;
3022
+ for (const line2 of text.split("\n")) write(`[${stage}] ${line2}`);
3023
+ } : void 0
3024
+ })
3025
+ }), { onTrace: options.onTrace });
3026
+ return result;
3027
+ });
3028
+ return () => {
3029
+ dispose();
3030
+ globals[LANES_KEY] = previousLanes;
3031
+ worker?.process.shutdown();
3032
+ worker = null;
3033
+ };
3034
+ }
3035
+
3036
+ // src/cli/runTrace.ts
3037
+ import { AsyncLocalStorage } from "node:async_hooks";
3038
+ import fs4 from "node:fs";
3039
+ import path5 from "node:path";
3040
+ var MAX_NODE_LOGS = 500;
3041
+ var MAX_LOG_LINE = 2e3;
3042
+ var cookingNode = new AsyncLocalStorage();
3043
+ function traceNodeScope(nodeId, run) {
3044
+ return cookingNode.run(nodeId, run);
3045
+ }
3046
+ var RunTrace = class {
3047
+ constructor(graphFile, cascade, seed) {
3048
+ this.graphFile = graphFile;
3049
+ this.cascade = cascade;
3050
+ this.seed = seed;
3051
+ this.runs = [];
3052
+ this.current = null;
3053
+ /** Nodes of the latest run still cooking, by id. */
3054
+ this.cooking = /* @__PURE__ */ new Map();
3055
+ /** Stage calls made outside any node (a run with no graph events). */
3056
+ this.loose = [];
3057
+ this.startedAt = /* @__PURE__ */ new Date();
3058
+ this.runStarted = null;
3059
+ this.envNames = [];
3060
+ }
3061
+ /** The node a stage call or log line belongs to. */
3062
+ owner() {
3063
+ const id = cookingNode.getStore();
3064
+ if (id !== void 0) {
3065
+ const scoped = this.cooking.get(id)?.node;
3066
+ if (scoped) return scoped;
3067
+ }
3068
+ return this.current?.node;
3069
+ }
3070
+ /** Follow one loaded graph's events. Returns the unsubscribe. */
3071
+ attach(graph) {
3072
+ const modules = new Map(graph.inspect().nodes.map((node) => [node.id, node.moduleId]));
3073
+ return graph.subscribe((event) => {
3074
+ if (event.type === "run:start") {
3075
+ this.runs.push({ runId: event.runId, frame: graph.getFrame(), nodes: [] });
3076
+ this.runStarted = performance.now();
3077
+ } else if (event.type === "run:complete") {
3078
+ const run = this.runs[this.runs.length - 1];
3079
+ if (run && this.runStarted !== null) run.ms = Math.round(performance.now() - this.runStarted);
3080
+ this.runStarted = null;
3081
+ } else if (event.type === "node:start") {
3082
+ const run = this.runs[this.runs.length - 1];
3083
+ if (!run) return;
3084
+ const node = { id: event.nodeId, module: modules.get(event.nodeId) ?? "", ms: 0, stages: [] };
3085
+ run.nodes.push(node);
3086
+ this.current = { node, started: performance.now() };
3087
+ this.cooking.set(event.nodeId, this.current);
3088
+ } else if (event.type === "node:complete") {
3089
+ const cooking = this.cooking.get(event.nodeId);
3090
+ if (!cooking) return;
3091
+ cooking.node.ms = Math.round(performance.now() - cooking.started);
3092
+ this.cooking.delete(event.nodeId);
3093
+ if (this.current === cooking) this.current = null;
3094
+ }
3095
+ });
3096
+ }
3097
+ /**
3098
+ * The latest run's whole frame took `frameMs`, its outputs saved: what is
3099
+ * left after the cook is the save. Called by the frame loop per frame and
3100
+ * once by a still run.
3101
+ */
3102
+ recordSaved(frameMs) {
3103
+ const run = this.runs[this.runs.length - 1];
3104
+ if (!run) return;
3105
+ run.frameMs = Math.round(frameMs);
3106
+ if (run.ms !== void 0) run.saveMs = Math.max(0, run.frameMs - run.ms);
3107
+ }
3108
+ /** Record a stage call against the node cooking now. */
3109
+ recordStage(trace) {
3110
+ const node = this.owner();
3111
+ if (node) node.stages.push(trace);
3112
+ else this.loose.push(trace);
3113
+ }
3114
+ /**
3115
+ * Record a line printed while a node cooks, against that node. Returns
3116
+ * false when no node is cooking, so the caller leaves the line alone (the
3117
+ * CLI's own messages are not node output).
3118
+ */
3119
+ recordLog(level, text) {
3120
+ const node = this.owner();
3121
+ if (!node) return false;
3122
+ for (const line2 of text.split("\n")) {
3123
+ if (!line2.trim()) continue;
3124
+ node.logs ??= [];
3125
+ if (node.logs.length >= MAX_NODE_LOGS) {
3126
+ node.logsDropped = (node.logsDropped ?? 0) + 1;
3127
+ continue;
3128
+ }
3129
+ node.logs.push({ level, text: line2.length > MAX_LOG_LINE ? `${line2.slice(0, MAX_LOG_LINE)}\u2026` : line2 });
3130
+ }
3131
+ return true;
3132
+ }
3133
+ /** Every logged line in order, with its node. */
3134
+ logs() {
3135
+ return this.runs.flatMap((run) => run.nodes.flatMap((node) => (node.logs ?? []).map((line2) => ({ node: node.id, ...line2 }))));
3136
+ }
3137
+ /** The names (never the values) of variables `--env` set, for the trace. */
3138
+ setEnvNames(names) {
3139
+ this.envNames = [...names];
3140
+ }
3141
+ /** Every stage call in order, with its node. */
3142
+ stages() {
3143
+ const list = [];
3144
+ for (const run of this.runs) {
3145
+ for (const node of run.nodes) for (const stage of node.stages) list.push({ node: node.id, ...stage });
3146
+ }
3147
+ for (const stage of this.loose) list.push({ node: null, ...stage });
3148
+ return list;
3149
+ }
3150
+ toJSON() {
3151
+ return {
3152
+ format: 1,
3153
+ graph: this.graphFile,
3154
+ startedAt: this.startedAt.toISOString(),
3155
+ cascade: this.cascade,
3156
+ seed: this.seed,
3157
+ ...this.envNames.length ? { env: this.envNames } : {},
3158
+ runs: this.runs
3159
+ };
3160
+ }
3161
+ /** Write the trace into the project and return its project-relative path. */
3162
+ write(projectRoot) {
3163
+ const stamp = this.startedAt.toISOString().replace(/[:.]/g, "-");
3164
+ const name = `${stamp}-${path5.basename(this.graphFile, ".cascade")}.json`;
3165
+ const relative = path5.posix.join(".cascade", "runs", name);
3166
+ const full = path5.join(projectRoot, relative);
3167
+ fs4.mkdirSync(path5.dirname(full), { recursive: true });
3168
+ fs4.writeFileSync(full, JSON.stringify(this.toJSON(), null, 2) + "\n");
3169
+ pruneTraces(path5.dirname(full));
3170
+ return relative;
3171
+ }
3172
+ };
3173
+ var CONSOLE_LEVELS = ["log", "info", "warn", "error", "debug"];
3174
+ function captureNodeConsole(trace) {
3175
+ const original = Object.fromEntries(CONSOLE_LEVELS.map((level) => [level, console[level]]));
3176
+ for (const level of CONSOLE_LEVELS) {
3177
+ console[level] = (...args) => {
3178
+ trace.recordLog(level, args.map((arg) => typeof arg === "string" ? arg : arg instanceof Error ? arg.stack ?? arg.message : safeJson(arg)).join(" "));
3179
+ original[level](...args);
3180
+ };
3181
+ }
3182
+ return () => {
3183
+ for (const level of CONSOLE_LEVELS) console[level] = original[level];
3184
+ };
3185
+ }
3186
+ function safeJson(value) {
3187
+ try {
3188
+ return JSON.stringify(value) ?? String(value);
3189
+ } catch {
3190
+ return String(value);
3191
+ }
3192
+ }
3193
+ var KEPT_TRACES = 200;
3194
+ function pruneTraces(directory) {
3195
+ const traces = fs4.readdirSync(directory).filter((name) => name.endsWith(".json")).sort();
3196
+ for (const name of traces.slice(0, Math.max(0, traces.length - KEPT_TRACES))) {
3197
+ fs4.rmSync(path5.join(directory, name), { force: true });
3198
+ }
3199
+ }
3200
+
2400
3201
  // src/cli/projectRuntime.ts
3202
+ function headlessExecution() {
3203
+ return { values: "trust", concurrency: installedStageLanes(), nodeScope: traceNodeScope };
3204
+ }
2401
3205
  function createProjectHost(file, registrations) {
2402
- const project = new ProjectRoot(path5.dirname(path5.resolve(file)));
3206
+ const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
2403
3207
  return createNodeRuntimeHost({
2404
3208
  modules: resolver(registrations),
2405
3209
  shell: project.shell,
@@ -2524,6 +3328,7 @@ async function checkProjectGraph(file, document2, strict = false) {
2524
3328
  throw new Error(`Static check requires definition-v1 modules; dynamic modules: ${dynamic.join(", ")}`);
2525
3329
  }
2526
3330
  await preflightAgainstRunHost(file, prepared.registrations, document2);
3331
+ reportNodeShape(prepared.shape);
2527
3332
  return unresolved;
2528
3333
  }
2529
3334
  async function runDeterministicProjectGraph(file, document2, entryNode, observe, afterRun) {
@@ -2533,7 +3338,10 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
2533
3338
  const host = createProjectHost(file, prepared.registrations);
2534
3339
  const runtime = createRuntime({
2535
3340
  host,
2536
- nodes: prepared.registrations
3341
+ nodes: prepared.registrations,
3342
+ // A run trusts what its nodes hand over instead of walking it to freeze
3343
+ // (Studio and `cook` keep `"freeze"`), and overlaps independent stages.
3344
+ ...headlessExecution()
2537
3345
  });
2538
3346
  let graph;
2539
3347
  try {
@@ -2547,6 +3355,7 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
2547
3355
  if (result.status !== "completed") {
2548
3356
  throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
2549
3357
  }
3358
+ await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
2550
3359
  await afterRun?.(graph);
2551
3360
  return true;
2552
3361
  } finally {
@@ -2570,6 +3379,7 @@ async function cookProjectGraph(file, document2, target, observe) {
2570
3379
  const result = await graph.run(target ? { target: { kind: "node", nodeId: target } } : {});
2571
3380
  if (result.status !== "completed")
2572
3381
  throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
3382
+ await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
2573
3383
  const outputs = /* @__PURE__ */ new Map();
2574
3384
  for (const node of graph.inspect().nodes) outputs.set(node.id, new Map(graph.getOutputs(node.id)));
2575
3385
  return { outputs, assets: host.capabilities?.assets };
@@ -2585,11 +3395,15 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2585
3395
  const prepared = await prepare(file, document2, true);
2586
3396
  reportUnresolved(prepared);
2587
3397
  if (!usesDeterministicHost(document2, prepared)) return null;
2588
- const project = new ProjectRoot(path5.dirname(path5.resolve(file)));
3398
+ const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
2589
3399
  const host = createProjectHost(file, prepared.registrations);
2590
3400
  const runtime = createRuntime({
2591
3401
  host,
2592
- nodes: prepared.registrations
3402
+ nodes: prepared.registrations,
3403
+ // Every frame hands over fresh values; walking them to freeze was most
3404
+ // of a big-array frame. Independent stages (the morph's four renders)
3405
+ // cook side by side on the stage pool.
3406
+ ...headlessExecution()
2593
3407
  });
2594
3408
  let graph;
2595
3409
  const width = sequenceWidth(options.end);
@@ -2605,9 +3419,10 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2605
3419
  const targets = options.nodes?.length ? options.nodes : options.entryNode ? [options.entryNode] : [];
2606
3420
  const extra = options.outNodes ?? [];
2607
3421
  const out = outDirectory(options.out, (relative) => project.resolve(relative));
2608
- await fs4.mkdir(out.directory, { recursive: true });
3422
+ await fs5.mkdir(out.directory, { recursive: true });
2609
3423
  const missing = unknownNodes(graph, [...targets, ...extra]);
2610
3424
  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).`);
3425
+ const imageOutputs = planImageOutputs(graph, { nodes: targets, extra });
2611
3426
  for await (const frame of options.frameSource ?? frameRange(options.start, options.end, options.step ?? 1)) {
2612
3427
  const frameStarted = performance.now();
2613
3428
  if (options.signal?.aborted) {
@@ -2639,7 +3454,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2639
3454
  `frame ${frame} ${result.status} \u2014 ${result.diagnostics.map((item) => item.message).join("; ") || "no diagnostics"}`
2640
3455
  );
2641
3456
  }
2642
- const outputs = collectImageOutputs(graph, { nodes: targets, extra });
3457
+ const outputs = imageOutputs.collect();
2643
3458
  if (!outputs.length) {
2644
3459
  throw new Error(
2645
3460
  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."
@@ -2651,7 +3466,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2651
3466
  if (!source) continue;
2652
3467
  const name = sequenceFileName(output.nodeId, frame, source, width, output.shared ? output.port : void 0);
2653
3468
  const relative = source.replace(/^\.?\//, "");
2654
- await fs4.copyFile(project.resolveMedia(relative), path5.join(out.directory, name));
3469
+ await copyImageFile(project, relative, path6.join(out.directory, name));
2655
3470
  files.push(out.spell(name));
2656
3471
  written.push({ nodeId: output.nodeId, port: output.port, source: relative, file: out.spell(name) });
2657
3472
  }
@@ -2659,6 +3474,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2659
3474
  await options.onFrame?.({ frame, outputs: written, durationMs: performance.now() - frameStarted });
2660
3475
  if (options.verbose) console.log(` frame ${frame}`);
2661
3476
  }
3477
+ await settleProjectImages(project);
2662
3478
  try {
2663
3479
  gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
2664
3480
  } catch {
@@ -2691,7 +3507,7 @@ async function inspectProjectGraph(file, document2) {
2691
3507
  };
2692
3508
  });
2693
3509
  return {
2694
- file: path5.resolve(file),
3510
+ file: path6.resolve(file),
2695
3511
  version: document2.version ?? null,
2696
3512
  deterministic: nodes.length > 0 && nodes.every((node) => node.classification === "definition-v1"),
2697
3513
  nodes,
@@ -2702,11 +3518,12 @@ async function prepare(file, document2, compileExecutors) {
2702
3518
  if (!document2 || typeof document2 !== "object" || !Array.isArray(document2.nodes)) {
2703
3519
  throw new Error("Graph document must contain a nodes array");
2704
3520
  }
2705
- const projectRoot = path5.dirname(path5.resolve(file));
3521
+ const projectRoot = path6.dirname(path6.resolve(file));
2706
3522
  const registrations = [];
2707
3523
  const deterministic = /* @__PURE__ */ new Set();
2708
3524
  const seen = /* @__PURE__ */ new Set();
2709
3525
  const unresolved = [];
3526
+ const shape = [];
2710
3527
  for (const node of document2.nodes) {
2711
3528
  const id = moduleId(node);
2712
3529
  if (!id || seen.has(id)) continue;
@@ -2728,7 +3545,7 @@ async function prepare(file, document2, compileExecutors) {
2728
3545
  registrations.push(savedRegistration(id, node));
2729
3546
  continue;
2730
3547
  }
2731
- const source = await fs4.readFile(moduleFile, "utf8");
3548
+ const source = await fs5.readFile(moduleFile, "utf8");
2732
3549
  const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
2733
3550
  const extracted = extractNodeDefinition(source, moduleFile, ts);
2734
3551
  if (!extracted.ok) {
@@ -2744,24 +3561,34 @@ async function prepare(file, document2, compileExecutors) {
2744
3561
  if (architectureDiagnostics.length) {
2745
3562
  throw new Error(architectureDiagnostics.map(formatDiagnostic).join("\n"));
2746
3563
  }
3564
+ shape.push(...nodeShapeWarnings(source, moduleFile, ts, id));
2747
3565
  let executor;
2748
3566
  deterministic.add(id);
2749
3567
  registrations.push({
2750
3568
  kind: "definition-v1",
2751
3569
  moduleId: id,
2752
3570
  definition: extracted.definition,
2753
- loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile) : async () => {
3571
+ loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile).then((execute) => (
3572
+ // A server node may read an image input with `node:fs`, the one
3573
+ // reader the host cannot make wait, so it starts once every image
3574
+ // written behind is on disk. Portable and browser nodes cannot
3575
+ // reach the filesystem and keep the in-memory handoff.
3576
+ extracted.definition.runsOn === "server" ? async (...args) => {
3577
+ await settleProjectImages({ root: projectRoot });
3578
+ return execute(...args);
3579
+ } : execute
3580
+ )) : async () => {
2754
3581
  throw new Error("Static validation never loads execute");
2755
3582
  }
2756
3583
  });
2757
3584
  }
2758
- return { registrations, deterministic, unresolved };
3585
+ return { registrations, deterministic, unresolved, shape };
2759
3586
  }
2760
3587
  async function projectModuleFile(root, node, id) {
2761
3588
  const tried = [];
2762
3589
  const source = node.source;
2763
3590
  if (source && typeof source === "object" && source.type === "project" && typeof source.file === "string") {
2764
- const candidate2 = path5.resolve(root, source.file);
3591
+ const candidate2 = path6.resolve(root, source.file);
2765
3592
  if (within(root, candidate2)) {
2766
3593
  tried.push(candidate2);
2767
3594
  if (await exists(candidate2)) return { file: candidate2, tried };
@@ -2769,7 +3596,7 @@ async function projectModuleFile(root, node, id) {
2769
3596
  }
2770
3597
  if (!id.startsWith("project.")) return { file: null, tried };
2771
3598
  const name = id.slice("project.".length);
2772
- const candidate = path5.resolve(root, "nodes", name, "index.ts");
3599
+ const candidate = path6.resolve(root, "nodes", name, "index.ts");
2773
3600
  if (!within(root, candidate)) return { file: null, tried };
2774
3601
  tried.push(candidate);
2775
3602
  return { file: await exists(candidate) ? candidate : null, tried };
@@ -2787,7 +3614,7 @@ function reExportedDefinition(source) {
2787
3614
  });
2788
3615
  }
2789
3616
  function unresolvedModule(id, node, tried, root) {
2790
- const relative = tried.map((candidate) => path5.relative(root, candidate) || candidate);
3617
+ const relative = tried.map((candidate) => path6.relative(root, candidate) || candidate);
2791
3618
  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
3619
  return {
2793
3620
  phase: "prepare",
@@ -2797,9 +3624,9 @@ function unresolvedModule(id, node, tried, root) {
2797
3624
  };
2798
3625
  }
2799
3626
  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`);
3627
+ const cache = path6.join(projectRoot, ".cascade-cache", "nodes");
3628
+ await fs5.mkdir(cache, { recursive: true });
3629
+ const output = path6.join(cache, `${path6.basename(path6.dirname(moduleFile))}.mjs`);
2803
3630
  const staging = `${output}.${process.pid}.${Date.now()}.tmp`;
2804
3631
  await build({
2805
3632
  entryPoints: [moduleFile],
@@ -2812,24 +3639,27 @@ async function compileExecute(projectRoot, moduleFile) {
2812
3639
  sourcemap: "inline",
2813
3640
  plugins: [cliImageModule()]
2814
3641
  });
2815
- await fs4.rename(staging, output);
3642
+ await fs5.rename(staging, output);
2816
3643
  const loaded = await import(`${pathToFileURL2(output).href}?v=${Date.now()}`);
2817
3644
  if (typeof loaded.execute !== "function") throw new Error(`${moduleFile} does not export execute`);
2818
3645
  return loaded.execute;
2819
3646
  }
2820
3647
  function cliImageModule() {
2821
- const here = path5.dirname(fileURLToPath(import.meta.url));
2822
- const source = [
2823
- path5.join(here, "runtime", "image.ts"),
3648
+ const here = path6.dirname(fileURLToPath(import.meta.url));
3649
+ const shim = (name) => [
3650
+ path6.join(here, "runtime", `${name}.ts`),
2824
3651
  // dist/cli/runtime, copied by build-cli
2825
- path5.join(here, "..", "..", "server", "src", "runtime", "image.ts")
3652
+ path6.join(here, "..", "..", "server", "src", "runtime", `${name}.ts`)
2826
3653
  // running from src
2827
- ].find((candidate) => existsSync(candidate));
3654
+ ].find((candidate) => existsSync2(candidate));
3655
+ const sources = { image: shim("image"), io: shim("io") };
2828
3656
  return {
2829
3657
  name: "cascade-cli-image",
2830
3658
  setup(builder) {
2831
- if (!source) return;
2832
- builder.onResolve({ filter: /^(?:@field\/)?cascade\/image$/ }, () => ({ path: source }));
3659
+ builder.onResolve({ filter: /^(?:@field\/)?cascade\/(?:image|io)$/ }, (args) => {
3660
+ const source = sources[args.path.endsWith("/io") ? "io" : "image"];
3661
+ return source ? { path: source } : void 0;
3662
+ });
2833
3663
  }
2834
3664
  };
2835
3665
  }
@@ -2919,12 +3749,12 @@ function formatDiagnostic(diagnostic) {
2919
3749
  return `${location}${diagnostic.code}: ${diagnostic.message}`;
2920
3750
  }
2921
3751
  function within(root, candidate) {
2922
- const relative = path5.relative(path5.resolve(root), candidate);
2923
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
3752
+ const relative = path6.relative(path6.resolve(root), candidate);
3753
+ return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
2924
3754
  }
2925
3755
  async function exists(file) {
2926
3756
  try {
2927
- await fs4.access(file);
3757
+ await fs5.access(file);
2928
3758
  return true;
2929
3759
  } catch {
2930
3760
  return false;
@@ -2989,6 +3819,10 @@ export {
2989
3819
  validateNodeModuleArchitecture,
2990
3820
  installProjectIo,
2991
3821
  saveStillImages,
3822
+ installProjectStages,
3823
+ cookingNode,
3824
+ RunTrace,
3825
+ captureNodeConsole,
2992
3826
  createProjectHost,
2993
3827
  classifyPreflight,
2994
3828
  validateProjectGraph,
@@ -3003,4 +3837,4 @@ export {
3003
3837
  builtinModuleIds,
3004
3838
  diagnoseGraph
3005
3839
  };
3006
- //# sourceMappingURL=chunk-EUIAC3U3.js.map
3840
+ //# sourceMappingURL=chunk-QKL7CCRS.js.map