@field/cascade 0.8.0 → 0.10.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 (115) hide show
  1. package/AGENTS.md +17 -1
  2. package/CHANGELOG.md +214 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -25
  5. package/dist/assets/CodeEditor-DSZBpzdy.js +623 -0
  6. package/dist/assets/CodePanel-sY6sZa7B.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRKnqZhp.js} +1 -1
  8. package/dist/assets/{ExportDialog-CjTbbTZJ.js → ExportDialog-C7dVGua4.js} +2 -2
  9. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +1 -0
  10. package/dist/assets/ProjectSettings-Cw17nNe3.js +2 -0
  11. package/dist/assets/SettingsDialog-p25h3tj0.js +1 -0
  12. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-DzFLUotB.js} +2 -2
  13. package/dist/assets/{core-BInLG4gw.js → core-BICrEt0y.js} +8 -8
  14. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-D7TXw57I.js} +1 -1
  15. package/dist/assets/definitions-DUww2Rt-.js +1 -0
  16. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-DvkCO3ko.js} +1 -1
  17. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-BA2lxsF1.js} +1 -1
  18. package/dist/assets/geo-C2RHOVHC.js +1 -0
  19. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-BcDiyPd8.js} +1 -1
  20. package/dist/assets/{html-CVpSv_k9.js → html-C7O4TyhX.js} +1 -1
  21. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-WaDGF3Mj.js} +1 -1
  22. package/dist/assets/{image-BLn--oSF.js → image-DR4oeyJB.js} +3 -3
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-DyvDC6pS.js +219 -0
  25. package/dist/assets/index-client-DqYmKstU.js +1 -0
  26. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-CpIEK7tz.js} +1 -1
  27. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-B2FltW0f.js} +1 -1
  28. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  29. package/dist/assets/{liquid-efUz51Wf.js → liquid-CN6FFHZE.js} +1 -1
  30. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-QhV9XtLp.js} +1 -1
  31. package/dist/assets/math-DaDEgWKt.js +1 -0
  32. package/dist/assets/{mdx-DP_sjGVy.js → mdx-DE89pafV.js} +1 -1
  33. package/dist/assets/pop-pLH7L0PW.js +1 -0
  34. package/dist/assets/{python-aAuP3LUi.js → python-ByTllTB9.js} +1 -1
  35. package/dist/assets/{razor-7M6sMmC5.js → razor-USYlRRCE.js} +1 -1
  36. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  37. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BXpN8iUz.js} +1 -1
  38. package/dist/assets/{typescript-ddhYBGyT.js → typescript-DInBjGQQ.js} +1 -1
  39. package/dist/assets/{workers-X5la8DDT.js → workers-C7Ye5B7D.js} +1 -1
  40. package/dist/assets/{xml-BzbXa3Ps.js → xml-U0Uocqyo.js} +1 -1
  41. package/dist/assets/{yaml-CXs-VzxL.js → yaml-CBw_PVYf.js} +1 -1
  42. package/dist/cli/index.js +19978 -3423
  43. package/dist/cli/runtime/stage.ts +70 -10
  44. package/dist/contracts/bundle.d.ts +75 -0
  45. package/dist/contracts/bundle.js +112 -0
  46. package/dist/contracts/definition.d.ts +67 -4
  47. package/dist/contracts/document.d.ts +29 -0
  48. package/dist/contracts/editor.d.ts +172 -0
  49. package/dist/contracts/editor.js +2 -0
  50. package/dist/contracts/index.d.ts +2 -0
  51. package/dist/contracts/index.js +2 -0
  52. package/dist/contracts/validate.js +59 -3
  53. package/dist/contracts/values.d.ts +3 -3
  54. package/dist/contracts/values.js +1 -0
  55. package/dist/index.html +4 -4
  56. package/dist/player/app.js +17 -5
  57. package/dist/player/editor.js +1894 -0
  58. package/dist/player/editorEngine.js +477 -0
  59. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  60. package/dist/runtime/builtins/core/bundle.js +81 -0
  61. package/dist/runtime/builtins/core/index.d.ts +3 -2
  62. package/dist/runtime/builtins/core/index.js +6 -2
  63. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  64. package/dist/runtime/builtins/core/routing.js +28 -0
  65. package/dist/runtime/edit/document.d.ts +94 -0
  66. package/dist/runtime/edit/document.js +194 -0
  67. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  68. package/dist/runtime/edit/graphEditor.js +893 -0
  69. package/dist/runtime/edit/index.d.ts +12 -0
  70. package/dist/runtime/edit/index.js +6 -0
  71. package/dist/runtime/edit/layout.d.ts +50 -0
  72. package/dist/runtime/edit/layout.js +126 -0
  73. package/dist/runtime/edit/operations.d.ts +65 -0
  74. package/dist/runtime/edit/operations.js +57 -0
  75. package/dist/runtime/edit/parameters.d.ts +75 -0
  76. package/dist/runtime/edit/parameters.js +284 -0
  77. package/dist/runtime/edit/read.d.ts +35 -0
  78. package/dist/runtime/edit/read.js +65 -0
  79. package/dist/runtime/editor/index.d.ts +104 -0
  80. package/dist/runtime/editor/index.js +439 -0
  81. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  82. package/dist/runtime/expressions/bundlePaths.js +55 -0
  83. package/dist/runtime/expressions/index.d.ts +2 -0
  84. package/dist/runtime/expressions/index.js +1 -0
  85. package/dist/runtime/index.d.ts +2 -0
  86. package/dist/runtime/index.js +2 -0
  87. package/dist/runtime/params/animator.d.ts +5 -0
  88. package/dist/runtime/params/animator.js +16 -0
  89. package/dist/runtime/runtime.js +238 -12
  90. package/dist/runtime/seed.d.ts +29 -0
  91. package/dist/runtime/seed.js +52 -0
  92. package/dist/runtime/types.d.ts +12 -0
  93. package/dist/stage.js +40 -9
  94. package/dist/studio/panel.d.ts +2 -0
  95. package/doc/CLI_REFERENCE.md +100 -0
  96. package/doc/EDITOR_EXPORT.md +226 -0
  97. package/doc/HEADLESS_GPU.md +27 -0
  98. package/doc/MCP.md +53 -0
  99. package/doc/NODE_REFERENCE.md +66 -0
  100. package/doc/PROJECT_AUTHORING.md +137 -0
  101. package/package.json +15 -6
  102. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  103. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  104. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  105. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  106. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  107. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  108. package/dist/assets/geo-D2VPjAdt.js +0 -1
  109. package/dist/assets/index-CMiBdEuT.js +0 -212
  110. package/dist/assets/index-DMIuml9a.css +0 -1
  111. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  112. package/dist/assets/legacy-ZA7textG.js +0 -2
  113. package/dist/assets/math-B6hpxPJO.js +0 -1
  114. package/dist/assets/pop-CYUIjT6x.js +0 -1
  115. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Bundle field references in expressions.
3
+ *
4
+ * `ch("format.dpi")` reads field `dpi` of the bundle wired into this node's
5
+ * `format` input. The path names an input, not another node's output, on
6
+ * purpose: the wire is the dependency, so the scheduler orders the producer
7
+ * first and an edit upstream reaches this parameter. Reading another node's
8
+ * output by absolute path would be a dependency no wire records, cooked in
9
+ * whatever order the graph happened to be in.
10
+ *
11
+ * Port and parameter names are identifiers, so a dot can only mean a field.
12
+ */
13
+ export interface BundleFieldReference {
14
+ /** The input port, on the node whose parameter this is. */
15
+ readonly port: string;
16
+ readonly field: string;
17
+ /** The path as written. */
18
+ readonly path: string;
19
+ }
20
+ /** `format.dpi` or `./format.dpi` as port and field; null for anything else. */
21
+ export declare function parseBundleFieldPath(path: string): {
22
+ port: string;
23
+ field: string;
24
+ } | null;
25
+ /** Every relative bundle field an expression reads, in source order. */
26
+ export declare function bundleFieldReferences(expression: string): BundleFieldReference[];
27
+ /**
28
+ * An absolute or parent path whose last segment looks like `port.field`:
29
+ * an attempt to read another node's bundle without a wire. Returned so a
30
+ * checker can say what to do instead.
31
+ */
32
+ export declare function remoteBundlePath(path: string): {
33
+ node: string;
34
+ port: string;
35
+ field: string;
36
+ } | null;
37
+ //# sourceMappingURL=bundlePaths.d.ts.map
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Bundle field references in expressions.
3
+ *
4
+ * `ch("format.dpi")` reads field `dpi` of the bundle wired into this node's
5
+ * `format` input. The path names an input, not another node's output, on
6
+ * purpose: the wire is the dependency, so the scheduler orders the producer
7
+ * first and an edit upstream reaches this parameter. Reading another node's
8
+ * output by absolute path would be a dependency no wire records, cooked in
9
+ * whatever order the graph happened to be in.
10
+ *
11
+ * Port and parameter names are identifiers, so a dot can only mean a field.
12
+ */
13
+ const REFERENCE = /\b(?:ch|chs|chv)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
14
+ /** `format.dpi` or `./format.dpi` as port and field; null for anything else. */
15
+ export function parseBundleFieldPath(path) {
16
+ const local = path.startsWith("./") ? path.slice(2) : path;
17
+ if (local.includes("/"))
18
+ return null;
19
+ const dot = local.indexOf(".");
20
+ if (dot <= 0 || dot === local.length - 1)
21
+ return null;
22
+ return { port: local.slice(0, dot), field: local.slice(dot + 1) };
23
+ }
24
+ /** Every relative bundle field an expression reads, in source order. */
25
+ export function bundleFieldReferences(expression) {
26
+ const references = [];
27
+ REFERENCE.lastIndex = 0;
28
+ let match;
29
+ while ((match = REFERENCE.exec(expression)) !== null) {
30
+ const parsed = parseBundleFieldPath(match[1]);
31
+ if (parsed)
32
+ references.push({ ...parsed, path: match[1] });
33
+ }
34
+ return references;
35
+ }
36
+ /**
37
+ * An absolute or parent path whose last segment looks like `port.field`:
38
+ * an attempt to read another node's bundle without a wire. Returned so a
39
+ * checker can say what to do instead.
40
+ */
41
+ export function remoteBundlePath(path) {
42
+ const local = path.startsWith("./") ? path.slice(2) : path;
43
+ const slash = local.lastIndexOf("/");
44
+ if (slash === -1)
45
+ return null;
46
+ const last = local.slice(slash + 1);
47
+ const dot = last.indexOf(".");
48
+ if (dot <= 0 || dot === last.length - 1)
49
+ return null;
50
+ const node = local.slice(0, slash).split("/").filter((part) => part && part !== "." && part !== "..").pop();
51
+ if (!node)
52
+ return null;
53
+ return { node, port: last.slice(0, dot), field: last.slice(dot + 1) };
54
+ }
55
+ //# sourceMappingURL=bundlePaths.js.map
@@ -6,6 +6,8 @@
6
6
  * evaluate parameters with the same engine.
7
7
  */
8
8
  export { ExpressionEvaluator } from "./evaluator.js";
9
+ export { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath } from "./bundlePaths.js";
10
+ export type { BundleFieldReference } from "./bundlePaths.js";
9
11
  export { clamp, fit, fit01, lerp, noise, padzero, random, smooth } from "./math.js";
10
12
  export { TIME_IDENTIFIERS, TIME_TOKENS, hasTimeReference, preprocessExpression } from "./preprocess.js";
11
13
  export { DEFAULT_FPS, MIN_FRAME, TimeState } from "./time.js";
@@ -6,6 +6,7 @@
6
6
  * evaluate parameters with the same engine.
7
7
  */
8
8
  export { ExpressionEvaluator } from "./evaluator.js";
9
+ export { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath } from "./bundlePaths.js";
9
10
  export { clamp, fit, fit01, lerp, noise, padzero, random, smooth } from "./math.js";
10
11
  export { TIME_IDENTIFIERS, TIME_TOKENS, hasTimeReference, preprocessExpression } from "./preprocess.js";
11
12
  export { DEFAULT_FPS, MIN_FRAME, TimeState } from "./time.js";
@@ -7,6 +7,8 @@ export * from "./geometry/index.js";
7
7
  export * from "./gpu.js";
8
8
  export * from "./params/index.js";
9
9
  export * from "./scene/index.js";
10
+ export * from "./seed.js";
10
11
  export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
11
12
  export type * from "./types.js";
13
+ export * from "./edit/index.js";
12
14
  //# sourceMappingURL=index.d.ts.map
@@ -7,5 +7,7 @@ export * from "./geometry/index.js";
7
7
  export * from "./gpu.js";
8
8
  export * from "./params/index.js";
9
9
  export * from "./scene/index.js";
10
+ export * from "./seed.js";
10
11
  export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
12
+ export * from "./edit/index.js";
11
13
  //# sourceMappingURL=index.js.map
@@ -33,6 +33,9 @@ export interface PropAnimatorTopology<TNode extends AnimatableNode> {
33
33
  node(id: string): TNode | undefined;
34
34
  /** The node feeding `node`'s data input at `index`, for `opinput()`. */
35
35
  inputNode(node: TNode, index: number): TNode | null;
36
+ /** The value wired into `node`'s input `name`, for bundle field reads
37
+ * (`ch("format.dpi")`). Optional: a host without it reads none. */
38
+ inputValue?(node: TNode, name: string): unknown;
36
39
  /** A failed expression. The parameter has already fallen back to its stored
37
40
  * value; this is so the host can say so rather than render a wrong number
38
41
  * in silence. */
@@ -50,6 +53,8 @@ export declare class PropAnimator<TNode extends AnimatableNode> {
50
53
  setFps(fps: number): void;
51
54
  /** Write every bound parameter's value at the current frame into `props`. */
52
55
  resolveAll(): void;
56
+ /** Re-resolve one node's bound parameters, e.g. once its inputs exist. */
57
+ resolveNode(node: TNode): void;
53
58
  /**
54
59
  * One parameter, at the current frame.
55
60
  *
@@ -19,6 +19,7 @@
19
19
  * deterministic.
20
20
  */
21
21
  import { ExpressionEvaluator } from "../expressions/evaluator.js";
22
+ import { parseBundleFieldPath } from "../expressions/bundlePaths.js";
22
23
  import { isBoundProp, resolvePropBinding } from "./resolve.js";
23
24
  export class PropAnimator {
24
25
  topology;
@@ -45,6 +46,11 @@ export class PropAnimator {
45
46
  for (const name of Object.keys(node.bindings))
46
47
  this.resolve(node, name);
47
48
  }
49
+ /** Re-resolve one node's bound parameters, e.g. once its inputs exist. */
50
+ resolveNode(node) {
51
+ for (const name of Object.keys(node.bindings))
52
+ this.resolve(node, name);
53
+ }
48
54
  /**
49
55
  * One parameter, at the current frame.
50
56
  *
@@ -99,6 +105,16 @@ export class PropAnimator {
99
105
  // like it works until somebody keys the parameter it points at.
100
106
  if (owner.bindings[name])
101
107
  return { value: this.resolve(owner, name) };
108
+ // `format.dpi`: a field of the bundle wired into input `format`.
109
+ const field = parseBundleFieldPath(name);
110
+ if (field && this.topology.inputValue) {
111
+ const bundle = this.topology.inputValue(owner, field.port);
112
+ if (bundle === null || typeof bundle !== "object")
113
+ return null;
114
+ if (!Object.prototype.hasOwnProperty.call(bundle, field.field))
115
+ return null;
116
+ return { value: bundle[field.field] };
117
+ }
102
118
  if (name in owner.props)
103
119
  return { value: owner.props[name] };
104
120
  if (name in owner.inputs)
@@ -1,10 +1,12 @@
1
- import { canConnectTypes, validateNodeDefinition, } from "@field/cascade/contracts";
1
+ import { bundleFieldType, bundleSchemasCompatible, bundleValueProblem, canConnectTypes, isBundleValue, selectBundleFields, suggestName, unknownFieldMessage, validateNodeDefinition, } from "@field/cascade/contracts";
2
2
  import { CascadeRuntimeError } from "./error.js";
3
3
  import { builtinNodeRegistrations } from "./builtins/index.js";
4
4
  import { deserializeChannel, isEmptyChannel } from "./animation/channel.js";
5
5
  import { PropAnimator, isBoundProp } from "./params/index.js";
6
+ import { bundleFieldReferences, parseBundleFieldPath, remoteBundlePath, } from "./expressions/bundlePaths.js";
6
7
  import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
7
8
  import { isGpuHost } from "./gpu.js";
9
+ import { DEFAULT_GRAPH_SEED, graphSeed, nodeSeed, seededRandom } from "./seed.js";
8
10
  /**
9
11
  * Preflight codes that describe something other than "this graph cannot run
10
12
  * here", so `run` proceeds and a checker reports them as warnings.
@@ -164,7 +166,11 @@ class Runtime {
164
166
  const connections = (document.connections ?? []).map((connection, index) => materializeConnection(connection, index, byId));
165
167
  validateCardinality(byId, connections);
166
168
  validateTopology(nodes, connections);
167
- const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations);
169
+ const graph = new Graph(this.host, nodes, connections, () => this.graphs.delete(graph), document.annotations, graphSeed(document));
170
+ // The graph's own frame rate, saved with it; a run's `fps` still overrides.
171
+ const fps = document.timeline?.fps;
172
+ if (typeof fps === "number" && Number.isFinite(fps) && fps > 0)
173
+ graph.setFps(fps);
168
174
  this.graphs.add(graph);
169
175
  return graph;
170
176
  }
@@ -181,6 +187,7 @@ class Graph {
181
187
  nodes;
182
188
  connections;
183
189
  onDispose;
190
+ seed;
184
191
  graphState = "ready";
185
192
  byId;
186
193
  dataConnectionsByInput;
@@ -206,11 +213,14 @@ class Graph {
206
213
  animator;
207
214
  frame = MIN_FRAME;
208
215
  fps = DEFAULT_FPS;
209
- constructor(host, nodes, connections, onDispose, annotations = []) {
216
+ constructor(host, nodes, connections, onDispose, annotations = [],
217
+ /** The document's seed; each node's `context.seed` derives from it. */
218
+ seed = DEFAULT_GRAPH_SEED) {
210
219
  this.host = host;
211
220
  this.nodes = nodes;
212
221
  this.connections = connections;
213
222
  this.onDispose = onDispose;
223
+ this.seed = seed;
214
224
  this.byId = new Map(nodes.map((node) => [node.id, node]));
215
225
  const byInput = new Map();
216
226
  const byNode = new Map();
@@ -251,6 +261,7 @@ class Graph {
251
261
  const source = feeding[index]?.source.nodeId;
252
262
  return (source ? this.byId.get(source) : undefined) ?? null;
253
263
  },
264
+ inputValue: (node, name) => this.dataInputValue(node, name),
254
265
  // A failed expression has already fallen back to the stored value, so it
255
266
  // is a diagnostic rather than a stopped run — but it is never silent: a
256
267
  // parameter that quietly reverts is the hardest fault to see in a
@@ -447,7 +458,7 @@ class Graph {
447
458
  // still receives a plain number and never reads a clock.
448
459
  else
449
460
  this.animator.resolveAll();
450
- return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId);
461
+ return this.trackRun(options.target ?? { kind: "all" }, options.signal, options.runId, undefined, options.reuse === true || options.reuse === "strict" ? options.reuse : false);
451
462
  }
452
463
  trigger(nodeId, inputName, payload, options = {}) {
453
464
  const node = this.requireNode(nodeId);
@@ -486,8 +497,8 @@ class Graph {
486
497
  this.disposal ??= this.performDispose();
487
498
  return this.disposal;
488
499
  }
489
- trackRun(target, signal, runId, injection) {
490
- const running = this.executeRun(target, signal, runId, injection);
500
+ trackRun(target, signal, runId, injection, reuse = false) {
501
+ const running = this.executeRun(target, signal, runId, injection, reuse);
491
502
  this.activeRun = running;
492
503
  const clear = () => {
493
504
  if (this.activeRun === running)
@@ -510,7 +521,7 @@ class Graph {
510
521
  this.listeners.clear();
511
522
  this.onDispose();
512
523
  }
513
- async executeRun(target, externalSignal, requestedId, injection) {
524
+ async executeRun(target, externalSignal, requestedId, injection, reuse = false) {
514
525
  this.requireReady();
515
526
  const runId = requestedId ?? `run-${this.runCounter++}`;
516
527
  const signal = new MutableAbortSignal((diagnostic) => this.host.report(diagnostic));
@@ -565,10 +576,20 @@ class Graph {
565
576
  */
566
577
  const insideFeedback = this.feedbackDescendants();
567
578
  const ordered = topological(this.nodes, this.connections).filter((node) => initial.includes(node) && !insideFeedback.has(node.id));
579
+ /** Nodes that cooked in this run, for `reuse: "strict"`. */
580
+ const cooked = new Set();
568
581
  for (const node of ordered) {
569
582
  if (signal.aborted)
570
583
  break;
584
+ if (reuse && this.reusable(node) && !(reuse === "strict" && this.feedsFrom(node, cooked))) {
585
+ this.emit({ type: "node:start", runId, nodeId: node.id });
586
+ this.emit({ type: "node:complete", runId, nodeId: node.id });
587
+ continue;
588
+ }
571
589
  await this.executeNode(node, runId, signal, {}, enqueue);
590
+ cooked.add(node.id);
591
+ if (!signal.aborted)
592
+ node.lastCook = this.cookKey(node);
572
593
  }
573
594
  if (injection && !signal.aborted)
574
595
  enqueue(injection.nodeId, injection.inputName, injection.payload, {
@@ -645,14 +666,23 @@ class Graph {
645
666
  return [name, Object.freeze(values)];
646
667
  }
647
668
  const connection = connections[0];
669
+ const value = connection
670
+ ? this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName)
671
+ : node.inputs[name];
672
+ // A consumer that declares the fields it reads sees only those, so
673
+ // the declaration is the dependency rather than a comment about it.
648
674
  return [
649
675
  name,
650
- connection
651
- ? this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName)
652
- : node.inputs[name],
676
+ definition.type === "bundle" && definition.fields
677
+ ? selectBundleFields(value, definition.fields)
678
+ : value,
653
679
  ];
654
680
  }));
655
681
  Object.freeze(inputs);
682
+ // A parameter whose expression reads a wired bundle field resolves now,
683
+ // when the bundle has been cooked, not at the start of the run.
684
+ if (readsBundleFields(node))
685
+ this.animator.resolveNode(node);
656
686
  if (this.isRemote(node)) {
657
687
  const bridge = this.host.capabilities.serverBridge;
658
688
  if (!bridge)
@@ -680,7 +710,7 @@ class Graph {
680
710
  const definition = node.definition.outputs?.[name];
681
711
  if (!definition || definition.kind !== "data")
682
712
  continue;
683
- const value = runtimeValue(response.outputs[name], definition.type, false, `${node.id}.${name}`);
713
+ const value = outputValue(response.outputs[name], definition, `${node.id}.${name}`);
684
714
  node.outputs.set(name, value);
685
715
  this.emit({
686
716
  type: "node:output",
@@ -709,7 +739,7 @@ class Graph {
709
739
  }),
710
740
  }
711
741
  : {
712
- set: (value) => pendingData.set(name, runtimeValue(value, definition.type, false, `${node.id}.${name}`)),
742
+ set: (value) => pendingData.set(name, outputValue(value, definition, `${node.id}.${name}`)),
713
743
  },
714
744
  ]));
715
745
  const capabilityEntries = [];
@@ -733,6 +763,7 @@ class Graph {
733
763
  const progress = {
734
764
  report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event }),
735
765
  };
766
+ const seed = nodeSeed(this.seed, node.id);
736
767
  await node.execute({
737
768
  nodeId: node.id,
738
769
  inputs,
@@ -741,6 +772,9 @@ class Graph {
741
772
  capabilities,
742
773
  signal,
743
774
  progress,
775
+ seed,
776
+ // Fresh per cook: a node that draws N numbers draws the same N each time.
777
+ random: seededRandom(seed),
744
778
  });
745
779
  if (signal.aborted)
746
780
  return;
@@ -831,6 +865,79 @@ class Graph {
831
865
  if (!this.host.capabilities[capability])
832
866
  push("runtime/missing-capability", node.id, `Missing capability ${capability} for ${node.moduleId} (node ${node.id}) — the ${this.host.environment} host ${this.hostProvides()}`);
833
867
  }
868
+ diagnostics.push(...this.bundleDiagnostics(nodes));
869
+ return diagnostics;
870
+ }
871
+ /**
872
+ * Bundle fields read by name, checked against what the producer declares:
873
+ * a consumer's declared `fields`, an expression's `ch("format.dpi")`, and a
874
+ * Pick's `field`. A producer that declares no fields (Pack, a subnet Input,
875
+ * a project node that left them out) cannot be checked and is not. Reading
876
+ * another node's bundle by absolute path is refused with what to do instead.
877
+ */
878
+ bundleDiagnostics(nodes) {
879
+ const diagnostics = [];
880
+ const push = (nodeId, code, message, at = nodeId) => diagnostics.push({ phase: "preflight", code, path: at, message });
881
+ const producerFields = (node, input) => {
882
+ const connection = this.dataConnectionsByInput.get(`${node.id}\0${input}`)?.[0];
883
+ if (!connection)
884
+ return null;
885
+ const output = this.byId.get(connection.source.nodeId)?.definition.outputs?.[connection.source.outputName];
886
+ if (output?.kind !== "data" || output.type !== "bundle" || !output.fields)
887
+ return null;
888
+ return Object.keys(output.fields);
889
+ };
890
+ for (const node of nodes) {
891
+ for (const [name, input] of Object.entries(node.definition.inputs ?? {})) {
892
+ if (input.kind !== "data" || input.type !== "bundle" || !input.fields)
893
+ continue;
894
+ const known = producerFields(node, name);
895
+ if (!known)
896
+ continue;
897
+ for (const field of input.fields)
898
+ if (!known.includes(field))
899
+ push(node.id, "runtime/bundle-unknown-field", unknownFieldMessage(field, known, `${node.id}.${name} (declared fields)`), `${node.id}.${name}`);
900
+ }
901
+ if (node.moduleId === "cascade.core.Pick") {
902
+ const field = String(node.props.field ?? "").trim();
903
+ const known = producerFields(node, "bundle");
904
+ if (field && known && !known.includes(field))
905
+ push(node.id, "runtime/bundle-unknown-field", unknownFieldMessage(field, known, `${node.id}.field`), `${node.id}.field`);
906
+ }
907
+ for (const [prop, binding] of Object.entries(node.bindings)) {
908
+ if (!binding.expression)
909
+ continue;
910
+ for (const path of expressionPaths(binding.expression)) {
911
+ const remote = remoteBundlePath(path);
912
+ if (remote) {
913
+ const owner = this.byId.get(remote.node);
914
+ const output = owner?.definition.outputs?.[remote.port];
915
+ if (output?.kind === "data" && output.type === "bundle")
916
+ push(node.id, "runtime/bundle-remote-read", `${node.id}.${prop} reads "${path}", another node's bundle, without a wire. `
917
+ + `Wire ${remote.node}.${remote.port} into a bundle input on ${node.id} and read "${remote.port}.${remote.field}" instead`, `${node.id}.${prop}`);
918
+ continue;
919
+ }
920
+ const reference = parseBundleFieldPath(path);
921
+ if (!reference)
922
+ continue;
923
+ const input = node.definition.inputs?.[reference.port];
924
+ if (!input) {
925
+ const ports = Object.entries(node.definition.inputs ?? {})
926
+ .filter(([, item]) => item.kind === "data" && item.type === "bundle")
927
+ .map(([portName]) => portName);
928
+ const suggestion = suggestName(reference.port, ports);
929
+ push(node.id, "runtime/bundle-unknown-port", `${node.id}.${prop} reads "${path}", but ${node.moduleId} has no input "${reference.port}"`
930
+ + (suggestion ? ` — did you mean "${suggestion}.${reference.field}"?` : ""), `${node.id}.${prop}`);
931
+ continue;
932
+ }
933
+ if (input.kind !== "data" || input.type !== "bundle")
934
+ continue;
935
+ const known = producerFields(node, reference.port);
936
+ if (known && !known.includes(reference.field))
937
+ push(node.id, "runtime/bundle-unknown-field", unknownFieldMessage(reference.field, known, `${node.id}.${prop}`), `${node.id}.${prop}`);
938
+ }
939
+ }
940
+ }
834
941
  return diagnostics;
835
942
  }
836
943
  /**
@@ -1012,6 +1119,66 @@ class Graph {
1012
1119
  * per step. Computed once per run rather than per step: the hierarchy cannot
1013
1120
  * change mid-run, and this is walked for every node in the order.
1014
1121
  */
1122
+ /**
1123
+ * Early cutoff for `run({ reuse: true })`: a node whose wired inputs are the
1124
+ * very same values and whose resolved props are equal keeps its outputs.
1125
+ *
1126
+ * Sound because a definition-v1 node may observe only its inputs, its props
1127
+ * and declared capabilities, and time reaches it through props. Upstream
1128
+ * reuse keeps output identity, so one unchanged stage skips everything below
1129
+ * it that is unchanged too. Trigger-driven, feedback and remote nodes always
1130
+ * cook. Off by default: a host opts in when it re-runs a graph on every edit.
1131
+ */
1132
+ /** Whether any data input of `node` (or, for a subnet, any inner output)
1133
+ * comes from a node in `cooked`. */
1134
+ feedsFrom(node, cooked) {
1135
+ for (const name of Object.keys(node.definition.inputs ?? {})) {
1136
+ for (const connection of this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [])
1137
+ if (cooked.has(connection.source.nodeId))
1138
+ return true;
1139
+ }
1140
+ if (node.moduleId === "cascade.core.Input" && node.parent && cooked.size)
1141
+ return true;
1142
+ if (node.moduleId === "cascade.core.Subnet")
1143
+ for (const child of this.nodes)
1144
+ if (child.parent === node.id && cooked.has(child.id))
1145
+ return true;
1146
+ return false;
1147
+ }
1148
+ reusable(node) {
1149
+ const last = node.lastCook;
1150
+ if (!last || this.isRemote(node) || node.definition.container === "feedback")
1151
+ return false;
1152
+ if (Object.values(node.definition.inputs ?? {}).some((input) => input.kind === "trigger"))
1153
+ return false;
1154
+ if (Object.values(node.definition.outputs ?? {}).some((output) => output.kind === "trigger"))
1155
+ return false;
1156
+ const next = this.cookKey(node);
1157
+ return sameValues(last.inputs, next.inputs, Object.is) && sameValues(last.props, next.props, sameJson);
1158
+ }
1159
+ cookKey(node) {
1160
+ const inputs = [];
1161
+ for (const [name, definition] of Object.entries(node.definition.inputs ?? {})) {
1162
+ if (definition.kind !== "data")
1163
+ continue;
1164
+ if (node.moduleId === "cascade.core.Input" && name === "value" && node.parent) {
1165
+ const parent = this.requireNode(node.parent);
1166
+ inputs.push(this.dataInputValue(parent, `input_${integerProp(node.props.inputIndex)}`));
1167
+ continue;
1168
+ }
1169
+ const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
1170
+ if (!connections.length)
1171
+ inputs.push(node.inputs[name]);
1172
+ for (const connection of connections)
1173
+ inputs.push(this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
1174
+ }
1175
+ // A subnet's outputs are its inner Output nodes' values, which are inputs in all but name.
1176
+ if (node.moduleId === "cascade.core.Subnet")
1177
+ for (const child of this.nodes)
1178
+ if (child.parent === node.id && child.moduleId === "cascade.core.Output")
1179
+ inputs.push(child.outputs.get("output"));
1180
+ return { inputs, props: Object.keys(node.definition.props ?? {}).map((name) => node.props[name]) };
1181
+ }
1015
1182
  feedbackDescendants() {
1016
1183
  const containers = new Set(this.nodes
1017
1184
  .filter((node) => node.definition.container === "feedback")
@@ -1480,6 +1647,12 @@ function materializeConnection(connection, index, nodes) {
1480
1647
  input.kind === "data" &&
1481
1648
  !canConnectTypes(output.type, input.type))
1482
1649
  misuse("runtime/connection-type", `Connection ${index} has incompatible types ${output.type} and ${input.type}`);
1650
+ if (output.kind === "data" &&
1651
+ input.kind === "data" &&
1652
+ output.type === "bundle" &&
1653
+ input.type === "bundle" &&
1654
+ !bundleSchemasCompatible(output.schema, input.schema))
1655
+ misuse("runtime/connection-schema", `Connection ${index} wires a ${output.schema} bundle into an input that takes ${input.schema}`);
1483
1656
  return Object.freeze({
1484
1657
  source: Object.freeze(endpoints.source),
1485
1658
  target: Object.freeze(endpoints.target),
@@ -1766,6 +1939,42 @@ const TUPLE_LENGTHS = {
1766
1939
  mat4: 16,
1767
1940
  color: 4,
1768
1941
  };
1942
+ /**
1943
+ * An output value, checked against its declaration. A bundle with declared
1944
+ * fields may only set those, and each one must be a valid value of its type.
1945
+ */
1946
+ function outputValue(value, definition, path) {
1947
+ if (definition.type !== "bundle" || !definition.fields)
1948
+ return runtimeValue(value, definition.type, false, path);
1949
+ const fields = definition.fields;
1950
+ const problem = bundleValueProblem(value, fields);
1951
+ if (problem)
1952
+ misuse("runtime/value-type", `${path} ${problem}`);
1953
+ if (isBundleValue(value))
1954
+ for (const [field, item] of Object.entries(value))
1955
+ runtimeValue(item, bundleFieldType(fields[field]), true, `${path}.${field}`);
1956
+ return runtimeValue(value, "bundle", false, path);
1957
+ }
1958
+ /** Every path an expression names through ch/chs/chv. */
1959
+ function expressionPaths(expression) {
1960
+ const paths = [];
1961
+ const pattern = /\b(?:ch|chs|chv)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
1962
+ let match;
1963
+ while ((match = pattern.exec(expression)) !== null)
1964
+ paths.push(match[1]);
1965
+ return paths;
1966
+ }
1967
+ /** Whether any of a node's bound parameters reads a wired bundle field. */
1968
+ function readsBundleFields(node) {
1969
+ for (const binding of Object.values(node.bindings))
1970
+ if (binding.expression)
1971
+ for (const reference of bundleFieldReferences(binding.expression)) {
1972
+ const input = node.definition.inputs?.[reference.port];
1973
+ if (input?.kind === "data" && input.type === "bundle")
1974
+ return true;
1975
+ }
1976
+ return false;
1977
+ }
1769
1978
  function runtimeValue(value, type, allowUndefined, path) {
1770
1979
  if (value === undefined) {
1771
1980
  if (allowUndefined)
@@ -1783,7 +1992,9 @@ function runtimeValue(value, type, allowUndefined, path) {
1783
1992
  value.every((item) => type.endsWith("i")
1784
1993
  ? typeof item === "number" && Number.isInteger(item)
1785
1994
  : typeof item === "number" && Number.isFinite(item))) ||
1995
+ (type === "bundle" && isBundleValue(value)) ||
1786
1996
  (!tupleLength &&
1997
+ type !== "bundle" &&
1787
1998
  type !== "float" &&
1788
1999
  type !== "int" &&
1789
2000
  type !== "bool" &&
@@ -1792,4 +2003,19 @@ function runtimeValue(value, type, allowUndefined, path) {
1792
2003
  misuse("runtime/value-type", `${path} is not a valid ${type}`);
1793
2004
  return snapshot(value);
1794
2005
  }
2006
+ function sameValues(a, b, equal) {
2007
+ return a.length === b.length && a.every((value, index) => equal(value, b[index]));
2008
+ }
2009
+ function sameJson(a, b) {
2010
+ if (Object.is(a, b))
2011
+ return true;
2012
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
2013
+ return false;
2014
+ try {
2015
+ return JSON.stringify(a) === JSON.stringify(b);
2016
+ }
2017
+ catch {
2018
+ return false;
2019
+ }
2020
+ }
1795
2021
  //# sourceMappingURL=runtime.js.map
@@ -0,0 +1,29 @@
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 declare 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 declare function graphSeed(document: Readonly<{
20
+ seed?: unknown;
21
+ }> | null | undefined): number;
22
+ /**
23
+ * A node's seed: FNV-1a over `<graph seed>:<node id>`, finished with
24
+ * MurmurHash3's avalanche so neighbouring ids give unrelated seeds.
25
+ */
26
+ export declare function nodeSeed(seedOfGraph: number, nodeId: string): number;
27
+ /** Mulberry32: a small, fast stream of floats in [0, 1) from one seed. */
28
+ export declare function seededRandom(seed: number): () => number;
29
+ //# sourceMappingURL=seed.d.ts.map