@field/cascade 0.11.0 → 0.12.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 (216) hide show
  1. package/AGENTS.md +7 -4
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +195 -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-RQZ4SX5R.js → chunk-2JEU3MGB.js} +2 -2
  73. package/dist/cli/{chunk-WKURDVYP.js → chunk-3RXL5GHB.js} +28 -331
  74. package/dist/cli/chunk-53LGFPWX.js +138 -0
  75. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  76. package/dist/cli/chunk-73S4JZXK.js +336 -0
  77. package/dist/cli/{chunk-YAT5Y25U.js → chunk-7QE2ZNSN.js} +3 -3
  78. package/dist/cli/{chunk-SGJAET2L.js → chunk-BPODRH52.js} +8 -4
  79. package/dist/cli/{chunk-XWDSKG7N.js → chunk-BZ63XQ3I.js} +2 -2
  80. package/dist/cli/{chunk-FCDXJ6EK.js → chunk-CTPLRSWF.js} +2 -13
  81. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  82. package/dist/cli/{chunk-WIH4TEIS.js → chunk-EZRWJ4G2.js} +1 -1
  83. package/dist/cli/{chunk-BIJO5OFS.js → chunk-FWMSGKEH.js} +1 -1
  84. package/dist/cli/{chunk-POMMNKZZ.js → chunk-G23A2BZZ.js} +38 -5
  85. package/dist/cli/{chunk-35D343DC.js → chunk-GRIN2DDP.js} +19 -1
  86. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  87. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  88. package/dist/cli/{chunk-GFS6W3C5.js → chunk-L6B6FUQP.js} +54 -32
  89. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  90. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  91. package/dist/cli/{chunk-EUIAC3U3.js → chunk-MP653A5O.js} +884 -124
  92. package/dist/cli/{chunk-A4OAMOZH.js → chunk-NJFMDXHR.js} +13 -3
  93. package/dist/cli/{chunk-3JTJHF7V.js → chunk-OM6BEME6.js} +4 -4
  94. package/dist/cli/{chunk-DUIUFVU2.js → chunk-ST6LROUG.js} +49 -23
  95. package/dist/cli/{chunk-RLCAJBVR.js → chunk-TJW67QSI.js} +8 -7
  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-JJN5Z3ZC.js} +23 -17
  102. package/dist/cli/{commands-Q23M26QM.js → commands-PHYFYA3F.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-ORWQRNYP.js} +36 -6
  106. package/dist/cli/{edit-JHCPTQFT.js → edit-S5KEQ5RA.js} +3 -3
  107. package/dist/cli/editorCommands-NFR32JOY.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-EOUOBOIJ.js} +4 -4
  114. package/dist/cli/{graphFile-VPSQ6OF7.js → graphFile-YTGJB5F5.js} +25 -19
  115. package/dist/cli/{headlessCanvas-5XLDZT7M.js → headlessCanvas-ENRHC7GS.js} +3 -2
  116. package/dist/cli/{helpCommand-3VRE3KVO.js → helpCommand-PULH6ZYB.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-JVZFQOEX.js} +4 -4
  121. package/dist/cli/{parity-KINARCP2.js → parity-X6DEH2DV.js} +42 -36
  122. package/dist/cli/{pop-USDFXB7H.js → pop-B7SIIYKN.js} +5 -5
  123. package/dist/cli/project-VYGIA7JX.js +12 -0
  124. package/dist/cli/{projectDefinition-DMTZN2NE.js → projectDefinition-F3D7UIHJ.js} +8 -8
  125. package/dist/cli/{projectRuntime-Z5PJOH47.js → projectRuntime-2Q4JKEH3.js} +19 -12
  126. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  127. package/dist/cli/{render-YURAPPUI.js → render-PT4MWI6U.js} +24 -22
  128. package/dist/cli/{runCommand-PI764LEG.js → runCommand-HNPQVZMS.js} +3 -3
  129. package/dist/cli/runner-5XPXVW7J.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-MHF6TNRK.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-WPPMGGW7.js +44 -0
  138. package/dist/contracts/capabilities.d.ts +18 -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 +3 -2
  152. package/dist/player/editor.js +3 -2
  153. package/dist/player/editorEngine.js +3 -2
  154. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  155. package/dist/runtime/builtins/assert/checks.js +207 -0
  156. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  157. package/dist/runtime/builtins/assert/definitions.js +54 -0
  158. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  159. package/dist/runtime/builtins/assert/index.js +36 -0
  160. package/dist/runtime/builtins/image/contactSheet.d.ts +3 -165
  161. package/dist/runtime/builtins/image/contactSheet.js +0 -49
  162. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  163. package/dist/runtime/builtins/image/definitions.js +343 -0
  164. package/dist/runtime/builtins/image/file.d.ts +8 -0
  165. package/dist/runtime/builtins/image/file.js +37 -0
  166. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  167. package/dist/runtime/builtins/image/filters.js +394 -0
  168. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  169. package/dist/runtime/builtins/image/generators.js +144 -0
  170. package/dist/runtime/builtins/image/index.d.ts +947 -4
  171. package/dist/runtime/builtins/image/index.js +41 -6
  172. package/dist/runtime/builtins/image/io.d.ts +100 -0
  173. package/dist/runtime/builtins/image/io.js +142 -0
  174. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  175. package/dist/runtime/builtins/image/planar.js +274 -0
  176. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  177. package/dist/runtime/builtins/image/simplex.js +92 -0
  178. package/dist/runtime/builtins/image/text.d.ts +4 -0
  179. package/dist/runtime/builtins/image/text.js +141 -0
  180. package/dist/runtime/builtins/index.d.ts +3 -0
  181. package/dist/runtime/builtins/index.js +7 -0
  182. package/dist/runtime/gpu.d.ts +25 -0
  183. package/dist/runtime/gpu.js +40 -0
  184. package/dist/runtime/runtime.d.ts +2 -0
  185. package/dist/runtime/runtime.js +268 -44
  186. package/dist/runtime/types.d.ts +30 -0
  187. package/doc/CLI_REFERENCE.md +21 -3
  188. package/doc/EDITOR_EXPORT.md +26 -0
  189. package/doc/MCP.md +2 -1
  190. package/doc/NODE_REFERENCE.md +286 -14
  191. package/doc/PROJECT_AUTHORING.md +98 -3
  192. package/package.json +4 -1
  193. package/dist/assets/CodePanel-BU-autmq.js +0 -2
  194. package/dist/assets/CustomNodeDialog-BRpDfOV5.js +0 -1
  195. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +0 -1
  196. package/dist/assets/ProjectSettings-DLOXLxcA.js +0 -2
  197. package/dist/assets/definitions-GreEMXLV.js +0 -1
  198. package/dist/assets/geo-u0n_jOsD.js +0 -1
  199. package/dist/assets/image-cAa-Nu9O.js +0 -2062
  200. package/dist/assets/index-5ZtDAuy2.js +0 -220
  201. package/dist/assets/index-7vae6YFC.css +0 -1
  202. package/dist/assets/math-DioIQOPJ.js +0 -1
  203. package/dist/assets/pop-DC_TQG9t.js +0 -1
  204. package/dist/cli/chunk-36P5K3SD.js +0 -631
  205. package/dist/cli/chunk-4UDFYF5G.js +0 -132
  206. package/dist/cli/chunk-53NMBJAO.js +0 -35
  207. package/dist/cli/chunk-MIIEFUGG.js +0 -623
  208. package/dist/cli/chunk-QBBKRNGY.js +0 -90
  209. package/dist/cli/chunk-WPUDGEFY.js +0 -20
  210. package/dist/cli/editorCommands-UIF5YLUZ.js +0 -33
  211. package/dist/cli/project-K3QIPGBB.js +0 -12
  212. package/dist/cli/registerStandardNodes-S2VLMF6J.js +0 -3081
  213. package/dist/cli/runner-RA6UKEPD.js +0 -37
  214. package/dist/cli/webBuild-GT4PZT5D.js +0 -37
  215. package/dist/runtime/builtins/headless.d.ts +0 -19
  216. 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-2JEU3MGB.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]));
@@ -2211,56 +2401,198 @@ import { pathToFileURL } from "node:url";
2211
2401
 
2212
2402
  // src/cli/headlessIo.ts
2213
2403
  import crypto from "node:crypto";
2404
+ import { existsSync } from "node:fs";
2214
2405
  import fs2 from "node:fs/promises";
2215
2406
  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
2407
  var CACHE_DIR = ".cascade-cache";
2230
2408
  function contentAddressed(relative, body) {
2231
- const hash = crypto.createHash("sha1").update(body).digest("hex").slice(0, 12);
2409
+ return addressed(relative, crypto.createHash("sha1").update(body).digest("hex").slice(0, 12));
2410
+ }
2411
+ function pixelAddressed(relative, pixels) {
2412
+ const hash = crypto.createHash("sha1").update(`${pixels.width}x${pixels.height}\0`).update(pixels.data).digest("hex").slice(0, 12);
2413
+ return addressed(relative, hash);
2414
+ }
2415
+ function addressed(relative, hash) {
2232
2416
  const directory = path2.posix.dirname(relative);
2233
2417
  const name = path2.posix.basename(relative);
2234
2418
  const [stem, ...rest] = name.split(".");
2235
2419
  const tail = rest.length ? `.${rest.join(".")}` : "";
2236
2420
  return path2.posix.join(directory, `${stem}.${hash}${tail}`);
2237
2421
  }
2422
+ var INTERMEDIATE_PNG = { level: 1, adaptiveFiltering: false };
2423
+ var FINAL_PNG = { level: 6, adaptiveFiltering: true };
2424
+ var sharpLoad;
2425
+ function loadSharp() {
2426
+ sharpLoad ??= import("sharp").then((module) => module.default, () => null);
2427
+ return sharpLoad;
2428
+ }
2429
+ async function encodePixelsPng(pixels, options = INTERMEDIATE_PNG) {
2430
+ const sharp = await loadSharp();
2431
+ if (sharp) {
2432
+ const raw = Buffer.from(pixels.data.buffer, pixels.data.byteOffset, pixels.data.byteLength);
2433
+ return sharp(raw, { raw: { width: pixels.width, height: pixels.height, channels: 4 } }).png({ compressionLevel: options.level, adaptiveFiltering: options.adaptiveFiltering }).toBuffer();
2434
+ }
2435
+ const Canvas = globalThis.OffscreenCanvas;
2436
+ const Data = globalThis.ImageData;
2437
+ if (!Canvas || !Data) throw new Error("cascade/io: cannot encode a PNG here (neither sharp nor a canvas is installed)");
2438
+ const canvas = new Canvas(pixels.width, pixels.height);
2439
+ canvas.getContext("2d").putImageData(new Data(pixels.data, pixels.width, pixels.height), 0, 0);
2440
+ return new Uint8Array(await (await canvas.convertToBlob({ type: "image/png" })).arrayBuffer());
2441
+ }
2442
+ async function writeWhole(full, data) {
2443
+ await fs2.mkdir(path2.dirname(full), { recursive: true });
2444
+ const staging = `${full}.${process.pid}.${crypto.randomBytes(4).toString("hex")}.tmp`;
2445
+ await fs2.writeFile(staging, data);
2446
+ await fs2.rename(staging, full);
2447
+ }
2448
+ var RESIDENT_BUDGET_BYTES = 512 * 1024 * 1024;
2449
+ var ResidentImages = class {
2450
+ constructor(project, budget = RESIDENT_BUDGET_BYTES) {
2451
+ this.project = project;
2452
+ this.budget = budget;
2453
+ this.held = /* @__PURE__ */ new Map();
2454
+ this.bytes = 0;
2455
+ /** Files written behind, for a test or a trace. */
2456
+ this.encodes = 0;
2457
+ }
2458
+ /** Hold the pixels, start the write behind, return the path at once. */
2459
+ keep(relative, pixels) {
2460
+ const target = pixelAddressed(relative, pixels);
2461
+ const known = this.held.get(target);
2462
+ if (known) {
2463
+ this.touch(target, known);
2464
+ return target;
2465
+ }
2466
+ const full = this.project.resolve(target);
2467
+ const entry = {
2468
+ pixels,
2469
+ done: false,
2470
+ written: (async () => {
2471
+ if (existsSync(full)) return;
2472
+ this.encodes += 1;
2473
+ await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
2474
+ })()
2475
+ };
2476
+ entry.written.then(() => {
2477
+ entry.done = true;
2478
+ this.trim();
2479
+ }, () => {
2480
+ entry.done = true;
2481
+ });
2482
+ this.held.set(target, entry);
2483
+ this.bytes += pixels.data.byteLength;
2484
+ this.trim();
2485
+ return target;
2486
+ }
2487
+ /** The pixels behind a path, while held. */
2488
+ pixels(relative) {
2489
+ const entry = this.held.get(relative);
2490
+ if (!entry) return void 0;
2491
+ this.touch(relative, entry);
2492
+ return entry.pixels;
2493
+ }
2494
+ /** Resolve when `relative` (or every held image) is a whole file. */
2495
+ async settle(relative) {
2496
+ if (relative !== void 0) {
2497
+ await this.held.get(relative)?.written;
2498
+ return;
2499
+ }
2500
+ await Promise.all([...this.held.values()].filter((entry) => !entry.done).map((entry) => entry.written));
2501
+ }
2502
+ touch(key, entry) {
2503
+ this.held.delete(key);
2504
+ this.held.set(key, entry);
2505
+ }
2506
+ /** Let the oldest written images go until under budget; a pending one stays. */
2507
+ trim() {
2508
+ if (this.bytes <= this.budget) return;
2509
+ for (const [key, entry] of this.held) {
2510
+ if (this.bytes <= this.budget) break;
2511
+ if (!entry.done) continue;
2512
+ this.held.delete(key);
2513
+ this.bytes -= entry.pixels.data.byteLength;
2514
+ }
2515
+ }
2516
+ };
2517
+ var residentStores = /* @__PURE__ */ new Map();
2518
+ function residentImages(project) {
2519
+ return residentStores.get(project.root);
2520
+ }
2521
+ async function settleProjectImages(project) {
2522
+ await residentImages(project)?.settle();
2523
+ }
2524
+ async function copyImageFile(project, relative, destination) {
2525
+ const clean = relative.replace(/^\.?\//, "");
2526
+ const store = residentImages(project);
2527
+ const pixels = /\.png$/i.test(clean) ? store?.pixels(clean) : void 0;
2528
+ if (pixels) {
2529
+ await writeWhole(destination, await encodePixelsPng(pixels, FINAL_PNG));
2530
+ return;
2531
+ }
2532
+ await store?.settle(clean);
2533
+ await fs2.copyFile(project.resolveMedia(clean), destination);
2534
+ }
2238
2535
  function createIoBridge(project) {
2239
- return {
2536
+ const confined = (relative) => {
2537
+ const clean = relative.replace(/^\.?\//, "");
2538
+ if (!clean.startsWith(`${CACHE_DIR}/`)) {
2539
+ throw new Error(`cascade/io: writes are confined to ${CACHE_DIR}/ \u2014 got ${clean}`);
2540
+ }
2541
+ return clean;
2542
+ };
2543
+ const bridge = {
2544
+ /** An intermediate image: held in memory when the run keeps pixels, else a fast PNG now. */
2545
+ async keep(relative, pixels) {
2546
+ const clean = confined(relative);
2547
+ if (!pixels.width || !pixels.height) throw new Error(`cascade/io: refusing to store an empty image to ${clean}`);
2548
+ const store = residentImages(project);
2549
+ if (store) return store.keep(clean, pixels);
2550
+ const target = pixelAddressed(clean, pixels);
2551
+ const full = project.resolve(target);
2552
+ if (!existsSync(full)) await writeWhole(full, await encodePixelsPng(pixels, INTERMEDIATE_PNG));
2553
+ return target;
2554
+ },
2555
+ pixels(relative) {
2556
+ return residentImages(project)?.pixels(relative.replace(/^\.?\//, ""));
2557
+ },
2240
2558
  async read(relative) {
2241
- const full = project.resolveMedia(relative.replace(/^\.?\//, ""));
2559
+ const clean = relative.replace(/^\.?\//, "");
2560
+ const full = project.resolveMedia(clean);
2242
2561
  try {
2562
+ await residentImages(project)?.settle(clean);
2243
2563
  return await fs2.readFile(full);
2244
2564
  } catch (error) {
2245
2565
  throw new Error(`cascade/io: cannot load ${relative} \u2014 ${error instanceof Error ? error.message : error}`);
2246
2566
  }
2247
2567
  },
2248
2568
  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
- }
2569
+ const clean = confined(relative);
2253
2570
  if (!data.byteLength) throw new Error(`cascade/io: refusing to write an empty file to ${clean}`);
2254
2571
  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);
2572
+ await writeWhole(project.resolve(target), data);
2258
2573
  return target;
2259
2574
  }
2260
2575
  };
2576
+ return bridge;
2261
2577
  }
2262
- function installProjectIo(project) {
2263
- return installIoBridge(createIoBridge(project));
2578
+ function installProjectIo(project, options = {}) {
2579
+ let removeStore = () => {
2580
+ };
2581
+ if (options.resident) {
2582
+ const store = new ResidentImages(project);
2583
+ const previous = residentStores.get(project.root);
2584
+ residentStores.set(project.root, store);
2585
+ removeStore = () => {
2586
+ if (residentStores.get(project.root) !== store) return;
2587
+ if (previous) residentStores.set(project.root, previous);
2588
+ else residentStores.delete(project.root);
2589
+ };
2590
+ }
2591
+ const removeBridge = installIoBridge(createIoBridge(project));
2592
+ return () => {
2593
+ removeBridge();
2594
+ removeStore();
2595
+ };
2264
2596
  }
2265
2597
 
2266
2598
  // src/cli/nodeAssets.ts
@@ -2280,7 +2612,18 @@ function createNodeAssetCapability(project) {
2280
2612
  const written = await io.write(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, data);
2281
2613
  return { path: written, ...metadata.mediaType ? { mediaType: metadata.mediaType } : {} };
2282
2614
  },
2615
+ // `cascade/image`'s `writeImage` hands an intermediate over as pixels:
2616
+ // the same file policy as `write`, through the bridge's fast PNG.
2617
+ async keepImage(pixels, metadata) {
2618
+ const written = await io.keep(`${CACHE_DIR2}/${assetFileName(metadata.suggestedName)}`, pixels);
2619
+ return { path: written, mediaType: "image/png" };
2620
+ },
2621
+ // And reads them back from memory while the run holds them.
2622
+ imagePixels(ref) {
2623
+ return io.pixels(String(ref.path));
2624
+ },
2283
2625
  async resolveUrl(ref) {
2626
+ await settleProjectImages(project);
2284
2627
  const full = project.resolveMedia(String(ref.path).replace(/^\.?\//, ""));
2285
2628
  return { value: pathToFileURL(full).href, release: () => {
2286
2629
  } };
@@ -2300,24 +2643,43 @@ function looksLikeImage(value) {
2300
2643
  return IMAGE_EXTENSIONS.has(path4.extname(file).toLowerCase());
2301
2644
  }
2302
2645
  function collectImageOutputs(graph, options = {}) {
2646
+ return planImageOutputs(graph, options).collect();
2647
+ }
2648
+ function planImageOutputs(graph, options = {}) {
2303
2649
  const wanted = new Set(options.nodes ?? []);
2304
2650
  const extra = new Set(options.extra ?? []);
2305
- const found = [];
2306
2651
  const inspection = graph.inspect();
2307
2652
  const isAssertion = (moduleId2) => moduleId2.startsWith("cascade.assert.");
2308
2653
  const assertions = new Set(inspection.nodes.filter((node) => isAssertion(node.moduleId)).map((node) => node.id));
2309
2654
  const consumed = new Set(inspection.connections.filter((connection) => !assertions.has(connection.target.nodeId)).map((connection) => `${connection.source.nodeId}\0${connection.source.outputName}`));
2655
+ const candidates = [];
2310
2656
  for (const node of inspection.nodes) {
2311
2657
  const named = wanted.has(node.id) || extra.has(node.id);
2312
2658
  if (wanted.size && !named) continue;
2313
2659
  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
- }
2660
+ const ports = Object.entries(node.outputs).filter(([, port]) => port.type === "image" || port.type === "any").map(([name, port]) => ({
2661
+ name,
2662
+ type: port.type,
2663
+ // Consumed downstream: not a final image, though it still counts
2664
+ // towards `shared` when it carries one.
2665
+ skip: !named && port.connected && consumed.has(`${node.id}\0${name}`)
2666
+ }));
2667
+ if (ports.length) candidates.push({ nodeId: node.id, ports });
2319
2668
  }
2320
- return found;
2669
+ return {
2670
+ collect() {
2671
+ const found = [];
2672
+ for (const candidate of candidates) {
2673
+ const values = graph.getOutputs(candidate.nodeId);
2674
+ const ports = candidate.ports.map((port) => ({ ...port, value: values.get(port.name) })).filter((port) => port.value !== void 0 && (port.type === "image" || looksLikeImage(port.value)));
2675
+ for (const port of ports) {
2676
+ if (port.skip) continue;
2677
+ found.push({ nodeId: candidate.nodeId, port: port.name, value: port.value, shared: ports.length > 1 });
2678
+ }
2679
+ }
2680
+ return found;
2681
+ }
2682
+ };
2321
2683
  }
2322
2684
  function unknownNodes(graph, ids) {
2323
2685
  const known = new Set(graph.inspect().nodes.map((node) => node.id));
@@ -2346,7 +2708,8 @@ async function saveStillImages(graph, project, options) {
2346
2708
  continue;
2347
2709
  }
2348
2710
  const name = stillFileName(output, relative);
2349
- await fs3.copyFile(source, path4.join(out.directory, name));
2711
+ if (path4.isAbsolute(relative)) await fs3.copyFile(source, path4.join(out.directory, name));
2712
+ else await copyImageFile(project, relative, path4.join(out.directory, name));
2350
2713
  saved.push({ node: output.nodeId, port: output.port, source, file: out.spell(name) });
2351
2714
  }
2352
2715
  if (out && !saved.length) {
@@ -2378,7 +2741,7 @@ function createDawnGpuHost() {
2378
2741
  try {
2379
2742
  const adapter = await gpu.requestAdapter();
2380
2743
  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();
2744
+ device = await requestDeviceWithAdapterLimits(adapter);
2382
2745
  const info = adapter.info;
2383
2746
  return { device, info: {
2384
2747
  vendor: info.vendor,
@@ -2397,9 +2760,376 @@ function createDawnGpuHost() {
2397
2760
  });
2398
2761
  }
2399
2762
 
2763
+ // src/cli/nodeShape.ts
2764
+ var LONG_EXECUTE_LINES = 150;
2765
+ var MIXED_JOB_COUNT = 4;
2766
+ var JOB_ORDER = ["load", "generate", "colour", "draw", "export"];
2767
+ var BY_NAME = {
2768
+ load: /^(readFile(Sync)?|loadImage|loadFont|loadJson|readJson|fetch|decodeImage)$/,
2769
+ generate: /^(random|rng|hash\d*|noise\w*|\w*[nN]oise\d*|fbm|simplex\w*|perlin\w*|worley\w*|poisson\w*|scatter\w*)$/,
2770
+ colour: /^(oklch|oklab|\w*[oO]klab\w*|\w*[oO]klch\w*|okMix|hsl\w*|palette\w*|mixColou?r|lerpColou?r|\w*Palette)$/,
2771
+ draw: /^(getContext|beginPath|fillRect|strokeRect|drawImage|putImageData|createImageData|arc|lineTo|createRenderPipeline|createComputePipeline|draw|drawIndexed|dispatchWorkgroups)$/,
2772
+ export: /^(writeFile(Sync)?|savePng|saveSvg|saveJpe?g|toBuffer|toBlob|toDataURL|encodePng|encodeJpe?g|encodeImage)$/
2773
+ };
2774
+ var CAPABILITY_RECEIVER = /(^|\.)(files|assets)$/;
2775
+ function measureNodeShape(source, fileName, ts2) {
2776
+ const file = ts2.createSourceFile(fileName, source, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TS);
2777
+ let executeLines = null;
2778
+ let executeLine = null;
2779
+ const jobs = /* @__PURE__ */ new Set();
2780
+ for (const statement of file.statements) {
2781
+ const body = executeBody(statement);
2782
+ if (!body) continue;
2783
+ const start = file.getLineAndCharacterOfPosition(body.getStart(file)).line;
2784
+ const end = file.getLineAndCharacterOfPosition(body.getEnd()).line;
2785
+ executeLines = end - start + 1;
2786
+ executeLine = file.getLineAndCharacterOfPosition(statement.getStart(file)).line + 1;
2787
+ }
2788
+ visit(file);
2789
+ return { executeLines, executeLine, jobs: JOB_ORDER.filter((job) => jobs.has(job)) };
2790
+ function executeBody(statement) {
2791
+ if (ts2.isFunctionDeclaration(statement) && statement.name?.text === "execute") return statement.body;
2792
+ if (ts2.isVariableStatement(statement)) {
2793
+ for (const declaration of statement.declarationList.declarations) {
2794
+ if (!ts2.isIdentifier(declaration.name) || declaration.name.text !== "execute") continue;
2795
+ const init = declaration.initializer;
2796
+ if (init && (ts2.isArrowFunction(init) || ts2.isFunctionExpression(init))) return init.body;
2797
+ }
2798
+ }
2799
+ return void 0;
2800
+ }
2801
+ function visit(node) {
2802
+ if (ts2.isCallExpression(node)) classify(node.expression);
2803
+ ts2.forEachChild(node, visit);
2804
+ }
2805
+ function classify(callee) {
2806
+ let name;
2807
+ if (ts2.isIdentifier(callee)) name = callee.text;
2808
+ else if (ts2.isPropertyAccessExpression(callee)) {
2809
+ name = callee.name.text;
2810
+ const receiver = callee.expression.getText(file);
2811
+ if (CAPABILITY_RECEIVER.test(receiver)) {
2812
+ if (/^(read|list|stat|resolveUrl)$/.test(name)) jobs.add("load");
2813
+ if (name === "write") jobs.add("export");
2814
+ }
2815
+ if (receiver === "Math" && name === "random") {
2816
+ jobs.add("generate");
2817
+ return;
2818
+ }
2819
+ }
2820
+ if (!name) return;
2821
+ for (const job of JOB_ORDER) if (BY_NAME[job].test(name)) jobs.add(job);
2822
+ }
2823
+ }
2824
+ function nodeShapeWarnings(source, fileName, ts2, moduleId2) {
2825
+ const shape = measureNodeShape(source, fileName, ts2);
2826
+ const where = moduleId2 ?? fileName;
2827
+ const out = [];
2828
+ if (shape.executeLines !== null && shape.executeLines > LONG_EXECUTE_LINES) {
2829
+ out.push({
2830
+ phase: "shape",
2831
+ code: "shape/long-execute",
2832
+ message: `${where}: execute is ${shape.executeLines} lines (over ${LONG_EXECUTE_LINES}). Split it into single-job nodes wired in the graph.`,
2833
+ file: fileName,
2834
+ line: shape.executeLine ?? void 0
2835
+ });
2836
+ }
2837
+ const loadsAndExports = shape.jobs.includes("load") && shape.jobs.includes("export") && shape.jobs.length > 2;
2838
+ if (shape.jobs.length >= MIXED_JOB_COUNT || loadsAndExports) {
2839
+ out.push({
2840
+ phase: "shape",
2841
+ code: "shape/mixed-jobs",
2842
+ message: `${where}: one node does ${shape.jobs.join(", ")} (judged from the calls it makes). Give each job its own node.`,
2843
+ file: fileName
2844
+ });
2845
+ }
2846
+ return out;
2847
+ }
2848
+ function reportNodeShape(warnings) {
2849
+ if (!warnings.length) return;
2850
+ console.warn([
2851
+ `Node shape \u2014 ${warnings.length === 1 ? "one warning" : `${warnings.length} warnings`} (advice only, the check still passes):`,
2852
+ ...warnings.map((item) => ` ${item.code}: ${item.message}`),
2853
+ ' See "Design the graph" in doc/PROJECT_AUTHORING.md.'
2854
+ ].join("\n"));
2855
+ }
2856
+
2857
+ // server/src/runtime/stage.ts
2858
+ var BRIDGE_KEY = "__cascadeStageBridge";
2859
+ function installStageBridge(bridge) {
2860
+ const globals = globalThis;
2861
+ const previous = globals[BRIDGE_KEY];
2862
+ globals[BRIDGE_KEY] = bridge ?? void 0;
2863
+ return () => {
2864
+ globals[BRIDGE_KEY] = previous;
2865
+ };
2866
+ }
2867
+
2868
+ // src/cli/projectStages.ts
2869
+ function stageWorkerCount(config, poolSize) {
2870
+ if (poolSize !== void 0) return Math.max(1, Math.floor(poolSize));
2871
+ const fromEnv = Number(process.env.CASCADE_STAGE_WORKERS);
2872
+ if (Number.isInteger(fromEnv) && fromEnv >= 1) return fromEnv;
2873
+ return config.workers ?? defaultStageWorkerCount();
2874
+ }
2875
+ var LANES_KEY = "__cascadeStageLanes";
2876
+ function installedStageLanes() {
2877
+ const lanes = globalThis[LANES_KEY];
2878
+ return typeof lanes === "number" && lanes > 1 ? lanes : 1;
2879
+ }
2880
+ function installProjectStages(project, options = {}) {
2881
+ let worker = null;
2882
+ const warmWorker = (config) => {
2883
+ if (options.worker === false || !config.worker) return null;
2884
+ const entrypoint = config.worker;
2885
+ const sources = stageSources(project, config);
2886
+ const size = stageWorkerCount(config, options.poolSize);
2887
+ const key = `${config.python}\0${entrypoint}\0${sources.roots.join("\0")}\0${size}`;
2888
+ if (worker?.key !== key) {
2889
+ worker?.process.shutdown();
2890
+ worker = {
2891
+ key,
2892
+ process: new PythonWorkerPool(size, () => new PythonWorker(project, entrypoint, config.python, process.env, {
2893
+ sources,
2894
+ // The one-shot path's bound, not Studio's two minutes: an offline
2895
+ // render may run a stage for several.
2896
+ requestTimeoutMs: 10 * 60 * 1e3,
2897
+ // What a stage prints is shown with --verbose only, as before.
2898
+ onStderr: options.onOutput ? (text) => {
2899
+ for (const line2 of text.split("\n")) options.onOutput(null, "stderr", line2);
2900
+ } : options.verbose ? (text) => {
2901
+ for (const line2 of text.split("\n")) console.warn(`[stage-worker] ${line2}`);
2902
+ } : () => {
2903
+ }
2904
+ }))
2905
+ };
2906
+ }
2907
+ return worker.process;
2908
+ };
2909
+ const globals = globalThis;
2910
+ const previousLanes = globals[LANES_KEY];
2911
+ try {
2912
+ const config = readStageConfig(project);
2913
+ globals[LANES_KEY] = options.worker === false || !config.worker ? 1 : stageWorkerCount(config, options.poolSize);
2914
+ } catch {
2915
+ globals[LANES_KEY] = 1;
2916
+ }
2917
+ if (options.prewarm) {
2918
+ try {
2919
+ warmWorker(readStageConfig(project))?.warm();
2920
+ } catch {
2921
+ }
2922
+ }
2923
+ const dispose = installStageBridge(async (stage, args, callOptions) => {
2924
+ await settleProjectImages(project);
2925
+ const config = readStageConfig(project);
2926
+ const warm = warmWorker(config);
2927
+ if (warm) {
2928
+ const { result: result2 } = await callStage(project, config, stage, args, (envelope) => warm.callWithMeta(stage, args, { signal: callOptions?.signal, cascade: envelope }), { onTrace: options.onTrace });
2929
+ return result2;
2930
+ }
2931
+ const { result } = await callStage(project, config, stage, args, async (envelope) => ({
2932
+ result: await runProjectStage(project, stage, args, {
2933
+ // A superseded cook kills its subprocess rather than waiting for it.
2934
+ signal: callOptions?.signal,
2935
+ cascade: envelope,
2936
+ codeHash: envelope.code,
2937
+ /**
2938
+ * A stage can log, and until 0.8 nobody could read it: `stderr` was
2939
+ * consulted only on failure and `stdout` only for its last line. Behind
2940
+ * `--verbose`, because a stage that prints per mark would bury the
2941
+ * render it is part of; prefixed with the stage name, because a graph
2942
+ * runs several.
2943
+ */
2944
+ onOutput: options.onOutput ? (stream, text) => {
2945
+ for (const line2 of text.split("\n")) options.onOutput(stage, stream, line2);
2946
+ } : options.verbose ? (stream, text) => {
2947
+ const write = stream === "stderr" ? console.warn : console.log;
2948
+ for (const line2 of text.split("\n")) write(`[${stage}] ${line2}`);
2949
+ } : void 0
2950
+ })
2951
+ }), { onTrace: options.onTrace });
2952
+ return result;
2953
+ });
2954
+ return () => {
2955
+ dispose();
2956
+ globals[LANES_KEY] = previousLanes;
2957
+ worker?.process.shutdown();
2958
+ worker = null;
2959
+ };
2960
+ }
2961
+
2962
+ // src/cli/runTrace.ts
2963
+ import { AsyncLocalStorage } from "node:async_hooks";
2964
+ import fs4 from "node:fs";
2965
+ import path5 from "node:path";
2966
+ var MAX_NODE_LOGS = 500;
2967
+ var MAX_LOG_LINE = 2e3;
2968
+ var cookingNode = new AsyncLocalStorage();
2969
+ function traceNodeScope(nodeId, run) {
2970
+ return cookingNode.run(nodeId, run);
2971
+ }
2972
+ var RunTrace = class {
2973
+ constructor(graphFile, cascade, seed) {
2974
+ this.graphFile = graphFile;
2975
+ this.cascade = cascade;
2976
+ this.seed = seed;
2977
+ this.runs = [];
2978
+ this.current = null;
2979
+ /** Nodes of the latest run still cooking, by id. */
2980
+ this.cooking = /* @__PURE__ */ new Map();
2981
+ /** Stage calls made outside any node (a run with no graph events). */
2982
+ this.loose = [];
2983
+ this.startedAt = /* @__PURE__ */ new Date();
2984
+ this.runStarted = null;
2985
+ this.envNames = [];
2986
+ }
2987
+ /** The node a stage call or log line belongs to. */
2988
+ owner() {
2989
+ const id = cookingNode.getStore();
2990
+ if (id !== void 0) {
2991
+ const scoped = this.cooking.get(id)?.node;
2992
+ if (scoped) return scoped;
2993
+ }
2994
+ return this.current?.node;
2995
+ }
2996
+ /** Follow one loaded graph's events. Returns the unsubscribe. */
2997
+ attach(graph) {
2998
+ const modules = new Map(graph.inspect().nodes.map((node) => [node.id, node.moduleId]));
2999
+ return graph.subscribe((event) => {
3000
+ if (event.type === "run:start") {
3001
+ this.runs.push({ runId: event.runId, frame: graph.getFrame(), nodes: [] });
3002
+ this.runStarted = performance.now();
3003
+ } else if (event.type === "run:complete") {
3004
+ const run = this.runs[this.runs.length - 1];
3005
+ if (run && this.runStarted !== null) run.ms = Math.round(performance.now() - this.runStarted);
3006
+ this.runStarted = null;
3007
+ } else if (event.type === "node:start") {
3008
+ const run = this.runs[this.runs.length - 1];
3009
+ if (!run) return;
3010
+ const node = { id: event.nodeId, module: modules.get(event.nodeId) ?? "", ms: 0, stages: [] };
3011
+ run.nodes.push(node);
3012
+ this.current = { node, started: performance.now() };
3013
+ this.cooking.set(event.nodeId, this.current);
3014
+ } else if (event.type === "node:complete") {
3015
+ const cooking = this.cooking.get(event.nodeId);
3016
+ if (!cooking) return;
3017
+ cooking.node.ms = Math.round(performance.now() - cooking.started);
3018
+ this.cooking.delete(event.nodeId);
3019
+ if (this.current === cooking) this.current = null;
3020
+ }
3021
+ });
3022
+ }
3023
+ /**
3024
+ * The latest run's whole frame took `frameMs`, its outputs saved: what is
3025
+ * left after the cook is the save. Called by the frame loop per frame and
3026
+ * once by a still run.
3027
+ */
3028
+ recordSaved(frameMs) {
3029
+ const run = this.runs[this.runs.length - 1];
3030
+ if (!run) return;
3031
+ run.frameMs = Math.round(frameMs);
3032
+ if (run.ms !== void 0) run.saveMs = Math.max(0, run.frameMs - run.ms);
3033
+ }
3034
+ /** Record a stage call against the node cooking now. */
3035
+ recordStage(trace) {
3036
+ const node = this.owner();
3037
+ if (node) node.stages.push(trace);
3038
+ else this.loose.push(trace);
3039
+ }
3040
+ /**
3041
+ * Record a line printed while a node cooks, against that node. Returns
3042
+ * false when no node is cooking, so the caller leaves the line alone (the
3043
+ * CLI's own messages are not node output).
3044
+ */
3045
+ recordLog(level, text) {
3046
+ const node = this.owner();
3047
+ if (!node) return false;
3048
+ for (const line2 of text.split("\n")) {
3049
+ if (!line2.trim()) continue;
3050
+ node.logs ??= [];
3051
+ if (node.logs.length >= MAX_NODE_LOGS) {
3052
+ node.logsDropped = (node.logsDropped ?? 0) + 1;
3053
+ continue;
3054
+ }
3055
+ node.logs.push({ level, text: line2.length > MAX_LOG_LINE ? `${line2.slice(0, MAX_LOG_LINE)}\u2026` : line2 });
3056
+ }
3057
+ return true;
3058
+ }
3059
+ /** Every logged line in order, with its node. */
3060
+ logs() {
3061
+ return this.runs.flatMap((run) => run.nodes.flatMap((node) => (node.logs ?? []).map((line2) => ({ node: node.id, ...line2 }))));
3062
+ }
3063
+ /** The names (never the values) of variables `--env` set, for the trace. */
3064
+ setEnvNames(names) {
3065
+ this.envNames = [...names];
3066
+ }
3067
+ /** Every stage call in order, with its node. */
3068
+ stages() {
3069
+ const list = [];
3070
+ for (const run of this.runs) {
3071
+ for (const node of run.nodes) for (const stage of node.stages) list.push({ node: node.id, ...stage });
3072
+ }
3073
+ for (const stage of this.loose) list.push({ node: null, ...stage });
3074
+ return list;
3075
+ }
3076
+ toJSON() {
3077
+ return {
3078
+ format: 1,
3079
+ graph: this.graphFile,
3080
+ startedAt: this.startedAt.toISOString(),
3081
+ cascade: this.cascade,
3082
+ seed: this.seed,
3083
+ ...this.envNames.length ? { env: this.envNames } : {},
3084
+ runs: this.runs
3085
+ };
3086
+ }
3087
+ /** Write the trace into the project and return its project-relative path. */
3088
+ write(projectRoot) {
3089
+ const stamp = this.startedAt.toISOString().replace(/[:.]/g, "-");
3090
+ const name = `${stamp}-${path5.basename(this.graphFile, ".cascade")}.json`;
3091
+ const relative = path5.posix.join(".cascade", "runs", name);
3092
+ const full = path5.join(projectRoot, relative);
3093
+ fs4.mkdirSync(path5.dirname(full), { recursive: true });
3094
+ fs4.writeFileSync(full, JSON.stringify(this.toJSON(), null, 2) + "\n");
3095
+ pruneTraces(path5.dirname(full));
3096
+ return relative;
3097
+ }
3098
+ };
3099
+ var CONSOLE_LEVELS = ["log", "info", "warn", "error", "debug"];
3100
+ function captureNodeConsole(trace) {
3101
+ const original = Object.fromEntries(CONSOLE_LEVELS.map((level) => [level, console[level]]));
3102
+ for (const level of CONSOLE_LEVELS) {
3103
+ console[level] = (...args) => {
3104
+ trace.recordLog(level, args.map((arg) => typeof arg === "string" ? arg : arg instanceof Error ? arg.stack ?? arg.message : safeJson(arg)).join(" "));
3105
+ original[level](...args);
3106
+ };
3107
+ }
3108
+ return () => {
3109
+ for (const level of CONSOLE_LEVELS) console[level] = original[level];
3110
+ };
3111
+ }
3112
+ function safeJson(value) {
3113
+ try {
3114
+ return JSON.stringify(value) ?? String(value);
3115
+ } catch {
3116
+ return String(value);
3117
+ }
3118
+ }
3119
+ var KEPT_TRACES = 200;
3120
+ function pruneTraces(directory) {
3121
+ const traces = fs4.readdirSync(directory).filter((name) => name.endsWith(".json")).sort();
3122
+ for (const name of traces.slice(0, Math.max(0, traces.length - KEPT_TRACES))) {
3123
+ fs4.rmSync(path5.join(directory, name), { force: true });
3124
+ }
3125
+ }
3126
+
2400
3127
  // src/cli/projectRuntime.ts
3128
+ function headlessExecution() {
3129
+ return { values: "trust", concurrency: installedStageLanes(), nodeScope: traceNodeScope };
3130
+ }
2401
3131
  function createProjectHost(file, registrations) {
2402
- const project = new ProjectRoot(path5.dirname(path5.resolve(file)));
3132
+ const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
2403
3133
  return createNodeRuntimeHost({
2404
3134
  modules: resolver(registrations),
2405
3135
  shell: project.shell,
@@ -2524,6 +3254,7 @@ async function checkProjectGraph(file, document2, strict = false) {
2524
3254
  throw new Error(`Static check requires definition-v1 modules; dynamic modules: ${dynamic.join(", ")}`);
2525
3255
  }
2526
3256
  await preflightAgainstRunHost(file, prepared.registrations, document2);
3257
+ reportNodeShape(prepared.shape);
2527
3258
  return unresolved;
2528
3259
  }
2529
3260
  async function runDeterministicProjectGraph(file, document2, entryNode, observe, afterRun) {
@@ -2533,7 +3264,10 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
2533
3264
  const host = createProjectHost(file, prepared.registrations);
2534
3265
  const runtime = createRuntime({
2535
3266
  host,
2536
- nodes: prepared.registrations
3267
+ nodes: prepared.registrations,
3268
+ // A run trusts what its nodes hand over instead of walking it to freeze
3269
+ // (Studio and `cook` keep `"freeze"`), and overlaps independent stages.
3270
+ ...headlessExecution()
2537
3271
  });
2538
3272
  let graph;
2539
3273
  try {
@@ -2547,6 +3281,7 @@ async function runDeterministicProjectGraph(file, document2, entryNode, observe,
2547
3281
  if (result.status !== "completed") {
2548
3282
  throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
2549
3283
  }
3284
+ await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
2550
3285
  await afterRun?.(graph);
2551
3286
  return true;
2552
3287
  } finally {
@@ -2570,6 +3305,7 @@ async function cookProjectGraph(file, document2, target, observe) {
2570
3305
  const result = await graph.run(target ? { target: { kind: "node", nodeId: target } } : {});
2571
3306
  if (result.status !== "completed")
2572
3307
  throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
3308
+ await settleProjectImages({ root: path6.dirname(path6.resolve(file)) });
2573
3309
  const outputs = /* @__PURE__ */ new Map();
2574
3310
  for (const node of graph.inspect().nodes) outputs.set(node.id, new Map(graph.getOutputs(node.id)));
2575
3311
  return { outputs, assets: host.capabilities?.assets };
@@ -2585,11 +3321,15 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2585
3321
  const prepared = await prepare(file, document2, true);
2586
3322
  reportUnresolved(prepared);
2587
3323
  if (!usesDeterministicHost(document2, prepared)) return null;
2588
- const project = new ProjectRoot(path5.dirname(path5.resolve(file)));
3324
+ const project = new ProjectRoot(path6.dirname(path6.resolve(file)));
2589
3325
  const host = createProjectHost(file, prepared.registrations);
2590
3326
  const runtime = createRuntime({
2591
3327
  host,
2592
- nodes: prepared.registrations
3328
+ nodes: prepared.registrations,
3329
+ // Every frame hands over fresh values; walking them to freeze was most
3330
+ // of a big-array frame. Independent stages (the morph's four renders)
3331
+ // cook side by side on the stage pool.
3332
+ ...headlessExecution()
2593
3333
  });
2594
3334
  let graph;
2595
3335
  const width = sequenceWidth(options.end);
@@ -2605,9 +3345,10 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2605
3345
  const targets = options.nodes?.length ? options.nodes : options.entryNode ? [options.entryNode] : [];
2606
3346
  const extra = options.outNodes ?? [];
2607
3347
  const out = outDirectory(options.out, (relative) => project.resolve(relative));
2608
- await fs4.mkdir(out.directory, { recursive: true });
3348
+ await fs5.mkdir(out.directory, { recursive: true });
2609
3349
  const missing = unknownNodes(graph, [...targets, ...extra]);
2610
3350
  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).`);
3351
+ const imageOutputs = planImageOutputs(graph, { nodes: targets, extra });
2611
3352
  for await (const frame of options.frameSource ?? frameRange(options.start, options.end, options.step ?? 1)) {
2612
3353
  const frameStarted = performance.now();
2613
3354
  if (options.signal?.aborted) {
@@ -2639,7 +3380,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2639
3380
  `frame ${frame} ${result.status} \u2014 ${result.diagnostics.map((item) => item.message).join("; ") || "no diagnostics"}`
2640
3381
  );
2641
3382
  }
2642
- const outputs = collectImageOutputs(graph, { nodes: targets, extra });
3383
+ const outputs = imageOutputs.collect();
2643
3384
  if (!outputs.length) {
2644
3385
  throw new Error(
2645
3386
  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 +3392,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2651
3392
  if (!source) continue;
2652
3393
  const name = sequenceFileName(output.nodeId, frame, source, width, output.shared ? output.port : void 0);
2653
3394
  const relative = source.replace(/^\.?\//, "");
2654
- await fs4.copyFile(project.resolveMedia(relative), path5.join(out.directory, name));
3395
+ await copyImageFile(project, relative, path6.join(out.directory, name));
2655
3396
  files.push(out.spell(name));
2656
3397
  written.push({ nodeId: output.nodeId, port: output.port, source: relative, file: out.spell(name) });
2657
3398
  }
@@ -2659,6 +3400,7 @@ async function renderDeterministicProjectFrames(file, document2, options) {
2659
3400
  await options.onFrame?.({ frame, outputs: written, durationMs: performance.now() - frameStarted });
2660
3401
  if (options.verbose) console.log(` frame ${frame}`);
2661
3402
  }
3403
+ await settleProjectImages(project);
2662
3404
  try {
2663
3405
  gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
2664
3406
  } catch {
@@ -2691,7 +3433,7 @@ async function inspectProjectGraph(file, document2) {
2691
3433
  };
2692
3434
  });
2693
3435
  return {
2694
- file: path5.resolve(file),
3436
+ file: path6.resolve(file),
2695
3437
  version: document2.version ?? null,
2696
3438
  deterministic: nodes.length > 0 && nodes.every((node) => node.classification === "definition-v1"),
2697
3439
  nodes,
@@ -2702,11 +3444,12 @@ async function prepare(file, document2, compileExecutors) {
2702
3444
  if (!document2 || typeof document2 !== "object" || !Array.isArray(document2.nodes)) {
2703
3445
  throw new Error("Graph document must contain a nodes array");
2704
3446
  }
2705
- const projectRoot = path5.dirname(path5.resolve(file));
3447
+ const projectRoot = path6.dirname(path6.resolve(file));
2706
3448
  const registrations = [];
2707
3449
  const deterministic = /* @__PURE__ */ new Set();
2708
3450
  const seen = /* @__PURE__ */ new Set();
2709
3451
  const unresolved = [];
3452
+ const shape = [];
2710
3453
  for (const node of document2.nodes) {
2711
3454
  const id = moduleId(node);
2712
3455
  if (!id || seen.has(id)) continue;
@@ -2728,7 +3471,7 @@ async function prepare(file, document2, compileExecutors) {
2728
3471
  registrations.push(savedRegistration(id, node));
2729
3472
  continue;
2730
3473
  }
2731
- const source = await fs4.readFile(moduleFile, "utf8");
3474
+ const source = await fs5.readFile(moduleFile, "utf8");
2732
3475
  const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
2733
3476
  const extracted = extractNodeDefinition(source, moduleFile, ts);
2734
3477
  if (!extracted.ok) {
@@ -2744,24 +3487,34 @@ async function prepare(file, document2, compileExecutors) {
2744
3487
  if (architectureDiagnostics.length) {
2745
3488
  throw new Error(architectureDiagnostics.map(formatDiagnostic).join("\n"));
2746
3489
  }
3490
+ shape.push(...nodeShapeWarnings(source, moduleFile, ts, id));
2747
3491
  let executor;
2748
3492
  deterministic.add(id);
2749
3493
  registrations.push({
2750
3494
  kind: "definition-v1",
2751
3495
  moduleId: id,
2752
3496
  definition: extracted.definition,
2753
- loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile) : async () => {
3497
+ loadExecute: compileExecutors ? () => executor ??= compileExecute(projectRoot, moduleFile).then((execute) => (
3498
+ // A server node may read an image input with `node:fs`, the one
3499
+ // reader the host cannot make wait, so it starts once every image
3500
+ // written behind is on disk. Portable and browser nodes cannot
3501
+ // reach the filesystem and keep the in-memory handoff.
3502
+ extracted.definition.runsOn === "server" ? async (...args) => {
3503
+ await settleProjectImages({ root: projectRoot });
3504
+ return execute(...args);
3505
+ } : execute
3506
+ )) : async () => {
2754
3507
  throw new Error("Static validation never loads execute");
2755
3508
  }
2756
3509
  });
2757
3510
  }
2758
- return { registrations, deterministic, unresolved };
3511
+ return { registrations, deterministic, unresolved, shape };
2759
3512
  }
2760
3513
  async function projectModuleFile(root, node, id) {
2761
3514
  const tried = [];
2762
3515
  const source = node.source;
2763
3516
  if (source && typeof source === "object" && source.type === "project" && typeof source.file === "string") {
2764
- const candidate2 = path5.resolve(root, source.file);
3517
+ const candidate2 = path6.resolve(root, source.file);
2765
3518
  if (within(root, candidate2)) {
2766
3519
  tried.push(candidate2);
2767
3520
  if (await exists(candidate2)) return { file: candidate2, tried };
@@ -2769,7 +3522,7 @@ async function projectModuleFile(root, node, id) {
2769
3522
  }
2770
3523
  if (!id.startsWith("project.")) return { file: null, tried };
2771
3524
  const name = id.slice("project.".length);
2772
- const candidate = path5.resolve(root, "nodes", name, "index.ts");
3525
+ const candidate = path6.resolve(root, "nodes", name, "index.ts");
2773
3526
  if (!within(root, candidate)) return { file: null, tried };
2774
3527
  tried.push(candidate);
2775
3528
  return { file: await exists(candidate) ? candidate : null, tried };
@@ -2787,7 +3540,7 @@ function reExportedDefinition(source) {
2787
3540
  });
2788
3541
  }
2789
3542
  function unresolvedModule(id, node, tried, root) {
2790
- const relative = tried.map((candidate) => path5.relative(root, candidate) || candidate);
3543
+ const relative = tried.map((candidate) => path6.relative(root, candidate) || candidate);
2791
3544
  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
3545
  return {
2793
3546
  phase: "prepare",
@@ -2797,9 +3550,9 @@ function unresolvedModule(id, node, tried, root) {
2797
3550
  };
2798
3551
  }
2799
3552
  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`);
3553
+ const cache = path6.join(projectRoot, ".cascade-cache", "nodes");
3554
+ await fs5.mkdir(cache, { recursive: true });
3555
+ const output = path6.join(cache, `${path6.basename(path6.dirname(moduleFile))}.mjs`);
2803
3556
  const staging = `${output}.${process.pid}.${Date.now()}.tmp`;
2804
3557
  await build({
2805
3558
  entryPoints: [moduleFile],
@@ -2812,24 +3565,27 @@ async function compileExecute(projectRoot, moduleFile) {
2812
3565
  sourcemap: "inline",
2813
3566
  plugins: [cliImageModule()]
2814
3567
  });
2815
- await fs4.rename(staging, output);
3568
+ await fs5.rename(staging, output);
2816
3569
  const loaded = await import(`${pathToFileURL2(output).href}?v=${Date.now()}`);
2817
3570
  if (typeof loaded.execute !== "function") throw new Error(`${moduleFile} does not export execute`);
2818
3571
  return loaded.execute;
2819
3572
  }
2820
3573
  function cliImageModule() {
2821
- const here = path5.dirname(fileURLToPath(import.meta.url));
2822
- const source = [
2823
- path5.join(here, "runtime", "image.ts"),
3574
+ const here = path6.dirname(fileURLToPath(import.meta.url));
3575
+ const shim = (name) => [
3576
+ path6.join(here, "runtime", `${name}.ts`),
2824
3577
  // dist/cli/runtime, copied by build-cli
2825
- path5.join(here, "..", "..", "server", "src", "runtime", "image.ts")
3578
+ path6.join(here, "..", "..", "server", "src", "runtime", `${name}.ts`)
2826
3579
  // running from src
2827
- ].find((candidate) => existsSync(candidate));
3580
+ ].find((candidate) => existsSync2(candidate));
3581
+ const sources = { image: shim("image"), io: shim("io") };
2828
3582
  return {
2829
3583
  name: "cascade-cli-image",
2830
3584
  setup(builder) {
2831
- if (!source) return;
2832
- builder.onResolve({ filter: /^(?:@field\/)?cascade\/image$/ }, () => ({ path: source }));
3585
+ builder.onResolve({ filter: /^(?:@field\/)?cascade\/(?:image|io)$/ }, (args) => {
3586
+ const source = sources[args.path.endsWith("/io") ? "io" : "image"];
3587
+ return source ? { path: source } : void 0;
3588
+ });
2833
3589
  }
2834
3590
  };
2835
3591
  }
@@ -2919,12 +3675,12 @@ function formatDiagnostic(diagnostic) {
2919
3675
  return `${location}${diagnostic.code}: ${diagnostic.message}`;
2920
3676
  }
2921
3677
  function within(root, candidate) {
2922
- const relative = path5.relative(path5.resolve(root), candidate);
2923
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
3678
+ const relative = path6.relative(path6.resolve(root), candidate);
3679
+ return relative === "" || !relative.startsWith("..") && !path6.isAbsolute(relative);
2924
3680
  }
2925
3681
  async function exists(file) {
2926
3682
  try {
2927
- await fs4.access(file);
3683
+ await fs5.access(file);
2928
3684
  return true;
2929
3685
  } catch {
2930
3686
  return false;
@@ -2989,6 +3745,10 @@ export {
2989
3745
  validateNodeModuleArchitecture,
2990
3746
  installProjectIo,
2991
3747
  saveStillImages,
3748
+ installProjectStages,
3749
+ cookingNode,
3750
+ RunTrace,
3751
+ captureNodeConsole,
2992
3752
  createProjectHost,
2993
3753
  classifyPreflight,
2994
3754
  validateProjectGraph,
@@ -3003,4 +3763,4 @@ export {
3003
3763
  builtinModuleIds,
3004
3764
  diagnoseGraph
3005
3765
  };
3006
- //# sourceMappingURL=chunk-EUIAC3U3.js.map
3766
+ //# sourceMappingURL=chunk-MP653A5O.js.map