@field/cascade 0.12.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 (35) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +1 -1
  4. package/dist/cli/{chunk-OM6BEME6.js → chunk-2L34ERS2.js} +3 -3
  5. package/dist/cli/{chunk-3RXL5GHB.js → chunk-JEW5MPGI.js} +4 -4
  6. package/dist/cli/{chunk-ST6LROUG.js → chunk-OBZAP3GG.js} +4 -4
  7. package/dist/cli/{chunk-73S4JZXK.js → chunk-QAGATJGE.js} +5 -5
  8. package/dist/cli/{chunk-MP653A5O.js → chunk-QKL7CCRS.js} +84 -10
  9. package/dist/cli/{chunk-2JEU3MGB.js → chunk-W5PJ5RMP.js} +155 -83
  10. package/dist/cli/{chunk-EU4ZFZ7Y.js → chunk-W7OUAT7J.js} +2 -2
  11. package/dist/cli/{cliCommands-JJN5Z3ZC.js → cliCommands-XVFRD7CF.js} +3 -3
  12. package/dist/cli/{commands-PHYFYA3F.js → commands-M2XLTPTZ.js} +7 -7
  13. package/dist/cli/{doctorCommand-ORWQRNYP.js → doctorCommand-RAEI7GVY.js} +2 -2
  14. package/dist/cli/{editorCommands-NFR32JOY.js → editorCommands-5BBG4S6V.js} +6 -6
  15. package/dist/cli/{graphCommands-EOUOBOIJ.js → graphCommands-MULQ7VD2.js} +2 -2
  16. package/dist/cli/{graphFile-YTGJB5F5.js → graphFile-CITF36ZM.js} +3 -3
  17. package/dist/cli/{helpCommand-PULH6ZYB.js → helpCommand-X2QDANM3.js} +2 -2
  18. package/dist/cli/index.js +13 -13
  19. package/dist/cli/{mcp-JVZFQOEX.js → mcp-YRIGRLYQ.js} +3 -3
  20. package/dist/cli/{parity-X6DEH2DV.js → parity-3N3HTKWX.js} +4 -4
  21. package/dist/cli/{project-VYGIA7JX.js → project-SJF5ZF3F.js} +2 -2
  22. package/dist/cli/{projectRuntime-2Q4JKEH3.js → projectRuntime-NY4XMDFH.js} +3 -3
  23. package/dist/cli/{render-PT4MWI6U.js → render-Y23W6MY6.js} +10 -5
  24. package/dist/cli/{runCommand-HNPQVZMS.js → runCommand-XOG7AVC5.js} +3 -3
  25. package/dist/cli/{runner-5XPXVW7J.js → runner-DNJO5WO2.js} +5 -5
  26. package/dist/cli/{share-MHF6TNRK.js → share-B7HTCOJX.js} +5 -5
  27. package/dist/cli/{webBuild-WPPMGGW7.js → webBuild-2K4COCGY.js} +4 -4
  28. package/dist/contracts/capabilities.d.ts +21 -0
  29. package/dist/player/app.js +8 -5
  30. package/dist/player/embed.js +2 -0
  31. package/dist/player/types.d.ts +6 -0
  32. package/dist/runtime/definition/architecture.js +89 -0
  33. package/doc/PROJECT_AUTHORING.md +60 -1
  34. package/doc/WEB_PLAYER.md +9 -1
  35. package/package.json +1 -1
@@ -18,6 +18,15 @@ export function validateNodeModuleArchitecture(source, fileName, ts) {
18
18
  const diagnostics = [];
19
19
  const exportsDefinition = file.statements.some((statement) => ts.isVariableStatement(statement) && isDefinition(statement));
20
20
  for (const statement of file.statements) {
21
+ if (ts.isVariableStatement(statement) &&
22
+ !isDefinition(statement) &&
23
+ !supersededByDefinition(statement) &&
24
+ isPureConstant(statement)) {
25
+ // A helper: `const TAU = Math.PI * 2`, `const PALETTE = [...] as const`,
26
+ // `const lerp = (a, b, t) => a + (b - a) * t`. Immutable, so it cannot
27
+ // carry anything from one cook to the next.
28
+ continue;
29
+ }
21
30
  if (ts.isVariableStatement(statement) && !isDefinition(statement)) {
22
31
  // `icon` and `runsOn` get their own sentence, because the general
23
32
  // message sends you looking for the wrong thing. A dynamic node declares
@@ -97,6 +106,86 @@ export function validateNodeModuleArchitecture(source, fileName, ts) {
97
106
  ? declaration.name.text
98
107
  : null;
99
108
  }
109
+ /**
110
+ * Whether a module-level declaration is an immutable helper rather than
111
+ * state: `const` only, and every initializer pure (see `isPure`).
112
+ */
113
+ function isPureConstant(statement) {
114
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const))
115
+ return false;
116
+ return statement.declarationList.declarations.every((declaration) => ts.isIdentifier(declaration.name) &&
117
+ declaration.initializer !== undefined &&
118
+ isPure(declaration.initializer, false));
119
+ }
120
+ /**
121
+ * An expression that creates no mutable value: literals, arithmetic and
122
+ * logic over them, references and property reads (`Math.PI`), functions
123
+ * (they close over nothing mutable, since nothing at module level is), and
124
+ * array/object literals only when frozen — `as const` or `Object.freeze(…)`.
125
+ * Calls, `new`, regular expressions (`lastIndex` is state) and a bare
126
+ * array or object are not.
127
+ */
128
+ function isPure(node, frozen) {
129
+ if (ts.isNumericLiteral(node) || ts.isStringLiteral(node) || ts.isBigIntLiteral(node) ||
130
+ ts.isNoSubstitutionTemplateLiteral(node) ||
131
+ node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword ||
132
+ node.kind === ts.SyntaxKind.NullKeyword)
133
+ return true;
134
+ if (ts.isIdentifier(node))
135
+ return true;
136
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
137
+ return true;
138
+ if (ts.isParenthesizedExpression(node))
139
+ return isPure(node.expression, frozen);
140
+ if (ts.isAsExpression(node))
141
+ return isPure(node.expression, frozen || isConstAssertion(node.type));
142
+ if (ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node))
143
+ return isPure(node.expression, frozen);
144
+ if (ts.isPrefixUnaryExpression(node))
145
+ return node.operator !== ts.SyntaxKind.PlusPlusToken &&
146
+ node.operator !== ts.SyntaxKind.MinusMinusToken &&
147
+ isPure(node.operand, false);
148
+ if (ts.isBinaryExpression(node))
149
+ return !isAssignment(node.operatorToken.kind) &&
150
+ isPure(node.left, false) && isPure(node.right, false);
151
+ if (ts.isConditionalExpression(node))
152
+ return isPure(node.condition, false) && isPure(node.whenTrue, frozen) && isPure(node.whenFalse, frozen);
153
+ if (ts.isTemplateExpression(node))
154
+ return node.templateSpans.every((span) => isPure(span.expression, false));
155
+ if (ts.isPropertyAccessExpression(node))
156
+ return isPure(node.expression, false);
157
+ if (ts.isElementAccessExpression(node))
158
+ return isPure(node.expression, false) && isPure(node.argumentExpression, false);
159
+ if (ts.isArrayLiteralExpression(node))
160
+ return frozen && node.elements.every((element) => !ts.isSpreadElement(element) && isPure(element, true));
161
+ if (ts.isObjectLiteralExpression(node))
162
+ return frozen && node.properties.every((property) => ts.isPropertyAssignment(property)
163
+ ? !ts.isComputedPropertyName(property.name) && isPure(property.initializer, true)
164
+ : ts.isShorthandPropertyAssignment(property));
165
+ if (ts.isCallExpression(node) &&
166
+ ts.isPropertyAccessExpression(node.expression) &&
167
+ ts.isIdentifier(node.expression.expression) &&
168
+ node.expression.expression.text === "Object" &&
169
+ node.expression.name.text === "freeze" &&
170
+ node.arguments.length === 1) {
171
+ const [argument] = node.arguments;
172
+ // Object.freeze is shallow: a nested array or object needs its own freeze.
173
+ return ts.isArrayLiteralExpression(argument)
174
+ ? argument.elements.every((element) => !ts.isSpreadElement(element) && isPure(element, false))
175
+ : ts.isObjectLiteralExpression(argument)
176
+ ? argument.properties.every((property) => ts.isPropertyAssignment(property) &&
177
+ !ts.isComputedPropertyName(property.name) &&
178
+ isPure(property.initializer, false))
179
+ : false;
180
+ }
181
+ return false;
182
+ }
183
+ function isConstAssertion(type) {
184
+ return ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === "const";
185
+ }
186
+ function isAssignment(kind) {
187
+ return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
188
+ }
100
189
  function isDefinition(statement) {
101
190
  return Boolean(statement.declarationList.flags & ts.NodeFlags.Const) &&
102
191
  Boolean(statement.modifiers?.some((item) => item.kind === ts.SyntaxKind.ExportKeyword)) &&
@@ -196,6 +196,65 @@ The initiating node ID is captured as `api.sourceNodeId`; `setParam` uses the sa
196
196
  }
197
197
  ```
198
198
 
199
+ ### Reading another project's data
200
+
201
+ File reads stay inside the project: a `..` path or a symlink out of it is
202
+ refused. To read another folder, declare it as a read-only root:
203
+
204
+ ```json
205
+ {
206
+ "reads": ["../other-sketch/data"]
207
+ }
208
+ ```
209
+
210
+ Each entry is a folder relative to the project (absolute and `~` paths are
211
+ refused). The `files` capability's `read`, `list` and `stat`, `cascade/io`'s
212
+ loaders and Studio's media route may then resolve into it, whether the path
213
+ reaches it with `..` (`../other-sketch/data/points.csv`) or through a symlink in
214
+ the project (`data -> ../other-sketch/data`). What counts is where the file
215
+ really is, so a symlink inside the read root that points elsewhere is still
216
+ refused. Writes into a read root are always refused. The declaration is the
217
+ project's, in `cascade.json`, never the graph's; Studio's Project Settings
218
+ cannot change it.
219
+
220
+ ### Named exports and a keyed cache
221
+
222
+ A node with the `files` capability (`runsOn: 'server'`) writes under the name
223
+ it gives, never renamed by a content hash, but only into `.cascade-cache/`. To
224
+ write a stably named export, such as a ladder's chosen variant, declare the
225
+ folder:
226
+
227
+ ```json
228
+ {
229
+ "writes": ["renders"]
230
+ }
231
+ ```
232
+
233
+ ```ts
234
+ await context.capabilities.files.write('renders/cloud-final.png', bytes, { signal: context.signal });
235
+ ```
236
+
237
+ Each entry is a folder inside the project; `..`, absolute paths, the project
238
+ root, `nodes`, `.git` and `.cascade` are refused, and a symlink out of a write
239
+ folder is refused like any other. `cascade/io`'s `write` and `saveImage` keep
240
+ content-hashed names in `.cascade-cache/`: they are intermediates between
241
+ nodes, and the hash is what stops a fixed name handing on the wrong pixels.
242
+
243
+ For a cache a node must find again on the next run, by a key it derives from
244
+ everything the bytes depend on, use `files.cache`:
245
+
246
+ ```ts
247
+ const { files } = context.capabilities;
248
+ const key = `stage/${hashOf(inputs, props)}`;
249
+ const hit = await files.cache.get(key, { signal: context.signal }); // Uint8Array | null
250
+ if (!hit) await files.cache.put(key, computed, { signal: context.signal });
251
+ ```
252
+
253
+ Entries live in `.cascade-cache/keyed/<key>`; keys are letters, digits, `.`,
254
+ `_` and `-` in `/`-separated parts, each starting with a letter or digit.
255
+ Writes are atomic, so parallel render workers can share one cache. The cache
256
+ rule above still holds: it may only memoize work from wired inputs and props.
257
+
199
258
  Read settings without giving nodes write access:
200
259
 
201
260
  ```ts
@@ -241,7 +300,7 @@ Clean graph design is clean code design:
241
300
  - **One responsibility per node**, named for what it does (`layout_rings`, not `main`).
242
301
  - **Data flows through typed ports**, never through module-level state or files one node writes and another reads behind the graph's back.
243
302
  - **What varies is a parameter**; seeds, counts and colours are on the node, not constants in the code.
244
- - **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse.
303
+ - **Shared helpers go in `lib/` or the kit** (`KIT.md`); the node is the unit you compose and reuse. A helper only this node uses may stay in its file: function declarations and immutable `const`s (`const TAU = Math.PI * 2`, `const PALETTE = [...] as const`, `const lerp = (a, b, t) => …`) pass `cascade check`. Anything that can change between cooks (`let`, `new Map()`, a bare array or object, a regex, a call) is module state and is refused (`architecture/module-state`).
245
304
 
246
305
  Split a node when it does two of load, generate, style, render and export, or when its `execute` grows past about 150 lines. `cascade check` warns on both (`shape/long-execute`, `shape/mixed-jobs`); the warnings are advice and never fail the check. Drawing with noise and colour is one job's worth of calls, so it only flags a module doing four of the five, or loading and exporting in the same node.
247
306
 
package/doc/WEB_PLAYER.md CHANGED
@@ -54,6 +54,12 @@ player.play();
54
54
  await player.dispose();
55
55
  ```
56
56
 
57
+ For a neutral preview page, `chrome: false` hides the player's dark frame, Play
58
+ button and frame field, leaving the output alone on a transparent page; the
59
+ controller still plays, seeks and reports errors. The plain-iframe form is
60
+ `player.html?chrome=0`. The other options have URL forms too: `frame`, `fps`,
61
+ `autoplay=1`, `outputNode` with `outputPort`.
62
+
57
63
  The generic `mountPlayer` export from `cascade/player` accepts the same options
58
64
  plus `url`, pointing to a built `player.html`. Each mount creates an iframe, so
59
65
  two sketches can use the same node IDs and cache filenames without sharing data.
@@ -70,7 +76,9 @@ serialized and playback coalesces requests instead of building a catch-up queue.
70
76
 
71
77
  - `play()`, `pause()`, `seek(frame)`.
72
78
  - `setInput(nodeId, name, value)` and `setProp(nodeId, name, value)`, which await
73
- the resulting cook.
79
+ the resulting cook. Every cook re-runs only the nodes whose inputs or props
80
+ changed and everything downstream of them; an unchanged upstream node keeps
81
+ its outputs (`run({ reuse: "strict" })`).
74
82
  - `selectOutput(nodeId, port)` and `getOutput(nodeId, port)`.
75
83
  - `downloadOutput()`, returning the selected output as a `Blob`.
76
84
  - `resize(width, height)` for the display, without changing authored resolution.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Visual programming for generative art",
5
5
  "license": "MIT",
6
6
  "author": "FIELD.IO <marcus@field.io>",