@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
@@ -1,11 +1,11 @@
1
- import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
1
+ import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, Color, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
2
2
  import { CascadeRuntimeError } from "./error.js";
3
3
  import { builtinNodeRegistrations } from "./builtins/index.js";
4
4
  import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
5
5
  import { PropAnimator, isBoundProp } from "./params/index.js";
6
6
  import { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath, } from "./expressions/bundlePaths.js";
7
7
  import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
8
- import { isGpuHost } from "./gpu.js";
8
+ import { gpuLimitShortfall, isGpuHost } from "./gpu.js";
9
9
  import { DEFAULT_GRAPH_SEED, graphSeed, nodeSeed, seededRandom } from "./seed.js";
10
10
  /**
11
11
  * Preflight codes that describe something other than "this graph cannot run
@@ -113,16 +113,23 @@ class MutableAbortSignal {
113
113
  }
114
114
  }
115
115
  export function createRuntime(options) {
116
- return new Runtime(options.host, [...builtinNodeRegistrations, ...(options.nodes ?? [])]);
116
+ return new Runtime(options.host, [...builtinNodeRegistrations, ...(options.nodes ?? [])], {
117
+ values: options.values === "trust" ? "trust" : "freeze",
118
+ concurrency: Math.max(1, Math.floor(Number(options.concurrency ?? 1)) || 1),
119
+ ...(options.nodeScope ? { nodeScope: options.nodeScope } : {}),
120
+ });
117
121
  }
122
+ const SERIAL_EXECUTION = { values: "freeze", concurrency: 1 };
118
123
  class Runtime {
119
124
  host;
125
+ execution;
120
126
  registrations = new Map();
121
127
  graphs = new Set();
122
128
  sealed = false;
123
129
  disposed = false;
124
- constructor(host, registrations) {
130
+ constructor(host, registrations, execution = SERIAL_EXECUTION) {
125
131
  this.host = host;
132
+ this.execution = execution;
126
133
  for (const registration of registrations)
127
134
  this.registerNode(registration);
128
135
  }
@@ -166,7 +173,7 @@ class Runtime {
166
173
  const connections = (document.connections ?? []).map((connection, index) => materializeConnection(connection, index, byId));
167
174
  validateCardinality(byId, connections);
168
175
  validateTopology(nodes, connections);
169
- const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document));
176
+ const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document), this.execution);
170
177
  // The graph's own frame rate, saved with it; a run's `fps` still overrides.
171
178
  const fps = document.timeline?.fps;
172
179
  if (typeof fps === "number" && Number.isFinite(fps) && fps > 0)
@@ -188,6 +195,7 @@ class Graph {
188
195
  connections;
189
196
  onDispose;
190
197
  seed;
198
+ execution;
191
199
  graphState = "ready";
192
200
  byId;
193
201
  dataConnectionsByInput;
@@ -213,14 +221,20 @@ class Graph {
213
221
  animator;
214
222
  frame = MIN_FRAME;
215
223
  fps = DEFAULT_FPS;
224
+ /** What happens to a value a node hands over; see `RuntimeValuePolicy`. */
225
+ values;
216
226
  constructor(host, nodes, connections, onDispose, annotations = [],
217
227
  /** The document's seed; each node's `context.seed` derives from it. */
218
- seed = DEFAULT_GRAPH_SEED) {
228
+ seed = DEFAULT_GRAPH_SEED,
229
+ /** Value policy, concurrency, node scope; see `CreateRuntimeOptions`. */
230
+ execution = SERIAL_EXECUTION) {
219
231
  this.host = host;
220
232
  this.nodes = nodes;
221
233
  this.connections = connections;
222
234
  this.onDispose = onDispose;
223
235
  this.seed = seed;
236
+ this.execution = execution;
237
+ this.values = execution.values;
224
238
  this.byId = new Map(nodes.map((node) => [node.id, node]));
225
239
  const byInput = new Map();
226
240
  const byNode = new Map();
@@ -316,7 +330,7 @@ class Graph {
316
330
  type: definition.kind === "trigger" ? "trigger" : definition.type,
317
331
  connected: this.connectedOutputs.has(`${node.id}\0${name}`),
318
332
  ...(definition.kind === "data" && node.outputs.has(name)
319
- ? { value: snapshot(node.outputs.get(name)) }
333
+ ? { value: this.expose(node.outputs.get(name)) }
320
334
  : {}),
321
335
  },
322
336
  ])),
@@ -479,7 +493,15 @@ class Graph {
479
493
  return () => this.listeners.delete(listener);
480
494
  }
481
495
  getOutput(nodeId, outputName) {
482
- return snapshot(this.requireNode(nodeId).outputs.get(outputName));
496
+ return this.expose(this.requireNode(nodeId).outputs.get(outputName));
497
+ }
498
+ /**
499
+ * An output value as callers see it. Under `"freeze"` every output was
500
+ * deep-frozen when it was set, so this is the value itself (the snapshot is
501
+ * a lookup); under `"trust"` it is handed out as the producer made it.
502
+ */
503
+ expose(value) {
504
+ return this.values === "trust" ? value : snapshot(value);
483
505
  }
484
506
  getGraphOutput(name) {
485
507
  const node = this.graphOutputs.get(name);
@@ -490,7 +512,7 @@ class Graph {
490
512
  getOutputs(nodeId) {
491
513
  return immutableMap([...this.requireNode(nodeId).outputs].map(([name, value]) => [
492
514
  name,
493
- snapshot(value),
515
+ this.expose(value),
494
516
  ]));
495
517
  }
496
518
  dispose() {
@@ -578,19 +600,23 @@ class Graph {
578
600
  const ordered = topological(this.nodes, this.connections).filter((node) => initial.includes(node) && !insideFeedback.has(node.id));
579
601
  /** Nodes that cooked in this run, for `reuse: "strict"`. */
580
602
  const cooked = new Set();
581
- for (const node of ordered) {
582
- if (signal.aborted)
583
- break;
584
- if (reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked))) {
585
- this.emit({ type: "node:start", runId, nodeId: node.id });
586
- this.emit({ type: "node:complete", runId, nodeId: node.id });
587
- continue;
603
+ const skip = (node) => !!reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked));
604
+ if (this.execution.concurrency > 1)
605
+ await this.cookConcurrently(ordered, runId, signal, enqueue, skip, cooked);
606
+ else
607
+ for (const node of ordered) {
608
+ if (signal.aborted)
609
+ break;
610
+ if (skip(node)) {
611
+ this.emit({ type: "node:start", runId, nodeId: node.id });
612
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
613
+ continue;
614
+ }
615
+ await this.executeNode(node, runId, signal, {}, enqueue);
616
+ cooked.add(node.id);
617
+ if (!signal.aborted)
618
+ node.lastCook = this.cookKey(node);
588
619
  }
589
- await this.executeNode(node, runId, signal, {}, enqueue);
590
- cooked.add(node.id);
591
- if (!signal.aborted)
592
- node.lastCook = this.cookKey(node);
593
- }
594
620
  if (injection && !signal.aborted)
595
621
  enqueue(injection.nodeId, injection.inputName, injection.payload, {
596
622
  kind: "external",
@@ -628,7 +654,90 @@ class Graph {
628
654
  this.active = undefined;
629
655
  }
630
656
  }
657
+ /**
658
+ * The run's main order with up to `concurrency` nodes cooking at once.
659
+ *
660
+ * Nodes START strictly in topological order: the next node starts once
661
+ * everything it reads has completed and a slot is free, and nothing later
662
+ * in the order overtakes it. So the `node:start` events — and a run trace
663
+ * built from them — come in the same order as a serial run on every run;
664
+ * only completions interleave. A Kahn order puts siblings side by side (the
665
+ * morph's four renders are adjacent), which is where the overlap is.
666
+ *
667
+ * A feedback container, a node with trigger ports and a GPU node cook
668
+ * alone: their state (the carried step, the trigger queue's sequence, one
669
+ * device) is ordered by the serial loop and not worth sharing.
670
+ *
671
+ * On a failure nothing new starts, the nodes already cooking finish, and
672
+ * the failure earliest in the order is the run's, so the message does not
673
+ * depend on which stage happened to answer first.
674
+ */
675
+ async cookConcurrently(ordered, runId, signal, enqueue, skip, cooked) {
676
+ const position = new Map(ordered.map((node, index) => [node.id, index]));
677
+ const needs = new Map();
678
+ for (const dependency of graphDependencies(this.nodes, this.connections)) {
679
+ if (!position.has(dependency.sourceId) || !position.has(dependency.targetId))
680
+ continue;
681
+ const list = needs.get(dependency.targetId) ?? [];
682
+ list.push(dependency.sourceId);
683
+ needs.set(dependency.targetId, list);
684
+ }
685
+ const done = new Set();
686
+ const running = new Map();
687
+ let exclusiveRunning = false;
688
+ let failure;
689
+ let next = 0;
690
+ for (;;) {
691
+ while (!failure &&
692
+ !signal.aborted &&
693
+ !exclusiveRunning &&
694
+ next < ordered.length &&
695
+ running.size < this.execution.concurrency) {
696
+ const node = ordered[next];
697
+ if (!(needs.get(node.id) ?? []).every((id) => done.has(id)))
698
+ break;
699
+ const alone = cooksAlone(node);
700
+ if (alone && running.size)
701
+ break;
702
+ next++;
703
+ if (skip(node)) {
704
+ this.emit({ type: "node:start", runId, nodeId: node.id });
705
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
706
+ done.add(node.id);
707
+ continue;
708
+ }
709
+ if (alone)
710
+ exclusiveRunning = true;
711
+ const index = position.get(node.id);
712
+ const task = this.executeNode(node, runId, signal, {}, enqueue).then(() => {
713
+ cooked.add(node.id);
714
+ if (!signal.aborted)
715
+ node.lastCook = this.cookKey(node);
716
+ done.add(node.id);
717
+ }, (error) => {
718
+ if (!failure || index < failure.index)
719
+ failure = { index, error };
720
+ }).finally(() => {
721
+ running.delete(node.id);
722
+ if (alone)
723
+ exclusiveRunning = false;
724
+ });
725
+ running.set(node.id, task);
726
+ }
727
+ if (!running.size)
728
+ break;
729
+ await Promise.race(running.values());
730
+ }
731
+ if (failure)
732
+ throw failure.error;
733
+ }
631
734
  async executeNode(node, runId, signal, triggers, enqueue) {
735
+ const scope = this.execution.nodeScope;
736
+ if (!scope)
737
+ return this.cookNode(node, runId, signal, triggers, enqueue);
738
+ return scope(node.id, () => this.cookNode(node, runId, signal, triggers, enqueue));
739
+ }
740
+ async cookNode(node, runId, signal, triggers, enqueue) {
632
741
  this.emit({ type: "node:start", runId, nodeId: node.id });
633
742
  /**
634
743
  * `cascade.core.Previous` reads the carried state off its container, the
@@ -710,7 +819,7 @@ class Graph {
710
819
  const definition = node.definition.outputs?.[name];
711
820
  if (!definition || definition.kind !== "data")
712
821
  continue;
713
- const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`);
822
+ const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`, this.values);
714
823
  node.outputs.set(name, value);
715
824
  this.emit({
716
825
  type: "node:output",
@@ -739,7 +848,7 @@ class Graph {
739
848
  }),
740
849
  }
741
850
  : {
742
- set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`)),
851
+ set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`, this.values)),
743
852
  },
744
853
  ]));
745
854
  const capabilityEntries = [];
@@ -747,7 +856,11 @@ class Graph {
747
856
  const installed = this.host.capabilities[name];
748
857
  if (name === "gpu" && isGpuHost(installed)) {
749
858
  await installed.ensure();
750
- capabilityEntries.push([name, installed.forNode(node.id)]);
859
+ const gpu = installed.forNode(node.id);
860
+ const shortfall = gpuLimitShortfall(node.id, node.definition.gpuLimits, gpu);
861
+ if (shortfall)
862
+ throw new Error(shortfall);
863
+ capabilityEntries.push([name, gpu]);
751
864
  }
752
865
  else {
753
866
  capabilityEntries.push([
@@ -801,12 +914,14 @@ class Graph {
801
914
  if (pendingData.has(name)) {
802
915
  const value = pendingData.get(name);
803
916
  node.outputs.set(name, value);
917
+ // The stored value is already frozen (or trusted), so the event
918
+ // carries it as is rather than a fresh copy nobody may read.
804
919
  this.emit({
805
920
  type: "node:output",
806
921
  runId,
807
922
  nodeId: node.id,
808
923
  outputName: name,
809
- value: snapshot(value),
924
+ value: this.expose(value),
810
925
  });
811
926
  }
812
927
  for (const emitted of pendingTriggers)
@@ -1262,7 +1377,8 @@ class Graph {
1262
1377
  }
1263
1378
  return Object.freeze({
1264
1379
  result: carried,
1265
- history: Object.freeze(history),
1380
+ // Recorded as settled, so reading `history` back is not a copy.
1381
+ history: settle(history),
1266
1382
  });
1267
1383
  }
1268
1384
  finally {
@@ -1406,7 +1522,10 @@ function materialize(authored, registration) {
1406
1522
  const object = saved && typeof saved === "object" && "value" in saved
1407
1523
  ? saved
1408
1524
  : null;
1409
- const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
1525
+ const stored = object ? object.value : saved;
1526
+ const value = runtimeValue(
1527
+ // A colour saved as `{ r, g, b }` by the class image nodes reads as one.
1528
+ definition.type === "color" ? Color.upgrade(stored) : stored, definition.type, false, `${authored.id}.${name}`);
1410
1529
  props[name] = value;
1411
1530
  // A stored plain value is an author's decision and beats the declared
1412
1531
  // default expression, so any binding seeded above has to go.
@@ -1702,6 +1821,18 @@ function snapshotRegistration(registration) {
1702
1821
  const definition = snapshot(registration.definition);
1703
1822
  return Object.freeze({ ...registration, definition });
1704
1823
  }
1824
+ /** A node `cookConcurrently` never overlaps with another; see there. */
1825
+ function cooksAlone(node) {
1826
+ if (node.definition.container === "feedback")
1827
+ return true;
1828
+ if (node.definition.capabilities?.includes("gpu"))
1829
+ return true;
1830
+ const ports = [
1831
+ ...Object.values(node.definition.inputs ?? {}),
1832
+ ...Object.values(node.definition.outputs ?? {}),
1833
+ ];
1834
+ return ports.some((port) => port.kind === "trigger");
1835
+ }
1705
1836
  function isGated(node) {
1706
1837
  return Object.values(node.definition.inputs ?? {}).some((input) => input.kind === "trigger");
1707
1838
  }
@@ -1882,7 +2013,103 @@ function misuse(code, message) {
1882
2013
  throw runtimeError(code, message);
1883
2014
  }
1884
2015
  function snapshot(value) {
1885
- return cloneValue(value, new WeakSet());
2016
+ const clone = cloneValue(value, new WeakSet());
2017
+ if (clone !== null && typeof clone === "object")
2018
+ settled.add(clone);
2019
+ return clone;
2020
+ }
2021
+ /**
2022
+ * Plain arrays and objects already checked and deep-frozen — by `cloneValue`
2023
+ * or `settle` — so a second snapshot of the same value is a lookup, not a
2024
+ * copy. A runtime value is read far more often than it is made: the producer
2025
+ * sets it once, then the event, `getOutputs`, `finish` and `inspect` each
2026
+ * used to clone it again (a 1M-point array four times a frame).
2027
+ *
2028
+ * Only roots and containers of objects are recorded; a leaf of primitives
2029
+ * (a `[x, y]` point) is cheaper to re-walk than to record a million times.
2030
+ */
2031
+ const settled = new WeakSet();
2032
+ /** Plain containers copied by `cloneValue`, for the copy-count tests. */
2033
+ let copiedContainers = 0;
2034
+ /** How many plain arrays/objects the runtime has copied in this process. */
2035
+ export function runtimeValueCopies() {
2036
+ return copiedContainers;
2037
+ }
2038
+ function isPlainContainer(value) {
2039
+ if (Array.isArray(value))
2040
+ return true;
2041
+ const prototype = Object.getPrototypeOf(value);
2042
+ return prototype === Object.prototype || prototype === null;
2043
+ }
2044
+ function checkScalar(value) {
2045
+ if (typeof value === "number" && !Number.isFinite(value))
2046
+ misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
2047
+ if (typeof value === "function" ||
2048
+ typeof value === "symbol" ||
2049
+ typeof value === "bigint")
2050
+ misuse("runtime/non-serializable-value", `Runtime values cannot contain ${typeof value}`);
2051
+ }
2052
+ /**
2053
+ * Check a value a node handed over and deep-freeze it where it stands: the
2054
+ * same rules as `cloneValue` (finite numbers, no functions, no cycles, class
2055
+ * instances and typed arrays passed through untouched), without allocating.
2056
+ * The producer gave it away with `set()`; freezing is what keeps the
2057
+ * consumers' view of it from changing under them.
2058
+ */
2059
+ function settle(value) {
2060
+ if (value === null || typeof value !== "object") {
2061
+ checkScalar(value);
2062
+ return value;
2063
+ }
2064
+ if (!settled.has(value) && isPlainContainer(value)) {
2065
+ settleContainer(value, new WeakSet());
2066
+ settled.add(value);
2067
+ }
2068
+ return value;
2069
+ }
2070
+ /** Returns whether `value` held any plain container (and so was recorded). */
2071
+ function settleContainer(value, ancestors) {
2072
+ let nested = false;
2073
+ const visit = (item) => {
2074
+ if (item === null || typeof item !== "object") {
2075
+ checkScalar(item);
2076
+ return;
2077
+ }
2078
+ if (settled.has(item) || !isPlainContainer(item))
2079
+ return;
2080
+ if (ancestors.has(item))
2081
+ misuse("runtime/non-serializable-value", "Runtime values cannot contain cycles");
2082
+ if (!nested) {
2083
+ nested = true;
2084
+ ancestors.add(value);
2085
+ }
2086
+ if (settleContainer(item, ancestors))
2087
+ settled.add(item);
2088
+ };
2089
+ if (Array.isArray(value))
2090
+ for (let index = 0; index < value.length; index++)
2091
+ visit(value[index]);
2092
+ else
2093
+ for (const key of Object.keys(value))
2094
+ visit(value[key]);
2095
+ if (nested)
2096
+ ancestors.delete(value);
2097
+ Object.freeze(value);
2098
+ return nested;
2099
+ }
2100
+ /**
2101
+ * `"trust"`: the top level is checked by `runtimeValue`'s type test and
2102
+ * frozen shallowly, so `push` or `sort` on a handed-over array still throws;
2103
+ * nested contents are not walked.
2104
+ */
2105
+ function trust(value) {
2106
+ if (value === null || typeof value !== "object") {
2107
+ checkScalar(value);
2108
+ return value;
2109
+ }
2110
+ if (isPlainContainer(value))
2111
+ Object.freeze(value);
2112
+ return value;
1886
2113
  }
1887
2114
  function immutableMap(entries) {
1888
2115
  const source = new Map(entries);
@@ -1902,22 +2129,15 @@ function immutableMap(entries) {
1902
2129
  return view;
1903
2130
  }
1904
2131
  function cloneValue(value, ancestors) {
1905
- if (typeof value === "number" && !Number.isFinite(value))
1906
- misuse("runtime/non-serializable-value", "Runtime numbers must be finite");
1907
- if (typeof value === "function" ||
1908
- typeof value === "symbol" ||
1909
- typeof value === "bigint")
1910
- misuse("runtime/non-serializable-value", `Runtime values cannot contain ${typeof value}`);
2132
+ checkScalar(value);
1911
2133
  if (value === undefined || value === null || typeof value !== "object")
1912
2134
  return value;
1913
- const prototype = Object.getPrototypeOf(value);
1914
- if (!Array.isArray(value) &&
1915
- prototype !== Object.prototype &&
1916
- prototype !== null)
2135
+ if (settled.has(value) || !isPlainContainer(value))
1917
2136
  return value;
1918
2137
  if (ancestors.has(value))
1919
2138
  misuse("runtime/non-serializable-value", "Runtime values cannot contain cycles");
1920
2139
  ancestors.add(value);
2140
+ copiedContainers++;
1921
2141
  const clone = Array.isArray(value)
1922
2142
  ? value.map((item) => cloneValue(item, ancestors))
1923
2143
  : Object.fromEntries(Object.entries(value).map(([key, item]) => [
@@ -1943,17 +2163,19 @@ const TUPLE_LENGTHS = {
1943
2163
  * An output value, checked against its declaration. A bundle with declared
1944
2164
  * fields may only set those, and each one must be a valid value of its type.
1945
2165
  */
1946
- function outputValue(value, definition, path) {
2166
+ function outputValue(value, definition, path, policy) {
2167
+ // Handed over by the producer, so adopted (frozen where it stands), not copied.
2168
+ const adopt = policy === "trust" ? trust : settle;
1947
2169
  if (definition.type !== "bundle" || !definition.fields)
1948
- return runtimeValue(value, definition.type, false, path);
2170
+ return runtimeValue(value, definition.type, false, path, adopt);
1949
2171
  const fields = definition.fields;
1950
2172
  const problem = bundleValueProblem(value, fields);
1951
2173
  if (problem)
1952
2174
  misuse("runtime/value-type", `${path} ${problem}`);
1953
2175
  if (isBundleValue(value))
1954
2176
  for (const [field, item] of Object.entries(value))
1955
- runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`);
1956
- return runtimeValue(value, "bundle", false, path);
2177
+ runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`, (checked) => checked);
2178
+ return runtimeValue(value, "bundle", false, path, adopt);
1957
2179
  }
1958
2180
  /** Every path an expression names through ch/chs/chv. */
1959
2181
  function expressionPaths(expression) {
@@ -1975,7 +2197,9 @@ function readsBundleFields(node) {
1975
2197
  }
1976
2198
  return false;
1977
2199
  }
1978
- function runtimeValue(value, type, allowUndefined, path) {
2200
+ function runtimeValue(value, type, allowUndefined, path,
2201
+ /** How a checked value is kept: copied (a caller's value) by default. */
2202
+ keep = snapshot) {
1979
2203
  if (value === undefined) {
1980
2204
  if (allowUndefined)
1981
2205
  return undefined;
@@ -2001,7 +2225,7 @@ function runtimeValue(value, type, allowUndefined, path) {
2001
2225
  type !== "string");
2002
2226
  if (!valid)
2003
2227
  misuse("runtime/value-type", `${path} is not a valid ${type}`);
2004
- return snapshot(value);
2228
+ return keep(value);
2005
2229
  }
2006
2230
  function sameValues(a, b, equal) {
2007
2231
  return a.length === b.length && a.every((value, index) => equal(value, b[index]));
@@ -216,9 +216,39 @@ export type RuntimeEvent = {
216
216
  readonly type: "run:complete";
217
217
  readonly result: RunResult;
218
218
  };
219
+ /**
220
+ * What the runtime does with a value a node hands to `outputs.x.set()`.
221
+ *
222
+ * - `"freeze"` (default): checked and deep-frozen in place, never copied. A
223
+ * node that mutates its output after handing it over, or mutates an input,
224
+ * throws instead of corrupting another node's view.
225
+ * - `"trust"`: checked at the top level only and shallow-frozen; nested
226
+ * contents are the producer's word. For headless runs (`cascade run`), where
227
+ * a 1M-point array walked every frame was most of the frame.
228
+ *
229
+ * Either way nothing is cloned on the way through: reading an output back
230
+ * (`getOutputs`, `inspect`, `node:output`) hands out the same frozen value.
231
+ */
232
+ export type RuntimeValuePolicy = "freeze" | "trust";
219
233
  export interface CreateRuntimeOptions {
220
234
  readonly host: RuntimeHost;
221
235
  readonly nodes?: readonly NodeRegistration[];
236
+ /** See `RuntimeValuePolicy`. Default `"freeze"`. */
237
+ readonly values?: RuntimeValuePolicy;
238
+ /**
239
+ * How many nodes one run may cook at once (default 1: one after another).
240
+ * Nodes still START in topological order, a node only once everything it
241
+ * reads has completed, so the `node:start` sequence and the outputs are the
242
+ * same as a serial run; only completions interleave. Feedback containers,
243
+ * trigger-wired and GPU nodes always cook alone.
244
+ */
245
+ readonly concurrency?: number;
246
+ /**
247
+ * Wraps each node's execute. A Node host uses it to carry the node id
248
+ * through the cook's async work (AsyncLocalStorage), so a stage call or a
249
+ * log line is attributed to its node when several cook at once.
250
+ */
251
+ readonly nodeScope?: <T>(nodeId: string, run: () => T) => T;
222
252
  }
223
253
  export interface CascadeRuntime {
224
254
  registerNode(registration: NodeRegistration): void;
@@ -50,6 +50,7 @@ Running, sharing, locks and Studio. `cascade --help` has their options; `cascade
50
50
  | `cascade parity <graph-file>` | Cook headless twice and through Studio's path; report per-node differences. |
51
51
  | `cascade build <graph-file> --out <new-directory>` | A standalone browser player ([WEB_PLAYER.md](WEB_PLAYER.md)). |
52
52
  | `cascade export <graph-file> (--plan \| --out <dir>)` | The graph's published editor as a static site ([EDITOR_EXPORT.md](EDITOR_EXPORT.md)). |
53
+ | `cascade share <graph-file \| site-directory> [--name <slug>] [--scope <team>] [--protected]` | One link for the team: export the editor (or take a static directory with an `index.html`), deploy it to the Vercel project `cascade-<slug>` (team `field-io`), print the production URL (`data.url` with `--json`) and record it in the project's `shares.json`. Public unless `--protected` ([EDITOR_EXPORT.md](EDITOR_EXPORT.md#share-as-a-link)). |
53
54
  | `cascade publish <graph-file> <node>.<prop>` | Publish a node prop as an editor parameter. |
54
55
  | `cascade freeze <graph-file> <node> --fields <a,b>` | Snapshot chosen fields of a live data source for export. |
55
56
  | `cascade scene <graph-file> <scene.json> --out <new-graph-file>` | Apply a scene file to a copy of the graph. |
@@ -58,7 +59,7 @@ Running, sharing, locks and Studio. `cascade --help` has their options; `cascade
58
59
  | `cascade lock verify [<lock-id>...]` | Replay locks and report which still match. |
59
60
  | `cascade replay <lock-id>` | Re-render a lock from its stored code and compare fingerprints. |
60
61
  | `cascade locks` | List the project's locks. |
61
- | `cascade ladder <graph-file> <node> --name <name> --vary <node.prop>=<a,b,c> \| --seeds <a,b,c>` | Cook 3 to 5 variants (values zipped across params, or graph seeds) from the stored code; write `ladders/<id>/<n>.png`, a labelled contact sheet `ladders/<id>/sheet.png` and the record `ladders/<id>.json` (per variant: graph, params, seed, inputs, fingerprint; the code and git commit once). |
62
+ | `cascade ladder <graph-file> <node> --name <name> --vary <node.prop>=<a,b,c> \| --seeds <a,b,c> [--cairn <vault dir> [--vault <dir>]]` | Cook 3 to 5 variants (values zipped across params, or graph seeds) from the stored code; write `ladders/<id>/<n>.png`, a labelled contact sheet `ladders/<id>/sheet.png` and the record `ladders/<id>.json` (per variant: graph, params, seed, inputs, fingerprint; the code and git commit once). `--cairn` copies each variant to `<dir>/<id>-<nn>.png` and prints a `cairn:choice` card (`--json`: `data.cairnChoice`) to post in the Cairn chat; see [Picking on the phone](#picking-a-ladder-variant-in-cairn). |
62
63
  | `cascade pick <ladder-id> <n> [--name <lock name>]` | At approval, write `locks/<id>.json` for variant `n` from the ladder's record, without cooking again; `cascade replay` reproduces that image pixel for pixel. |
63
64
  | `cascade help node [<type>] [--project <dir>]` | A node type's inputs, props and outputs (type, default, range, description), where it runs and its capabilities, read from the installed package or the project's `nodes/`; without a type, every node type. No repository checkout needed. |
64
65
  | `cascade doctor [project-directory] [--fix]` | What a headless run here will find: the declared Python environment, the stage interpreter, ffmpeg, the canvas. `--fix` builds the environment. See [Python for stages](#python-for-stages-cascadejson-python). |
@@ -137,6 +138,23 @@ Opened in Studio, the top level reads `motif → transform → render`, with the
137
138
 
138
139
  `cascade run <graph> --out <dir>` copies each image output to `<dir>/<node>.<ext>`, or `<dir>/<node>.<port>.<ext>` for a node with more than one image output. With `--frames` the same names carry the frame: `<node>[.<port>].<frame>.<ext>`. Which outputs: every image output nothing downstream consumes (a `cascade.core.Null` passing an image counts), or those of `--node`; `--out-node <id>` (repeatable) adds a mid-graph node's. `--out` may be project-relative or absolute; `files` in the result is spelled the same way. `run --json` lists every image output as `data.images: [{ node, port, source, file? }]`, with or without `--out`. The MCP `cook` and `render` tools take `out` and `outNodes`.
139
140
 
141
+ ## Picking a ladder variant in Cairn
142
+
143
+ Marcus picks on his phone, in the Cairn web chat, by tapping a picture; the tap locks it. The loop for an agent:
144
+
145
+ 1. `cascade ladder <graph> <node> --name <name> --seeds 1,2,3 --cairn <vault>/<space>/_attachments/ladders/<id> --json` (`<id>` is the name lower-cased with dashes). Each variant is copied to `<dir>/<id>-<nn>.png`, and `data.cairnChoice` is the card. At most 8 variants.
146
+ 2. Post `data.cairnChoice` in the reply exactly as returned:
147
+
148
+ ````text
149
+ ```cairn:choice
150
+ {"prompt":"Dial: which one? (ladder dial, index.cascade render)","options":[{"label":"seed 1","value":"pick:dial:1","image":"Work/Projects/Cascade/_attachments/ladders/dial/dial-01.png"}, …]}
151
+ ```
152
+ ````
153
+
154
+ `label` says what the variant changed (`seed 1`, `hue=0.3`); `image` is relative to the vault, the nearest `.obsidian` (then `.git`) above `--cairn`, or `--vault`.
155
+ 3. A tap comes back as `Picked: seed 1 (pick:dial:1)`. Run `cascade pick dial 1 --project <project>` (MCP: `pick { "ladder": "dial", "variant": 1 }`).
156
+ 4. Reply with the locked image, `![[<that option's image>]]`, and the lock id.
157
+
140
158
  ## Checks that fail a run (`cascade.assert.*`)
141
159
 
142
160
  Put the check in the graph instead of a script beside it:
@@ -153,7 +171,7 @@ cascade run index.cascade --json # exit 1: { "ok": false, "error": { "messa
153
171
 
154
172
  ## Debug output and environment (`.cascade/runs/`, `--env`)
155
173
 
156
- Every run's trace in `.cascade/runs/<time>-<graph>.json` holds, per node, what it printed while it cooked: `console.log/info/warn/error/debug` from the node, and its Python stages' stderr, as `logs: [{ level, text }]` (500 lines per node, the rest counted in `logsDropped`). `run --json` gives the same lines as `data.logs: [{ node, level, text }]`. A failed run writes its trace too, and the error ends with `Run trace: <path>`.
174
+ Every run's trace in `.cascade/runs/<time>-<graph>.json` holds, per node, what it printed while it cooked: `console.log/info/warn/error/debug` from the node, and its Python stages' stderr, as `logs: [{ level, text }]` (500 lines per node, the rest counted in `logsDropped`). `run --json` gives the same lines as `data.logs: [{ node, level, text }]`. A failed run writes its trace too, and the error ends with `Run trace: <path>`. Each run (one per frame with `--frames`) carries `ms`, the cook; `frameMs`, the whole frame to its outputs on disk; and `saveMs`, the difference (collecting and writing the outputs), beside each node's own `ms`.
157
175
 
158
176
  `--env NAME=value` (repeatable) sets a variable for the run: Python stages, the warm stage worker and `cascade render`'s worker processes see it, so `CLOUD_ANALYST_ONLINE=1` no longer needs a shell prefix. The trace records the names under `env`, never the values. The MCP `cook` and `render` tools take `env: { NAME: "value" }`.
159
177
 
@@ -168,4 +186,4 @@ A project whose stages import numpy or Pillow declares them, and Cascade keeps a
168
186
  }
169
187
  ```
170
188
 
171
- `requirements` is a list of pip specifiers or a project-relative requirements file; `interpreter` (default `python3`) is what creates the venv. `cascade doctor` reports it as `current`, `missing` or `stale` (cascade.json changed since it was built); `cascade doctor --fix` creates or rebuilds it, moving an old one to `.cascade-cache/python/_archive/`. While one is declared, stages run under its interpreter instead of a bare `python`/`python3` in `exec.stages.python` (a project-relative executable there still wins), and a stage call with the environment missing or stale fails with the command that fixes it, rather than later on an `ImportError`. MCP: `doctor { "fix": true }`.
189
+ `requirements` is a list of pip specifiers or a project-relative requirements file; `interpreter` (default `python3`) is what creates the venv. `cascade doctor` reports it as `current`, `missing` or `stale` (cascade.json changed since it was built); `cascade doctor --fix` creates or rebuilds it, moving an old one to `.cascade-cache/python/_archive/`. With none declared, doctor fails when a stage source imports numpy and the stage interpreter cannot (`stage imports`), which is how a project on the system Python renders nothing. While one is declared, stages run under its interpreter instead of a bare `python`/`python3` in `exec.stages.python` (a project-relative executable there still wins), and a stage call with the environment missing or stale fails with the command that fixes it, rather than later on an `ImportError`. MCP: `doctor { "fix": true }`.
@@ -95,6 +95,32 @@ prints the same manifest without building anything.
95
95
  Vercel Authentication. Add `--public` when the editor is meant to be open to
96
96
  anyone: a readable site is then reported as intended instead of failing.
97
97
 
98
+ ## Share as a link
99
+
100
+ `cascade share` is the one step from a graph to a URL for the team:
101
+
102
+ ```sh
103
+ cascade share plotter-social.cascade # https://cascade-plotter-social.vercel.app
104
+ cascade share renders/shelf --name commons-shelf # any static directory with an index.html
105
+ ```
106
+
107
+ It exports the editor into a scratch directory with the same allowlist and
108
+ leak scan as `cascade export` (a directory is copied without dotfiles or
109
+ `licensed/` and scanned the same way), deploys it to production in the Vercel
110
+ project `cascade-<slug>` (team `field-io`, `--scope` for another), and prints
111
+ the project's production URL. The slug is `--name`, else the editor id or the
112
+ directory name. `--json` gives `data.url`, `data.deployment` and `data.shares`.
113
+
114
+ Each share is recorded in the project's `shares.json` (the directory holding
115
+ the graph, or the nearest `cascade.json` above a shared directory), one entry
116
+ per source with its `name`, `project`, `url` and `sharedAt`. Sharing again
117
+ replaces the entry and redeploys to the same URL.
118
+
119
+ A share is public: that is what a link someone can open is. The export
120
+ boundary above is the gate, so review `cascade export --plan` first.
121
+ `--protected` instead requires Vercel Authentication on the project, as
122
+ `cascade export --deploy vercel` does without `--public`.
123
+
98
124
  ## The `editor` section
99
125
 
100
126
  It sits beside `nodes` in the `.cascade` file. The runtime ignores it, and
package/doc/MCP.md CHANGED
@@ -36,8 +36,9 @@ With a global install (`npm i -g @field/cascade`), use `"command": "cascade", "a
36
36
  | `cook` | `cascade run <graph> [--node <id>]` |
37
37
  | `render` | `cascade run <graph> --frames … [--fps] [--out] [--node]… [--scene]` |
38
38
  | `export` | `cascade export <graph> --plan \| --out <dir>` |
39
+ | `share` | `cascade share <graph \| directory> [--name] [--scope] [--protected]` (deploys; `data.url` is the link) |
39
40
  | `lock`, `replay`, `locks` | `cascade lock <graph> <node> --name`, `cascade replay <id> [--live]`, `cascade locks` |
40
- | `ladder`, `pick` | `cascade ladder <graph> <node> --name --vary node.prop=a,b,c \| --seeds a,b,c` (`vary` is `{ "node.prop": [a, b, c] }`), `cascade pick <ladder> <n>` |
41
+ | `ladder`, `pick` | `cascade ladder <graph> <node> --name --vary node.prop=a,b,c \| --seeds a,b,c` (`vary` is `{ "node.prop": [a, b, c] }`; `cairn`, an absolute vault directory, returns `data.cairnChoice`, a tap-to-pick card for the Cairn chat: see [Picking in Cairn](CLI_REFERENCE.md#picking-a-ladder-variant-in-cairn)), `cascade pick <ladder> <n>` (on "Picked: … (pick:<id>:<n>)", `ladder` <id>, `variant` <n>) |
41
42
  | `node_help` | `cascade help node [<type>]` (a node type's declaration; every type without one) |
42
43
  | `doctor` | `cascade doctor [--fix]` (Python environment, stage interpreter, ffmpeg, canvas; `fix` builds the environment) |
43
44