@field/cascade 0.7.1 → 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 (117) hide show
  1. package/AGENTS.md +17 -1
  2. package/CHANGELOG.md +260 -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-BRKnqZhp.js +1 -0
  8. package/dist/assets/{ExportDialog-BJu2Bpvv.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-DzFLUotB.js +3 -0
  13. package/dist/assets/{core-DlkL34JF.js → core-BICrEt0y.js} +8 -8
  14. package/dist/assets/{cssMode-DaSSFZYi.js → cssMode-D7TXw57I.js} +1 -1
  15. package/dist/assets/definitions-DUww2Rt-.js +1 -0
  16. package/dist/assets/{editor.api-CHUMRaMm.js → editor.api-DvkCO3ko.js} +1 -1
  17. package/dist/assets/{freemarker2-DQ604q2A.js → freemarker2-BA2lxsF1.js} +1 -1
  18. package/dist/assets/geo-C2RHOVHC.js +1 -0
  19. package/dist/assets/{handlebars-09pcWz8x.js → handlebars-BcDiyPd8.js} +1 -1
  20. package/dist/assets/{html-DtIKrZiD.js → html-C7O4TyhX.js} +1 -1
  21. package/dist/assets/{htmlMode-DgJ_4m1_.js → htmlMode-WaDGF3Mj.js} +1 -1
  22. package/dist/assets/{image-CtUE4_I-.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-kbIBYYGo.js → javascript-CpIEK7tz.js} +1 -1
  27. package/dist/assets/{jsonMode-DblCG64t.js → jsonMode-B2FltW0f.js} +1 -1
  28. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  29. package/dist/assets/{liquid-D-qdTGhQ.js → liquid-CN6FFHZE.js} +1 -1
  30. package/dist/assets/{lspLanguageFeatures-Bc6azcFQ.js → lspLanguageFeatures-QhV9XtLp.js} +1 -1
  31. package/dist/assets/math-DaDEgWKt.js +1 -0
  32. package/dist/assets/{mdx-TXg4xsyD.js → mdx-DE89pafV.js} +1 -1
  33. package/dist/assets/pop-pLH7L0PW.js +1 -0
  34. package/dist/assets/{python-C4SGyXJW.js → python-ByTllTB9.js} +1 -1
  35. package/dist/assets/{razor-BZ-lEgY9.js → razor-USYlRRCE.js} +1 -1
  36. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  37. package/dist/assets/{tsMode-BoaO1D7x.js → tsMode-BXpN8iUz.js} +1 -1
  38. package/dist/assets/{typescript-D87dYk9k.js → typescript-DInBjGQQ.js} +1 -1
  39. package/dist/assets/{workers-ZrST7klJ.js → workers-C7Ye5B7D.js} +1 -1
  40. package/dist/assets/{xml-DT6r4F7G.js → xml-U0Uocqyo.js} +1 -1
  41. package/dist/assets/{yaml-DxmQ8XYb.js → yaml-CBw_PVYf.js} +1 -1
  42. package/dist/cli/index.js +20070 -3487
  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 +6 -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-D-N0C2jT.js +0 -623
  103. package/dist/assets/CodePanel-EN411mp5.js +0 -2
  104. package/dist/assets/CustomNodeDialog-6z2n27VV.js +0 -1
  105. package/dist/assets/OpenGraphDialog-fMZjzIR3.js +0 -1
  106. package/dist/assets/ProjectSettings-BQ8PGNqo.js +0 -2
  107. package/dist/assets/SettingsDialog-IbYLm8Bx.js +0 -1
  108. package/dist/assets/VersionHistoryDialog-Bgh-q5Jg.js +0 -3
  109. package/dist/assets/definitions-CQBdsJYJ.js +0 -1
  110. package/dist/assets/geo-DcmMAcqM.js +0 -1
  111. package/dist/assets/index-CnylT98t.js +0 -212
  112. package/dist/assets/index-client-BoJgsN9M.js +0 -1
  113. package/dist/assets/index-ilgbOu6G.css +0 -1
  114. package/dist/assets/legacy-CAj2_7QE.js +0 -2
  115. package/dist/assets/math-CY11tkoN.js +0 -1
  116. package/dist/assets/pop-Dr5-ARqQ.js +0 -1
  117. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -109,6 +109,33 @@ declare const nullDefinition: {
109
109
  };
110
110
  };
111
111
  };
112
+ /**
113
+ * `cascade.core.Frozen` — a snapshot standing in for a live data source.
114
+ *
115
+ * `cascade export` writes one in place of a node the author froze with
116
+ * `cascade freeze`: the chosen fields of that node's output, and nothing else.
117
+ * It has no inputs, so nothing upstream of a frozen source ships.
118
+ */
119
+ declare const frozenDefinition: {
120
+ readonly apiVersion: 1;
121
+ readonly runsOn: "portable";
122
+ readonly label: "Frozen";
123
+ readonly description: "Emits a frozen snapshot of a data source's chosen values";
124
+ readonly icon: "Snowflake";
125
+ readonly outputs: {
126
+ readonly value: {
127
+ readonly kind: "data";
128
+ readonly type: "any";
129
+ };
130
+ };
131
+ readonly props: {
132
+ readonly value: {
133
+ readonly type: "any";
134
+ readonly default: null;
135
+ };
136
+ };
137
+ };
138
+ export declare const frozenRegistration: DefinitionNodeRegistration<typeof frozenDefinition>;
112
139
  export declare const switchRegistration: DefinitionNodeRegistration<typeof switchDefinition>;
113
140
  export declare const mergeRegistration: DefinitionNodeRegistration<typeof mergeDefinition>;
114
141
  export declare const selectRegistration: DefinitionNodeRegistration<typeof selectDefinition>;
@@ -61,6 +61,34 @@ const nullDefinition = {
61
61
  output: { kind: "data", type: "any" },
62
62
  },
63
63
  };
64
+ /**
65
+ * `cascade.core.Frozen` — a snapshot standing in for a live data source.
66
+ *
67
+ * `cascade export` writes one in place of a node the author froze with
68
+ * `cascade freeze`: the chosen fields of that node's output, and nothing else.
69
+ * It has no inputs, so nothing upstream of a frozen source ships.
70
+ */
71
+ const frozenDefinition = {
72
+ apiVersion: 1,
73
+ runsOn: "portable",
74
+ label: "Frozen",
75
+ description: "Emits a frozen snapshot of a data source's chosen values",
76
+ icon: "Snowflake",
77
+ outputs: {
78
+ value: { kind: "data", type: "any" },
79
+ },
80
+ props: {
81
+ value: { type: "any", default: null },
82
+ },
83
+ };
84
+ export const frozenRegistration = {
85
+ kind: "definition-v1",
86
+ moduleId: "cascade.core.Frozen",
87
+ definition: frozenDefinition,
88
+ loadExecute: async () => ({ outputs, props }) => {
89
+ outputs.value.set(props.value);
90
+ },
91
+ };
64
92
  export const switchRegistration = {
65
93
  kind: "definition-v1",
66
94
  moduleId: "cascade.core.Switch",
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The `.cascade` document as plain, mutable JSON — the shape the editor works
3
+ * on — and the small vocabulary for reading it: wires, ports, positions and
4
+ * key order.
5
+ *
6
+ * The editor deliberately edits the parsed JSON rather than a model built from
7
+ * it. A model would have to re-serialise every node on save, and every field it
8
+ * did not know about (a colour, a promotion, a key some other tool wrote) would
9
+ * either be dropped or reordered. Editing in place means a command touches the
10
+ * keys it is about and nothing else, which is what makes the diff minimal.
11
+ */
12
+ import type { NodeDefinition } from "@field/cascade/contracts";
13
+ import type { Point } from "./layout.js";
14
+ export type JsonObject = Record<string, unknown>;
15
+ export interface EditableNode extends JsonObject {
16
+ id: string;
17
+ module?: string;
18
+ type?: string;
19
+ parent?: string;
20
+ props?: JsonObject;
21
+ }
22
+ export interface EditableDocument extends JsonObject {
23
+ nodes: EditableNode[];
24
+ connections?: unknown[];
25
+ annotations?: JsonObject[];
26
+ }
27
+ export declare const SUBNET = "cascade.core.Subnet";
28
+ export declare const INPUT = "cascade.core.Input";
29
+ export declare const OUTPUT = "cascade.core.Output";
30
+ /** One end of a wire: a node, a port name, and the port's index on that node. */
31
+ export interface Endpoint {
32
+ readonly node: string;
33
+ readonly port: string;
34
+ readonly index: number;
35
+ }
36
+ export interface Wire {
37
+ readonly from: Endpoint;
38
+ readonly to: Endpoint;
39
+ }
40
+ export type WireStyle = "tuple" | "object";
41
+ /**
42
+ * Read either connection form. Studio writes `[[id, index, name], [id, index,
43
+ * name]]`; the contracts also allow `{ source, target }`. Anything else is
44
+ * reported as `null`, never guessed at.
45
+ */
46
+ export declare function readWire(raw: unknown): Wire | null;
47
+ export declare function writeWire(wire: Wire, style: WireStyle): unknown;
48
+ /** Whichever form the file already uses, so a new wire looks like its neighbours. */
49
+ export declare function wireStyleOf(document: EditableDocument): WireStyle;
50
+ /** `a.out -> b.in`: the same spelling the CLI takes. */
51
+ export declare function wireLabel(wire: Wire): string;
52
+ export declare function sameEndpoint(a: Endpoint, b: Endpoint): boolean;
53
+ export declare function moduleOf(node: EditableNode): string;
54
+ export declare function parentOf(node: EditableNode): string | null;
55
+ export declare function positionOf(node: EditableNode): Point;
56
+ /** Keep whichever shape the node already uses; Studio's `[x, y]` otherwise. */
57
+ export declare function setPosition(node: EditableNode, point: Point): void;
58
+ /**
59
+ * The key order Studio writes a node in (`Graph.nodeToJSON`). A key the editor
60
+ * adds goes where Studio would have put it, so a node edited by the CLI and
61
+ * then saved by Studio does not reorder.
62
+ */
63
+ export declare const NODE_KEY_ORDER: readonly ["id", "module", "type", "position", "source", "file", "code", "comment", "color", "parent", "params", "promotions", "inputs", "props", "bypass", "cook", "state"];
64
+ /**
65
+ * Set a key without moving any other. An existing key keeps its place; a new
66
+ * one is inserted before the first key that Studio writes after it, or at the
67
+ * end when it is not one Studio knows.
68
+ */
69
+ export declare function setOrderedKey(target: JsonObject, key: string, value: unknown, order: readonly string[]): void;
70
+ export interface PortInfo {
71
+ readonly name: string;
72
+ readonly index: number;
73
+ readonly kind: "data" | "trigger";
74
+ readonly type: string;
75
+ readonly variadic: boolean;
76
+ }
77
+ export interface NodePorts {
78
+ readonly inputs: readonly PortInfo[];
79
+ readonly outputs: readonly PortInfo[];
80
+ }
81
+ /** Ports in declaration order, which is the order Studio indexes them in. */
82
+ export declare function portsFromDefinition(definition: NodeDefinition): NodePorts;
83
+ /**
84
+ * A subnet's ports are not declared anywhere: they are its `Input` and
85
+ * `Output` children, named `input_<n>` and `output_<n>`. The runtime derives
86
+ * them the same way (`indexedBoundaryPorts` in runtime.ts); this mirrors it so
87
+ * the editor and the runtime agree on what a subnet exposes.
88
+ */
89
+ export declare function subnetPorts(children: readonly EditableNode[]): NodePorts;
90
+ export declare function typesConnect(from: PortInfo | undefined, to: PortInfo | undefined): boolean;
91
+ export declare function integer(value: unknown): number;
92
+ /** Deep equality for JSON values, insensitive to key order. */
93
+ export declare function jsonEqual(a: unknown, b: unknown): boolean;
94
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1,194 @@
1
+ /**
2
+ * The `.cascade` document as plain, mutable JSON — the shape the editor works
3
+ * on — and the small vocabulary for reading it: wires, ports, positions and
4
+ * key order.
5
+ *
6
+ * The editor deliberately edits the parsed JSON rather than a model built from
7
+ * it. A model would have to re-serialise every node on save, and every field it
8
+ * did not know about (a colour, a promotion, a key some other tool wrote) would
9
+ * either be dropped or reordered. Editing in place means a command touches the
10
+ * keys it is about and nothing else, which is what makes the diff minimal.
11
+ */
12
+ import { canConnectTypes } from "@field/cascade/contracts";
13
+ export const SUBNET = "cascade.core.Subnet";
14
+ export const INPUT = "cascade.core.Input";
15
+ export const OUTPUT = "cascade.core.Output";
16
+ /**
17
+ * Read either connection form. Studio writes `[[id, index, name], [id, index,
18
+ * name]]`; the contracts also allow `{ source, target }`. Anything else is
19
+ * reported as `null`, never guessed at.
20
+ */
21
+ export function readWire(raw) {
22
+ if (Array.isArray(raw) && raw.length === 2 && Array.isArray(raw[0]) && Array.isArray(raw[1])) {
23
+ const [from, to] = raw;
24
+ if (typeof from[0] !== "string" || typeof to[0] !== "string")
25
+ return null;
26
+ return {
27
+ from: { node: from[0], index: Number(from[1] ?? 0), port: String(from[2] ?? "") },
28
+ to: { node: to[0], index: Number(to[1] ?? 0), port: String(to[2] ?? "") },
29
+ };
30
+ }
31
+ if (raw && typeof raw === "object" && "source" in raw && "target" in raw) {
32
+ const { source, target } = raw;
33
+ return {
34
+ from: { node: source.nodeId, port: source.outputName, index: 0 },
35
+ to: { node: target.nodeId, port: target.inputName, index: 0 },
36
+ };
37
+ }
38
+ return null;
39
+ }
40
+ export function writeWire(wire, style) {
41
+ if (style === "object") {
42
+ return {
43
+ source: { nodeId: wire.from.node, outputName: wire.from.port },
44
+ target: { nodeId: wire.to.node, inputName: wire.to.port },
45
+ };
46
+ }
47
+ return [
48
+ [wire.from.node, wire.from.index, wire.from.port],
49
+ [wire.to.node, wire.to.index, wire.to.port],
50
+ ];
51
+ }
52
+ /** Whichever form the file already uses, so a new wire looks like its neighbours. */
53
+ export function wireStyleOf(document) {
54
+ const first = document.connections?.[0];
55
+ return first && !Array.isArray(first) ? "object" : "tuple";
56
+ }
57
+ /** `a.out -> b.in`: the same spelling the CLI takes. */
58
+ export function wireLabel(wire) {
59
+ return `${wire.from.node}.${wire.from.port} -> ${wire.to.node}.${wire.to.port}`;
60
+ }
61
+ export function sameEndpoint(a, b) {
62
+ return a.node === b.node && a.port === b.port;
63
+ }
64
+ export function moduleOf(node) {
65
+ return String(node.module ?? node.type ?? "");
66
+ }
67
+ export function parentOf(node) {
68
+ return typeof node.parent === "string" && node.parent ? node.parent : null;
69
+ }
70
+ export function positionOf(node) {
71
+ const position = node.position;
72
+ if (Array.isArray(position))
73
+ return { x: Number(position[0]) || 0, y: Number(position[1]) || 0 };
74
+ if (position && typeof position === "object") {
75
+ const { x, y } = position;
76
+ return { x: Number(x) || 0, y: Number(y) || 0 };
77
+ }
78
+ return { x: 0, y: 0 };
79
+ }
80
+ /** Keep whichever shape the node already uses; Studio's `[x, y]` otherwise. */
81
+ export function setPosition(node, point) {
82
+ const position = node.position;
83
+ const value = position && typeof position === "object" && !Array.isArray(position)
84
+ ? { x: point.x, y: point.y }
85
+ : [point.x, point.y];
86
+ setOrderedKey(node, "position", value, NODE_KEY_ORDER);
87
+ }
88
+ /**
89
+ * The key order Studio writes a node in (`Graph.nodeToJSON`). A key the editor
90
+ * adds goes where Studio would have put it, so a node edited by the CLI and
91
+ * then saved by Studio does not reorder.
92
+ */
93
+ export const NODE_KEY_ORDER = [
94
+ "id", "module", "type", "position", "source", "file", "code", "comment", "color",
95
+ "parent", "params", "promotions", "inputs", "props", "bypass", "cook", "state",
96
+ ];
97
+ /**
98
+ * Set a key without moving any other. An existing key keeps its place; a new
99
+ * one is inserted before the first key that Studio writes after it, or at the
100
+ * end when it is not one Studio knows.
101
+ */
102
+ export function setOrderedKey(target, key, value, order) {
103
+ if (key in target) {
104
+ target[key] = value;
105
+ return;
106
+ }
107
+ const rank = order.indexOf(key);
108
+ const entries = Object.entries(target);
109
+ const at = rank === -1 ? -1 : entries.findIndex(([name]) => {
110
+ const other = order.indexOf(name);
111
+ return other > rank;
112
+ });
113
+ if (at === -1) {
114
+ target[key] = value;
115
+ return;
116
+ }
117
+ for (const [name] of entries)
118
+ delete target[name];
119
+ entries.splice(at, 0, [key, value]);
120
+ for (const [name, entry] of entries)
121
+ target[name] = entry;
122
+ }
123
+ /** Ports in declaration order, which is the order Studio indexes them in. */
124
+ export function portsFromDefinition(definition) {
125
+ const list = (record) => Object.entries(record ?? {}).map(([name, port], index) => {
126
+ const value = port;
127
+ return {
128
+ name,
129
+ index,
130
+ kind: value.kind === "trigger" ? "trigger" : "data",
131
+ type: value.type ?? "any",
132
+ variadic: value.variadic === true,
133
+ };
134
+ });
135
+ return {
136
+ inputs: list(definition.inputs),
137
+ outputs: list(definition.outputs),
138
+ };
139
+ }
140
+ /**
141
+ * A subnet's ports are not declared anywhere: they are its `Input` and
142
+ * `Output` children, named `input_<n>` and `output_<n>`. The runtime derives
143
+ * them the same way (`indexedBoundaryPorts` in runtime.ts); this mirrors it so
144
+ * the editor and the runtime agree on what a subnet exposes.
145
+ */
146
+ export function subnetPorts(children) {
147
+ const boundary = (module, indexProp, prefix) => {
148
+ const found = children
149
+ .filter((child) => moduleOf(child) === module)
150
+ .map((child) => ({ index: integer(child.props?.[indexProp]), type: String(child.props?.dataType || "any") }))
151
+ .filter((entry) => entry.index >= 0);
152
+ const max = found.length ? Math.max(...found.map((entry) => entry.index)) : -1;
153
+ return Array.from({ length: max + 1 }, (_, index) => ({
154
+ name: `${prefix}_${index}`,
155
+ index,
156
+ kind: "data",
157
+ type: found.find((entry) => entry.index === index)?.type ?? "any",
158
+ variadic: false,
159
+ }));
160
+ };
161
+ return {
162
+ inputs: boundary(INPUT, "inputIndex", "input"),
163
+ outputs: boundary(OUTPUT, "outputIndex", "output"),
164
+ };
165
+ }
166
+ export function typesConnect(from, to) {
167
+ if (!from || !to)
168
+ return true;
169
+ if (from.kind !== to.kind)
170
+ return false;
171
+ return from.kind === "trigger" || canConnectTypes(from.type, to.type);
172
+ }
173
+ export function integer(value) {
174
+ const number = Number(value ?? 0);
175
+ return Number.isInteger(number) ? number : -1;
176
+ }
177
+ /** Deep equality for JSON values, insensitive to key order. */
178
+ export function jsonEqual(a, b) {
179
+ if (a === b)
180
+ return true;
181
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== "object")
182
+ return false;
183
+ if (Array.isArray(a) !== Array.isArray(b))
184
+ return false;
185
+ if (Array.isArray(a)) {
186
+ const other = b;
187
+ return a.length === other.length && a.every((value, index) => jsonEqual(value, other[index]));
188
+ }
189
+ const left = a;
190
+ const right = b;
191
+ const keys = Object.keys(left);
192
+ return keys.length === Object.keys(right).length && keys.every((key) => key in right && jsonEqual(left[key], right[key]));
193
+ }
194
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Editing a `.cascade` document: add and remove nodes, wire and unwire ports,
3
+ * set parameters, and group nodes into subnets.
4
+ *
5
+ * One implementation behind every surface — `cascade node add`, the MCP
6
+ * server's `node_add`, and anything else that edits a graph without Studio —
7
+ * so a command and a tool call cannot disagree about what an edit means.
8
+ *
9
+ * Three rules hold for every operation, because they are what keeps a graph
10
+ * an agent edited readable to the person who opens it afterwards:
11
+ *
12
+ * - **Minimal diffs.** Only the keys an operation is about change. Node ids are
13
+ * stable, untouched nodes keep their place in the file, a new key goes where
14
+ * Studio would write it, and a rewired connection is rewritten in place
15
+ * rather than removed and appended. An edit followed by its inverse leaves
16
+ * the file byte-identical (tests/graph-edit.test.ts).
17
+ * - **Placed, not piled.** A node the editor creates is positioned next to what
18
+ * it is wired to and in line with the graph's existing flow (`layout.ts`).
19
+ * - **Subnets stay closed.** A wire never crosses a subnet boundary directly;
20
+ * moving nodes in or out of a subnet rewires through its `Input` and
21
+ * `Output` nodes, creating them when needed and removing them when unused.
22
+ *
23
+ * The editor is pure: it edits a parsed document in memory and resolves node
24
+ * definitions through a callback. Reading, validating and writing files is the
25
+ * host's job (`src/cli/graphFile.ts`).
26
+ */
27
+ import type { NodeDefinition } from "@field/cascade/contracts";
28
+ import { type EditableDocument, type EditableNode, type NodePorts } from "./document.js";
29
+ import { type Point } from "./layout.js";
30
+ import { type ParameterChange } from "./parameters.js";
31
+ export interface GraphEditorOptions {
32
+ /** The definition of a module id, or `undefined` when it has none (a dynamic or missing module). */
33
+ readonly definitionOf: (moduleId: string) => NodeDefinition | undefined;
34
+ }
35
+ /** A refused edit. The document is left as it was before the operation began. */
36
+ export declare class GraphEditError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ export interface AddNodeInput {
40
+ readonly module: string;
41
+ readonly id?: string;
42
+ /** The subnet to create the node in. Omitted: the top level. */
43
+ readonly parent?: string;
44
+ readonly position?: Point;
45
+ /** Parameter values, as JSON values or (with `fromText`) command-line text. */
46
+ readonly props?: Readonly<Record<string, unknown>>;
47
+ readonly fromText?: boolean;
48
+ readonly comment?: string;
49
+ }
50
+ export interface NodeSummary {
51
+ readonly id: string;
52
+ readonly module: string;
53
+ readonly parent: string | null;
54
+ readonly position: Point;
55
+ }
56
+ export declare class GraphEditor {
57
+ readonly document: EditableDocument;
58
+ private readonly options;
59
+ readonly warnings: string[];
60
+ constructor(document: EditableDocument, options: GraphEditorOptions);
61
+ addNode(input: AddNodeInput): {
62
+ node: NodeSummary;
63
+ };
64
+ /**
65
+ * Remove a node and every wire touching it. A subnet with children is
66
+ * refused unless `recursive`, because removing a container silently would
67
+ * remove everything in it.
68
+ */
69
+ removeNode(id: string, options?: {
70
+ recursive?: boolean;
71
+ }): {
72
+ removed: string[];
73
+ wires: string[];
74
+ };
75
+ /**
76
+ * Wire `from` (`node.output`) to `to` (`node.input`). A port name may be
77
+ * left off when the node has exactly one port on that side.
78
+ *
79
+ * The two nodes may be in different subnets: the wire is then routed
80
+ * through the subnets' `Output` and `Input` nodes, which are created (or
81
+ * reused) as needed, so no wire ever crosses a boundary directly.
82
+ *
83
+ * A node wired for the first time is moved next to its new neighbour, so a
84
+ * node created by `node add` and then connected ends up where a person would
85
+ * have put it. Pass `layout: false` to leave positions alone.
86
+ */
87
+ connect(from: string, to: string, options?: {
88
+ layout?: boolean;
89
+ }): {
90
+ wire: string;
91
+ replaced?: string;
92
+ moved?: NodeSummary;
93
+ };
94
+ /**
95
+ * The endpoint on `level`'s canvas that carries `source`: the source itself
96
+ * when it is already there, otherwise a subnet port reached by passing out
97
+ * through `Output` nodes to the common ancestor and in through `Input` nodes
98
+ * to `level`. Existing boundaries carrying the same value are reused.
99
+ */
100
+ private routeAcross;
101
+ /**
102
+ * Remove wires. `disconnect a.out b.in` removes that wire; `disconnect b.in`
103
+ * removes whatever feeds that input; `disconnect a.out` removes everything
104
+ * that output feeds.
105
+ */
106
+ disconnect(first: string, second?: string): {
107
+ removed: string[];
108
+ };
109
+ /** Change `node.param`: its value, expression or keyframes. */
110
+ setParameter(target: string, change: ParameterChange): {
111
+ node: string;
112
+ param: string;
113
+ field: string;
114
+ before: unknown;
115
+ after: unknown;
116
+ };
117
+ /**
118
+ * Group sibling nodes into a new subnet. Wires that now cross its boundary
119
+ * are routed through new `Input` and `Output` nodes, named after the port
120
+ * they carry, so the subnet's ports read as what flows through them.
121
+ */
122
+ createSubnet(input: {
123
+ nodes: readonly string[];
124
+ id?: string;
125
+ label?: string;
126
+ }): {
127
+ subnet: NodeSummary;
128
+ inputs: string[];
129
+ outputs: string[];
130
+ };
131
+ /** The inverse of `createSubnet`: lift the children out and remove the subnet. */
132
+ dissolveSubnet(id: string): {
133
+ lifted: string[];
134
+ };
135
+ /**
136
+ * Move sibling nodes to another subnet (`null` for the top level), one level
137
+ * at a time — out to the common ancestor, then in — rewiring through each
138
+ * boundary on the way.
139
+ */
140
+ moveNodes(ids: readonly string[], to: string | null): {
141
+ moved: string[];
142
+ to: string | null;
143
+ placed: NodeSummary[];
144
+ };
145
+ /**
146
+ * Move sibling nodes from the subnet's parent into the subnet.
147
+ *
148
+ * A wire from a member to the subnet's own input collapses (the member is
149
+ * now inside, so it feeds that input's consumers directly), and a wire from
150
+ * outside to a member is routed through an `Input` — reusing one that
151
+ * already carries the same source. The mirror holds for outputs. Wires are
152
+ * rewritten in place where possible, which is what lets `moveOut` restore
153
+ * the original file exactly.
154
+ */
155
+ private moveIn;
156
+ /** Move nodes out of a subnet into its parent. The mirror of `moveIn`. */
157
+ private moveOut;
158
+ /** An `Output` in the subnet already fed by `source`, or a new one. */
159
+ private outputFor;
160
+ /** An `Input` in the subnet whose outside feed is `source`. */
161
+ private inputCarrying;
162
+ private createBoundary;
163
+ /**
164
+ * Name new boundary nodes after what they carry, readably. A port name is
165
+ * the natural label (`moment`, `canvas`), but not when it says nothing
166
+ * (`value`, `output`) or when two ports of the subnet would share it — three
167
+ * `mask` outputs read as one. Then the producing node's id is the label:
168
+ * `masks-out-cloud-mask` beside `masks-out-primary-lobe`.
169
+ */
170
+ private nameBoundaries;
171
+ private relabelBoundary;
172
+ /** What a value is called: its port name, or for a subnet's own port, the name of the boundary behind it. */
173
+ private labelOf;
174
+ /**
175
+ * Remove boundary node k and keep the subnet's ports contiguous: the ports
176
+ * above it shift down one, and so do the wires that use them. Wires to the
177
+ * removed port itself are marked in `removed` for the caller to drop.
178
+ */
179
+ private removeBoundaryNode;
180
+ /** `rm` of a boundary node: the subnet loses that port. */
181
+ private removeBoundary;
182
+ private boundaryIndex;
183
+ private boundaryNode;
184
+ private subnetPort;
185
+ findNode(id: string): EditableNode | undefined;
186
+ requireNode(id: string): EditableNode;
187
+ childrenOf(parent: string | null): EditableNode[];
188
+ private descendantsOf;
189
+ private ancestorsOf;
190
+ private requireContainer;
191
+ private requireSiblings;
192
+ /** The ports a node has, or `null` when they are not statically known. */
193
+ portsOf(node: EditableNode): NodePorts | null;
194
+ private portInfo;
195
+ /** `node.port` to a checked endpoint. A missing port name is filled in when only one would fit. */
196
+ private resolveEndpoint;
197
+ private indexFromExistingWires;
198
+ private isWired;
199
+ private setParameterOn;
200
+ private connections;
201
+ /** Every connection with its array index; unreadable ones have `wire: null` and are left alone. */
202
+ private wires;
203
+ private appendWire;
204
+ private replaceWire;
205
+ private removeWires;
206
+ /** Insert after the last node of the same subnet, so a subnet's children stay together in the file. */
207
+ private insertNode;
208
+ private placeFor;
209
+ private relayout;
210
+ /** The flow of one canvas: the median offset along its wires. */
211
+ private stepIn;
212
+ private forgetInAnnotations;
213
+ uniqueId(base: string): string;
214
+ summary(node: EditableNode): NodeSummary;
215
+ }
216
+ /** `a.b.port` splits at the last dot: node `a.b`, port `port`. No dot: the node alone. */
217
+ export declare function splitEndpoint(text: string): {
218
+ node: string;
219
+ port: string;
220
+ };
221
+ /** `project.InkVocabulary` becomes `ink-vocabulary`; `cascade.geo.CopyToPoints`, `copy-to-points`. */
222
+ export declare function defaultIdFor(module: string): string;
223
+ //# sourceMappingURL=graphEditor.d.ts.map