@field/cascade 0.3.0 → 0.3.1

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 (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/assets/{CodeEditor-BADFbTJS.js → CodeEditor-D22fVi-4.js} +4 -4
  3. package/dist/assets/{CodePanel-faK6PRfz.js → CodePanel-DPNEYfcR.js} +2 -2
  4. package/dist/assets/{CustomNodeDialog-DhlKuviy.js → CustomNodeDialog-DeHCAvgb.js} +1 -1
  5. package/dist/assets/{ProjectSettings-D6a6kjqR.js → ProjectSettings-DWpVtWEx.js} +1 -1
  6. package/dist/assets/{SettingsDialog-DJZdIvLC.js → SettingsDialog-za00IS3L.js} +1 -1
  7. package/dist/assets/{VersionHistoryDialog-C1X3HOhc.js → VersionHistoryDialog-DTMGhI0_.js} +1 -1
  8. package/dist/assets/{core-CiuiuWZi.js → core-JsyKMO1k.js} +1 -1
  9. package/dist/assets/{cssMode-D-NwZSMJ.js → cssMode-emdRheFK.js} +1 -1
  10. package/dist/assets/{editor.api-DT3WEvBd.js → editor.api-xHX-fMSo.js} +1 -1
  11. package/dist/assets/{freemarker2-eKNYQaiz.js → freemarker2-C3M8_Gs3.js} +1 -1
  12. package/dist/assets/geo-BAR6lQp5.js +1 -0
  13. package/dist/assets/{handlebars-C2w1ggK2.js → handlebars-CBMurtFk.js} +1 -1
  14. package/dist/assets/{html-DQb41jyo.js → html-xbLQwknw.js} +1 -1
  15. package/dist/assets/{htmlMode-IhNlh6Lv.js → htmlMode-2wKdDTon.js} +1 -1
  16. package/dist/assets/{image-DMCXBY-_.js → image-i1j6nWKe.js} +1 -1
  17. package/dist/assets/{index-HTCN44kj.js → index-CMcH8AgP.js} +35 -35
  18. package/dist/assets/{javascript-BIT6jMLC.js → javascript-DJaOo_2I.js} +1 -1
  19. package/dist/assets/{jsonMode-CNfyB1ro.js → jsonMode-Bheas8_0.js} +1 -1
  20. package/dist/assets/{liquid-CmSmJmOG.js → liquid-BCj_BGbM.js} +1 -1
  21. package/dist/assets/{lspLanguageFeatures-BqMm3uMP.js → lspLanguageFeatures-BfIsx_xz.js} +1 -1
  22. package/dist/assets/{mdx-6r6YgOFw.js → mdx-DwiQDiVO.js} +1 -1
  23. package/dist/assets/{python-x1H0TQuI.js → python-BjrB5nqj.js} +1 -1
  24. package/dist/assets/{razor-Dzl1vZFm.js → razor-CfoBmGDs.js} +1 -1
  25. package/dist/assets/{tsMode-n6DMMe3a.js → tsMode-BcEoMSxp.js} +1 -1
  26. package/dist/assets/{typescript-ziEz65xN.js → typescript-DIg7TEmQ.js} +1 -1
  27. package/dist/assets/{workers-D-qUdp-r.js → workers-vXYuwtPw.js} +1 -1
  28. package/dist/assets/{xml-DMZjt5lE.js → xml-DexLQIQy.js} +1 -1
  29. package/dist/assets/{yaml-DFe5qZSO.js → yaml-DUnD_1dE.js} +1 -1
  30. package/dist/cli/index.js +317 -49
  31. package/dist/cli/runtime/gpu.ts +76 -0
  32. package/dist/contracts/capabilities.d.ts +69 -6
  33. package/dist/contracts/definition.d.ts +21 -1
  34. package/dist/contracts/schema.js +8 -5
  35. package/dist/contracts/validate.js +11 -2
  36. package/dist/gpu.d.ts +71 -0
  37. package/dist/gpu.js +53 -0
  38. package/dist/index.html +1 -1
  39. package/dist/runtime/runtime.d.ts +23 -3
  40. package/dist/runtime/runtime.js +118 -24
  41. package/dist/runtime/types.d.ts +3 -3
  42. package/package.json +5 -1
  43. package/dist/assets/geo-DUvVbLnx.js +0 -1
@@ -1,4 +1,4 @@
1
- import type { AssetRef, ImageRef, JsonValue, TextureHandle } from "./values.js";
1
+ import type { AssetRef, ImageRef, JsonValue } from "./values.js";
2
2
  export interface CascadeAbortSignal {
3
3
  readonly aborted: boolean;
4
4
  readonly reason?: unknown;
@@ -66,10 +66,73 @@ export interface PythonCapability {
66
66
  progress: ProgressReporter;
67
67
  }): Promise<JsonValue | AssetRef | ImageRef>;
68
68
  }
69
- export interface WebGLCapability {
70
- createTexture(descriptor: JsonValue, options: {
71
- signal: CascadeAbortSignal;
72
- }): Promise<ResourceLease<TextureHandle>>;
69
+ /**
70
+ * A GPU a node can compute on, and the state that has to outlive one cook.
71
+ *
72
+ * This replaces `WebGLCapability`, which nothing implemented, nothing declared
73
+ * and no host installed — its one method was WebGL2-shaped, and keeping both
74
+ * names would have meant two devices in one page. Two devices cannot exchange
75
+ * a texture: an allocation made on one is not bindable, sampleable or copyable
76
+ * on the other, and the failure is a validation error at bind time rather than
77
+ * anything a graph author would recognise. So a shared device is not a
78
+ * preference, it is the precondition for a chain of GPU nodes at all.
79
+ *
80
+ * **Stage 0 deliberately carries three members and no textures.** Ports still
81
+ * pass `image`, so every picture is byte-identical to before; what this buys is
82
+ * one device instead of one per node module, an owner for `device.lost`, and
83
+ * somewhere legitimate to keep a pipeline — which is what lets the
84
+ * `architecture/ambient-state` rule stay strict. The texture store, the
85
+ * generation-stamped handles and the readback are Stage 1, and the shape of
86
+ * this interface is meant to be lived with for a few days first.
87
+ *
88
+ * The `GPUDevice` type comes from `@webgpu/types`, added to this package's
89
+ * `tsconfig.json` as its only `types` entry. Contracts compiles with
90
+ * `lib: ["ES2022"]` and no DOM on purpose — it is the vocabulary both hosts
91
+ * share, so pulling in the whole browser would let a browser-only type reach a
92
+ * portable definition by accident. A hand-written stub was the alternative and
93
+ * would have been worse than a dependency: it would shadow the genuine
94
+ * `GPUDevice` at the node's own call site, so every real method call would
95
+ * stop typechecking.
96
+ *
97
+ * The device is raw on purpose. A wrapped pipeline API would be a second thing
98
+ * to learn, would lag the spec, and would fight the reason to reach for WebGPU
99
+ * at all, which is that you write WGSL and encode passes. The capability wraps
100
+ * only what carries lifetime or identity, because those are the parts a host
101
+ * has to be able to account for.
102
+ */
103
+ export interface GpuCapability {
104
+ /**
105
+ * The one device for this page. Shared by every node declaring `gpu`, which
106
+ * is the whole point — see the note above on why two would be useless.
107
+ */
108
+ readonly device: GPUDevice;
109
+ /**
110
+ * Who is actually rendering, recorded so a difference between two runs of
111
+ * the same graph is attributable rather than a mystery. WebGPU output can
112
+ * differ across adapters, and today there is no way to answer "which one".
113
+ */
114
+ readonly adapterInfo: Readonly<{
115
+ vendor: string;
116
+ architecture: string;
117
+ device: string;
118
+ description: string;
119
+ }>;
120
+ /** What this adapter will actually allow — `maxTextureDimension3D` and the
121
+ * rest. Read at runtime rather than assumed; the 3D default is far below
122
+ * the 2D one and a hard-coded guess would be wrong on some machines. */
123
+ readonly limits: Readonly<Record<string, number>>;
124
+ /**
125
+ * A resource that outlives one cook — a compiled pipeline, a sampler, a
126
+ * bind group layout. Keyed per node by the host, so two instances of one
127
+ * module do not share a cache entry and a module-level `Map` is never the
128
+ * right answer again.
129
+ *
130
+ * `create` runs once per key. `destroy` is called when the entry is dropped:
131
+ * when the device is lost, or when the graph disposes. This is the member
132
+ * that makes `architecture/module-state` keepable — it was previously
133
+ * forbidding the only available way to hold a pipeline.
134
+ */
135
+ cache<T>(key: string, create: (device: GPUDevice) => T, destroy?: (value: T) => void): T;
73
136
  }
74
137
  export interface ShellRunResult {
75
138
  readonly stdout: string;
@@ -102,7 +165,7 @@ export interface CapabilityMap {
102
165
  readonly assets: AssetCapability;
103
166
  readonly media: MediaCapability;
104
167
  readonly python: PythonCapability;
105
- readonly webgl: WebGLCapability;
168
+ readonly gpu: GpuCapability;
106
169
  readonly shell: ShellCapability;
107
170
  }
108
171
  export type NodeCapabilityName = keyof CapabilityMap;
@@ -1,7 +1,7 @@
1
1
  import type { CapabilityMap, CascadeAbortSignal, NodeCapabilityName, ProgressReporter } from "./capabilities.js";
2
2
  import type { CascadeType, CoreType, JsonValue, NamespacedType, SerializableValueForType, ValueForType } from "./values.js";
3
3
  export type RuntimeEnvironment = "portable" | "browser" | "server";
4
- export type CapabilityForEnvironment<E extends RuntimeEnvironment> = E extends "portable" ? "assets" | "media" : E extends "browser" ? "assets" | "media" | "webgl" : "files" | "assets" | "media" | "python" | "shell";
4
+ export type CapabilityForEnvironment<E extends RuntimeEnvironment> = E extends "portable" ? "assets" | "media" : E extends "browser" ? "assets" | "media" | "gpu" : "files" | "assets" | "media" | "python" | "shell";
5
5
  export type DataTypeForEnvironment<E extends RuntimeEnvironment> = E extends "browser" ? CascadeType : Exclude<CascadeType, "texture">;
6
6
  type ControlFor<T extends CascadeType> = T extends "float" ? "number" | "slider" | "range" : T extends "int" ? "int" | "slider" | "range" : T extends "bool" ? "boolean" : T extends "string" ? "text" | "textarea" | "select" : T extends "vec2" | "vec3" | "vec4" | "vec2i" | "vec3i" | "vec4i" ? "vector" : T extends "mat2" | "mat3" | "mat4" ? "matrix" : T extends "color" ? "color" : T extends "image" ? "image" : T extends "asset" ? "asset" | "select" : T extends "texture" ? never : "select";
7
7
  /**
@@ -80,6 +80,26 @@ export type PropDefinition<T extends PropDataType> = {
80
80
  readonly default: SerializableValueForType<T>;
81
81
  readonly label?: string;
82
82
  readonly description?: string;
83
+ /**
84
+ * An expression the prop starts with, so a node can come out of the box
85
+ * already moving — `sine-oscillator` and `ripple` baked in `$T` as dynamic
86
+ * nodes and shipped a static 0 once converted, which is what this restores.
87
+ * Marcus's decision, 2026-09-08, asked as a taste question about his own
88
+ * tool: a node like that should self-animate when dropped.
89
+ *
90
+ * **It is a default, so it loses to a stored value like any other.** A
91
+ * document that saved a plain number for this prop keeps that number and
92
+ * does not silently start animating on load. That is the mirror of the
93
+ * dropped-`params` fault: whatever the author set has to win, and the test
94
+ * for this asserts the losing case rather than the winning one.
95
+ *
96
+ * Applied only when the document stored nothing at all for the prop. It is a
97
+ * real expression once applied, not a hidden fallback — visible in the
98
+ * Inspector, editable, and deletable, which is how a stored expression
99
+ * already reads and the only version that does not lie about where the
100
+ * number came from.
101
+ */
102
+ readonly expression?: string;
83
103
  } & NumericMetadata<T> & AcceptMetadata<T> & SelectMetadata<T>;
84
104
  export type AnyPropDefinition = {
85
105
  [T in PropDataType]: PropDefinition<T>;
@@ -41,10 +41,13 @@ const dataProperties = {
41
41
  const data = object(dataProperties, ["kind", "type"]);
42
42
  const output = object({ kind: dataProperties.kind, type: dataProperties.type, description }, ["kind", "type"]);
43
43
  const { kind: _kind, variadic: _variadic, ...propProperties } = dataProperties;
44
- const prop = object({ ...propProperties, label: { type: "string" } }, [
45
- "type",
46
- "default",
47
- ]);
44
+ const prop = object({
45
+ ...propProperties,
46
+ label: { type: "string" },
47
+ // Props only, not inputs: an input's value comes from whatever is wired to
48
+ // it, so a default expression there would be describing the wrong thing.
49
+ expression: { type: "string", minLength: 1 },
50
+ }, ["type", "default"]);
48
51
  function environmentCapabilities(runsOn, capabilities) {
49
52
  return {
50
53
  if: { properties: { runsOn: { const: runsOn } } },
@@ -72,7 +75,7 @@ export const nodeDefinitionSchema = {
72
75
  }, ["apiVersion", "runsOn"]),
73
76
  allOf: [
74
77
  environmentCapabilities("portable", ["assets", "media"]),
75
- environmentCapabilities("browser", ["assets", "media", "webgl"]),
78
+ environmentCapabilities("browser", ["assets", "media", "gpu"]),
76
79
  environmentCapabilities("server", [
77
80
  "files",
78
81
  "assets",
@@ -14,7 +14,7 @@ const CONTROLS = new Set(["number", "slider", "range", "int", "boolean", "text",
14
14
  */
15
15
  export const CAPABILITIES_BY_ENVIRONMENT = {
16
16
  portable: ["assets", "media"],
17
- browser: ["assets", "media", "webgl"],
17
+ browser: ["assets", "media", "gpu"],
18
18
  server: ["files", "assets", "media", "python", "shell"],
19
19
  };
20
20
  const ALLOWED = CAPABILITIES_BY_ENVIRONMENT;
@@ -22,7 +22,7 @@ const fields = (names) => new Set(names.split(" "));
22
22
  const TOP_FIELDS = fields("apiVersion runsOn container capabilities label description icon inputs outputs props");
23
23
  const DATA_FIELDS = fields("kind type default description variadic min max step accept control options");
24
24
  const OUTPUT_FIELDS = fields("kind type description");
25
- const PROP_FIELDS = fields("type default label description min max step accept control options");
25
+ const PROP_FIELDS = fields("type default label description min max step accept control options expression");
26
26
  function diagnostic(code, message, path) {
27
27
  return { phase: "definition", code: `definition/${code}`, message, path };
28
28
  }
@@ -194,6 +194,15 @@ export function validateNodeDefinition(value) {
194
194
  !Number.isFinite(raw.step) ||
195
195
  raw.step <= 0))
196
196
  add("invalid-step", "step must be greater than zero", `${path}.step`);
197
+ if (section === "props" &&
198
+ "expression" in raw &&
199
+ (typeof raw.expression !== "string" || raw.expression.length === 0))
200
+ add("invalid-expression", "expression must be a non-empty string", `${path}.expression`);
201
+ for (const field of ["min", "max"]) {
202
+ if (field in raw &&
203
+ (typeof raw[field] !== "number" || !Number.isFinite(raw[field])))
204
+ add(`invalid-${field}`, `${field} must be a finite number`, `${path}.${field}`);
205
+ }
197
206
  if (typeof raw.min === "number" &&
198
207
  typeof raw.max === "number" &&
199
208
  raw.min > raw.max)
package/dist/gpu.d.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `cascade/gpu` — WebGPU's flag namespaces as plain values.
3
+ *
4
+ * Small, and it was load-bearing. TypeScript's DOM library declares the WebGPU
5
+ * *interfaces* but not the constant objects, so the only way to reach
6
+ * `GPUTextureUsage.RENDER_ATTACHMENT` was off `globalThis` — which
7
+ * `architecture/ambient-state` correctly forbids inside a node module, because
8
+ * an ambient read makes a node's inputs invisible to the static reader that
9
+ * lets `cascade check` say anything without cooking.
10
+ *
11
+ * So a sketch had to choose between a rule and a working shader, and it chose
12
+ * a project library file to hide the read in. Both stay honest now: the rule
13
+ * keeps its teeth and a node imports these instead. That is the whole reason
14
+ * this file exists, and it is why it had to land with the capability rather
15
+ * than after it — a rule whose only workaround is removed at the same moment
16
+ * is a rule; one enforced a release earlier is a nuisance.
17
+ *
18
+ * The values are fixed by the WebGPU specification, not by an implementation,
19
+ * which is what makes writing them down safe rather than a guess. They are
20
+ * checked against a real browser's own globals in `tests/gpu-usage-flags.test.ts`
21
+ * — the numbers below were verified against Chrome rather than recalled.
22
+ */
23
+ /** `GPUBufferUsage`. */
24
+ export declare const BufferUsage: {
25
+ readonly MAP_READ: 1;
26
+ readonly MAP_WRITE: 2;
27
+ readonly COPY_SRC: 4;
28
+ readonly COPY_DST: 8;
29
+ readonly INDEX: 16;
30
+ readonly VERTEX: 32;
31
+ readonly UNIFORM: 64;
32
+ readonly STORAGE: 128;
33
+ readonly INDIRECT: 256;
34
+ readonly QUERY_RESOLVE: 512;
35
+ };
36
+ /** `GPUTextureUsage`. */
37
+ export declare const TextureUsage: {
38
+ readonly COPY_SRC: 1;
39
+ readonly COPY_DST: 2;
40
+ readonly TEXTURE_BINDING: 4;
41
+ readonly STORAGE_BINDING: 8;
42
+ readonly RENDER_ATTACHMENT: 16;
43
+ /**
44
+ * Chrome 147 has this and it is newer than the rest — verified present
45
+ * 2026-09-08 alongside the others, but do not assume every implementation
46
+ * carries it. Included rather than omitted deliberately: leaving it out
47
+ * would send a node that wants it back to `globalThis`, which is the exact
48
+ * ambient read this file exists to remove, so an honest constant with a
49
+ * caveat beats an omission that forces a rule violation.
50
+ */
51
+ readonly TRANSIENT_ATTACHMENT: 32;
52
+ };
53
+ /** `GPUShaderStage`, for a bind group layout entry's `visibility`. */
54
+ export declare const ShaderStage: {
55
+ readonly VERTEX: 1;
56
+ readonly FRAGMENT: 2;
57
+ readonly COMPUTE: 4;
58
+ };
59
+ /** `GPUMapMode`, for `buffer.mapAsync`. */
60
+ export declare const MapMode: {
61
+ readonly READ: 1;
62
+ readonly WRITE: 2;
63
+ };
64
+ /** `GPUColorWrite`, for a colour target's `writeMask`. */
65
+ export declare const ColorWrite: {
66
+ readonly RED: 1;
67
+ readonly GREEN: 2;
68
+ readonly BLUE: 4;
69
+ readonly ALPHA: 8;
70
+ readonly ALL: 15;
71
+ };
package/dist/gpu.js ADDED
@@ -0,0 +1,53 @@
1
+ // server/src/runtime/gpu.ts
2
+ var BufferUsage = {
3
+ MAP_READ: 1,
4
+ MAP_WRITE: 2,
5
+ COPY_SRC: 4,
6
+ COPY_DST: 8,
7
+ INDEX: 16,
8
+ VERTEX: 32,
9
+ UNIFORM: 64,
10
+ STORAGE: 128,
11
+ INDIRECT: 256,
12
+ QUERY_RESOLVE: 512
13
+ };
14
+ var TextureUsage = {
15
+ COPY_SRC: 1,
16
+ COPY_DST: 2,
17
+ TEXTURE_BINDING: 4,
18
+ STORAGE_BINDING: 8,
19
+ RENDER_ATTACHMENT: 16,
20
+ /**
21
+ * Chrome 147 has this and it is newer than the rest — verified present
22
+ * 2026-09-08 alongside the others, but do not assume every implementation
23
+ * carries it. Included rather than omitted deliberately: leaving it out
24
+ * would send a node that wants it back to `globalThis`, which is the exact
25
+ * ambient read this file exists to remove, so an honest constant with a
26
+ * caveat beats an omission that forces a rule violation.
27
+ */
28
+ TRANSIENT_ATTACHMENT: 32
29
+ };
30
+ var ShaderStage = {
31
+ VERTEX: 1,
32
+ FRAGMENT: 2,
33
+ COMPUTE: 4
34
+ };
35
+ var MapMode = {
36
+ READ: 1,
37
+ WRITE: 2
38
+ };
39
+ var ColorWrite = {
40
+ RED: 1,
41
+ GREEN: 2,
42
+ BLUE: 4,
43
+ ALPHA: 8,
44
+ ALL: 15
45
+ };
46
+ export {
47
+ BufferUsage,
48
+ ColorWrite,
49
+ MapMode,
50
+ ShaderStage,
51
+ TextureUsage
52
+ };
53
+ //# sourceMappingURL=gpu.js.map
package/dist/index.html CHANGED
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Cascade</title>
7
- <script type="module" crossorigin src="/assets/index-HTCN44kj.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-CMcH8AgP.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/legacy-CAj2_7QE.js">
9
9
  <link rel="modulepreload" crossorigin href="/assets/index-client-BoJgsN9M.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-BBA1JqHx.js">
@@ -16,9 +16,29 @@ import type { CascadeRuntime, CreateRuntimeOptions } from "./types.js";
16
16
  * leftover `params` array runs perfectly well on its defaults, which is
17
17
  * precisely the problem.
18
18
  *
19
- * This set lives here rather than in the CLI because the run path and the
20
- * check path both need it, and it was a duplicated rule that let `validate`
21
- * and `check` pass graphs `run` then rejected.
19
+ * It was briefly an error, and Marcus reverted that on 2026-09-08 with the
20
+ * evidence that decides it: while converting sixty-two nodes across four
21
+ * sketches, every one of them was a mixed graph for most of the day, and as an
22
+ * error an unconverted or half-converted sketch **stops validating at all** —
23
+ * which is the one thing you need while converting it. The warning is the
24
+ * conversion checklist; as an error it is a locked door on the room you are
25
+ * working in.
26
+ *
27
+ * The fair case for the error was that the warning never fired, and that was
28
+ * true: `check` bailed on dynamic modules before preflight ran. The fix for
29
+ * that was the reporting *order*, not the severity.
30
+ *
31
+ * This set lives here rather than in the CLI because both paths read it, and a
32
+ * duplicated copy is what let `validate` and `check` pass graphs `run` then
33
+ * rejected.
34
+ *
35
+ * **It classifies a static check, and it is not the run gate.** Those are
36
+ * different questions and conflating them was a real bug of mine, caught by
37
+ * Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
38
+ * still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
39
+ * it means executing and failing on an undefined capability instead of saying
40
+ * why. What a static check downgrades is the *checker's reach*; what a run
41
+ * tolerates is much narrower. See `RUN_TOLERATED_CODES`.
22
42
  */
23
43
  export declare const PREFLIGHT_WARNING_CODES: ReadonlySet<string>;
24
44
  export declare function createRuntime(options: CreateRuntimeOptions): CascadeRuntime;
@@ -21,14 +21,46 @@ import { DEFAULT_FPS, MIN_FRAME } from "./expressions/time.js";
21
21
  * leftover `params` array runs perfectly well on its defaults, which is
22
22
  * precisely the problem.
23
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.
24
+ * It was briefly an error, and Marcus reverted that on 2026-09-08 with the
25
+ * evidence that decides it: while converting sixty-two nodes across four
26
+ * sketches, every one of them was a mixed graph for most of the day, and as an
27
+ * error an unconverted or half-converted sketch **stops validating at all** —
28
+ * which is the one thing you need while converting it. The warning is the
29
+ * conversion checklist; as an error it is a locked door on the room you are
30
+ * working in.
31
+ *
32
+ * The fair case for the error was that the warning never fired, and that was
33
+ * true: `check` bailed on dynamic modules before preflight ran. The fix for
34
+ * that was the reporting *order*, not the severity.
35
+ *
36
+ * This set lives here rather than in the CLI because both paths read it, and a
37
+ * duplicated copy is what let `validate` and `check` pass graphs `run` then
38
+ * rejected.
39
+ *
40
+ * **It classifies a static check, and it is not the run gate.** Those are
41
+ * different questions and conflating them was a real bug of mine, caught by
42
+ * Codex's own comment on 2026-09-08: *execution is stricter.* A server run must
43
+ * still refuse a browser node — you cannot cook WebGPU headlessly, so tolerating
44
+ * it means executing and failing on an undefined capability instead of saying
45
+ * why. What a static check downgrades is the *checker's reach*; what a run
46
+ * tolerates is much narrower. See `RUN_TOLERATED_CODES`.
27
47
  */
28
48
  export const PREFLIGHT_WARNING_CODES = new Set([
29
49
  "runtime/preflight-environment",
30
50
  "runtime/stray-params",
31
51
  ]);
52
+ /**
53
+ * The only preflight code that does not stop a run.
54
+ *
55
+ * A stranded `params` value is a fact about the document: the graph cooks
56
+ * perfectly well on its defaults, which is exactly the problem, and refusing to
57
+ * run is the worse answer. Everything else in the preflight is a reason this
58
+ * host cannot execute this graph — an environment mismatch and a missing
59
+ * capability both mean the cook would fail partway with a worse message.
60
+ */
61
+ const RUN_TOLERATED_CODES = new Set([
62
+ "runtime/stray-params",
63
+ ]);
32
64
  class MutableAbortSignal {
33
65
  report;
34
66
  aborted = false;
@@ -148,6 +180,10 @@ class Graph {
148
180
  onDispose;
149
181
  graphState = "ready";
150
182
  byId;
183
+ dataConnectionsByInput;
184
+ dataConnectionsByNode;
185
+ connectedInputs;
186
+ connectedOutputs;
151
187
  listeners = new Set();
152
188
  diagnostics = [];
153
189
  active;
@@ -173,6 +209,34 @@ class Graph {
173
209
  this.connections = connections;
174
210
  this.onDispose = onDispose;
175
211
  this.byId = new Map(nodes.map((node) => [node.id, node]));
212
+ const byInput = new Map();
213
+ const byNode = new Map();
214
+ const connectedInputs = new Set();
215
+ const connectedOutputs = new Set();
216
+ for (const connection of connections) {
217
+ connectedInputs.add(`${connection.target.nodeId}\0${connection.target.inputName}`);
218
+ connectedOutputs.add(`${connection.source.nodeId}\0${connection.source.outputName}`);
219
+ if (connection.kind !== "data")
220
+ continue;
221
+ const inputKey = `${connection.target.nodeId}\0${connection.target.inputName}`;
222
+ const inputConnections = byInput.get(inputKey) ?? [];
223
+ inputConnections.push(connection);
224
+ byInput.set(inputKey, inputConnections);
225
+ const nodeConnections = byNode.get(connection.target.nodeId) ?? [];
226
+ nodeConnections.push(connection);
227
+ byNode.set(connection.target.nodeId, nodeConnections);
228
+ }
229
+ this.dataConnectionsByInput = new Map([...byInput].map(([key, inputConnections]) => [
230
+ key,
231
+ Object.freeze(inputConnections.sort((first, second) => (first.variadicIndex ?? first.order) -
232
+ (second.variadicIndex ?? second.order))),
233
+ ]));
234
+ this.dataConnectionsByNode = new Map([...byNode].map(([key, nodeConnections]) => [
235
+ key,
236
+ Object.freeze(nodeConnections.sort((first, second) => first.order - second.order)),
237
+ ]));
238
+ this.connectedInputs = connectedInputs;
239
+ this.connectedOutputs = connectedOutputs;
176
240
  this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
177
241
  this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
178
242
  this.authoredAnnotations = Object.freeze(annotations.map((annotation) => snapshot(annotation)));
@@ -180,10 +244,7 @@ class Graph {
180
244
  nodes,
181
245
  node: (id) => this.byId.get(id),
182
246
  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);
247
+ const feeding = this.dataConnectionsByNode.get(node.id) ?? [];
187
248
  const source = feeding[index]?.source.nodeId;
188
249
  return (source ? this.byId.get(source) : undefined) ?? null;
189
250
  },
@@ -228,8 +289,7 @@ class Graph {
228
289
  {
229
290
  kind: definition.kind,
230
291
  type: definition.kind === "trigger" ? "trigger" : definition.type,
231
- connected: this.connections.some((connection) => connection.target.nodeId === node.id &&
232
- connection.target.inputName === name),
292
+ connected: this.connectedInputs.has(`${node.id}\0${name}`),
233
293
  ...(definition.kind === "data"
234
294
  ? { value: snapshot(node.inputs[name]) }
235
295
  : {}),
@@ -240,8 +300,7 @@ class Graph {
240
300
  {
241
301
  kind: definition.kind,
242
302
  type: definition.kind === "trigger" ? "trigger" : definition.type,
243
- connected: this.connections.some((connection) => connection.source.nodeId === node.id &&
244
- connection.source.outputName === name),
303
+ connected: this.connectedOutputs.has(`${node.id}\0${name}`),
245
304
  ...(definition.kind === "data" && node.outputs.has(name)
246
305
  ? { value: snapshot(node.outputs.get(name)) }
247
306
  : {}),
@@ -549,10 +608,7 @@ class Graph {
549
608
  const index = integerProp(node.props.inputIndex);
550
609
  return [name, this.dataInputValue(parent, `input_${index}`)];
551
610
  }
552
- const connections = this.connections.filter((item) => item.kind === "data" &&
553
- item.target.nodeId === node.id &&
554
- item.target.inputName === name).sort((first, second) => (first.variadicIndex ?? first.order) -
555
- (second.variadicIndex ?? second.order));
611
+ const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
556
612
  if (definition.variadic) {
557
613
  const values = connections.map((connection) => this.requireNode(connection.source.nodeId).outputs.get(connection.source.outputName));
558
614
  if (!values.length && node.inputs[name] !== undefined)
@@ -740,7 +796,7 @@ class Graph {
740
796
  : `provides no capabilities`;
741
797
  }
742
798
  assertPreflight(nodes) {
743
- const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !PREFLIGHT_WARNING_CODES.has(diagnostic.code));
799
+ const [first] = this.preflightDiagnostics(nodes).filter((diagnostic) => !RUN_TOLERATED_CODES.has(diagnostic.code));
744
800
  if (first)
745
801
  throw new CascadeRuntimeError(first.code, [first]);
746
802
  }
@@ -754,7 +810,8 @@ class Graph {
754
810
  }
755
811
  runSelection(target) {
756
812
  if (target.kind === "all") {
757
- const initial = this.nodes.filter((node) => !isGated(node) && !this.hasGatedDataAncestor(node.id));
813
+ const gatedDataDescendants = this.gatedDataDescendants();
814
+ const initial = this.nodes.filter((node) => !isGated(node) && !gatedDataDescendants.has(node.id));
758
815
  const ids = new Set(initial.map((node) => node.id));
759
816
  let changed = true;
760
817
  while (changed) {
@@ -799,8 +856,25 @@ class Graph {
799
856
  }
800
857
  return this.nodes.filter((node) => ids.has(node.id));
801
858
  }
802
- hasGatedDataAncestor(nodeId) {
803
- return this.dataClosure(nodeId).some(isGated);
859
+ gatedDataDescendants() {
860
+ const outgoing = new Map();
861
+ for (const dependency of dataDependencies(this.nodes, this.connections)) {
862
+ const targets = outgoing.get(dependency.sourceId) ?? [];
863
+ targets.push(dependency.targetId);
864
+ outgoing.set(dependency.sourceId, targets);
865
+ }
866
+ const queue = this.nodes.filter(isGated).map((node) => node.id);
867
+ const descendants = new Set(queue);
868
+ let queueIndex = 0;
869
+ while (queueIndex < queue.length) {
870
+ for (const targetId of outgoing.get(queue[queueIndex++]) ?? []) {
871
+ if (descendants.has(targetId))
872
+ continue;
873
+ descendants.add(targetId);
874
+ queue.push(targetId);
875
+ }
876
+ }
877
+ return descendants;
804
878
  }
805
879
  triggerDataAncestors(nodeId) {
806
880
  const dependencies = this.dataClosure(nodeId).filter((node) => node.id !== nodeId);
@@ -949,7 +1023,21 @@ function materialize(authored, registration) {
949
1023
  for (const [name, definition] of Object.entries(registration.definition.props ?? {}))
950
1024
  props[name] = runtimeValue(definition.default, definition.type, false, `${authored.id}.${name}`);
951
1025
  const bindings = {};
952
- for (const [name, saved] of Object.entries(authored.props ?? {})) {
1026
+ // A declared default expression, applied only where the document stored
1027
+ // nothing at all for the prop. The loop below then runs over what *was*
1028
+ // stored, so an authored value or an authored expression overwrites this —
1029
+ // which is the whole contract: a default expression is a default, and a file
1030
+ // that saved a plain number keeps its number rather than quietly animating.
1031
+ const storedProps = authored.props ?? {};
1032
+ for (const [name, definition] of Object.entries(registration.definition.props ?? {})) {
1033
+ if (!definition.expression || name in storedProps)
1034
+ continue;
1035
+ bindings[name] = {
1036
+ value: props[name],
1037
+ expression: definition.expression,
1038
+ };
1039
+ }
1040
+ for (const [name, saved] of Object.entries(storedProps)) {
953
1041
  const definition = registration.definition.props?.[name];
954
1042
  if (!definition)
955
1043
  continue;
@@ -961,8 +1049,12 @@ function materialize(authored, registration) {
961
1049
  : null;
962
1050
  const value = runtimeValue(object ? object.value : saved, definition.type, false, `${authored.id}.${name}`);
963
1051
  props[name] = value;
964
- if (!object)
1052
+ // A stored plain value is an author's decision and beats the declared
1053
+ // default expression, so any binding seeded above has to go.
1054
+ if (!object) {
1055
+ delete bindings[name];
965
1056
  continue;
1057
+ }
966
1058
  const channel = deserializeChannel(object.channel);
967
1059
  const expression = typeof object.expression === "string" && object.expression
968
1060
  ? object.expression
@@ -1265,6 +1357,7 @@ function validateTopology(nodes, connections) {
1265
1357
  topological(nodes, connections);
1266
1358
  }
1267
1359
  function topological(nodes, connections) {
1360
+ const byId = new Map(nodes.map((node) => [node.id, node]));
1268
1361
  const indegree = new Map(nodes.map((node) => [node.id, 0]));
1269
1362
  const outgoing = new Map(nodes.map((node) => [node.id, []]));
1270
1363
  for (const dependency of graphDependencies(nodes, connections)) {
@@ -1273,13 +1366,14 @@ function topological(nodes, connections) {
1273
1366
  }
1274
1367
  const queue = nodes.filter((node) => indegree.get(node.id) === 0);
1275
1368
  const result = [];
1276
- while (queue.length) {
1277
- const node = queue.shift();
1369
+ let queueIndex = 0;
1370
+ while (queueIndex < queue.length) {
1371
+ const node = queue[queueIndex++];
1278
1372
  result.push(node);
1279
1373
  for (const id of outgoing.get(node.id)) {
1280
1374
  indegree.set(id, indegree.get(id) - 1);
1281
1375
  if (indegree.get(id) === 0)
1282
- queue.push(nodes.find((candidate) => candidate.id === id));
1376
+ queue.push(byId.get(id));
1283
1377
  }
1284
1378
  }
1285
1379
  if (result.length !== nodes.length)
@@ -1,4 +1,4 @@
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";
1
+ import type { AssetCapability, AssetRef, CascadeAbortSignal, CascadeDocument, CascadePreset, Diagnostic, FileCapability, ImageRef, JsonValue, MediaCapability, NodeDefinition, NodeExecute, ProgressReporter, PythonCapability, SerializableValue, ShellCapability, TextureHandle, GpuCapability } from "@field/cascade/contracts";
2
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";
@@ -31,7 +31,7 @@ export interface RuntimeCapabilities {
31
31
  readonly assets?: AssetCapability;
32
32
  readonly media?: MediaCapability;
33
33
  readonly python?: PythonCapability;
34
- readonly webgl?: WebGLCapability;
34
+ readonly gpu?: GpuCapability;
35
35
  readonly shell?: ShellCapability;
36
36
  readonly serverBridge?: ServerBridgeCapability;
37
37
  }
@@ -246,7 +246,7 @@ export interface LoadedCascadeGraph {
246
246
  getOutputs(nodeId: string): ReadonlyMap<string, unknown>;
247
247
  dispose(): Promise<void>;
248
248
  }
249
- export interface NodeRuntimeHostOptions extends Omit<RuntimeCapabilities, "webgl"> {
249
+ export interface NodeRuntimeHostOptions extends Omit<RuntimeCapabilities, "gpu"> {
250
250
  readonly modules: RuntimeHost["modules"];
251
251
  readonly legacyShell?: LegacyShellBindingFactory & Readonly<{
252
252
  kind: "server-direct";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@field/cascade",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Visual programming for generative art",
5
5
  "license": "MIT",
6
6
  "author": "FIELD.IO <marcus@field.io>",
@@ -83,6 +83,10 @@
83
83
  "types": "./dist/io.d.ts",
84
84
  "default": "./dist/io.js"
85
85
  },
86
+ "./gpu": {
87
+ "types": "./dist/gpu.d.ts",
88
+ "default": "./dist/gpu.js"
89
+ },
86
90
  "./net": {
87
91
  "types": "./dist/net.d.ts",
88
92
  "default": "./dist/net.js"
@@ -1 +0,0 @@
1
- import{b as e,f as t,s as n,x as r}from"./index-HTCN44kj.js";r(n,{assets:e});export{t as geoLibrary};