@field/cascade 0.2.22 → 0.3.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 (228) hide show
  1. package/AGENTS.md +47 -7
  2. package/ARCHITECTURE.md +30 -2
  3. package/CHANGELOG.md +89 -0
  4. package/DESIGN.md +10 -5
  5. package/README.md +103 -7
  6. package/dist/assets/CodeEditor-BADFbTJS.js +623 -0
  7. package/dist/assets/CodePanel-faK6PRfz.js +2 -0
  8. package/dist/assets/CustomNodeDialog-DhlKuviy.js +1 -0
  9. package/dist/assets/{ExportDialog-DWN4nhXI.js → ExportDialog-BJu2Bpvv.js} +2 -2
  10. package/dist/assets/ProjectSettings-D6a6kjqR.js +2 -0
  11. package/dist/assets/SettingsDialog-DJZdIvLC.js +1 -0
  12. package/dist/assets/VersionHistoryDialog-C1X3HOhc.js +3 -0
  13. package/dist/assets/{core-Bji4aZUU.js → core-CiuiuWZi.js} +29 -38
  14. package/dist/assets/{cssMode-B6Z_Q1Kh.js → cssMode-D-NwZSMJ.js} +1 -1
  15. package/dist/assets/{editor.api-jMoW_43X.js → editor.api-DT3WEvBd.js} +1 -1
  16. package/dist/assets/{freemarker2-DvvPV6m5.js → freemarker2-eKNYQaiz.js} +1 -1
  17. package/dist/assets/geo-DUvVbLnx.js +1 -0
  18. package/dist/assets/{handlebars-B8rfuSIa.js → handlebars-C2w1ggK2.js} +1 -1
  19. package/dist/assets/{html-DyHm6JCp.js → html-DQb41jyo.js} +1 -1
  20. package/dist/assets/{htmlMode-E9XLWE5u.js → htmlMode-IhNlh6Lv.js} +1 -1
  21. package/dist/assets/{image-BUcQC7PG.js → image-DMCXBY-_.js} +26 -40
  22. package/dist/assets/index-BDV5xGBZ.css +1 -0
  23. package/dist/assets/index-HTCN44kj.js +212 -0
  24. package/dist/assets/index-client-BoJgsN9M.js +1 -0
  25. package/dist/assets/{javascript-C_bM5PCN.js → javascript-BIT6jMLC.js} +1 -1
  26. package/dist/assets/{jsonMode-BdyVS7G4.js → jsonMode-CNfyB1ro.js} +1 -1
  27. package/dist/assets/{legacy-DIu3XRwl.js → legacy-CAj2_7QE.js} +2 -2
  28. package/dist/assets/{liquid-CDkn3spA.js → liquid-CmSmJmOG.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-AB5DMAsr.js → lspLanguageFeatures-BqMm3uMP.js} +1 -1
  30. package/dist/assets/{mdx-BvH_NjDN.js → mdx-6r6YgOFw.js} +1 -1
  31. package/dist/assets/{nodeTypeUtils-CweA3NHd.js → nodeTypeUtils-BBA1JqHx.js} +1 -1
  32. package/dist/assets/{python-B7uii4fi.js → python-x1H0TQuI.js} +1 -1
  33. package/dist/assets/{razor-Z48fWdJc.js → razor-Dzl1vZFm.js} +1 -1
  34. package/dist/assets/{tsMode-BDgacVG4.js → tsMode-n6DMMe3a.js} +1 -1
  35. package/dist/assets/{typescript-Derb6Oqa.js → typescript-ziEz65xN.js} +1 -1
  36. package/dist/assets/{workers-BtoMX_pm.js → workers-D-qUdp-r.js} +1 -1
  37. package/dist/assets/{xml-B1CaFSrG.js → xml-DMZjt5lE.js} +1 -1
  38. package/dist/assets/{yaml-HmIeA0KH.js → yaml-DFe5qZSO.js} +1 -1
  39. package/dist/cli/index.js +16194 -10195
  40. package/dist/cli/runtime/io.ts +93 -7
  41. package/dist/contracts/animation.d.ts +40 -0
  42. package/dist/contracts/animation.js +14 -0
  43. package/dist/contracts/definition.d.ts +39 -1
  44. package/dist/contracts/document.d.ts +15 -0
  45. package/dist/contracts/index.d.ts +1 -0
  46. package/dist/contracts/index.js +1 -0
  47. package/dist/contracts/validate.d.ts +10 -1
  48. package/dist/contracts/validate.js +9 -1
  49. package/dist/graphs/default.cascade +1 -1
  50. package/dist/index.html +5 -5
  51. package/dist/io.d.ts +196 -0
  52. package/dist/io.js +303 -0
  53. package/dist/net.d.ts +1 -0
  54. package/dist/net.js +28 -0
  55. package/dist/runtime/animation/channel.d.ts +108 -0
  56. package/dist/runtime/animation/channel.js +204 -0
  57. package/dist/runtime/animation/frameRange.d.ts +78 -0
  58. package/dist/runtime/animation/frameRange.js +76 -0
  59. package/dist/runtime/animation/index.d.ts +10 -0
  60. package/dist/runtime/animation/index.js +8 -0
  61. package/dist/runtime/builtins/core/random.d.ts +1 -1
  62. package/dist/runtime/builtins/core/remap.d.ts +1 -1
  63. package/dist/runtime/builtins/geo/copyToPoints.d.ts +1 -1
  64. package/dist/runtime/builtins/geo/copyToPoints.js +1 -1
  65. package/dist/runtime/builtins/geo/generators.d.ts +1 -1
  66. package/dist/runtime/builtins/geo/merge.d.ts +1 -1
  67. package/dist/runtime/builtins/geo/svgExport.d.ts +1 -1
  68. package/dist/runtime/builtins/geo/svgExport.js +1 -1
  69. package/dist/runtime/builtins/geo/transform.d.ts +1 -1
  70. package/dist/runtime/builtins/geo/transform.js +1 -1
  71. package/dist/runtime/definition/architecture.d.ts +1 -1
  72. package/dist/runtime/definition/extract.d.ts +1 -1
  73. package/dist/runtime/definition/extract.js +1 -1
  74. package/dist/runtime/error.d.ts +1 -1
  75. package/dist/runtime/expressions/evaluator.d.ts +53 -0
  76. package/dist/runtime/expressions/evaluator.js +193 -0
  77. package/dist/runtime/expressions/index.d.ts +15 -0
  78. package/dist/runtime/expressions/index.js +12 -0
  79. package/dist/runtime/expressions/math.d.ts +75 -0
  80. package/dist/runtime/expressions/math.js +140 -0
  81. package/dist/runtime/expressions/preprocess.d.ts +33 -0
  82. package/dist/runtime/expressions/preprocess.js +102 -0
  83. package/dist/runtime/expressions/scope.d.ts +27 -0
  84. package/dist/runtime/expressions/scope.js +10 -0
  85. package/dist/runtime/expressions/time.d.ts +54 -0
  86. package/dist/runtime/expressions/time.js +71 -0
  87. package/dist/runtime/expressions/types.d.ts +66 -0
  88. package/dist/runtime/expressions/types.js +9 -0
  89. package/dist/runtime/geometry/attributes.d.ts +2 -2
  90. package/dist/runtime/geometry/attributes.js +1 -1
  91. package/dist/runtime/geometry/builder.d.ts +1 -1
  92. package/dist/runtime/geometry/builder.js +1 -1
  93. package/dist/runtime/geometry/copy.d.ts +1 -1
  94. package/dist/runtime/geometry/copy.js +1 -1
  95. package/dist/runtime/geometry/generate.d.ts +1 -1
  96. package/dist/runtime/geometry/groups.d.ts +1 -1
  97. package/dist/runtime/geometry/groups.js +1 -1
  98. package/dist/runtime/geometry/matrix.d.ts +1 -1
  99. package/dist/runtime/geometry/merge.d.ts +1 -1
  100. package/dist/runtime/geometry/merge.js +1 -1
  101. package/dist/runtime/geometry/primitives.d.ts +1 -1
  102. package/dist/runtime/geometry/primitives.js +1 -1
  103. package/dist/runtime/geometry/promote.d.ts +1 -1
  104. package/dist/runtime/geometry/promote.js +1 -1
  105. package/dist/runtime/geometry/resample.d.ts +1 -1
  106. package/dist/runtime/geometry/resample.js +1 -1
  107. package/dist/runtime/geometry/svg.d.ts +1 -1
  108. package/dist/runtime/geometry/svg.js +1 -1
  109. package/dist/runtime/geometry/transform.d.ts +1 -1
  110. package/dist/runtime/geometry/transform.js +1 -1
  111. package/dist/runtime/index.d.ts +4 -1
  112. package/dist/runtime/index.js +4 -1
  113. package/dist/runtime/params/animator.d.ts +74 -0
  114. package/dist/runtime/params/animator.js +137 -0
  115. package/dist/runtime/params/index.d.ts +11 -0
  116. package/dist/runtime/params/index.js +9 -0
  117. package/dist/runtime/params/resolve.d.ts +73 -0
  118. package/dist/runtime/params/resolve.js +62 -0
  119. package/dist/runtime/runtime.d.ts +22 -0
  120. package/dist/runtime/runtime.js +242 -14
  121. package/dist/runtime/types.d.ts +30 -4
  122. package/dist/shell.d.ts +3 -3
  123. package/doc/NODE_REFERENCE.md +395 -0
  124. package/doc/PROJECT_AUTHORING.md +148 -6
  125. package/package.json +26 -3
  126. package/dist/assets/CodeEditor-DoaNEUaM.js +0 -641
  127. package/dist/assets/CodePanel-B0_2XJ-t.js +0 -2
  128. package/dist/assets/CustomNodeDialog-026lzecD.js +0 -1
  129. package/dist/assets/ProjectSettings-BzT164Yx.js +0 -2
  130. package/dist/assets/SettingsDialog-DmHb1V0r.js +0 -1
  131. package/dist/assets/VersionHistoryDialog-hH6VsXLX.js +0 -3
  132. package/dist/assets/geo-CXmCtzMM.js +0 -2
  133. package/dist/assets/index-BXYXSdgh.js +0 -224
  134. package/dist/assets/index-CrNke7SW.css +0 -1
  135. package/dist/assets/index-client-DN_FvjN8.js +0 -1
  136. package/dist/assets/quill-DjZ69wL_.js +0 -121
  137. package/dist/cli/index.js.map +0 -7
  138. package/dist/contracts/capabilities.d.ts.map +0 -1
  139. package/dist/contracts/capabilities.js.map +0 -1
  140. package/dist/contracts/color.d.ts.map +0 -1
  141. package/dist/contracts/color.js.map +0 -1
  142. package/dist/contracts/definition.d.ts.map +0 -1
  143. package/dist/contracts/definition.js.map +0 -1
  144. package/dist/contracts/document.d.ts.map +0 -1
  145. package/dist/contracts/document.js.map +0 -1
  146. package/dist/contracts/geometry.d.ts.map +0 -1
  147. package/dist/contracts/geometry.js.map +0 -1
  148. package/dist/contracts/index.d.ts.map +0 -1
  149. package/dist/contracts/index.js.map +0 -1
  150. package/dist/contracts/schema.d.ts.map +0 -1
  151. package/dist/contracts/schema.js.map +0 -1
  152. package/dist/contracts/validate.d.ts.map +0 -1
  153. package/dist/contracts/validate.js.map +0 -1
  154. package/dist/contracts/values.d.ts.map +0 -1
  155. package/dist/contracts/values.js.map +0 -1
  156. package/dist/graphs/default-orig.cascade +0 -128
  157. package/dist/runtime/browser.d.ts.map +0 -1
  158. package/dist/runtime/browser.js.map +0 -1
  159. package/dist/runtime/builtins/core/index.d.ts.map +0 -1
  160. package/dist/runtime/builtins/core/index.js.map +0 -1
  161. package/dist/runtime/builtins/core/random.d.ts.map +0 -1
  162. package/dist/runtime/builtins/core/random.js.map +0 -1
  163. package/dist/runtime/builtins/core/remap.d.ts.map +0 -1
  164. package/dist/runtime/builtins/core/remap.js.map +0 -1
  165. package/dist/runtime/builtins/core/routing.d.ts.map +0 -1
  166. package/dist/runtime/builtins/core/routing.js.map +0 -1
  167. package/dist/runtime/builtins/core/structural.d.ts.map +0 -1
  168. package/dist/runtime/builtins/core/structural.js.map +0 -1
  169. package/dist/runtime/builtins/geo/copyToPoints.d.ts.map +0 -1
  170. package/dist/runtime/builtins/geo/copyToPoints.js.map +0 -1
  171. package/dist/runtime/builtins/geo/definitions.d.ts.map +0 -1
  172. package/dist/runtime/builtins/geo/definitions.js.map +0 -1
  173. package/dist/runtime/builtins/geo/generators.d.ts.map +0 -1
  174. package/dist/runtime/builtins/geo/generators.js.map +0 -1
  175. package/dist/runtime/builtins/geo/index.d.ts.map +0 -1
  176. package/dist/runtime/builtins/geo/index.js.map +0 -1
  177. package/dist/runtime/builtins/geo/merge.d.ts.map +0 -1
  178. package/dist/runtime/builtins/geo/merge.js.map +0 -1
  179. package/dist/runtime/builtins/geo/namespace.d.ts.map +0 -1
  180. package/dist/runtime/builtins/geo/namespace.js.map +0 -1
  181. package/dist/runtime/builtins/geo/svgExport.d.ts.map +0 -1
  182. package/dist/runtime/builtins/geo/svgExport.js.map +0 -1
  183. package/dist/runtime/builtins/geo/transform.d.ts.map +0 -1
  184. package/dist/runtime/builtins/geo/transform.js.map +0 -1
  185. package/dist/runtime/builtins/index.d.ts.map +0 -1
  186. package/dist/runtime/builtins/index.js.map +0 -1
  187. package/dist/runtime/definition/architecture.d.ts.map +0 -1
  188. package/dist/runtime/definition/architecture.js.map +0 -1
  189. package/dist/runtime/definition/extract.d.ts.map +0 -1
  190. package/dist/runtime/definition/extract.js.map +0 -1
  191. package/dist/runtime/error.d.ts.map +0 -1
  192. package/dist/runtime/error.js.map +0 -1
  193. package/dist/runtime/geometry/attributes.d.ts.map +0 -1
  194. package/dist/runtime/geometry/attributes.js.map +0 -1
  195. package/dist/runtime/geometry/builder.d.ts.map +0 -1
  196. package/dist/runtime/geometry/builder.js.map +0 -1
  197. package/dist/runtime/geometry/copy.d.ts.map +0 -1
  198. package/dist/runtime/geometry/copy.js.map +0 -1
  199. package/dist/runtime/geometry/generate.d.ts.map +0 -1
  200. package/dist/runtime/geometry/generate.js.map +0 -1
  201. package/dist/runtime/geometry/groups.d.ts.map +0 -1
  202. package/dist/runtime/geometry/groups.js.map +0 -1
  203. package/dist/runtime/geometry/index.d.ts.map +0 -1
  204. package/dist/runtime/geometry/index.js.map +0 -1
  205. package/dist/runtime/geometry/matrix.d.ts.map +0 -1
  206. package/dist/runtime/geometry/matrix.js.map +0 -1
  207. package/dist/runtime/geometry/merge.d.ts.map +0 -1
  208. package/dist/runtime/geometry/merge.js.map +0 -1
  209. package/dist/runtime/geometry/primitives.d.ts.map +0 -1
  210. package/dist/runtime/geometry/primitives.js.map +0 -1
  211. package/dist/runtime/geometry/promote.d.ts.map +0 -1
  212. package/dist/runtime/geometry/promote.js.map +0 -1
  213. package/dist/runtime/geometry/resample.d.ts.map +0 -1
  214. package/dist/runtime/geometry/resample.js.map +0 -1
  215. package/dist/runtime/geometry/svg.d.ts.map +0 -1
  216. package/dist/runtime/geometry/svg.js.map +0 -1
  217. package/dist/runtime/geometry/transform.d.ts.map +0 -1
  218. package/dist/runtime/geometry/transform.js.map +0 -1
  219. package/dist/runtime/index.d.ts.map +0 -1
  220. package/dist/runtime/index.js.map +0 -1
  221. package/dist/runtime/node.d.ts.map +0 -1
  222. package/dist/runtime/node.js.map +0 -1
  223. package/dist/runtime/runtime.d.ts.map +0 -1
  224. package/dist/runtime/runtime.js.map +0 -1
  225. package/dist/runtime/types.d.ts.map +0 -1
  226. package/dist/runtime/types.js.map +0 -1
  227. package/dist/shell.js.map +0 -7
  228. package/dist/stage.js.map +0 -7
@@ -1,6 +1,34 @@
1
- import { canConnectTypes, validateNodeDefinition, } from "cascade/contracts";
1
+ import { canConnectTypes, validateNodeDefinition, } from "@field/cascade/contracts";
2
2
  import { CascadeRuntimeError } from "./error.js";
3
3
  import { builtinNodeRegistrations } from "./builtins/index.js";
4
+ import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
5
+ import { PropAnimator, isBoundProp } from "./params/index.js";
6
+ import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
7
+ /**
8
+ * Preflight codes that describe something other than "this graph cannot run
9
+ * here", so `run` proceeds and a checker reports them as warnings.
10
+ *
11
+ * False positives are worse than blindness: a validator that cries wolf gets
12
+ * ignored, and then it is no better than the one that saw nothing. So the line
13
+ * is drawn at what the diagnostic is about.
14
+ *
15
+ * `preflight-environment` is about the checker's reach — a node saying
16
+ * `runsOn: 'browser'` claims nothing about the CLI and is not defective
17
+ * because somebody validated it from a terminal.
18
+ *
19
+ * `stray-params` is about the document — values were lost on the way in, which
20
+ * is worth saying loudly and is not a reason to refuse to run: a graph with a
21
+ * leftover `params` array runs perfectly well on its defaults, which is
22
+ * precisely the problem.
23
+ *
24
+ * This set lives here rather than in the CLI because the run path and the
25
+ * check path both need it, and it was a duplicated rule that let `validate`
26
+ * and `check` pass graphs `run` then rejected.
27
+ */
28
+ export const PREFLIGHT_WARNING_CODES = new Set([
29
+ "runtime/preflight-environment",
30
+ "runtime/stray-params",
31
+ ]);
4
32
  class MutableAbortSignal {
5
33
  report;
6
34
  aborted = false;
@@ -129,6 +157,16 @@ class Graph {
129
157
  authoredAnnotations;
130
158
  graphInputs;
131
159
  graphOutputs;
160
+ /**
161
+ * The graph's animated parameters, and the clock they resolve against.
162
+ *
163
+ * The clock is a value the caller sets per run (`run({ frame })`), never one
164
+ * this graph reads: same graph plus same frame gives the same output, which
165
+ * is the only reason a headless render can be trusted.
166
+ */
167
+ animator;
168
+ frame = MIN_FRAME;
169
+ fps = DEFAULT_FPS;
132
170
  constructor(host, nodes, connections, onDispose, annotations = []) {
133
171
  this.host = host;
134
172
  this.nodes = nodes;
@@ -138,10 +176,47 @@ class Graph {
138
176
  this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
139
177
  this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
140
178
  this.authoredAnnotations = Object.freeze(annotations.map((annotation) => snapshot(annotation)));
179
+ this.animator = new PropAnimator({
180
+ nodes,
181
+ node: (id) => this.byId.get(id),
182
+ inputNode: (node, index) => {
183
+ const feeding = this.connections
184
+ .filter((connection) => connection.kind === "data" &&
185
+ connection.target.nodeId === node.id)
186
+ .sort((a, b) => a.order - b.order);
187
+ const source = feeding[index]?.source.nodeId;
188
+ return (source ? this.byId.get(source) : undefined) ?? null;
189
+ },
190
+ // A failed expression has already fallen back to the stored value, so it
191
+ // is a diagnostic rather than a stopped run — but it is never silent: a
192
+ // parameter that quietly reverts is the hardest fault to see in a
193
+ // hundred rendered frames.
194
+ report: (nodeId, propName, error) => this.reportDiagnostic({
195
+ phase: "run",
196
+ code: "runtime/expression-failed",
197
+ message: `${nodeId}.${propName}: ${error}`,
198
+ path: `${nodeId}.${propName}`,
199
+ }),
200
+ });
201
+ this.animator.setFps(this.fps);
202
+ this.animator.setFrame(this.frame);
203
+ // Resolve once at load, so `inspect()` and a preset applied before any run
204
+ // both see the parameter at the graph's own first frame rather than the
205
+ // number that was last saved.
206
+ this.animator.resolveAll();
141
207
  }
142
208
  get state() {
143
209
  return this.graphState;
144
210
  }
211
+ /**
212
+ * Everything that would stop this host running the graph, without running
213
+ * it: a node whose environment this host is not, a bridged node that cannot
214
+ * be bridged, a declared capability this host does not install. Empty means
215
+ * the graph clears the same gate `run` puts it through.
216
+ */
217
+ preflight() {
218
+ return this.preflightDiagnostics(this.nodes);
219
+ }
145
220
  inspect() {
146
221
  const nodes = this.nodes.map((node) => ({
147
222
  id: node.id,
@@ -212,7 +287,26 @@ class Graph {
212
287
  misuse("runtime/structural-prop", `${nodeId}.${propName} is structural and requires a graph reload`);
213
288
  if (!node.definition.props?.[propName])
214
289
  misuse("runtime/invalid-prop", `${nodeId}.${propName} is not a prop`);
215
- node.props[propName] = runtimeValue(value, node.definition.props[propName].type, false, `${nodeId}.${propName}`);
290
+ const resolved = runtimeValue(value, node.definition.props[propName].type, false, `${nodeId}.${propName}`);
291
+ node.props[propName] = resolved;
292
+ this.rebind(node, propName, resolved);
293
+ }
294
+ /**
295
+ * Keep a bound prop's STORED value in step with a direct write.
296
+ *
297
+ * A channel or an expression still wins on the next resolve — setting a
298
+ * keyed parameter's value does not delete its keys, in Studio or here — but
299
+ * the value written is the one the parameter falls back to, so dropping it
300
+ * would make a later `clearChannel` reinstate a number nobody chose.
301
+ */
302
+ rebind(node, propName, value) {
303
+ const binding = node.bindings[propName];
304
+ if (binding)
305
+ node.bindings[propName] = { ...binding, value };
306
+ }
307
+ reportDiagnostic(diagnostic) {
308
+ this.diagnostics.push(diagnostic);
309
+ this.emit({ type: "diagnostic", diagnostic });
216
310
  }
217
311
  async applyPreset(preset) {
218
312
  this.requireReady();
@@ -247,10 +341,50 @@ class Graph {
247
341
  }
248
342
  for (const [node, name, value] of inputDrafts)
249
343
  node.inputs[name] = value;
250
- for (const [node, name, value] of propDrafts)
344
+ for (const [node, name, value] of propDrafts) {
251
345
  node.props[name] = value;
346
+ this.rebind(node, name, value);
347
+ }
348
+ }
349
+ /**
350
+ * The frame this graph is cooking at, and the rate frames are counted in.
351
+ *
352
+ * Separate from `run` so a caller can scrub without cooking — and so that
353
+ * `setFrame` then `inspect()` reports a keyed parameter at that frame, which
354
+ * is what a panel needs and what `run` alone cannot give it.
355
+ */
356
+ setFrame(frame) {
357
+ if (!Number.isFinite(frame))
358
+ return;
359
+ this.frame = Math.max(MIN_FRAME, frame);
360
+ this.animator.setFrame(this.frame);
361
+ this.animator.resolveAll();
362
+ }
363
+ setFps(fps) {
364
+ if (!Number.isFinite(fps))
365
+ return;
366
+ this.fps = Math.max(1, fps);
367
+ this.animator.setFps(this.fps);
368
+ this.animator.resolveAll();
369
+ }
370
+ getFrame() {
371
+ return this.frame;
372
+ }
373
+ getFps() {
374
+ return this.fps;
252
375
  }
253
376
  run(options = {}) {
377
+ // The frame belongs to the run, not to the graph's history: a render loop
378
+ // states which frame it wants and gets exactly that, with no dependence on
379
+ // what the previous run left behind.
380
+ if (options.fps !== undefined)
381
+ this.setFps(options.fps);
382
+ if (options.frame !== undefined)
383
+ this.setFrame(options.frame);
384
+ // Every bound parameter, resolved before any node executes — so a node
385
+ // still receives a plain number and never reads a clock.
386
+ else
387
+ this.animator.resolveAll();
254
388
  return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId);
255
389
  }
256
390
  trigger(nodeId, inputName, payload, options = {}) {
@@ -339,7 +473,7 @@ class Graph {
339
473
  }
340
474
  : this.runSelection(target);
341
475
  const { reachable, initial } = selection;
342
- this.preflight(reachable);
476
+ this.assertPreflight(reachable);
343
477
  if (!signal.aborted)
344
478
  await Promise.all(reachable
345
479
  .filter((node) => !this.isRemote(node))
@@ -504,6 +638,7 @@ class Graph {
504
638
  report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
505
639
  };
506
640
  await node.execute({
641
+ nodeId: node.id,
507
642
  inputs,
508
643
  outputs,
509
644
  props,
@@ -544,33 +679,70 @@ class Graph {
544
679
  }
545
680
  this.emit({ type: "node:complete", runId, nodeId: node.id });
546
681
  }
547
- preflight(nodes) {
682
+ /**
683
+ * Every reason this host cannot run these nodes, collected rather than
684
+ * thrown. `assertPreflight` is the run path and raises the first of them;
685
+ * `preflight()` is the static path and hands the whole list to a caller that
686
+ * is not executing anything.
687
+ *
688
+ * There is deliberately one implementation. `validate` and `check` used to
689
+ * answer "will this graph run?" without ever consulting a host, so a node
690
+ * declaring a capability nobody installs passed both and then failed here —
691
+ * the two commands could not see the fault they exist to report. Any second
692
+ * copy of these rules would drift back into that.
693
+ */
694
+ preflightDiagnostics(nodes) {
695
+ const diagnostics = [];
696
+ const push = (code, nodeId, message) => diagnostics.push({ phase: "preflight", code, path: nodeId, message });
548
697
  for (const node of nodes) {
698
+ if (node.strayParams.length)
699
+ push("runtime/stray-params", node.id, `${node.moduleId} stores ${node.strayParams.join(", ")} under "params", which this runtime does not read — move ${node.strayParams.length === 1 ? "it" : "them"} to "props" or "inputs", whichever the definition declares, or the ${node.strayParams.length === 1 ? "value is" : "values are"} silently the default`);
549
700
  const remote = this.isRemote(node);
550
701
  const locationWorks = node.definition.runsOn === "portable" ||
551
702
  node.definition.runsOn === this.host.environment ||
552
703
  remote;
553
- if (!locationWorks)
554
- throw runtimeError("runtime/preflight-environment", `${node.moduleId} requires ${node.definition.runsOn}`);
704
+ if (!locationWorks) {
705
+ push("runtime/preflight-environment", node.id, `${node.moduleId} requires ${node.definition.runsOn}`);
706
+ continue;
707
+ }
555
708
  if (remote) {
556
709
  const hasTriggers = [
557
710
  ...Object.values(node.definition.inputs ?? {}),
558
711
  ...Object.values(node.definition.outputs ?? {}),
559
712
  ].some((port) => port.kind === "trigger");
560
713
  if (hasTriggers)
561
- throw runtimeError("runtime/preflight-bridge-trigger", `${node.moduleId} cannot send triggers over the server bridge`);
714
+ push("runtime/preflight-bridge-trigger", node.id, `${node.moduleId} cannot send triggers over the server bridge`);
562
715
  const hasTexture = [
563
716
  ...Object.values(node.definition.inputs ?? {}),
564
717
  ...Object.values(node.definition.outputs ?? {}),
565
718
  ].some((port) => port.kind === "data" && port.type === "texture");
566
719
  if (hasTexture)
567
- throw runtimeError("runtime/preflight-bridge-texture", `${node.moduleId} cannot send textures over the server bridge`);
720
+ push("runtime/preflight-bridge-texture", node.id, `${node.moduleId} cannot send textures over the server bridge`);
568
721
  }
569
722
  else
570
723
  for (const capability of node.definition.capabilities ?? [])
571
724
  if (!this.host.capabilities[capability])
572
- throw runtimeError("runtime/missing-capability", `Missing capability ${capability} for ${node.moduleId}`);
725
+ push("runtime/missing-capability", node.id, `Missing capability ${capability} for ${node.moduleId} (node ${node.id}) — the ${this.host.environment} host ${this.hostProvides()}`);
573
726
  }
727
+ return diagnostics;
728
+ }
729
+ /**
730
+ * What the host actually installed, asked of the host rather than written
731
+ * down. `serverBridge` is left out: it is how a node runs somewhere else,
732
+ * not a capability a definition can declare.
733
+ */
734
+ hostProvides() {
735
+ const supplied = Object.keys(this.host.capabilities)
736
+ .filter((name) => name !== "serverBridge" && !!this.host.capabilities[name])
737
+ .sort();
738
+ return supplied.length
739
+ ? `provides ${supplied.join(", ")}`
740
+ : `provides no capabilities`;
741
+ }
742
+ assertPreflight(nodes) {
743
+ const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !PREFLIGHT_WARNING_CODES.has(diagnostic.code));
744
+ if (first)
745
+ throw new CascadeRuntimeError(first.code, [first]);
574
746
  }
575
747
  isRemote(node) {
576
748
  return (this.host.environment === "browser" &&
@@ -776,12 +948,34 @@ function materialize(authored, registration) {
776
948
  }
777
949
  for (const [name, definition] of Object.entries(registration.definition.props ?? {}))
778
950
  props[name] = runtimeValue(definition.default, definition.type, false, `${authored.id}.${name}`);
951
+ const bindings = {};
779
952
  for (const [name, saved] of Object.entries(authored.props ?? {})) {
780
953
  const definition = registration.definition.props?.[name];
781
- if (definition)
782
- props[name] = runtimeValue(saved && typeof saved === "object" && "value" in saved
783
- ? saved.value
784
- : saved, definition.type, false, `${authored.id}.${name}`);
954
+ if (!definition)
955
+ continue;
956
+ // `{ value, expression?, channel? }` — the object form declared in
957
+ // contracts/animation.ts. A bare value is the same prop with nothing more
958
+ // to say, which is what every older file on disk contains.
959
+ const object = saved && typeof saved === "object" && "value" in saved
960
+ ? saved
961
+ : null;
962
+ const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
963
+ props[name] = value;
964
+ if (!object)
965
+ continue;
966
+ const channel = deserializeChannel(object.channel);
967
+ const expression = typeof object.expression === "string" && object.expression
968
+ ? object.expression
969
+ : undefined;
970
+ const binding = {
971
+ value,
972
+ ...(expression ? { expression } : {}),
973
+ ...(isEmptyChannel(channel) ? {} : { channel }),
974
+ };
975
+ // Only a prop that actually carries something, so `animated` stays a real
976
+ // question and an unanimated graph pays nothing for this.
977
+ if (isBoundProp(binding))
978
+ bindings[name] = binding;
785
979
  }
786
980
  return {
787
981
  id: authored.id,
@@ -791,9 +985,43 @@ function materialize(authored, registration) {
791
985
  definition: registration.definition,
792
986
  inputs,
793
987
  props,
988
+ bindings,
989
+ strayParams: strayParams(authored, registration),
794
990
  outputs: new Map(),
795
991
  };
796
992
  }
993
+ /**
994
+ * The names a document stored under `params` that this runtime will not read.
995
+ *
996
+ * Nothing above consults `params`: the loops that fill `inputs` and `props`
997
+ * read the definition and then the document's own `inputs` and `props`, so a
998
+ * value left in `params` is dropped without a word and the parameter keeps its
999
+ * default. That is not a hypothetical — on `cascade-logo` it put a seed back to
1000
+ * 7 and two rasterizers back to 1024, so three nodes wrote one file and the
1001
+ * drawing changed, with every command still reporting success.
1002
+ *
1003
+ * **Inputs count, not just props.** The first version of this checked props
1004
+ * alone and so was blind to exactly the case the conversions produce most: a
1005
+ * parameter another node drives has to be declared as an `inputs` entry,
1006
+ * because a v1 prop cannot be promoted to a pin. `field-logo`'s `angle` is one,
1007
+ * and a negative control over the pre-migration document named five nodes and
1008
+ * missed that one.
1009
+ *
1010
+ * Only names the definition actually declares are reported. A leftover key
1011
+ * naming nothing is a different fault, and reporting it would make this fire on
1012
+ * documents where no value was lost.
1013
+ */
1014
+ function strayParams(authored, registration) {
1015
+ const definition = registration.definition;
1016
+ const declared = new Set([
1017
+ ...Object.keys(definition.props ?? {}),
1018
+ ...Object.keys(definition.inputs ?? {}),
1019
+ ]);
1020
+ const names = Array.isArray(authored.params)
1021
+ ? authored.params.map((saved) => saved?.name)
1022
+ : Object.keys(authored.params ?? {});
1023
+ return names.filter((name) => typeof name === "string" && declared.has(name));
1024
+ }
797
1025
  function specializeCoreRegistration(registration, authored, nodes) {
798
1026
  if (registration.moduleId === "cascade.core.Input" ||
799
1027
  registration.moduleId === "cascade.core.Output") {
@@ -1,5 +1,5 @@
1
- import type { AssetCapability, AssetRef, CascadeAbortSignal, CascadeDocument, CascadePreset, Diagnostic, FileCapability, ImageRef, JsonValue, MediaCapability, NodeDefinition, NodeExecute, ProgressReporter, PythonCapability, SerializableValue, ShellCapability, TextureHandle, WebGLCapability } from "cascade/contracts";
2
- export type { CascadeDocument, CascadeDocumentAnnotation, CascadeDocumentConnection, CascadeDocumentNode, CascadePreset, } from "cascade/contracts";
1
+ import type { AssetCapability, AssetRef, CascadeAbortSignal, CascadeDocument, CascadePreset, Diagnostic, FileCapability, ImageRef, JsonValue, MediaCapability, NodeDefinition, NodeExecute, ProgressReporter, PythonCapability, SerializableValue, ShellCapability, TextureHandle, WebGLCapability } from "@field/cascade/contracts";
2
+ export type { CascadeDocument, CascadeDocumentAnnotation, CascadeDocumentConnection, CascadeDocumentNode, CascadePreset, } from "@field/cascade/contracts";
3
3
  export interface DefinitionNodeRegistration<D extends NodeDefinition = NodeDefinition> {
4
4
  readonly kind: "definition-v1";
5
5
  readonly moduleId: string;
@@ -88,6 +88,18 @@ export interface RunOptions {
88
88
  readonly target?: RunTarget;
89
89
  readonly signal?: CascadeAbortSignal;
90
90
  readonly runId?: string;
91
+ /**
92
+ * The frame to resolve animated parameters at — a keyframe channel is
93
+ * sampled here and `$F`/`$FF`/`$T` are derived from it. Fractional frames
94
+ * are kept. Omitted leaves the graph on the frame it is already on.
95
+ *
96
+ * A value the caller states rather than a clock the runtime reads: same
97
+ * graph plus same frame gives the same output, which is what makes an
98
+ * offline render reproducible.
99
+ */
100
+ readonly frame?: number;
101
+ /** Frames per second, for `$FPS` and for `$T`. Default 30. */
102
+ readonly fps?: number;
91
103
  }
92
104
  export interface RunResult {
93
105
  readonly runId: string;
@@ -163,13 +175,13 @@ export type RuntimeEvent = {
163
175
  readonly runId: string;
164
176
  readonly nodeId: string;
165
177
  readonly inputName: string;
166
- readonly event: import("cascade/contracts").TriggerEvent;
178
+ readonly event: import("@field/cascade/contracts").TriggerEvent;
167
179
  } | {
168
180
  readonly type: "trigger:delivered";
169
181
  readonly runId: string;
170
182
  readonly nodeId: string;
171
183
  readonly inputName: string;
172
- readonly event: import("cascade/contracts").TriggerEvent;
184
+ readonly event: import("@field/cascade/contracts").TriggerEvent;
173
185
  } | {
174
186
  readonly type: "node:output";
175
187
  readonly runId: string;
@@ -203,6 +215,12 @@ export interface CascadeRuntime {
203
215
  }
204
216
  export interface LoadedCascadeGraph {
205
217
  readonly state: GraphState;
218
+ /**
219
+ * Host-aware static gate: the diagnostics `run` would raise before executing
220
+ * anything, collected instead of thrown. This is what lets a validator see a
221
+ * missing capability, which is a fault it was previously blind to.
222
+ */
223
+ preflight(): readonly Diagnostic[];
206
224
  inspect(): GraphInspection;
207
225
  setInput(nodeId: string, inputName: string, value: unknown): Promise<void>;
208
226
  setGraphInput(name: string, value: unknown): Promise<void>;
@@ -213,6 +231,14 @@ export interface LoadedCascadeGraph {
213
231
  runId?: string;
214
232
  }>): Promise<RunResult>;
215
233
  run(options?: RunOptions): Promise<RunResult>;
234
+ /**
235
+ * Move the graph's clock without cooking. Animated parameters re-resolve
236
+ * immediately, so `inspect()` reports them at this frame.
237
+ */
238
+ setFrame(frame: number): void;
239
+ setFps(fps: number): void;
240
+ getFrame(): number;
241
+ getFps(): number;
216
242
  cancel(reason?: string): void;
217
243
  subscribe(listener: (event: RuntimeEvent) => void): () => void;
218
244
  getOutput(nodeId: string, outputName: string): unknown;
package/dist/shell.d.ts CHANGED
@@ -3,6 +3,6 @@ export {
3
3
  type ShellProcessErrorKind,
4
4
  type ShellRunOptions,
5
5
  type ShellRunResult,
6
- } from 'cascade/contracts';
7
- export declare function run(command: string, args?: readonly string[], options?: import('cascade/contracts').ShellRunOptions): Promise<import('cascade/contracts').ShellRunResult>;
8
- export declare function runJson<T = unknown>(command: string, args?: readonly string[], options?: import('cascade/contracts').ShellRunOptions): Promise<T>;
6
+ } from '@field/cascade/contracts';
7
+ export declare function run(command: string, args?: readonly string[], options?: import('@field/cascade/contracts').ShellRunOptions): Promise<import('@field/cascade/contracts').ShellRunResult>;
8
+ export declare function runJson<T = unknown>(command: string, args?: readonly string[], options?: import('@field/cascade/contracts').ShellRunOptions): Promise<T>;