@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
@@ -17,12 +17,52 @@
17
17
  * carries a project-relative path, never pixels.
18
18
  */
19
19
 
20
+ export interface StageCallOptions {
21
+ /**
22
+ * Abandons the stage. In Studio the request is withdrawn — dropped if it is
23
+ * still queued for the worker, interrupted if it is running — and the call
24
+ * rejects at once, so a superseded cook stops costing anything the moment it
25
+ * is superseded rather than when the stage happens to finish.
26
+ *
27
+ * Optional: when it is absent the signal of the cook in flight is used (see
28
+ * `COOK_SIGNAL_KEY`), so a node that never heard of this still cancels.
29
+ */
30
+ readonly signal?: AbortSignal;
31
+ }
32
+
20
33
  export interface StageBridge {
21
- (stage: string, args: Record<string, unknown>): Promise<unknown>;
34
+ (stage: string, args: Record<string, unknown>, options?: StageCallOptions): Promise<unknown>;
22
35
  }
23
36
 
24
37
  const BRIDGE_KEY = '__cascadeStageBridge';
25
38
 
39
+ /**
40
+ * Where the scheduler publishes the signal of the interactive cook in flight.
41
+ *
42
+ * On `globalThis` for the same reason as the bridge: every project module is
43
+ * compiled with its own copy of this file, so module state would not be shared
44
+ * with the scheduler that sets it. The interactive lane runs one node at a
45
+ * time, which is what makes a single ambient value correct: a stage started
46
+ * while node N cooks belongs to node N's cook. Read at call time, never later.
47
+ */
48
+ export const COOK_SIGNAL_KEY = '__cascadeCookSignal';
49
+
50
+ function ambientCookSignal(): AbortSignal | undefined {
51
+ const read = (globalThis as Record<string, unknown>)[COOK_SIGNAL_KEY];
52
+ if (typeof read !== 'function') return undefined;
53
+ const signal = (read as () => AbortSignal | null | undefined)();
54
+ return signal ?? undefined;
55
+ }
56
+
57
+ /** The rejection a withdrawn stage produces, recognisable without importing a
58
+ * class across module copies. */
59
+ function cancelledError(stage: string, signal: AbortSignal): Error {
60
+ const reason = signal.reason instanceof Error ? signal.reason.message : 'superseded';
61
+ const error = new Error(`stage "${stage}" was cancelled: ${reason}`);
62
+ error.name = 'AbortError';
63
+ return error;
64
+ }
65
+
26
66
  /**
27
67
  * Install the local transport. Called by a host that can reach the filesystem —
28
68
  * the CLI does this before running a graph. Returns a disposer so a host can
@@ -71,20 +111,27 @@ async function capability(): Promise<string> {
71
111
  return token;
72
112
  }
73
113
 
74
- async function overHttp<T>(stage: string, args: Record<string, unknown>): Promise<T> {
114
+ async function overHttp<T>(stage: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<T> {
75
115
  const postStage = async () => fetch('/api/exec', {
76
116
  method: 'POST',
77
117
  headers: { 'Content-Type': 'application/json', 'X-Cascade-Exec-Capability': await capability() },
78
118
  body: JSON.stringify({ stage, args }),
119
+ signal,
79
120
  });
80
121
 
81
- let response = await postStage();
82
- if (response.status === 403) {
83
- capabilityPromise = undefined;
122
+ let response: Response;
123
+ let body: { ok?: boolean; stdout?: string; stderr?: string; error?: string };
124
+ try {
84
125
  response = await postStage();
126
+ if (response.status === 403) {
127
+ capabilityPromise = undefined;
128
+ response = await postStage();
129
+ }
130
+ body = await response.json() as typeof body;
131
+ } catch (error) {
132
+ if (signal?.aborted) throw cancelledError(stage, signal);
133
+ throw error;
85
134
  }
86
-
87
- const body = await response.json() as { ok?: boolean; stdout?: string; stderr?: string; error?: string };
88
135
  if (!response.ok || !body.ok) {
89
136
  throw new Error(`stage "${stage}" failed: ${body.stderr || body.error || 'unknown error'}`);
90
137
  }
@@ -113,16 +160,29 @@ export async function stageAvailable(): Promise<boolean> {
113
160
  * rather than a URL parse error from a fetch that was never going to work —
114
161
  * which is what this whole file exists to stop happening.
115
162
  */
116
- export async function runStage<T = unknown>(stage: string, args: Record<string, unknown> = {}): Promise<T> {
163
+ export async function runStage<T = unknown>(
164
+ stage: string,
165
+ args: Record<string, unknown> = {},
166
+ options: StageCallOptions = {},
167
+ ): Promise<T> {
168
+ const signal = options.signal ?? ambientCookSignal();
169
+ if (signal?.aborted) throw cancelledError(stage, signal);
117
170
  const bridge = localBridge();
118
- if (bridge) return await bridge(stage, args) as T;
171
+ if (bridge) {
172
+ try {
173
+ return await bridge(stage, args, signal ? { signal } : undefined) as T;
174
+ } catch (error) {
175
+ if (signal?.aborted) throw cancelledError(stage, signal);
176
+ throw error;
177
+ }
178
+ }
119
179
  if (typeof document === 'undefined') {
120
180
  throw new Error(
121
181
  `Cannot run stage "${stage}": no host bridge is installed and there is no page to post from. ` +
122
182
  'A headless host must call installStageBridge() from cascade/stage before executing the graph.',
123
183
  );
124
184
  }
125
- return overHttp<T>(stage, args);
185
+ return overHttp<T>(stage, args, signal);
126
186
  }
127
187
 
128
188
  /** A scratch path for a node's output, scoped by node id so re-running a graph
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Bundles: one port carrying a set of named fields.
3
+ *
4
+ * Marcus, 2026-10-10, looking at a graph where one node fanned out a dozen
5
+ * outputs wired everywhere: *"Instead of having these nodes with many different
6
+ * outputs (more than 3 or 4 outputs), can we just export some kind of JSON
7
+ * object, a sort of multi-object, instead of outputting 12 different outputs?"*
8
+ *
9
+ * A bundle value is a plain object of fields. Each field is any ordinary
10
+ * Cascade value; an image or a cache entry travels as its reference (an
11
+ * `ImageRef`, an `AssetRef`), never as pixels, so a bundle is as cheap to pass
12
+ * as the fields it names.
13
+ *
14
+ * What makes it more than `object`:
15
+ *
16
+ * - a producer declares its fields (`fields: { dpi: "float", ... }`), so a
17
+ * checker can name an unknown field and suggest the right one;
18
+ * - an optional `schema` name (`format`, `signals`, `marks`) keeps two
19
+ * unrelated bundles from being wired together;
20
+ * - a consumer may declare the fields it reads (`fields: ["dpi"]`). It then
21
+ * sees only those, and Studio re-cooks it only when one of them changed;
22
+ * - expressions read a wired bundle's fields: `ch("format.dpi")`.
23
+ *
24
+ * Everything here is pure and dependency-free so the runtime, Studio and the
25
+ * CLI checker share one reading of the rules.
26
+ */
27
+ import type { CascadeType } from "./values.js";
28
+ /** A declared field: its type, or its type with words for whoever reads it. */
29
+ export type BundleFieldDefinition = CascadeType | Readonly<{
30
+ type: CascadeType;
31
+ description?: string;
32
+ }>;
33
+ /** A producer's declaration: field name to type. */
34
+ export type BundleFieldDefinitions = Readonly<Record<string, BundleFieldDefinition>>;
35
+ export type Bundle = Readonly<Record<string, unknown>>;
36
+ /** The type a field declaration names. */
37
+ export declare function bundleFieldType(definition: BundleFieldDefinition | undefined): string;
38
+ /** The description a field declaration carries, if any. */
39
+ export declare function bundleFieldDescription(definition: BundleFieldDefinition | undefined): string | undefined;
40
+ /**
41
+ * Whether two bundle ports may be wired. A port without a schema is generic and
42
+ * takes any bundle; two named schemas must agree.
43
+ */
44
+ export declare function bundleSchemasCompatible(output: string | undefined, input: string | undefined): boolean;
45
+ /** True for a value a bundle port can carry: a plain, non-array object. */
46
+ export declare function isBundleValue(value: unknown): value is Bundle;
47
+ /**
48
+ * A field path as an expression writes it: `format.dpi` is field `dpi` of the
49
+ * port `format`. Only the first dot splits, so a field name never needs
50
+ * escaping. Null when there is no dot to split on.
51
+ */
52
+ export declare function splitBundlePath(name: string): {
53
+ port: string;
54
+ field: string;
55
+ } | null;
56
+ /**
57
+ * A bundle narrowed to the fields a consumer declared. A field the producer
58
+ * did not set is absent rather than `undefined`, the same as on the full value.
59
+ */
60
+ export declare function selectBundleFields(value: unknown, fields: readonly string[]): Bundle | undefined;
61
+ /**
62
+ * The closest candidate to a misspelt name, for "did you mean". Edit distance
63
+ * with a threshold that scales with the length, so `dpu` finds `dpi` and
64
+ * `colour` does not find `count`.
65
+ */
66
+ export declare function suggestName(name: string, candidates: Iterable<string>): string | undefined;
67
+ /** "unknown field X — did you mean Y? (fields: a, b, c)" */
68
+ export declare function unknownFieldMessage(field: string, known: readonly string[], where: string): string;
69
+ /**
70
+ * What is wrong with a value a producer committed to a bundle port, or null.
71
+ * With declared fields, every key must be declared; a declared field may be
72
+ * absent. Field types are not checked here; the runtime does that per type.
73
+ */
74
+ export declare function bundleValueProblem(value: unknown, fields: BundleFieldDefinitions | undefined): string | null;
75
+ //# sourceMappingURL=bundle.d.ts.map
@@ -0,0 +1,112 @@
1
+ /** The type a field declaration names. */
2
+ export function bundleFieldType(definition) {
3
+ if (!definition)
4
+ return "any";
5
+ return typeof definition === "string" ? definition : definition.type;
6
+ }
7
+ /** The description a field declaration carries, if any. */
8
+ export function bundleFieldDescription(definition) {
9
+ return typeof definition === "object" && definition ? definition.description : undefined;
10
+ }
11
+ /**
12
+ * Whether two bundle ports may be wired. A port without a schema is generic and
13
+ * takes any bundle; two named schemas must agree.
14
+ */
15
+ export function bundleSchemasCompatible(output, input) {
16
+ return !output || !input || output === input;
17
+ }
18
+ /** True for a value a bundle port can carry: a plain, non-array object. */
19
+ export function isBundleValue(value) {
20
+ if (value === null || typeof value !== "object" || Array.isArray(value))
21
+ return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === Object.prototype || prototype === null;
24
+ }
25
+ /**
26
+ * A field path as an expression writes it: `format.dpi` is field `dpi` of the
27
+ * port `format`. Only the first dot splits, so a field name never needs
28
+ * escaping. Null when there is no dot to split on.
29
+ */
30
+ export function splitBundlePath(name) {
31
+ const dot = name.indexOf(".");
32
+ if (dot <= 0 || dot === name.length - 1)
33
+ return null;
34
+ return { port: name.slice(0, dot), field: name.slice(dot + 1) };
35
+ }
36
+ /**
37
+ * A bundle narrowed to the fields a consumer declared. A field the producer
38
+ * did not set is absent rather than `undefined`, the same as on the full value.
39
+ */
40
+ export function selectBundleFields(value, fields) {
41
+ if (!isBundleValue(value))
42
+ return value;
43
+ const selected = {};
44
+ for (const field of fields)
45
+ if (Object.prototype.hasOwnProperty.call(value, field))
46
+ selected[field] = value[field];
47
+ return Object.freeze(selected);
48
+ }
49
+ /**
50
+ * The closest candidate to a misspelt name, for "did you mean". Edit distance
51
+ * with a threshold that scales with the length, so `dpu` finds `dpi` and
52
+ * `colour` does not find `count`.
53
+ */
54
+ export function suggestName(name, candidates) {
55
+ let best;
56
+ let bestDistance = Infinity;
57
+ const lower = name.toLowerCase();
58
+ for (const candidate of candidates) {
59
+ const distance = editDistance(lower, candidate.toLowerCase());
60
+ if (distance < bestDistance) {
61
+ best = candidate;
62
+ bestDistance = distance;
63
+ }
64
+ }
65
+ if (best === undefined)
66
+ return undefined;
67
+ const limit = Math.max(1, Math.floor(Math.max(name.length, best.length) / 3));
68
+ return bestDistance <= limit ? best : undefined;
69
+ }
70
+ /** "unknown field X — did you mean Y? (fields: a, b, c)" */
71
+ export function unknownFieldMessage(field, known, where) {
72
+ const suggestion = suggestName(field, known);
73
+ return `${where} reads unknown bundle field "${field}"`
74
+ + (suggestion ? ` — did you mean "${suggestion}"?` : "")
75
+ + (known.length ? ` (fields: ${known.join(", ")})` : " (the bundle declares no fields)");
76
+ }
77
+ /**
78
+ * What is wrong with a value a producer committed to a bundle port, or null.
79
+ * With declared fields, every key must be declared; a declared field may be
80
+ * absent. Field types are not checked here; the runtime does that per type.
81
+ */
82
+ export function bundleValueProblem(value, fields) {
83
+ if (value === undefined || value === null)
84
+ return null;
85
+ if (!isBundleValue(value))
86
+ return "a bundle must be a plain object of fields";
87
+ if (!fields)
88
+ return null;
89
+ const known = Object.keys(fields);
90
+ for (const key of Object.keys(value))
91
+ if (!Object.prototype.hasOwnProperty.call(fields, key)) {
92
+ const suggestion = suggestName(key, known);
93
+ return `sets undeclared bundle field "${key}"`
94
+ + (suggestion ? ` — did you mean "${suggestion}"?` : "")
95
+ + ` (declared: ${known.join(", ") || "none"})`;
96
+ }
97
+ return null;
98
+ }
99
+ function editDistance(a, b) {
100
+ const previous = Array.from({ length: b.length + 1 }, (_, i) => i);
101
+ for (let i = 1; i <= a.length; i++) {
102
+ let diagonal = previous[0];
103
+ previous[0] = i;
104
+ for (let j = 1; j <= b.length; j++) {
105
+ const above = previous[j];
106
+ previous[j] = Math.min(previous[j] + 1, previous[j - 1] + 1, diagonal + (a[i - 1] === b[j - 1] ? 0 : 1));
107
+ diagonal = above;
108
+ }
109
+ }
110
+ return previous[b.length];
111
+ }
112
+ //# sourceMappingURL=bundle.js.map
@@ -1,4 +1,5 @@
1
1
  import type { CapabilityMap, CascadeAbortSignal, NodeCapabilityName, ProgressReporter } from "./capabilities.js";
2
+ import type { BundleFieldDefinition, BundleFieldDefinitions } from "./bundle.js";
2
3
  import type { CascadeType, CoreType, JsonValue, NamespacedType, SerializableValueForType, ValueForType } from "./values.js";
3
4
  export type RuntimeEnvironment = "portable" | "browser" | "server";
4
5
  export type CapabilityForEnvironment<E extends RuntimeEnvironment> = E extends "portable" ? "assets" | "media" | "gpu" : E extends "browser" ? "assets" | "media" | "gpu" : "files" | "assets" | "media" | "python" | "shell" | "gpu";
@@ -70,6 +71,27 @@ type SelectMetadata<T extends CascadeType> = {
70
71
  readonly control: Extract<ControlFor<T>, "select">;
71
72
  readonly options?: readonly SelectOption<T>[];
72
73
  };
74
+ /**
75
+ * Bundle ports name an optional `schema`, and declare fields from both ends:
76
+ * a producer declares what it carries (`fields: { dpi: "float" }`), a consumer
77
+ * the fields it reads (`fields: ["dpi"]`). A consumer that declares its fields
78
+ * sees only those and, in Studio, re-cooks only when one of them changed. See
79
+ * `bundle.ts`.
80
+ */
81
+ type BundleInputMetadata<T extends CascadeType> = T extends "bundle" ? {
82
+ readonly schema?: string;
83
+ readonly fields?: readonly string[];
84
+ } : {
85
+ readonly schema?: never;
86
+ readonly fields?: never;
87
+ };
88
+ type BundleOutputMetadata<T extends CascadeType> = T extends "bundle" ? {
89
+ readonly schema?: string;
90
+ readonly fields?: BundleFieldDefinitions;
91
+ } : {
92
+ readonly schema?: never;
93
+ readonly fields?: never;
94
+ };
73
95
  export interface TriggerInputDefinition {
74
96
  readonly kind: "trigger";
75
97
  readonly description?: string;
@@ -80,7 +102,7 @@ export type DataInputDefinition<T extends CascadeType> = {
80
102
  readonly default?: SerializableValueForType<T>;
81
103
  readonly description?: string;
82
104
  readonly variadic?: boolean;
83
- } & NumericMetadata<T> & AcceptMetadata<T> & SelectMetadata<T>;
105
+ } & NumericMetadata<T> & AcceptMetadata<T> & SelectMetadata<T> & BundleInputMetadata<T>;
84
106
  export type InputDefinitionFor<E extends RuntimeEnvironment> = TriggerInputDefinition | {
85
107
  [T in DataTypeForEnvironment<E>]: DataInputDefinition<T>;
86
108
  }[DataTypeForEnvironment<E>];
@@ -88,11 +110,11 @@ export interface TriggerOutputDefinition {
88
110
  readonly kind: "trigger";
89
111
  readonly description?: string;
90
112
  }
91
- export interface DataOutputDefinition<T extends CascadeType> {
113
+ export type DataOutputDefinition<T extends CascadeType> = {
92
114
  readonly kind: "data";
93
115
  readonly type: T;
94
116
  readonly description?: string;
95
- }
117
+ } & BundleOutputMetadata<T>;
96
118
  export type OutputDefinitionFor<E extends RuntimeEnvironment> = TriggerOutputDefinition | {
97
119
  [T in DataTypeForEnvironment<E>]: DataOutputDefinition<T>;
98
120
  }[DataTypeForEnvironment<E>];
@@ -168,6 +190,14 @@ export interface NodeDefinitionForEnvironment<E extends RuntimeEnvironment> {
168
190
  */
169
191
  readonly container?: "subnet" | "feedback";
170
192
  readonly capabilities?: readonly CapabilityForEnvironment<E>[];
193
+ /**
194
+ * `live` marks a node that reads data from outside the graph — an archive,
195
+ * a CLI, a cache, body data, the filesystem or a network. `cascade export`
196
+ * never ships one: the author freezes it into a snapshot of chosen values or
197
+ * the export fails. Server nodes and nodes declaring `files`, `python` or
198
+ * `shell` are treated as live whether or not they say so.
199
+ */
200
+ readonly source?: "live";
171
201
  readonly label?: string;
172
202
  readonly description?: string;
173
203
  readonly icon?: string;
@@ -191,11 +221,32 @@ export interface TriggerEvent {
191
221
  }>;
192
222
  }
193
223
  type Entries<D, K extends "inputs" | "outputs" | "props"> = D extends Record<K, infer R> ? R : Record<never, never>;
194
- type InputValue<I> = I extends TriggerInputDefinition ? TriggerEvent | undefined : I extends DataInputDefinition<infer T> ? I extends {
224
+ /** The value a declared bundle field holds. */
225
+ type BundleFieldValue<F> = F extends CascadeType ? ValueForType<F> : F extends {
226
+ readonly type: infer T extends CascadeType;
227
+ } ? ValueForType<T> : unknown;
228
+ /** A bundle output's value, typed from its declared fields. Every field is
229
+ * optional: a producer may leave one unset, and a consumer then reads none. */
230
+ type BundleOutputValue<O> = O extends {
231
+ readonly fields: infer F extends Readonly<Record<string, BundleFieldDefinition>>;
232
+ } ? {
233
+ readonly [K in keyof F]?: BundleFieldValue<F[K]>;
234
+ } : Readonly<Record<string, unknown>>;
235
+ /** A bundle input's value: only the declared fields, when it declares any. */
236
+ type BundleInputValue<I> = I extends {
237
+ readonly fields: readonly (infer K extends string)[];
238
+ } ? {
239
+ readonly [P in K]?: unknown;
240
+ } : Readonly<Record<string, unknown>>;
241
+ type InputValue<I> = I extends TriggerInputDefinition ? TriggerEvent | undefined : I extends DataInputDefinition<"bundle"> ? I extends {
242
+ readonly default: unknown;
243
+ } ? BundleInputValue<I> : BundleInputValue<I> | undefined : I extends DataInputDefinition<infer T> ? I extends {
195
244
  readonly variadic: true;
196
245
  } ? readonly ValueForType<T>[] : "default" extends keyof I ? ValueForType<T> : ValueForType<T> | undefined : never;
197
246
  type OutputValue<O> = O extends TriggerOutputDefinition ? {
198
247
  trigger(payload?: JsonValue): void;
248
+ } : O extends DataOutputDefinition<"bundle"> ? {
249
+ set(value: BundleOutputValue<O>): void;
199
250
  } : O extends DataOutputDefinition<infer T> ? {
200
251
  set(value: ValueForType<T>): void;
201
252
  } : never;
@@ -228,6 +279,18 @@ export interface NodeExecutionContext<D extends NodeDefinition> {
228
279
  readonly capabilities: DeclaredCapabilities<D>;
229
280
  readonly signal: CascadeAbortSignal;
230
281
  readonly progress: ProgressReporter;
282
+ /**
283
+ * This node's seed, derived from the graph's `seed` and the node id
284
+ * (`nodeSeed` in `@cascade/runtime`). Every host computes it the same way,
285
+ * so a seeded node draws the same thing headless and in Studio. Pass it on
286
+ * to anything random, including a Python stage's args.
287
+ */
288
+ readonly seed: number;
289
+ /**
290
+ * A stream of floats in [0, 1) seeded with `seed`, starting afresh at every
291
+ * cook. Use it instead of `Math.random`, which no host can reproduce.
292
+ */
293
+ readonly random: () => number;
231
294
  }
232
295
  export type NodeExecute<D extends NodeDefinition = NodeDefinition> = (context: NodeExecutionContext<D>) => void | Promise<void>;
233
296
  export {};
@@ -1,3 +1,4 @@
1
+ import type { EditorDeclaration } from "./editor.js";
1
2
  import type { SerializableValue } from "./values.js";
2
3
  export interface CascadeDocumentNode {
3
4
  readonly id: string;
@@ -54,6 +55,34 @@ export interface CascadeDocument {
54
55
  readonly nodes: readonly CascadeDocumentNode[];
55
56
  readonly annotations?: readonly CascadeDocumentAnnotation[];
56
57
  readonly connections?: readonly CascadeDocumentConnection[];
58
+ /** The shareable editor this graph declares; ignored by the runtime. See `editor.ts`. */
59
+ readonly editor?: EditorDeclaration;
60
+ /**
61
+ * The graph seed, an unsigned 32-bit integer; 0 when absent. Each node's
62
+ * `context.seed` is derived from it and the node id, so changing it
63
+ * reshuffles every seeded node at once.
64
+ */
65
+ readonly seed?: number;
66
+ /** The timeline the graph is played and rendered over. See `TimelineSettings`. */
67
+ readonly timeline?: TimelineSettings;
68
+ }
69
+ /**
70
+ * The frame rate, range and looping a graph is made for, saved with it.
71
+ *
72
+ * Without it every graph opened at 30 fps and frames 1-250, and a piece made
73
+ * for 24 fps over 594 frames (cloud-wall's H1 flipbook) had to be set up by
74
+ * hand on every open. Studio's timeline reads and writes it, the runtime takes
75
+ * its `fps` as the graph's own rate, and `cascade run --frames timeline`
76
+ * renders `start`-`end`. Every field is optional; absent ones keep the
77
+ * defaults (30 fps, 1-250, looping).
78
+ */
79
+ export interface TimelineSettings {
80
+ readonly fps?: number;
81
+ /** First frame, inclusive. */
82
+ readonly start?: number;
83
+ /** Last frame, inclusive. */
84
+ readonly end?: number;
85
+ readonly loop?: boolean;
57
86
  }
58
87
  export interface CascadePreset {
59
88
  readonly version: 1;
@@ -0,0 +1,172 @@
1
+ import type { SelectOption } from "./definition.js";
2
+ import type { JsonValue } from "./values.js";
3
+ /**
4
+ * The shareable editor a graph declares: which parameters someone else may
5
+ * change, which outputs they may export, the presets they may start from, and
6
+ * the assets that are allowed to leave the project with it.
7
+ *
8
+ * It sits beside `nodes` in the document, like Houdini's parameter interface on
9
+ * a digital asset sits beside its network. The runtime ignores it: a published
10
+ * parameter is an ordinary prop with a label, so a headless render, Studio and
11
+ * the exported editor cook the same graph. Only `cascade export`, the editor
12
+ * shell and scene files read it.
13
+ *
14
+ * Everything here is an allowlist. A parameter, output or asset that is not
15
+ * named does not reach the exported site, and a scene file carries values for
16
+ * named parameters only. That is the boundary a share relies on: an export is
17
+ * the selected subgraph plus the selected assets, never the project.
18
+ */
19
+ export interface EditorDeclaration {
20
+ /** Stable identity of the editor, written into every scene. `cloud-type`. */
21
+ readonly id: string;
22
+ readonly title?: string;
23
+ readonly description?: string;
24
+ /** Group names in display order. Groups not listed follow, in first-use order. */
25
+ readonly groups?: readonly string[];
26
+ readonly parameters?: readonly PublishedParameter[];
27
+ readonly outputs?: readonly PublishedOutput[];
28
+ readonly presets?: readonly EditorPreset[];
29
+ /**
30
+ * Project-relative files the author selects to ship. Literal asset values of
31
+ * published parameters are included too; nothing else is. A path inside a
32
+ * `licensed/` directory, a hidden file, or a gitignored file is refused.
33
+ */
34
+ readonly assets?: readonly string[];
35
+ /**
36
+ * Data-source nodes replaced by a snapshot of chosen values. A live source
37
+ * (server, files, Python, shell or a `source: "live"` definition) still wired
38
+ * into the exported subgraph fails the export.
39
+ */
40
+ readonly freeze?: readonly FrozenSource[];
41
+ /** Optional project panel module that extends or replaces the generated UI. */
42
+ readonly panel?: EditorPanelDeclaration;
43
+ /** The read-only stage view. On unless the author turns it off. */
44
+ readonly inspect?: boolean;
45
+ /**
46
+ * Parameter ids a phone keeps in reach at the bottom sheet's lowest snap
47
+ * point (the text, a Reroll). Defaults to the first text and seed parameters.
48
+ */
49
+ readonly peek?: readonly string[];
50
+ /**
51
+ * Values cooked while a slider is being dragged on a phone, for a quicker
52
+ * preview: a lower density, say. They never enter a scene or an export, and
53
+ * the real values cook again on release.
54
+ */
55
+ readonly draft?: Readonly<Record<string, JsonValue>>;
56
+ }
57
+ export type PublishedControl = "number" | "slider" | "int" | "bool" | "select" | "text" | "textarea" | "color" | "file" | "seed" | "vec2";
58
+ export interface PublishedParameter {
59
+ /** Scene key. Stable across renames of the node or the prop it targets. */
60
+ readonly id: string;
61
+ /** The node and prop whose value this parameter sets. */
62
+ readonly node: string;
63
+ readonly prop: string;
64
+ readonly label?: string;
65
+ readonly group?: string;
66
+ readonly order?: number;
67
+ readonly help?: string;
68
+ readonly unit?: string;
69
+ /** Overrides the control the prop's type implies. */
70
+ readonly control?: PublishedControl;
71
+ readonly min?: number;
72
+ readonly max?: number;
73
+ readonly step?: number;
74
+ readonly options?: readonly SelectOption<"string">[] | readonly SelectOption<"float">[];
75
+ /** File parameters: accepted extensions or media types (`.otf`, `image/*`). */
76
+ readonly accept?: readonly string[];
77
+ /**
78
+ * File parameters: whether a dropped file may travel inside a scene.
79
+ * `never` keeps it to a name and a hash (licensed fonts); `auto` embeds files
80
+ * up to 2 MB. Dropped files never leave the viewer's browser otherwise.
81
+ */
82
+ readonly embed?: "never" | "auto";
83
+ /** Shown only when other parameters have these values; a list must all hold. */
84
+ readonly visibleWhen?: VisibleWhen | readonly VisibleWhen[];
85
+ }
86
+ export type VisibleWhen = Readonly<{
87
+ param: string;
88
+ equals?: JsonValue;
89
+ in?: readonly JsonValue[];
90
+ not?: readonly JsonValue[];
91
+ }>;
92
+ export type PublishedOutputFormat = "png" | "svg" | "json" | "text";
93
+ export interface PublishedOutput {
94
+ readonly id: string;
95
+ readonly node: string;
96
+ readonly port: string;
97
+ readonly label?: string;
98
+ readonly format: PublishedOutputFormat;
99
+ /** Download name; `{id}` and `{scene}` are replaced. */
100
+ readonly filename?: string;
101
+ /** Published parameter ids shown beside this output's button (dpi, ground). */
102
+ readonly parameters?: readonly string[];
103
+ /** The output shown in the viewer. One output should set it. */
104
+ readonly view?: boolean;
105
+ }
106
+ export interface EditorPreset {
107
+ readonly id: string;
108
+ readonly label: string;
109
+ /** Published parameter id to value. Missing parameters take their defaults. */
110
+ readonly values: Readonly<Record<string, JsonValue>>;
111
+ /** Parameters the preset leaves as they are (colour, pen) instead of resetting them. */
112
+ readonly keep?: readonly string[];
113
+ }
114
+ export interface FrozenSource {
115
+ /** The live node the snapshot replaces. Everything upstream of it is dropped. */
116
+ readonly node: string;
117
+ readonly output: string;
118
+ /** Project-relative snapshot written by `cascade freeze`. */
119
+ readonly snapshot: string;
120
+ }
121
+ export interface EditorPanelDeclaration {
122
+ /** Project-relative module exporting `mount(target, api)`. */
123
+ readonly module: string;
124
+ /** `extend` (default) mounts it above the generated parameters. */
125
+ readonly placement?: "extend" | "replace";
126
+ }
127
+ /** What `cascade freeze` writes: exactly the chosen values, nothing else. */
128
+ export interface FrozenSnapshot {
129
+ readonly format: "cascade.frozen";
130
+ readonly version: 1;
131
+ readonly node: string;
132
+ readonly output: string;
133
+ readonly fields: readonly string[];
134
+ /** Fields the author marked as personal, flagged in every export plan. */
135
+ readonly personal?: readonly string[];
136
+ readonly value: JsonValue;
137
+ /** Project-relative files the value refers to, copied beside the snapshot. */
138
+ readonly files?: readonly string[];
139
+ readonly frozenAt: string;
140
+ }
141
+ /**
142
+ * A scene: what someone changed in an editor, as a file that opens onto the
143
+ * same graph in Studio, the CLI or the exported editor.
144
+ *
145
+ * It holds published parameter values and the files the person dropped, and
146
+ * nothing from the project. Version mismatches degrade: unknown parameters are
147
+ * ignored with a warning, missing ones take the graph's defaults.
148
+ */
149
+ export interface CascadeScene {
150
+ readonly format: "cascade.scene";
151
+ readonly version: 1;
152
+ readonly graph: Readonly<{
153
+ id: string;
154
+ version: string;
155
+ }>;
156
+ readonly name?: string;
157
+ readonly values: Readonly<Record<string, JsonValue>>;
158
+ /** Files for `file` parameters, by parameter id. */
159
+ readonly files?: Readonly<Record<string, SceneFile>>;
160
+ readonly savedAt?: string;
161
+ }
162
+ export interface SceneFile {
163
+ readonly name: string;
164
+ readonly mediaType?: string;
165
+ readonly size: number;
166
+ /** Hex SHA-256 of the bytes, to match a file the receiver already has. */
167
+ readonly sha256: string;
168
+ /** Base64 bytes, present only when the parameter allows embedding. */
169
+ readonly data?: string;
170
+ }
171
+ export declare const SCENE_EMBED_LIMIT: number;
172
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1,2 @@
1
+ export const SCENE_EMBED_LIMIT = 2 * 1024 * 1024;
2
+ //# sourceMappingURL=editor.js.map
@@ -1,9 +1,11 @@
1
1
  export * from "./animation.js";
2
+ export * from "./bundle.js";
2
3
  export * from "./camera.js";
3
4
  export * from "./capabilities.js";
4
5
  export * from "./color.js";
5
6
  export * from "./definition.js";
6
7
  export * from "./document.js";
8
+ export * from "./editor.js";
7
9
  export * from "./geometry.js";
8
10
  export * from "./particles.js";
9
11
  export * from "./scene.js";
@@ -1,9 +1,11 @@
1
1
  export * from "./animation.js";
2
+ export * from "./bundle.js";
2
3
  export * from "./camera.js";
3
4
  export * from "./capabilities.js";
4
5
  export * from "./color.js";
5
6
  export * from "./definition.js";
6
7
  export * from "./document.js";
8
+ export * from "./editor.js";
7
9
  export * from "./geometry.js";
8
10
  export * from "./particles.js";
9
11
  export * from "./scene.js";