@field/cascade 0.8.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/AGENTS.md +36 -3
  2. package/CHANGELOG.md +401 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -26
  5. package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
  6. package/dist/assets/CodePanel-BU-autmq.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  8. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
  9. package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
  10. package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
  11. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
  12. package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
  13. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
  14. package/dist/assets/definitions-GreEMXLV.js +1 -0
  15. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
  16. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
  17. package/dist/assets/geo-u0n_jOsD.js +1 -0
  18. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
  19. package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
  20. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
  21. package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
  22. package/dist/assets/index-5ZtDAuy2.js +220 -0
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-client-DqYmKstU.js +1 -0
  25. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
  26. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
  27. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  28. package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  30. package/dist/assets/math-DioIQOPJ.js +1 -0
  31. package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
  32. package/dist/assets/pop-DC_TQG9t.js +1 -0
  33. package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
  34. package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
  35. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  36. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
  37. package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
  38. package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
  39. package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
  40. package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
  41. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  42. package/dist/cli/chunk-2KXB47FP.js +115 -0
  43. package/dist/cli/chunk-35D343DC.js +1447 -0
  44. package/dist/cli/chunk-36P5K3SD.js +631 -0
  45. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  46. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  47. package/dist/cli/chunk-53NMBJAO.js +35 -0
  48. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  49. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  50. package/dist/cli/chunk-BP7UKF36.js +376 -0
  51. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  52. package/dist/cli/chunk-CO22XHNG.js +593 -0
  53. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  54. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  55. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  56. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  57. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  58. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  59. package/dist/cli/chunk-HFHWYL55.js +139 -0
  60. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  61. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  62. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  63. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  64. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  65. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  66. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  67. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  68. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  69. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  70. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  71. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  72. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  73. package/dist/cli/chunk-SGJAET2L.js +197 -0
  74. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  75. package/dist/cli/chunk-WD62JIP6.js +118 -0
  76. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  77. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  78. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  79. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  80. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  81. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  82. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  83. package/dist/cli/commands-Q23M26QM.js +1292 -0
  84. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  85. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  86. package/dist/cli/edit-JHCPTQFT.js +28 -0
  87. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  88. package/dist/cli/encode-HAUZTYPT.js +17 -0
  89. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  90. package/dist/cli/geo-4RNK3QVN.js +43 -0
  91. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  92. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  93. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  94. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  95. package/dist/cli/index.js +106 -14207
  96. package/dist/cli/math-ZRMLKU2D.js +16 -0
  97. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  98. package/dist/cli/parity-KINARCP2.js +328 -0
  99. package/dist/cli/pop-USDFXB7H.js +39 -0
  100. package/dist/cli/project-K3QIPGBB.js +12 -0
  101. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  102. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  103. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  104. package/dist/cli/render-YURAPPUI.js +436 -0
  105. package/dist/cli/runCommand-PI764LEG.js +198 -0
  106. package/dist/cli/runner-RA6UKEPD.js +37 -0
  107. package/dist/cli/runtime/image.ts +277 -0
  108. package/dist/cli/runtime/stage.ts +74 -11
  109. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  110. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  111. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  112. package/dist/contracts/bundle.d.ts +75 -0
  113. package/dist/contracts/bundle.js +112 -0
  114. package/dist/contracts/definition.d.ts +67 -4
  115. package/dist/contracts/document.d.ts +29 -0
  116. package/dist/contracts/documentFormat.d.ts +18 -0
  117. package/dist/contracts/documentFormat.js +195 -0
  118. package/dist/contracts/editor.d.ts +189 -0
  119. package/dist/contracts/editor.js +2 -0
  120. package/dist/contracts/index.d.ts +3 -0
  121. package/dist/contracts/index.js +3 -0
  122. package/dist/contracts/validate.js +59 -3
  123. package/dist/contracts/values.d.ts +3 -3
  124. package/dist/contracts/values.js +1 -0
  125. package/dist/image.d.ts +141 -0
  126. package/dist/image.js +127 -0
  127. package/dist/index.html +4 -4
  128. package/dist/player/app.js +17 -5
  129. package/dist/player/editor.js +1919 -0
  130. package/dist/player/editorEngine.js +477 -0
  131. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  132. package/dist/runtime/builtins/core/bundle.js +81 -0
  133. package/dist/runtime/builtins/core/index.d.ts +3 -2
  134. package/dist/runtime/builtins/core/index.js +6 -2
  135. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  136. package/dist/runtime/builtins/core/routing.js +28 -0
  137. package/dist/runtime/builtins/headless.d.ts +19 -0
  138. package/dist/runtime/builtins/headless.js +25 -0
  139. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  140. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  141. package/dist/runtime/builtins/image/index.d.ts +9 -0
  142. package/dist/runtime/builtins/image/index.js +9 -0
  143. package/dist/runtime/edit/document.d.ts +94 -0
  144. package/dist/runtime/edit/document.js +194 -0
  145. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  146. package/dist/runtime/edit/graphEditor.js +893 -0
  147. package/dist/runtime/edit/index.d.ts +12 -0
  148. package/dist/runtime/edit/index.js +6 -0
  149. package/dist/runtime/edit/layout.d.ts +50 -0
  150. package/dist/runtime/edit/layout.js +126 -0
  151. package/dist/runtime/edit/operations.d.ts +65 -0
  152. package/dist/runtime/edit/operations.js +57 -0
  153. package/dist/runtime/edit/parameters.d.ts +75 -0
  154. package/dist/runtime/edit/parameters.js +284 -0
  155. package/dist/runtime/edit/read.d.ts +35 -0
  156. package/dist/runtime/edit/read.js +65 -0
  157. package/dist/runtime/editor/index.d.ts +104 -0
  158. package/dist/runtime/editor/index.js +439 -0
  159. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  160. package/dist/runtime/expressions/bundlePaths.js +55 -0
  161. package/dist/runtime/expressions/index.d.ts +2 -0
  162. package/dist/runtime/expressions/index.js +1 -0
  163. package/dist/runtime/index.d.ts +2 -0
  164. package/dist/runtime/index.js +2 -0
  165. package/dist/runtime/params/animator.d.ts +5 -0
  166. package/dist/runtime/params/animator.js +16 -0
  167. package/dist/runtime/runtime.js +238 -12
  168. package/dist/runtime/seed.d.ts +29 -0
  169. package/dist/runtime/seed.js +52 -0
  170. package/dist/runtime/types.d.ts +12 -0
  171. package/dist/stage.js +43 -9
  172. package/dist/studio/panel.d.ts +2 -0
  173. package/doc/CLI_REFERENCE.md +171 -0
  174. package/doc/EDITOR_EXPORT.md +235 -0
  175. package/doc/HEADLESS_GPU.md +27 -0
  176. package/doc/MCP.md +69 -0
  177. package/doc/NODE_REFERENCE.md +208 -0
  178. package/doc/PROJECT_AUTHORING.md +258 -1
  179. package/package.json +29 -8
  180. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  181. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  182. package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
  183. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  184. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  185. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  186. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  187. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  188. package/dist/assets/geo-D2VPjAdt.js +0 -1
  189. package/dist/assets/index-CMiBdEuT.js +0 -212
  190. package/dist/assets/index-DMIuml9a.css +0 -1
  191. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  192. package/dist/assets/legacy-ZA7textG.js +0 -2
  193. package/dist/assets/math-B6hpxPJO.js +0 -1
  194. package/dist/assets/pop-CYUIjT6x.js +0 -1
  195. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Seeds: one number per graph, one derived number per node, the same in every
3
+ * host.
4
+ *
5
+ * A node that wants randomness gets `context.seed` and `context.random` from
6
+ * whichever host cooks it — the headless runtime here, Studio through its
7
+ * definition adapter — and both compute them with these functions. So the
8
+ * same graph draws the same dots headless and in Studio, and changing the
9
+ * graph's `seed` reshuffles every node at once while each keeps its own
10
+ * stream (doc/PRODUCTION_READINESS.md, item 5).
11
+ *
12
+ * The node id is part of the derivation, not its position: ids are stable
13
+ * across edits, so adding a node never reshuffles its neighbours.
14
+ */
15
+ /** The graph seed when a document does not state one. */
16
+ export const DEFAULT_GRAPH_SEED = 0;
17
+ /** A document's `seed`, as an unsigned 32-bit integer; the default when absent
18
+ * or not an integer. */
19
+ export function graphSeed(document) {
20
+ const value = document?.seed;
21
+ return typeof value === "number" && Number.isInteger(value) ? value >>> 0 : DEFAULT_GRAPH_SEED;
22
+ }
23
+ /**
24
+ * A node's seed: FNV-1a over `<graph seed>:<node id>`, finished with
25
+ * MurmurHash3's avalanche so neighbouring ids give unrelated seeds.
26
+ */
27
+ export function nodeSeed(seedOfGraph, nodeId) {
28
+ const text = `${seedOfGraph >>> 0}:${nodeId}`;
29
+ let hash = 0x811c9dc5;
30
+ for (let index = 0; index < text.length; index++) {
31
+ hash ^= text.charCodeAt(index);
32
+ hash = Math.imul(hash, 0x01000193);
33
+ }
34
+ hash ^= hash >>> 16;
35
+ hash = Math.imul(hash, 0x85ebca6b);
36
+ hash ^= hash >>> 13;
37
+ hash = Math.imul(hash, 0xc2b2ae35);
38
+ hash ^= hash >>> 16;
39
+ return hash >>> 0;
40
+ }
41
+ /** Mulberry32: a small, fast stream of floats in [0, 1) from one seed. */
42
+ export function seededRandom(seed) {
43
+ let state = seed >>> 0;
44
+ return () => {
45
+ state = (state + 0x6d2b79f5) >>> 0;
46
+ let value = state;
47
+ value = Math.imul(value ^ (value >>> 15), value | 1);
48
+ value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
49
+ return ((value ^ (value >>> 14)) >>> 0) / 0x1_0000_0000;
50
+ };
51
+ }
52
+ //# sourceMappingURL=seed.js.map
@@ -100,6 +100,18 @@ export interface RunOptions {
100
100
  readonly frame?: number;
101
101
  /** Frames per second, for `$FPS` and for `$T`. Default 30. */
102
102
  readonly fps?: number;
103
+ /**
104
+ * Keep the outputs of nodes whose inputs and props did not change since
105
+ * their last cook, instead of cooking the whole selection again. For hosts
106
+ * that re-run on every edit, such as an exported editor.
107
+ *
108
+ * `"strict"` also cooks every node downstream of one that cooked: equal
109
+ * inputs are not trusted when they come from a node that just ran, because
110
+ * a node that writes a fixed path and outputs the same path string would
111
+ * otherwise leave its consumers on the previous file. `cascade run
112
+ * --frames` uses it, so a frame re-cooks only what that frame changed.
113
+ */
114
+ readonly reuse?: boolean | "strict";
103
115
  }
104
116
  export interface RunResult {
105
117
  readonly runId: string;
package/dist/stage.js CHANGED
@@ -1,5 +1,18 @@
1
1
  // server/src/runtime/stage.ts
2
2
  var BRIDGE_KEY = "__cascadeStageBridge";
3
+ var COOK_SIGNAL_KEY = "__cascadeCookSignal";
4
+ function ambientCookSignal() {
5
+ const read = globalThis[COOK_SIGNAL_KEY];
6
+ if (typeof read !== "function") return void 0;
7
+ const signal = read();
8
+ return signal ?? void 0;
9
+ }
10
+ function cancelledError(stage, signal) {
11
+ const reason = signal.reason instanceof Error ? signal.reason.message : "superseded";
12
+ const error = new Error(`stage "${stage}" was cancelled: ${reason}`);
13
+ error.name = "AbortError";
14
+ return error;
15
+ }
3
16
  function installStageBridge(bridge) {
4
17
  const globals = globalThis;
5
18
  const previous = globals[BRIDGE_KEY];
@@ -36,18 +49,29 @@ async function capability() {
36
49
  if (token === null) throw new Error("Cascade exec capability is unavailable (404)");
37
50
  return token;
38
51
  }
39
- async function overHttp(stage, args) {
52
+ async function overHttp(stage, args, signal) {
40
53
  const postStage = async () => fetch("/api/exec", {
41
54
  method: "POST",
42
55
  headers: { "Content-Type": "application/json", "X-Cascade-Exec-Capability": await capability() },
43
- body: JSON.stringify({ stage, args })
56
+ // `cook`: a node's cook is a pure function of its inputs, so the server's
57
+ // default stage cache may serve it (see StageCachePolicy). A panel posts
58
+ // through its own api.runStage, without this, and is never cached unasked.
59
+ body: JSON.stringify({ stage, args, caller: "cook" }),
60
+ signal
44
61
  });
45
- let response = await postStage();
46
- if (response.status === 403) {
47
- capabilityPromise = void 0;
62
+ let response;
63
+ let body;
64
+ try {
48
65
  response = await postStage();
66
+ if (response.status === 403) {
67
+ capabilityPromise = void 0;
68
+ response = await postStage();
69
+ }
70
+ body = await response.json();
71
+ } catch (error) {
72
+ if (signal?.aborted) throw cancelledError(stage, signal);
73
+ throw error;
49
74
  }
50
- const body = await response.json();
51
75
  if (!response.ok || !body.ok) {
52
76
  throw new Error(`stage "${stage}" failed: ${body.stderr || body.error || "unknown error"}`);
53
77
  }
@@ -62,20 +86,30 @@ async function stageAvailable() {
62
86
  if (typeof document === "undefined") return false;
63
87
  return await discoverCapability() !== null;
64
88
  }
65
- async function runStage(stage, args = {}) {
89
+ async function runStage(stage, args = {}, options = {}) {
90
+ const signal = options.signal ?? ambientCookSignal();
91
+ if (signal?.aborted) throw cancelledError(stage, signal);
66
92
  const bridge = localBridge();
67
- if (bridge) return await bridge(stage, args);
93
+ if (bridge) {
94
+ try {
95
+ return await bridge(stage, args, signal ? { signal } : void 0);
96
+ } catch (error) {
97
+ if (signal?.aborted) throw cancelledError(stage, signal);
98
+ throw error;
99
+ }
100
+ }
68
101
  if (typeof document === "undefined") {
69
102
  throw new Error(
70
103
  `Cannot run stage "${stage}": no host bridge is installed and there is no page to post from. A headless host must call installStageBridge() from cascade/stage before executing the graph.`
71
104
  );
72
105
  }
73
- return overHttp(stage, args);
106
+ return overHttp(stage, args, signal);
74
107
  }
75
108
  function cachePath(nodeId, suffix) {
76
109
  return `.cascade-cache/${nodeId.replace(/[^a-zA-Z0-9_-]/g, "_")}${suffix}`;
77
110
  }
78
111
  export {
112
+ COOK_SIGNAL_KEY,
79
113
  cachePath,
80
114
  installStageBridge,
81
115
  runStage,
@@ -78,6 +78,8 @@ export interface ProjectPanelApi {
78
78
  * through the series API instead.
79
79
  */
80
80
  cook(options?: { readonly nodeId?: string }): Promise<void>;
81
+ /** Open the Agent panel with a draft for the user to review and send. */
82
+ openAgent(prompt: string): void;
81
83
  selectedNodeId(): string | null;
82
84
  readonly sourceNodeId: string | null;
83
85
  readonly signal: AbortSignal;
@@ -0,0 +1,171 @@
1
+ # CLI reference: editing graphs
2
+
3
+ Everything an agent needs to build or change a `.cascade` graph without hand-editing JSON. Each command is one graph concept (node, wire, parameter, subnet), checks the edit with the same code as `cascade check`, and writes the file atomically. Running, rendering, exporting and locks are listed under [The other commands](#the-other-commands); see also `cascade --help`, `doc/EDITOR_EXPORT.md` and `doc/WEB_PLAYER.md`.
4
+
5
+ ## The guarantees
6
+
7
+ - **Minimal diffs.** Only the keys an edit is about change. Node ids are stable, untouched nodes keep their place, a new key goes where Studio would write it, and the file keeps its own formatting: Studio's compact arrays, or plain `json.dump` indentation with `1.0` and `—` left as they were. An edit followed by its inverse leaves the file byte-identical (`tests/graph-edit.test.ts`).
8
+ - **Tidy placement.** A new node waits in a lane beside the graph until it is wired; the first `connect` moves it one step along the existing flow from its neighbour, beside anything already there. Studio flows top to bottom, and a graph laid out some other way (cloud-plots' diagonal stages) is followed instead. `--no-layout` leaves positions alone; `--at x,y` places a node exactly.
9
+ - **Closed subnets.** No wire crosses a subnet boundary directly. Connecting across one, moving nodes in or out, or grouping nodes routes the wire through the subnet's `Input` and `Output` nodes, named after what they carry (`motif-out-geometry`), reusing one that already carries the same value and removing one that is no longer used.
10
+ - **Refused, not broken.** An edit that would make `cascade check` fail (a cycle, a wrong type, a misspelt parameter, a value of the wrong type) is refused and nothing is written. A graph that already failed can still be repaired one edit at a time: only new failures count. A file that changed on disk while the edit ran (Studio saved, another agent wrote) is not overwritten.
11
+
12
+ ## Commands
13
+
14
+ | Command | What it does |
15
+ | --- | --- |
16
+ | `cascade graph create <graph> [--name <title>]` | A new empty graph. Never overwrites. |
17
+ | `cascade graph read <graph> [--in <subnet>]` | Nodes with stored parameters, subnets with their ports, wires as `a.out -> b.in`. |
18
+ | `cascade graph list [dir]` | Every `.cascade` under a directory, with node and wire counts. Skips `renders/`, `out/`, `dist/` and `node_modules/` below it. |
19
+ | `cascade node add <graph> <type> [--name <id>] [--in <subnet>] [--set k=v]... [--at x,y] [--comment <text>]` | Add a node. `<type>` is a module id or a short name: `Circle` is `cascade.geo.Circle`, `InkVocabulary` is `project.InkVocabulary`. The id defaults to the type in kebab case (`copy-to-points`, then `copy-to-points-2`). |
20
+ | `cascade node rm <graph> <node> [--recursive]` | Remove a node and its wires. A subnet with children needs `--recursive`. |
21
+ | `cascade connect <graph> <node.output> <node.input> [--no-layout]` | Wire two ports. A port can be left off when the node has one on that side. An input that was fed is rewired, and the old wire is reported. |
22
+ | `cascade disconnect <graph> <node.output> <node.input>` | Remove that wire. With one argument, `<node.input>` removes whatever feeds it, `<node.output>` everything it feeds. |
23
+ | `cascade set <graph> <node.param> <value>` | Store a value, read by the parameter's declared type: `0.5`, `true`, `640,480`, `#f4f1ea`, `"300"`. A value equal to the default is not stored. |
24
+ | `cascade set <graph> <node.param> --expr '<e>'` / `--clear-expr` | Bind or remove an expression (`$T * 0.1`). Props only. |
25
+ | `cascade set <graph> <node.param> --key 0=1 --key 120=1.4 [--interp linear]` / `--remove-key 120` | Set or remove keyframes on the prop's channel. Interpolation is `constant`, `linear` or `smooth` (the default). |
26
+ | `cascade set <graph> <node.param> --unset` | Forget the stored value and its bindings. |
27
+ | `cascade subnet create <graph> <node>... [--name <id>] [--label <text>]` | Group sibling nodes. The subnet takes the place of the first of them in the flow. |
28
+ | `cascade subnet move <graph> <node>... --to <subnet\|root>` | Move sibling nodes into a subnet or out to the top level, rewiring at each boundary. |
29
+ | `cascade subnet dissolve <graph> <subnet>` | Lift the nodes back out and remove the subnet. The inverse of `create`. |
30
+ | `cascade edit <graph> <operations.json \| ->` | A batch, all or nothing. Each operation is the JSON below. |
31
+
32
+ Every edit takes `--dry-run` (check, write nothing) and `--json`.
33
+
34
+ ## The other commands
35
+
36
+ Running, sharing, locks and Studio. `cascade --help` has their options; `cascade render --help` and `cascade lock --help` have their own.
37
+
38
+ | Command | What it does |
39
+ | --- | --- |
40
+ | `cascade [project-directory \| graph-file]` | Open Studio on a project or graph. |
41
+ | `cascade new <name>` | Create a Cascade project. |
42
+ | `cascade node <Name> [project-directory]` | Create a deterministic custom node. |
43
+ | `cascade projects [directory]` | Show or set the default projects directory. |
44
+ | `cascade serve start\|stop\|restart\|status\|logs\|install` | Run Studio as a managed background service. |
45
+ | `cascade run <graph-file> [options]` | Cook a graph headlessly; `--frames` renders a sequence. |
46
+ | `cascade render <graph-file>` | Render an animation across worker processes, then a preview video. |
47
+ | `cascade validate <graph-file>` | Validate a graph file without executing. |
48
+ | `cascade check <graph-file>` | Statically check node definitions, types and graph structure. |
49
+ | `cascade inspect <graph-file>` | Every node with its classification and full definition. |
50
+ | `cascade parity <graph-file>` | Cook headless twice and through Studio's path; report per-node differences. |
51
+ | `cascade build <graph-file> --out <new-directory>` | A standalone browser player ([WEB_PLAYER.md](WEB_PLAYER.md)). |
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 publish <graph-file> <node>.<prop>` | Publish a node prop as an editor parameter. |
54
+ | `cascade freeze <graph-file> <node> --fields <a,b>` | Snapshot chosen fields of a live data source for export. |
55
+ | `cascade scene <graph-file> <scene.json> --out <new-graph-file>` | Apply a scene file to a copy of the graph. |
56
+ | `cascade mcp [project-directory]` | Serve these commands as MCP tools over stdio ([MCP.md](MCP.md)). |
57
+ | `cascade lock <graph-file> <node> --name <name>` | Record a render (graph, params, seeds, inputs, code) in `locks/<id>.json`. |
58
+ | `cascade lock verify [<lock-id>...]` | Replay locks and report which still match. |
59
+ | `cascade replay <lock-id>` | Re-render a lock from its stored code and compare fingerprints. |
60
+ | `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 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
+ | `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
+ | `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). |
65
+
66
+ Every verb is one row in `src/cli/verbs.ts`: its usage and summary for `--help`, the handler that runs it, how it treats `--json`, and the MCP tools that call it. A new verb is a row there, its handler, and its line here (`tests/cli-verbs.test.ts` checks both this page and [MCP.md](MCP.md)).
67
+
68
+ ## `--json`, on every command
69
+
70
+ Every command, not only the editing ones, prints one envelope on stdout and nothing else:
71
+
72
+ ```json
73
+ { "ok": true, "data": { "file": "/…/index.cascade", "changed": true, "results": [
74
+ { "op": "set", "node": "render", "param": "strokeWidth", "field": "props", "before": 2, "after": 3 } ] },
75
+ "warnings": [] }
76
+ { "ok": false, "error": { "message": "render has no parameter \"strokewidth\"; props: size, filename, …" }, "warnings": [] }
77
+ ```
78
+
79
+ `data` is the command's structured result where it has one (the editing commands, `inspect`, `run`, `parity`), and `{ "output": [lines] }` for a command that only prints. What a command warns becomes `warnings`. The exit code is 0 for `ok` and 1 otherwise. `run --json` puts its manifest in `data`: the status, the seed, the path of the run trace in `.cascade/runs/`, the frames and files with `--frames`, and `stages`, one entry per Python stage call with its node, key and cache state (`hit`, `miss`, `off` and why). `parity --json` puts its report in `data`, with `data.ok` false when outputs differ; see [Seeds and parity](PROJECT_AUTHORING.md#seeds-and-parity).
80
+
81
+ ## Operations, for `cascade edit` and the MCP server
82
+
83
+ ```json
84
+ [
85
+ { "op": "node.add", "module": "Circle", "id": "dot", "in": "motif", "set": { "radius": [0.2, 0.2] } },
86
+ { "op": "connect", "from": "dot.geometry", "to": "copy.source" },
87
+ { "op": "disconnect", "from": "a.out", "to": "b.in" },
88
+ { "op": "set", "target": "render.opacity", "keys": [{ "frame": 0, "value": 0 }, { "frame": 48, "value": 1 }], "interpolation": "linear" },
89
+ { "op": "set", "target": "render.strokeWidth", "expression": "1 + $T * 0.1" },
90
+ { "op": "node.remove", "id": "old", "recursive": false },
91
+ { "op": "subnet.create", "nodes": ["a", "b"], "label": "Motif" },
92
+ { "op": "subnet.move", "nodes": ["c"], "to": "root" },
93
+ { "op": "subnet.dissolve", "id": "motif" }
94
+ ]
95
+ ```
96
+
97
+ Values in operations are JSON values, used as given and checked against the declared type.
98
+
99
+ ## The TypeScript API
100
+
101
+ The CLI and the MCP server are thin layers over one API, so a script can use it directly:
102
+
103
+ ```ts
104
+ import { GraphFile } from './src/cli/graphFile.js';
105
+
106
+ const graph = await GraphFile.open('index.cascade');
107
+ await graph.apply({ op: 'node.add', module: 'Circle', id: 'dot' });
108
+ await graph.apply({ op: 'connect', from: 'dot.geometry', to: 'render.scene' });
109
+ const { results, warnings } = await graph.save();
110
+ ```
111
+
112
+ The editor itself (`GraphEditor`, `readGraph`, the operation types) is environment-neutral and exported from `@field/cascade/runtime`: it edits a parsed document in memory and asks a callback for node definitions, so a browser host can use the same code.
113
+
114
+ ## A graph built from nothing
115
+
116
+ ```sh
117
+ cascade graph create index.cascade --name "Built from the CLI"
118
+ cascade node add index.cascade Circle --set radius=0.18,0.18 --set divisions=6
119
+ cascade node add index.cascade Rectangle --set size=2,2
120
+ cascade node add index.cascade CopyToPoints
121
+ cascade connect index.cascade circle copy-to-points.source
122
+ cascade connect index.cascade rectangle copy-to-points.target
123
+ cascade subnet create index.cascade circle rectangle copy-to-points --label Motif
124
+ cascade node add index.cascade Transform --set rotate=0.4
125
+ cascade connect index.cascade copy-to-points transform.geometry # routed out through motif-out-geometry
126
+ cascade node add index.cascade Camera
127
+ cascade node add index.cascade Render --set size=800,800 --set strokeWidth=3 --set 'background=#f4f1ea'
128
+ cascade connect index.cascade transform render.scene
129
+ cascade connect index.cascade camera render.camera
130
+ cascade set index.cascade render.opacity --key 1=0.25 --key 48=1 --interp linear
131
+ cascade check index.cascade && cascade run index.cascade --frames 1 --json
132
+ ```
133
+
134
+ Opened in Studio, the top level reads `motif → transform → render`, with the camera beside `transform`, and `motif` holds the circle and rectangle side by side above `copy-to-points` and its output.
135
+
136
+ ## Outputs as files (`run --out`)
137
+
138
+ `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
+ ## Checks that fail a run (`cascade.assert.*`)
141
+
142
+ Put the check in the graph instead of a script beside it:
143
+
144
+ ```sh
145
+ cascade node add index.cascade NotBlank --name poster-not-blank --set 'message="the poster rendered empty"'
146
+ cascade connect index.cascade render.image poster-not-blank.image
147
+ cascade node add index.cascade cascade.assert.Range --name density-ok --set min=0.2 --set max=0.8
148
+ cascade connect index.cascade stats.density density-ok.value
149
+ cascade run index.cascade --json # exit 1: { "ok": false, "error": { "message": "Assertion failed in \"density-ok\" (cascade.assert.Range): value 0.93 is outside [0.2, 0.8]." } }
150
+ ```
151
+
152
+ `Range` (a finite number in `[min, max]`), `Equals` (JSON-equal to `expected`, numbers within `tolerance`), `True`, and `NotBlank` (a PNG or PPM that is not one flat colour within `tolerance`, nor fully transparent). Each passes its input through and runs when the graph runs whole, or when the `--node` you name is downstream of it. An image wired only into assertions is still saved by `--out`. Headless only for now; see `doc/NODE_REFERENCE.md`.
153
+
154
+ ## Debug output and environment (`.cascade/runs/`, `--env`)
155
+
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>`.
157
+
158
+ `--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
+
160
+ ## Python for stages (`cascade.json` `python`)
161
+
162
+ A project whose stages import numpy or Pillow declares them, and Cascade keeps a virtual environment for it in `.cascade-cache/python/venv` (inside the project, never system-wide):
163
+
164
+ ```json
165
+ {
166
+ "exec": { "stages": { "entrypoint": "runtime/node_cli.py" } },
167
+ "python": { "requirements": ["numpy", "Pillow>=10"] }
168
+ }
169
+ ```
170
+
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 }`.
@@ -0,0 +1,235 @@
1
+ # Shareable editors
2
+
3
+ `cascade export` turns a graph into a small static website: a viewer, the
4
+ parameters you chose to publish, presets, scene files and export buttons. Send
5
+ the link to someone and they can change your scene, make new ones and send them
6
+ back, without Studio, an account or a server.
7
+
8
+ ## The boundary: what leaves the project
9
+
10
+ An export is an allowlist. Read this before sharing anything.
11
+
12
+ - **Only the selected subgraph ships.** That is the nodes your published
13
+ outputs and parameters depend on. Every other node, all annotations and
14
+ comments, and the graph's metadata stay home. `--plan` lists both sides.
15
+ - **Only selected assets ship.** These are the files in `editor.assets`, the
16
+ literal files a shipped prop points at, and the files of frozen snapshots.
17
+ Nothing else is copied: not the project directory, `.cascade-cache`,
18
+ credentials or Python environments.
19
+ - **Live data sources never ship.** A node that reads Observatory, a CLI, a
20
+ cache, body data, the filesystem or a network is live. That means a
21
+ `runsOn: 'server'` node, a node declaring `files`, `python` or `shell`, or a
22
+ node declaring `source: 'live'`. Freeze it with `cascade freeze` into a
23
+ snapshot of exactly the fields you name (one photo and a title, say). The
24
+ export ships the snapshot in place of the node and drops everything upstream.
25
+ If a live source is still wired in, the export fails and names it.
26
+ - **Personal fields are flagged.** Field names that look like notes,
27
+ transcripts, calendar, body or health data, locations or contacts are marked
28
+ PERSONAL in the plan, and so is any field you pass to `--personal`. Nothing is
29
+ frozen unless you name it, so the default is the minimum.
30
+ - **Licensed, hidden and ignored files are refused.** These are anything under
31
+ a `licensed/` directory, dotfiles such as `.env`, and anything git ignores.
32
+ Licensed fonts are dropped into the editor by the person using it. They are
33
+ read in their browser and never uploaded or bundled.
34
+ - **The built site is scanned before it is written.** The export stops if the
35
+ site contains Turso/libSQL URLs, tokens, API keys, private keys, JWTs,
36
+ `/Users/…`, `/home/…` or `~/` paths, Observatory endpoints, your home
37
+ directory, or the value of any secret-looking environment variable or
38
+ project `.env` entry.
39
+ - **The site makes no external calls.** It has no analytics. Its
40
+ `vercel.json` sets a Content Security Policy that only allows its own
41
+ origin, and it is marked noindex.
42
+ - **Scenes carry values, not project data.** A scene holds published parameter
43
+ values and the files the person dropped. A file parameter marked
44
+ `embed: "never"` (a licensed font) records only its name, size and SHA-256.
45
+
46
+ Every export writes `export-manifest.json`, listing each file, its size and
47
+ its source, each frozen field, and every PERSONAL flag. `cascade export --plan`
48
+ prints the same manifest without building anything.
49
+
50
+ ## Make a graph shareable in 5 steps
51
+
52
+ 1. **Publish the parameters** someone else may change:
53
+
54
+ ```sh
55
+ cascade publish index.cascade spec.capMm --label "Cap height" --group Type --min 3 --max 60 --unit mm
56
+ ```
57
+
58
+ This adds an entry to the graph's `editor.parameters`. In Studio, the
59
+ `share` toggle beside a parameter in the Inspector does the same, with
60
+ label, group and range fields. You can also edit the JSON directly (see the
61
+ format below).
62
+
63
+ 2. **Declare the outputs.** Add them to `editor.outputs`: one with `view: true`
64
+ is shown in the viewer, and each one gets an export button (`png`, `svg`,
65
+ `json` or `text`). Name the parameters that belong beside a button (dpi,
66
+ ground) in its `parameters`.
67
+
68
+ 3. **Freeze any live source** on the published path. Name exactly the fields to
69
+ keep:
70
+
71
+ ```sh
72
+ cascade freeze index.cascade moment --output moment --fields photos[0],instant --personal instant
73
+ ```
74
+
75
+ This writes `frozen/moment.json` plus any files those fields refer to.
76
+ Review them and commit them.
77
+
78
+ 4. **Review the plan.** Check the nodes, files and PERSONAL flags:
79
+
80
+ ```sh
81
+ cascade export index.cascade --plan
82
+ ```
83
+
84
+ 5. **Build, and deploy if you want to:**
85
+
86
+ ```sh
87
+ cascade export index.cascade --out export/site
88
+ cascade export index.cascade --out export/site --deploy vercel --project my-editor --scope field-io
89
+ ```
90
+
91
+ `--out` replaces only a previous export. `--deploy vercel` links in a scratch
92
+ directory, so no `.env.local` can land in the site. It deploys to
93
+ production, then checks that an anonymous request to
94
+ `https://<project>.vercel.app` is refused. If it is not, it says to turn on
95
+ Vercel Authentication. Add `--public` when the editor is meant to be open to
96
+ anyone: a readable site is then reported as intended instead of failing.
97
+
98
+ ## The `editor` section
99
+
100
+ It sits beside `nodes` in the `.cascade` file. The runtime ignores it, and
101
+ Studio keeps it verbatim when it saves. The types are in
102
+ `packages/contracts/src/editor.ts`.
103
+
104
+ ```json
105
+ "editor": {
106
+ "id": "cloud-type",
107
+ "title": "Cloud Type",
108
+ "groups": ["Input", "Focus", "Export"],
109
+ "parameters": [
110
+ { "id": "text", "node": "type-set", "prop": "text", "label": "Text", "group": "Input", "control": "textarea" },
111
+ { "id": "font", "node": "type-set", "prop": "font", "control": "file", "accept": [".otf", ".ttf"], "embed": "never",
112
+ "help": "Drop your licensed font. It stays in your browser." },
113
+ { "id": "angle", "node": "focus", "prop": "angle", "control": "slider", "min": -90, "max": 90, "unit": "°",
114
+ "visibleWhen": { "param": "field", "equals": "tilt" } },
115
+ { "id": "seed", "node": "spec", "prop": "seed", "control": "seed" }
116
+ ],
117
+ "outputs": [
118
+ { "id": "preview", "node": "preview-svg", "port": "svg", "format": "svg", "view": true },
119
+ { "id": "png", "node": "png", "port": "image", "format": "png", "parameters": ["dpi", "pngGround"] }
120
+ ],
121
+ "presets": [
122
+ { "id": "soft", "label": "Soft", "values": { "strength": 1 }, "keep": ["colour", "font"] }
123
+ ],
124
+ "assets": ["assets/fonts/InterDisplay-Medium.otf"],
125
+ "fonts": [{ "family": "DM Mono", "file": "assets/fonts/DMMono-Regular.woff2", "license": "OFL-1.1", "weight": 400 }],
126
+ "freeze": [{ "node": "moment", "output": "moment", "snapshot": "frozen/moment.json" }],
127
+ "panel": { "module": "editor/panel.ts", "placement": "extend" },
128
+ "inspect": true
129
+ }
130
+ ```
131
+
132
+ `fonts` ships open-licensed fonts with the site and loads them in the page
133
+ (`@font-face`) and in the engine worker (`FontFace`), so a panel or a canvas
134
+ that names the family draws with it. Each entry must say `"license": "OFL-1.1"`;
135
+ anything else, or a file under `licensed/`, is refused.
136
+
137
+ - **Parameter `id`** is the key a scene uses. It survives renaming the node
138
+ or the prop, so keep it stable.
139
+ - **`control`** is one of `number`, `slider`, `int`, `bool`, `select`
140
+ (`options`, bare or `{ value, label }`), `text`, `textarea`, `color`, `file`
141
+ (`accept`, `embed`), `seed` (with a Reroll button) and `vec2`. If omitted,
142
+ it comes from the prop's type.
143
+ - **`visibleWhen`** takes `{ param, equals | in | not }`, or a list of them
144
+ that must all hold.
145
+ - **Presets** set the values they name and reset everything else to the graph's
146
+ value, except the ids listed in `keep`. The editor also offers "Graph
147
+ defaults".
148
+ - **`panel`** is optional project code for the editor. It exports
149
+ `mount(target, api)` and may return a disposer. The `api` has `get`, `set`,
150
+ `values`, `subscribe` and `output`. A Svelte component works if the project
151
+ has `svelte` installed, because the export compiles `.svelte` imports with
152
+ the project's own compiler. `replace` hides the generated parameters.
153
+ - **Inspect** shows every stage and each of its outputs, read-only: geometry
154
+ drawn, images and SVG shown, and anything else as JSON.
155
+ - **`peek`** lists the parameter ids a phone keeps in reach at the bottom
156
+ sheet's lowest snap point, such as `["text", "seed"]`. By default these are
157
+ the first text and seed parameters.
158
+ - **`draft`** sets values that are cooked while someone drags a slider on a
159
+ phone, such as `{ "density": 0.5 }`. The preview is quicker during the drag,
160
+ and the real values cook again on release. Draft values never reach a scene
161
+ or an export.
162
+
163
+ ## On a phone
164
+
165
+ The same export works on a phone without any extra setup. The preview fills the
166
+ top of the screen. Pinch to zoom, drag to pan, and double-tap to fit. The
167
+ controls sit in a bottom sheet with a handle and three snap points (peek, half
168
+ and full), and the groups start folded. Every control is at least 44 px with
169
+ 16 px text, and numbers have − and + steppers. The header shrinks to the title
170
+ and a ⋯ menu, which holds the presets, the scene buttons, Inspect stages, the
171
+ exports and the last cook time. A pulsing dot shows the graph is cooking. Exports and
172
+ saved scenes open the share sheet (Save to Files, Photos, AirDrop) and fall back to a
173
+ download. File controls open the file picker. In landscape, the controls move to a side
174
+ panel. Desktop is unchanged.
175
+
176
+ ## Scene files
177
+
178
+ A scene is what someone saved in an editor (`*.scene.json`):
179
+
180
+ ```json
181
+ {
182
+ "format": "cascade.scene",
183
+ "version": 1,
184
+ "graph": { "id": "cloud-type", "version": "h77d97e88f213c05d" },
185
+ "name": "Derya, cover v2",
186
+ "values": { "text": "CLOUD\nOBSERVATORY", "strength": 0.3, "seed": 11 },
187
+ "files": {
188
+ "font": { "name": "HelveticaNowDisplay-Md.otf", "size": 81234, "sha256": "…" },
189
+ "image": { "name": "sky.png", "mediaType": "image/png", "size": 50211, "sha256": "…", "data": "<base64>" }
190
+ },
191
+ "savedAt": "2026-10-11T15:00:00Z"
192
+ }
193
+ ```
194
+
195
+ - **`graph.version`** is a hash of the editor id, the parameter interface and
196
+ the structure of the exported subgraph. Tuning a value in Studio leaves it
197
+ unchanged. Rewiring the graph or renaming a published parameter changes it.
198
+ - **A mismatch degrades rather than failing.** Another version or editor id is
199
+ a warning. Unknown parameters are ignored with a warning. A value of the
200
+ wrong kind is skipped with a warning. Missing parameters take the graph's
201
+ values.
202
+ - **Files:** a file up to 2 MB is embedded unless its parameter says
203
+ `embed: "never"`. A referenced file is matched by name and hash. In the
204
+ editor, that is a file the person dropped before in the same browser. In
205
+ Studio or the CLI, it is a file of that name under `assets/` (including
206
+ `assets/fonts/licensed/`) or `licensed/`. If no match is found, the graph's
207
+ file is kept and the person is told what to supply.
208
+
209
+ **Where scenes open:**
210
+
211
+ - **Exported editor:** "Open scene…" and "Save scene", or drop a
212
+ `.json` file on the viewer.
213
+ - **Studio:** File › Open Scene… applies a scene to the open graph as an
214
+ unsaved edit. Embedded files go into the project's `assets/`. File › Save
215
+ Scene… writes one.
216
+ - **CLI:**
217
+ - `cascade scene index.cascade s.scene.json --out new.cascade` writes a copy of the graph with the scene applied.
218
+ - `cascade run index.cascade --scene s.scene.json --write preview-svg.svg=out.svg` renders with a scene and writes a chosen output.
219
+
220
+ The same scene gives byte-identical output in the editor and under `cascade run`.
221
+
222
+ ## Limits
223
+
224
+ - Only browser-runnable definition-v1 nodes ship. Python, shell and dynamic
225
+ nodes are listed together as "not exportable". Port them to TypeScript, or
226
+ freeze their output. A node that draws ships when it uses `cascade/image`
227
+ (PROJECT_AUTHORING.md, "Draw an image in any host"); fonts and images it
228
+ reads must be shipped files (`editor.assets`, `editor.fonts`, frozen files).
229
+ - The editor cooks in a module worker. Unchanged stages are reused
230
+ (`run({ reuse: true })`), so only the stages after an edit re-cook, and the
231
+ page stays responsive while they do. Values cross to the page by structured
232
+ clone; a value that cannot be cloned is shown as a description.
233
+ - In Studio, a parameter's `share` toggle in the Inspector publishes it, with
234
+ label, group and range fields. Presets, outputs, `visibleWhen` and help text
235
+ are still edited with `cascade publish` or in the JSON.
@@ -109,6 +109,33 @@ It does not encode PNGs or perform colour conversion. Encode through the existin
109
109
  image/IO host and publish an `image` value. Graph-wide texture transport, HDR
110
110
  readback, and browser video uploads are outside this first implementation.
111
111
 
112
+ ## Determinism
113
+
114
+ A render pass is repeatable on one adapter: draws, and the blending between
115
+ them, are applied in submission order. `cascade parity` checks it
116
+ (`tests/parity-gpu.test.ts` draws 400 alpha-blended triangles and gets the same
117
+ bytes headless twice and through Studio's executor on Dawn). Three things break
118
+ it, and each has a fix:
119
+
120
+ - **Unordered float accumulation.** WGSL has integer atomics only, so a
121
+ Cascade compute shader cannot add floats atomically; accumulate in fixed
122
+ point (`atomicAdd` on `u32`/`i32` of a scaled value), which adds the same in
123
+ any order. The same hazard outside WGSL is the one that hit cloud-plots:
124
+ PyTorch's `index_add` on MPS adds floats with atomics, and on 12 Oct a
125
+ colliding scatter gave 40 different sums in 40 runs while fixed point gave
126
+ one. The fix there is the same (`src/cloud_plots/detsplat.py` in cloud-plots, opt-in per graph with
127
+ the `deterministic` prop).
128
+ - **Unsynchronised writes.** Two invocations writing one texel or buffer
129
+ element without a barrier or atomic leave whichever lands last.
130
+ - **A different adapter.** Rasterisation rules, `fma` contraction and
131
+ transcendental precision differ between GPUs and between Dawn and a browser's
132
+ WebGPU. Studio in a browser is a different adapter from headless Dawn, so
133
+ compare those with a tolerance: `cascade parity --tolerance <0-255>` counts a
134
+ channel difference up to that value as the same and still reports it.
135
+
136
+ Render into `rgba8unorm`, state the blend state on every pipeline rather than
137
+ relying on a default, and seed anything random from `context.seed`.
138
+
112
139
  ## Verification and limits
113
140
 
114
141
  ```sh