@field/cascade 0.8.0 → 0.11.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 (195) hide show
  1. package/AGENTS.md +36 -3
  2. package/CHANGELOG.md +401 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -26
  5. package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
  6. package/dist/assets/CodePanel-BU-autmq.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  8. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
  9. package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
  10. package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
  11. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
  12. package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
  13. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
  14. package/dist/assets/definitions-GreEMXLV.js +1 -0
  15. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
  16. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
  17. package/dist/assets/geo-u0n_jOsD.js +1 -0
  18. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
  19. package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
  20. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
  21. package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
  22. package/dist/assets/index-5ZtDAuy2.js +220 -0
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-client-DqYmKstU.js +1 -0
  25. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
  26. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
  27. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  28. package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  30. package/dist/assets/math-DioIQOPJ.js +1 -0
  31. package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
  32. package/dist/assets/pop-DC_TQG9t.js +1 -0
  33. package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
  34. package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
  35. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  36. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
  37. package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
  38. package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
  39. package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
  40. package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
  41. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  42. package/dist/cli/chunk-2KXB47FP.js +115 -0
  43. package/dist/cli/chunk-35D343DC.js +1447 -0
  44. package/dist/cli/chunk-36P5K3SD.js +631 -0
  45. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  46. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  47. package/dist/cli/chunk-53NMBJAO.js +35 -0
  48. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  49. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  50. package/dist/cli/chunk-BP7UKF36.js +376 -0
  51. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  52. package/dist/cli/chunk-CO22XHNG.js +593 -0
  53. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  54. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  55. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  56. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  57. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  58. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  59. package/dist/cli/chunk-HFHWYL55.js +139 -0
  60. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  61. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  62. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  63. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  64. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  65. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  66. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  67. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  68. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  69. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  70. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  71. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  72. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  73. package/dist/cli/chunk-SGJAET2L.js +197 -0
  74. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  75. package/dist/cli/chunk-WD62JIP6.js +118 -0
  76. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  77. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  78. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  79. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  80. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  81. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  82. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  83. package/dist/cli/commands-Q23M26QM.js +1292 -0
  84. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  85. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  86. package/dist/cli/edit-JHCPTQFT.js +28 -0
  87. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  88. package/dist/cli/encode-HAUZTYPT.js +17 -0
  89. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  90. package/dist/cli/geo-4RNK3QVN.js +43 -0
  91. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  92. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  93. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  94. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  95. package/dist/cli/index.js +106 -14207
  96. package/dist/cli/math-ZRMLKU2D.js +16 -0
  97. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  98. package/dist/cli/parity-KINARCP2.js +328 -0
  99. package/dist/cli/pop-USDFXB7H.js +39 -0
  100. package/dist/cli/project-K3QIPGBB.js +12 -0
  101. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  102. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  103. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  104. package/dist/cli/render-YURAPPUI.js +436 -0
  105. package/dist/cli/runCommand-PI764LEG.js +198 -0
  106. package/dist/cli/runner-RA6UKEPD.js +37 -0
  107. package/dist/cli/runtime/image.ts +277 -0
  108. package/dist/cli/runtime/stage.ts +74 -11
  109. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  110. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  111. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  112. package/dist/contracts/bundle.d.ts +75 -0
  113. package/dist/contracts/bundle.js +112 -0
  114. package/dist/contracts/definition.d.ts +67 -4
  115. package/dist/contracts/document.d.ts +29 -0
  116. package/dist/contracts/documentFormat.d.ts +18 -0
  117. package/dist/contracts/documentFormat.js +195 -0
  118. package/dist/contracts/editor.d.ts +189 -0
  119. package/dist/contracts/editor.js +2 -0
  120. package/dist/contracts/index.d.ts +3 -0
  121. package/dist/contracts/index.js +3 -0
  122. package/dist/contracts/validate.js +59 -3
  123. package/dist/contracts/values.d.ts +3 -3
  124. package/dist/contracts/values.js +1 -0
  125. package/dist/image.d.ts +141 -0
  126. package/dist/image.js +127 -0
  127. package/dist/index.html +4 -4
  128. package/dist/player/app.js +17 -5
  129. package/dist/player/editor.js +1919 -0
  130. package/dist/player/editorEngine.js +477 -0
  131. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  132. package/dist/runtime/builtins/core/bundle.js +81 -0
  133. package/dist/runtime/builtins/core/index.d.ts +3 -2
  134. package/dist/runtime/builtins/core/index.js +6 -2
  135. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  136. package/dist/runtime/builtins/core/routing.js +28 -0
  137. package/dist/runtime/builtins/headless.d.ts +19 -0
  138. package/dist/runtime/builtins/headless.js +25 -0
  139. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  140. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  141. package/dist/runtime/builtins/image/index.d.ts +9 -0
  142. package/dist/runtime/builtins/image/index.js +9 -0
  143. package/dist/runtime/edit/document.d.ts +94 -0
  144. package/dist/runtime/edit/document.js +194 -0
  145. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  146. package/dist/runtime/edit/graphEditor.js +893 -0
  147. package/dist/runtime/edit/index.d.ts +12 -0
  148. package/dist/runtime/edit/index.js +6 -0
  149. package/dist/runtime/edit/layout.d.ts +50 -0
  150. package/dist/runtime/edit/layout.js +126 -0
  151. package/dist/runtime/edit/operations.d.ts +65 -0
  152. package/dist/runtime/edit/operations.js +57 -0
  153. package/dist/runtime/edit/parameters.d.ts +75 -0
  154. package/dist/runtime/edit/parameters.js +284 -0
  155. package/dist/runtime/edit/read.d.ts +35 -0
  156. package/dist/runtime/edit/read.js +65 -0
  157. package/dist/runtime/editor/index.d.ts +104 -0
  158. package/dist/runtime/editor/index.js +439 -0
  159. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  160. package/dist/runtime/expressions/bundlePaths.js +55 -0
  161. package/dist/runtime/expressions/index.d.ts +2 -0
  162. package/dist/runtime/expressions/index.js +1 -0
  163. package/dist/runtime/index.d.ts +2 -0
  164. package/dist/runtime/index.js +2 -0
  165. package/dist/runtime/params/animator.d.ts +5 -0
  166. package/dist/runtime/params/animator.js +16 -0
  167. package/dist/runtime/runtime.js +238 -12
  168. package/dist/runtime/seed.d.ts +29 -0
  169. package/dist/runtime/seed.js +52 -0
  170. package/dist/runtime/types.d.ts +12 -0
  171. package/dist/stage.js +43 -9
  172. package/dist/studio/panel.d.ts +2 -0
  173. package/doc/CLI_REFERENCE.md +171 -0
  174. package/doc/EDITOR_EXPORT.md +235 -0
  175. package/doc/HEADLESS_GPU.md +27 -0
  176. package/doc/MCP.md +69 -0
  177. package/doc/NODE_REFERENCE.md +208 -0
  178. package/doc/PROJECT_AUTHORING.md +258 -1
  179. package/package.json +29 -8
  180. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  181. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  182. package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
  183. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  184. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  185. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  186. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  187. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  188. package/dist/assets/geo-D2VPjAdt.js +0 -1
  189. package/dist/assets/index-CMiBdEuT.js +0 -212
  190. package/dist/assets/index-DMIuml9a.css +0 -1
  191. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  192. package/dist/assets/legacy-ZA7textG.js +0 -2
  193. package/dist/assets/math-B6hpxPJO.js +0 -1
  194. package/dist/assets/pop-CYUIjT6x.js +0 -1
  195. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,2975 @@
1
+ import {
2
+ loadEmbeddedModule,
3
+ loadProjectModule
4
+ } from "./chunk-2KXB47FP.js";
5
+ import {
6
+ moduleNodeFunction
7
+ } from "./chunk-RLCAJBVR.js";
8
+ import "./chunk-53NMBJAO.js";
9
+ import "./chunk-SGJAET2L.js";
10
+ import {
11
+ Node,
12
+ canConnect,
13
+ getNodeClass,
14
+ getNodeDisplayName,
15
+ isColorValue,
16
+ isStandardLibraryNode,
17
+ normalizeColor,
18
+ normalizeType,
19
+ packagePathToType
20
+ } from "./chunk-FCDXJ6EK.js";
21
+ import "./chunk-RUU3YWXU.js";
22
+ import {
23
+ bundleSchemasCompatible,
24
+ unknownFieldMessage
25
+ } from "./chunk-GFS6W3C5.js";
26
+ import "./chunk-HFHWYL55.js";
27
+ import {
28
+ parseBundleFieldPath,
29
+ remoteBundlePath
30
+ } from "./chunk-CO22XHNG.js";
31
+ import "./chunk-A4OAMOZH.js";
32
+ import "./chunk-J7VK7MNF.js";
33
+ import "./chunk-BIJO5OFS.js";
34
+
35
+ // src/nodes/annotations/Annotation.ts
36
+ var Annotation = class extends Node {
37
+ constructor(id, type, graph) {
38
+ super(id, type, graph);
39
+ // Marker property for reliable type checking (survives HMR and serialization)
40
+ this.isAnnotation = true;
41
+ }
42
+ /**
43
+ * Annotations always have exactly one output at index 0
44
+ */
45
+ getOutput() {
46
+ return this.getOutputPort(0);
47
+ }
48
+ /**
49
+ * Annotations don't execute - override to no-op
50
+ */
51
+ async execute() {
52
+ }
53
+ };
54
+
55
+ // src/nodes/annotations/Image.ts
56
+ var ImageAnnotation = class extends Annotation {
57
+ constructor(id, graph) {
58
+ super(id, "Image", graph);
59
+ }
60
+ };
61
+
62
+ // src/nodes/annotations/Text.ts
63
+ var TextAnnotation = class extends Annotation {
64
+ constructor(id, graph) {
65
+ super(id, "Text", graph);
66
+ }
67
+ };
68
+
69
+ // src/nodes/annotations/Group.ts
70
+ var GroupAnnotation = class extends Annotation {
71
+ constructor(id, graph) {
72
+ super(id, "Group", graph);
73
+ }
74
+ };
75
+
76
+ // src/nodes/annotations/Line.ts
77
+ var LineAnnotation = class extends Annotation {
78
+ constructor(id, graph) {
79
+ super(id, "Line", graph);
80
+ }
81
+ };
82
+
83
+ // src/nodes/annotations/Polyline.ts
84
+ var PolylineAnnotation = class extends Annotation {
85
+ constructor(id, graph) {
86
+ super(id, "Polyline", graph);
87
+ }
88
+ };
89
+
90
+ // src/engine/AssetManager.ts
91
+ var BrowserAssetLoader = class {
92
+ async loadImage(path) {
93
+ return new Promise((resolve, reject) => {
94
+ if (typeof Image === "undefined") {
95
+ reject(new Error("Image API not available (not in browser environment)"));
96
+ return;
97
+ }
98
+ const img = new Image();
99
+ img.onload = () => resolve(img);
100
+ img.onerror = () => reject(new Error(`Failed to load image: ${path}`));
101
+ img.src = path;
102
+ });
103
+ }
104
+ async loadJSON(path) {
105
+ if (typeof fetch === "undefined") {
106
+ throw new Error("fetch API not available (not in browser environment)");
107
+ }
108
+ const response = await fetch(path);
109
+ if (!response.ok) {
110
+ throw new Error(`Failed to load JSON: ${path} - ${response.statusText}`);
111
+ }
112
+ return response.json();
113
+ }
114
+ async loadText(path) {
115
+ if (typeof fetch === "undefined") {
116
+ throw new Error("fetch API not available (not in browser environment)");
117
+ }
118
+ const response = await fetch(path);
119
+ if (!response.ok) {
120
+ throw new Error(`Failed to load text: ${path} - ${response.statusText}`);
121
+ }
122
+ return response.text();
123
+ }
124
+ async loadBinary(path) {
125
+ if (typeof fetch === "undefined") {
126
+ throw new Error("fetch API not available (not in browser environment)");
127
+ }
128
+ const response = await fetch(path);
129
+ if (!response.ok) {
130
+ throw new Error(`Failed to load binary: ${path} - ${response.statusText}`);
131
+ }
132
+ return response.arrayBuffer();
133
+ }
134
+ };
135
+ var AssetManager = class {
136
+ constructor(projectRoot = "", loader) {
137
+ this.cache = /* @__PURE__ */ new Map();
138
+ this.projectRoot = projectRoot;
139
+ this.loader = loader ?? new BrowserAssetLoader();
140
+ }
141
+ async load(path) {
142
+ if (this.cache.has(path)) {
143
+ return this.cache.get(path);
144
+ }
145
+ const absolutePath = this.resolve(path);
146
+ const type = this.getAssetType(path);
147
+ let data;
148
+ switch (type) {
149
+ case "image":
150
+ data = await this.loader.loadImage(absolutePath);
151
+ break;
152
+ case "json":
153
+ data = await this.loader.loadJSON(absolutePath);
154
+ break;
155
+ case "text":
156
+ data = await this.loader.loadText(absolutePath);
157
+ break;
158
+ default:
159
+ throw new Error(`Unsupported asset type: ${type}`);
160
+ }
161
+ const asset = {
162
+ id: this.generateId(path),
163
+ path,
164
+ type,
165
+ data,
166
+ size: 0,
167
+ metadata: {}
168
+ };
169
+ this.cache.set(path, asset);
170
+ return asset;
171
+ }
172
+ resolve(path) {
173
+ if (path.startsWith("http://") || path.startsWith("https://")) {
174
+ return path;
175
+ }
176
+ if (path.startsWith("/")) {
177
+ return path;
178
+ }
179
+ return `${this.projectRoot}${path}`;
180
+ }
181
+ getAssetType(path) {
182
+ const ext = path.split(".").pop()?.toLowerCase();
183
+ if (["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(ext || "")) return "image";
184
+ if (["mp3", "wav", "ogg", "m4a"].includes(ext || "")) return "audio";
185
+ if (["mp4", "webm", "mov"].includes(ext || "")) return "video";
186
+ if (ext === "json") return "json";
187
+ if (["txt", "md", "csv"].includes(ext || "")) return "text";
188
+ return "binary";
189
+ }
190
+ generateId(path) {
191
+ return path.replace(/[^a-zA-Z0-9]/g, "_");
192
+ }
193
+ getAsset(path) {
194
+ return this.cache.get(path);
195
+ }
196
+ clearCache() {
197
+ this.cache.clear();
198
+ }
199
+ removeAsset(path) {
200
+ this.cache.delete(path);
201
+ }
202
+ /**
203
+ * Reload an asset from its path (useful for hot reload)
204
+ */
205
+ async reloadAsset(path) {
206
+ this.cache.delete(path);
207
+ return this.load(path);
208
+ }
209
+ /**
210
+ * Reload all cached assets (useful for hot reload)
211
+ */
212
+ async reloadAll() {
213
+ const paths = Array.from(this.cache.keys());
214
+ this.cache.clear();
215
+ await Promise.all(paths.map((path) => this.load(path).catch((err) => {
216
+ console.warn(`Failed to reload asset ${path}:`, err);
217
+ })));
218
+ }
219
+ list() {
220
+ return Array.from(this.cache.values());
221
+ }
222
+ };
223
+
224
+ // src/engine/PackageManager.ts
225
+ var PackageManager = class {
226
+ constructor() {
227
+ this.cache = /* @__PURE__ */ new Map();
228
+ this.loadingPromises = /* @__PURE__ */ new Map();
229
+ this.cdnBaseUrl = "https://esm.sh";
230
+ }
231
+ /**
232
+ * Load an NPM package from CDN
233
+ * @param packageName Package name (e.g., 'three', 'lodash', '@tensorflow/tfjs')
234
+ * @param version Optional version (defaults to 'latest')
235
+ * @returns The loaded module
236
+ */
237
+ async load(packageName, version = "latest") {
238
+ const key = `${packageName}@${version}`;
239
+ if (this.cache.has(key)) {
240
+ return this.cache.get(key);
241
+ }
242
+ if (this.loadingPromises.has(key)) {
243
+ return this.loadingPromises.get(key);
244
+ }
245
+ const loadPromise = this.loadPackage(key, packageName, version);
246
+ this.loadingPromises.set(key, loadPromise);
247
+ try {
248
+ const module = await loadPromise;
249
+ this.cache.set(key, module);
250
+ return module;
251
+ } catch (error) {
252
+ this.loadingPromises.delete(key);
253
+ throw error;
254
+ }
255
+ }
256
+ async loadPackage(key, packageName, version) {
257
+ const versionPart = version === "latest" ? "" : `@${version}`;
258
+ const url = `${this.cdnBaseUrl}/${packageName}${versionPart}`;
259
+ try {
260
+ const module = await import(
261
+ /* @vite-ignore */
262
+ url
263
+ );
264
+ if (module.default && typeof module.default === "object") {
265
+ return module.default;
266
+ }
267
+ if (module.default !== void 0) {
268
+ return module.default;
269
+ }
270
+ return module;
271
+ } catch (error) {
272
+ throw new Error(
273
+ `Failed to load package ${key}: ${error.message || "Unknown error"}
274
+ URL: ${url}`
275
+ );
276
+ }
277
+ }
278
+ /**
279
+ * Search for packages in the NPM registry
280
+ * @param query Search query
281
+ * @param limit Maximum number of results (default: 20)
282
+ * @returns Array of package search results
283
+ */
284
+ async search(query, limit = 20) {
285
+ try {
286
+ const url = `https://registry.npmjs.org/-/v1/search?text=${encodeURIComponent(query)}&size=${limit}`;
287
+ const response = await fetch(url, {
288
+ method: "GET",
289
+ headers: {
290
+ "Accept": "application/json"
291
+ }
292
+ });
293
+ if (!response.ok) {
294
+ if (response.status === 0 || response.status === 403 || response.status === 404) {
295
+ console.warn("NPM registry search may be blocked by CORS. Using fallback.");
296
+ return [];
297
+ }
298
+ throw new Error(`Search failed: ${response.statusText}`);
299
+ }
300
+ const data = await response.json();
301
+ return data.objects?.map((result) => ({
302
+ name: result.package.name,
303
+ version: result.package.version,
304
+ description: result.package.description,
305
+ keywords: result.package.keywords || [],
306
+ author: result.package.author,
307
+ homepage: result.package.links?.homepage,
308
+ repository: result.package.links?.repository ? { url: result.package.links.repository } : void 0
309
+ })) || [];
310
+ } catch (error) {
311
+ if (error.message?.includes("CORS") || error.message?.includes("Failed to fetch")) {
312
+ console.warn("NPM registry search unavailable:", error.message);
313
+ return [];
314
+ }
315
+ throw new Error(`Failed to search packages: ${error.message || "Unknown error"}`);
316
+ }
317
+ }
318
+ /**
319
+ * Get package information from NPM registry
320
+ * @param packageName Package name
321
+ * @returns Package metadata
322
+ */
323
+ async getPackageInfo(packageName) {
324
+ try {
325
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
326
+ const response = await fetch(url);
327
+ if (!response.ok) {
328
+ if (response.status === 404) {
329
+ return null;
330
+ }
331
+ throw new Error(`Failed to fetch package info: ${response.statusText}`);
332
+ }
333
+ const data = await response.json();
334
+ const latestVersion = data["dist-tags"]?.latest || Object.keys(data.versions).pop();
335
+ const versionData = data.versions[latestVersion];
336
+ return {
337
+ name: data.name,
338
+ version: latestVersion,
339
+ description: versionData.description,
340
+ keywords: versionData.keywords || [],
341
+ author: versionData.author,
342
+ homepage: data.homepage || versionData.homepage,
343
+ repository: versionData.repository
344
+ };
345
+ } catch (error) {
346
+ throw new Error(`Failed to get package info: ${error.message || "Unknown error"}`);
347
+ }
348
+ }
349
+ /**
350
+ * Clear the package cache
351
+ */
352
+ clearCache() {
353
+ this.cache.clear();
354
+ this.loadingPromises.clear();
355
+ }
356
+ /**
357
+ * Remove a specific package from cache
358
+ */
359
+ removeFromCache(packageName, version) {
360
+ if (version) {
361
+ const key = `${packageName}@${version}`;
362
+ this.cache.delete(key);
363
+ this.loadingPromises.delete(key);
364
+ } else {
365
+ for (const key of this.cache.keys()) {
366
+ if (key.startsWith(`${packageName}@`)) {
367
+ this.cache.delete(key);
368
+ this.loadingPromises.delete(key);
369
+ }
370
+ }
371
+ }
372
+ }
373
+ /**
374
+ * Get list of cached packages
375
+ */
376
+ getCachedPackages() {
377
+ return Array.from(this.cache.keys());
378
+ }
379
+ };
380
+
381
+ // src/engine/ModuleResolver.ts
382
+ var ModuleResolver = class {
383
+ constructor(config) {
384
+ this.config = config;
385
+ }
386
+ /**
387
+ * Set file system functions for reading external files
388
+ * These are injected to allow different implementations (browser vs Node.js)
389
+ */
390
+ setFileSystemFunctions(readFn, existsFn) {
391
+ this.fileReadFn = readFn;
392
+ this.fileExistsFn = existsFn;
393
+ }
394
+ /**
395
+ * Resolve a module path to its source code and metadata
396
+ */
397
+ async resolve(modulePath) {
398
+ if (modulePath.startsWith("cascade.")) {
399
+ return this.resolveStdlib(modulePath);
400
+ }
401
+ if (modulePath.startsWith("local.")) {
402
+ return this.resolveEmbedded(modulePath);
403
+ }
404
+ for (const pkg of this.config.projectPackages) {
405
+ if (modulePath.startsWith(`${pkg.alias}.`)) {
406
+ return this.resolveProject(modulePath, pkg);
407
+ }
408
+ }
409
+ if (this.config.embeddedModules.has(modulePath)) {
410
+ return this.resolveEmbedded(modulePath);
411
+ }
412
+ return null;
413
+ }
414
+ /**
415
+ * Resolve a standard library module
416
+ * Class-based stdlib nodes don't have editable code
417
+ */
418
+ resolveStdlib(modulePath) {
419
+ const NodeClass = getNodeClass(modulePath);
420
+ if (!NodeClass) {
421
+ return null;
422
+ }
423
+ const source = {
424
+ type: "stdlib",
425
+ module: modulePath
426
+ };
427
+ const parts = modulePath.split(".");
428
+ const displayPath = `<cascade>/${parts.slice(1).join("/")}.ts`;
429
+ return {
430
+ source,
431
+ code: "",
432
+ // Class-based nodes don't have editable code
433
+ displayPath,
434
+ editable: false
435
+ };
436
+ }
437
+ /**
438
+ * Resolve an embedded module
439
+ */
440
+ resolveEmbedded(modulePath) {
441
+ const embedded = this.config.embeddedModules.get(modulePath);
442
+ if (!embedded) {
443
+ return null;
444
+ }
445
+ const source = {
446
+ type: "embedded",
447
+ module: modulePath,
448
+ code: embedded.code,
449
+ history: embedded.history || []
450
+ };
451
+ return {
452
+ source,
453
+ code: embedded.code,
454
+ displayPath: "<embedded>",
455
+ editable: true
456
+ };
457
+ }
458
+ /**
459
+ * Resolve a project module from an external file
460
+ */
461
+ async resolveProject(modulePath, pkg) {
462
+ const relativeParts = modulePath.replace(`${pkg.alias}.`, "").split(".");
463
+ const filePath = `${pkg.path}/${relativeParts.join("/")}.ts`;
464
+ const external = this.config.externalModules.get(modulePath);
465
+ let code = null;
466
+ let status = "synced";
467
+ if (this.fileReadFn && this.fileExistsFn) {
468
+ const fullPath = this.resolvePath(filePath);
469
+ const exists = await this.fileExistsFn(fullPath);
470
+ if (exists) {
471
+ code = await this.fileReadFn(fullPath);
472
+ if (code !== null) {
473
+ if (external && external.cachedCode !== code) {
474
+ status = "modified-external";
475
+ }
476
+ }
477
+ } else {
478
+ status = "missing";
479
+ code = external?.cachedCode || null;
480
+ }
481
+ } else if (external) {
482
+ code = external.cachedCode;
483
+ }
484
+ if (code === null) {
485
+ return null;
486
+ }
487
+ const source = {
488
+ type: "project",
489
+ module: modulePath,
490
+ file: filePath,
491
+ cachedCode: code,
492
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
493
+ status
494
+ };
495
+ return {
496
+ source,
497
+ code,
498
+ displayPath: filePath,
499
+ editable: true
500
+ };
501
+ }
502
+ /**
503
+ * Resolve a relative path to an absolute path
504
+ */
505
+ resolvePath(relativePath) {
506
+ if (!this.config.basePath) {
507
+ return relativePath;
508
+ }
509
+ if (relativePath.startsWith("./")) {
510
+ return `${this.config.basePath}/${relativePath.slice(2)}`;
511
+ }
512
+ if (relativePath.startsWith("/")) {
513
+ return relativePath;
514
+ }
515
+ return `${this.config.basePath}/${relativePath}`;
516
+ }
517
+ /**
518
+ * Determine the source type for a given module path
519
+ */
520
+ getSourceType(modulePath) {
521
+ if (modulePath.startsWith("cascade.")) {
522
+ return "stdlib";
523
+ }
524
+ if (modulePath.startsWith("local.")) {
525
+ return "embedded";
526
+ }
527
+ for (const pkg of this.config.projectPackages) {
528
+ if (modulePath.startsWith(`${pkg.alias}.`)) {
529
+ return "project";
530
+ }
531
+ }
532
+ if (this.config.embeddedModules.has(modulePath)) {
533
+ return "embedded";
534
+ }
535
+ return null;
536
+ }
537
+ /**
538
+ * Create a new embedded module with the given code
539
+ * Use this when creating custom nodes from scratch
540
+ */
541
+ createEmbeddedModule(modulePath, code, author = "user") {
542
+ const now = (/* @__PURE__ */ new Date()).toISOString();
543
+ const initialVersion = {
544
+ code,
545
+ timestamp: now,
546
+ author
547
+ };
548
+ const module = {
549
+ code,
550
+ history: [initialVersion],
551
+ created: now,
552
+ modified: now
553
+ };
554
+ this.config.embeddedModules.set(modulePath, module);
555
+ return { modulePath, module };
556
+ }
557
+ /**
558
+ * Create a new embedded module from stdlib (duplicate)
559
+ */
560
+ duplicateToEmbedded(stdlibPath, newName) {
561
+ const resolution = this.resolveStdlib(stdlibPath);
562
+ if (!resolution) {
563
+ return null;
564
+ }
565
+ const typeName = stdlibPath.split(".").pop() || "Custom";
566
+ const baseName = newName || typeName;
567
+ let modulePath = `local.${baseName}`;
568
+ let counter = 1;
569
+ while (this.config.embeddedModules.has(modulePath)) {
570
+ modulePath = `local.${baseName}${counter}`;
571
+ counter++;
572
+ }
573
+ const now = (/* @__PURE__ */ new Date()).toISOString();
574
+ const initialVersion = {
575
+ code: resolution.code,
576
+ timestamp: now,
577
+ author: "user"
578
+ };
579
+ const module = {
580
+ code: resolution.code,
581
+ history: [initialVersion],
582
+ created: now,
583
+ modified: now
584
+ };
585
+ this.config.embeddedModules.set(modulePath, module);
586
+ return { modulePath, module };
587
+ }
588
+ /**
589
+ * Create an external file from an embedded module (extract)
590
+ */
591
+ async extractToProject(embeddedPath, pkg, fileName) {
592
+ const embedded = this.config.embeddedModules.get(embeddedPath);
593
+ if (!embedded) {
594
+ return null;
595
+ }
596
+ const filePath = `${pkg.path}/${fileName}.ts`;
597
+ const modulePath = `${pkg.alias}.${fileName.replace(/\//g, ".")}`;
598
+ const external = {
599
+ file: filePath,
600
+ cachedCode: embedded.code,
601
+ lastSync: (/* @__PURE__ */ new Date()).toISOString()
602
+ };
603
+ this.config.externalModules.set(modulePath, external);
604
+ return { modulePath, filePath };
605
+ }
606
+ /**
607
+ * Update embedded module code and record in history
608
+ */
609
+ updateEmbeddedCode(modulePath, newCode, author = "user", prompt) {
610
+ const embedded = this.config.embeddedModules.get(modulePath);
611
+ if (!embedded) {
612
+ return false;
613
+ }
614
+ const now = (/* @__PURE__ */ new Date()).toISOString();
615
+ const version = {
616
+ code: newCode,
617
+ timestamp: now,
618
+ author,
619
+ prompt
620
+ };
621
+ embedded.history.push(version);
622
+ embedded.code = newCode;
623
+ embedded.modified = now;
624
+ return true;
625
+ }
626
+ /**
627
+ * Restore embedded module to a previous version
628
+ */
629
+ restoreVersion(modulePath, versionIndex) {
630
+ const embedded = this.config.embeddedModules.get(modulePath);
631
+ if (!embedded || versionIndex < 0 || versionIndex >= embedded.history.length) {
632
+ return false;
633
+ }
634
+ const version = embedded.history[versionIndex];
635
+ embedded.code = version.code;
636
+ embedded.modified = (/* @__PURE__ */ new Date()).toISOString();
637
+ embedded.history.push({
638
+ code: version.code,
639
+ timestamp: embedded.modified,
640
+ author: "user"
641
+ });
642
+ return true;
643
+ }
644
+ /**
645
+ * Get all registered project packages
646
+ */
647
+ getProjectPackages() {
648
+ return [...this.config.projectPackages];
649
+ }
650
+ /**
651
+ * Add a project package
652
+ */
653
+ addProjectPackage(pkg) {
654
+ const existing = this.config.projectPackages.find((p) => p.alias === pkg.alias);
655
+ if (existing) {
656
+ existing.path = pkg.path;
657
+ } else {
658
+ this.config.projectPackages.push(pkg);
659
+ }
660
+ }
661
+ /**
662
+ * Remove a project package
663
+ */
664
+ removeProjectPackage(alias) {
665
+ const index = this.config.projectPackages.findIndex((p) => p.alias === alias);
666
+ if (index >= 0) {
667
+ this.config.projectPackages.splice(index, 1);
668
+ return true;
669
+ }
670
+ return false;
671
+ }
672
+ /**
673
+ * Sync an external module with its file
674
+ */
675
+ async syncExternalModule(modulePath) {
676
+ const external = this.config.externalModules.get(modulePath);
677
+ if (!external || !this.fileReadFn || !this.fileExistsFn) {
678
+ return "missing";
679
+ }
680
+ const fullPath = this.resolvePath(external.file);
681
+ const exists = await this.fileExistsFn(fullPath);
682
+ if (!exists) {
683
+ return "missing";
684
+ }
685
+ const code = await this.fileReadFn(fullPath);
686
+ if (code === null) {
687
+ return "missing";
688
+ }
689
+ external.cachedCode = code;
690
+ external.lastSync = (/* @__PURE__ */ new Date()).toISOString();
691
+ return "synced";
692
+ }
693
+ /**
694
+ * Export configuration for serialization
695
+ */
696
+ exportConfig() {
697
+ return {
698
+ projectPackages: [...this.config.projectPackages],
699
+ embeddedModules: Object.fromEntries(this.config.embeddedModules),
700
+ externalModules: Object.fromEntries(this.config.externalModules)
701
+ };
702
+ }
703
+ /**
704
+ * Import configuration from deserialization
705
+ */
706
+ importConfig(config) {
707
+ if (config.projectPackages) {
708
+ this.config.projectPackages = [...config.projectPackages];
709
+ }
710
+ if (config.embeddedModules) {
711
+ this.config.embeddedModules = new Map(Object.entries(config.embeddedModules));
712
+ }
713
+ if (config.externalModules) {
714
+ this.config.externalModules = new Map(Object.entries(config.externalModules));
715
+ }
716
+ }
717
+ };
718
+ function createModuleResolver(basePath) {
719
+ return new ModuleResolver({
720
+ projectPackages: [],
721
+ embeddedModules: /* @__PURE__ */ new Map(),
722
+ externalModules: /* @__PURE__ */ new Map(),
723
+ basePath
724
+ });
725
+ }
726
+
727
+ // src/engine/GraphValidator.ts
728
+ var GraphValidator = class {
729
+ /**
730
+ * Detect cycles in the graph using DFS
731
+ * Returns array of node IDs involved in cycles
732
+ */
733
+ static detectCycles(graph) {
734
+ const cycles = [];
735
+ const visited = /* @__PURE__ */ new Set();
736
+ const recStack = /* @__PURE__ */ new Set();
737
+ const path = [];
738
+ const dfs = (nodeId) => {
739
+ if (recStack.has(nodeId)) {
740
+ const cycleStart = path.indexOf(nodeId);
741
+ if (cycleStart !== -1) {
742
+ cycles.push([...path.slice(cycleStart), nodeId]);
743
+ }
744
+ return true;
745
+ }
746
+ if (visited.has(nodeId)) {
747
+ return false;
748
+ }
749
+ visited.add(nodeId);
750
+ recStack.add(nodeId);
751
+ path.push(nodeId);
752
+ const node = graph.getNode(nodeId);
753
+ if (node) {
754
+ for (const output of node.outputs) {
755
+ for (const conn of output.connections) {
756
+ dfs(conn.to.nodeId);
757
+ }
758
+ }
759
+ }
760
+ recStack.delete(nodeId);
761
+ path.pop();
762
+ return false;
763
+ };
764
+ const nodes = graph.nodes;
765
+ for (const node of nodes) {
766
+ if (!visited.has(node.id)) {
767
+ dfs(node.id);
768
+ }
769
+ }
770
+ return cycles;
771
+ }
772
+ /**
773
+ * Check if adding a connection would create a cycle
774
+ */
775
+ static wouldCreateCycle(graph, fromNodeId, toNodeId) {
776
+ if (fromNodeId === toNodeId) {
777
+ return true;
778
+ }
779
+ const visited = /* @__PURE__ */ new Set();
780
+ const hasPath = (startId, targetId) => {
781
+ if (startId === targetId) return true;
782
+ if (visited.has(startId)) return false;
783
+ visited.add(startId);
784
+ const node = graph.getNode(startId);
785
+ if (!node) return false;
786
+ for (const output of node.outputs) {
787
+ for (const conn of output.connections) {
788
+ if (hasPath(conn.to.nodeId, targetId)) {
789
+ return true;
790
+ }
791
+ }
792
+ }
793
+ return false;
794
+ };
795
+ return hasPath(toNodeId, fromNodeId);
796
+ }
797
+ /**
798
+ * Perform topological sort of nodes
799
+ * Returns sorted node IDs, or throws if cycles exist
800
+ */
801
+ static topologicalSort(graph) {
802
+ const cycles = this.detectCycles(graph);
803
+ if (cycles.length > 0) {
804
+ throw new Error(
805
+ `Graph contains cycles: ${cycles.map((c) => c.join(" -> ")).join(", ")}`
806
+ );
807
+ }
808
+ const sorted = [];
809
+ const visited = /* @__PURE__ */ new Set();
810
+ const tempMark = /* @__PURE__ */ new Set();
811
+ const visit = (nodeId) => {
812
+ if (tempMark.has(nodeId)) {
813
+ throw new Error(`Cycle detected involving node ${nodeId}`);
814
+ }
815
+ if (visited.has(nodeId)) {
816
+ return;
817
+ }
818
+ tempMark.add(nodeId);
819
+ const node = graph.getNode(nodeId);
820
+ if (node) {
821
+ for (const input of node.inputs) {
822
+ for (const conn of input.connections) {
823
+ visit(conn.from.nodeId);
824
+ }
825
+ }
826
+ }
827
+ tempMark.delete(nodeId);
828
+ visited.add(nodeId);
829
+ sorted.push(nodeId);
830
+ };
831
+ const nodes = graph.nodes;
832
+ for (const node of nodes) {
833
+ if (!visited.has(node.id)) {
834
+ visit(node.id);
835
+ }
836
+ }
837
+ return sorted;
838
+ }
839
+ /**
840
+ * Validate a connection before it's added
841
+ */
842
+ static validateConnection(graph, fromNodeId, toNodeId, fromPortId, toPortId) {
843
+ const fromNode = graph.getNode(fromNodeId);
844
+ const toNode = graph.getNode(toNodeId);
845
+ if (!fromNode || !toNode) {
846
+ return {
847
+ type: "invalid_connection",
848
+ message: `Invalid connection: node not found`,
849
+ nodeIds: [fromNodeId, toNodeId]
850
+ };
851
+ }
852
+ const fromPort = fromNode.outputs.find((p) => p.id === fromPortId);
853
+ const toPort = toNode.inputs.find((p) => p.id === toPortId);
854
+ if (!fromPort || !toPort) {
855
+ return {
856
+ type: "invalid_connection",
857
+ message: `Invalid connection: port not found`,
858
+ nodeIds: [fromNodeId, toNodeId]
859
+ };
860
+ }
861
+ if (fromPort.portType !== toPort.portType) {
862
+ return {
863
+ type: "invalid_connection",
864
+ message: `Port type mismatch: cannot connect ${fromPort.portType} to ${toPort.portType}`,
865
+ nodeIds: [fromNodeId, toNodeId]
866
+ };
867
+ }
868
+ if (this.wouldCreateCycle(graph, fromNodeId, toNodeId)) {
869
+ return {
870
+ type: "cycle",
871
+ message: `Connection would create a cycle: ${fromNodeId} -> ${toNodeId}`,
872
+ nodeIds: [fromNodeId, toNodeId]
873
+ };
874
+ }
875
+ return null;
876
+ }
877
+ /**
878
+ * Validate the entire graph
879
+ */
880
+ static validateGraph(graph) {
881
+ const errors = [];
882
+ const warnings = [];
883
+ const cycles = this.detectCycles(graph);
884
+ if (cycles.length > 0) {
885
+ cycles.forEach((cycle) => {
886
+ errors.push({
887
+ type: "cycle",
888
+ message: `Cycle detected: ${cycle.join(" -> ")}`,
889
+ nodeIds: cycle
890
+ });
891
+ });
892
+ }
893
+ for (const conn of graph.connections) {
894
+ const fromNode = graph.getNode(conn.from.nodeId);
895
+ const toNode = graph.getNode(conn.to.nodeId);
896
+ if (!fromNode || !toNode) {
897
+ errors.push({
898
+ type: "invalid_connection",
899
+ message: `Connection references non-existent node`,
900
+ connectionId: conn.id,
901
+ nodeIds: [conn.from.nodeId, conn.to.nodeId]
902
+ });
903
+ continue;
904
+ }
905
+ const fromPort = fromNode.outputs.find((p) => p.id === conn.from.portId);
906
+ const toPort = toNode.inputs.find((p) => p.id === conn.to.portId);
907
+ if (!fromPort || !toPort) {
908
+ errors.push({
909
+ type: "invalid_connection",
910
+ message: `Connection references non-existent port`,
911
+ connectionId: conn.id,
912
+ nodeIds: [conn.from.nodeId, conn.to.nodeId]
913
+ });
914
+ continue;
915
+ }
916
+ if (fromPort.portType !== toPort.portType) {
917
+ errors.push({
918
+ type: "invalid_connection",
919
+ message: `Port type mismatch in connection`,
920
+ connectionId: conn.id,
921
+ nodeIds: [conn.from.nodeId, conn.to.nodeId]
922
+ });
923
+ }
924
+ }
925
+ const connectedNodes = /* @__PURE__ */ new Set();
926
+ for (const conn of graph.connections) {
927
+ connectedNodes.add(conn.from.nodeId);
928
+ connectedNodes.add(conn.to.nodeId);
929
+ }
930
+ const nodes = graph.nodes;
931
+ const orphanedNodes = nodes.filter((n) => {
932
+ const hasNoConnections = !connectedNodes.has(n.id);
933
+ const isEntryPoint = n.inputs.every((p) => p.connections.length === 0);
934
+ return hasNoConnections && !isEntryPoint;
935
+ });
936
+ if (orphanedNodes.length > 0) {
937
+ warnings.push({
938
+ type: "orphaned_node",
939
+ message: `Found ${orphanedNodes.length} orphaned node(s) with no connections`,
940
+ nodeIds: orphanedNodes.map((n) => n.id)
941
+ });
942
+ }
943
+ const entryPoints = nodes.filter(
944
+ (node) => node.inputs.every((p) => p.connections.length === 0)
945
+ );
946
+ if (entryPoints.length === 0 && nodes.length > 0) {
947
+ warnings.push({
948
+ type: "invalid_entry_point",
949
+ message: "No entry points found (all nodes have input connections)"
950
+ });
951
+ }
952
+ return {
953
+ valid: errors.length === 0,
954
+ errors,
955
+ warnings
956
+ };
957
+ }
958
+ };
959
+
960
+ // src/nodes/CookScheduler.ts
961
+ var COOK_SIGNAL_KEY = "__cascadeCookSignal";
962
+ var CookScheduler = class {
963
+ constructor(graph, debounceMs = 50) {
964
+ this.graph = graph;
965
+ this.debounceMs = debounceMs;
966
+ this.timer = null;
967
+ this.activeRun = null;
968
+ this.rerunRequested = false;
969
+ this.listeners = /* @__PURE__ */ new Set();
970
+ this.idleResolvers = /* @__PURE__ */ new Set();
971
+ this.backgroundAbort = null;
972
+ /** Set when something is invalidated while a run is active: the pass in
973
+ * progress stops after the node it is on, so the edit is cooked next rather
974
+ * than after everything that happened to be queued before it. */
975
+ this.invalidatedDuringRun = false;
976
+ /** Nodes on display, by id, counted because two Viewers can show one node. */
977
+ this.watched = /* @__PURE__ */ new Map();
978
+ this._status = {
979
+ phase: "idle",
980
+ pass: 0,
981
+ superseded: 0,
982
+ total: 0,
983
+ completed: 0,
984
+ currentNode: null,
985
+ startedAt: null,
986
+ elapsed: 0
987
+ };
988
+ }
989
+ get status() {
990
+ return this._status;
991
+ }
992
+ get isRunning() {
993
+ return this.activeRun !== null;
994
+ }
995
+ subscribe(listener) {
996
+ this.listeners.add(listener);
997
+ listener(this._status);
998
+ return () => this.listeners.delete(listener);
999
+ }
1000
+ /**
1001
+ * Declare that a node is on display, so it and everything it depends on cook
1002
+ * before anything else in a pass. Returns the release.
1003
+ *
1004
+ * Viewer-first order: a pass used to run strictly in topological order, so a
1005
+ * sink nobody was looking at (an export, a plot plan) could sit in front of
1006
+ * the node on screen. With a node watched, its upstream closure goes first
1007
+ * and the unwatched remainder runs after it in the same pass, still in
1008
+ * topological order. The closure is upstream-closed, so the order stays
1009
+ * valid. With nothing watched — the CLI, a test, cook-all — nothing changes.
1010
+ */
1011
+ watch(node) {
1012
+ const id = node.id;
1013
+ this.watched.set(id, (this.watched.get(id) ?? 0) + 1);
1014
+ let released = false;
1015
+ return () => {
1016
+ if (released) return;
1017
+ released = true;
1018
+ const count = (this.watched.get(id) ?? 1) - 1;
1019
+ if (count <= 0) this.watched.delete(id);
1020
+ else this.watched.set(id, count);
1021
+ };
1022
+ }
1023
+ /** The ids of nodes currently on display. */
1024
+ get watchedNodeIds() {
1025
+ return new Set(this.watched.keys());
1026
+ }
1027
+ /** @internal Called by Node whenever its sole cook state changes. */
1028
+ notifyNodeStateChange() {
1029
+ this.emit();
1030
+ }
1031
+ markStale(node, origin = "self") {
1032
+ if (origin === "self") node.markSelfDirty();
1033
+ const visited = /* @__PURE__ */ new Set();
1034
+ const visit = (candidate) => {
1035
+ if (visited.has(candidate.id)) return;
1036
+ visited.add(candidate.id);
1037
+ if (candidate.cookState !== "stale") candidate.invalidateCook();
1038
+ for (const output of candidate.outputs) {
1039
+ for (const connection of output.connections) {
1040
+ const downstream = this.graph.getNode(connection.to.nodeId);
1041
+ if (downstream) visit(downstream);
1042
+ }
1043
+ }
1044
+ };
1045
+ visit(node);
1046
+ if (this.activeRun) this.invalidatedDuringRun = true;
1047
+ if (this.graph.getNode(node.id) === node) this.schedule();
1048
+ }
1049
+ markDownstreamStale(node) {
1050
+ for (const output of node.outputs) {
1051
+ for (const connection of output.connections) {
1052
+ const downstream = this.graph.getNode(connection.to.nodeId);
1053
+ if (downstream) this.markStale(downstream, "upstream");
1054
+ }
1055
+ }
1056
+ }
1057
+ schedule() {
1058
+ this.preemptBackground("interactive work arrived");
1059
+ if (this.activeRun) {
1060
+ this.rerunRequested = true;
1061
+ return;
1062
+ }
1063
+ if (this.timer) clearTimeout(this.timer);
1064
+ const total = this.graph.nodes.filter((node) => node.cookState === "stale" || node.cookState === "queued").length;
1065
+ this.setStatus({ phase: "scheduled", total, completed: 0, currentNode: null, pass: 0, superseded: 0, startedAt: null, elapsed: 0 });
1066
+ this.timer = setTimeout(() => {
1067
+ this.timer = null;
1068
+ void this.flushScheduled();
1069
+ }, this.debounceMs);
1070
+ }
1071
+ async flush(entryNode) {
1072
+ await this.flushInternal(entryNode, true);
1073
+ }
1074
+ async flushScheduled() {
1075
+ await this.flushInternal(void 0, false);
1076
+ }
1077
+ async flushInternal(entryNode, retryErrors) {
1078
+ this.preemptBackground("interactive work arrived");
1079
+ if (this.timer) {
1080
+ clearTimeout(this.timer);
1081
+ this.timer = null;
1082
+ }
1083
+ if (retryErrors) this.prepareErrorRetry(entryNode);
1084
+ if (this.activeRun) {
1085
+ this.rerunRequested = true;
1086
+ await this.activeRun;
1087
+ return;
1088
+ }
1089
+ this.activeRun = this.run(entryNode).finally(() => {
1090
+ this.activeRun = null;
1091
+ });
1092
+ await this.activeRun;
1093
+ }
1094
+ /** Explicit execution retries failed nodes once; debounced invalidation does not. */
1095
+ prepareErrorRetry(entryNode) {
1096
+ const allowed = entryNode ? this.collectDownstream(entryNode) : null;
1097
+ for (const node of this.graph.nodes) {
1098
+ if (node.cookState === "error" && (!allowed || allowed.has(node.id))) {
1099
+ node.invalidateCook();
1100
+ }
1101
+ }
1102
+ }
1103
+ async whenIdle() {
1104
+ if (!this.activeRun && !this.timer) return;
1105
+ await new Promise((resolve) => this.idleResolvers.add(resolve));
1106
+ }
1107
+ dispose() {
1108
+ this.preemptBackground("the graph went away");
1109
+ if (this.timer) clearTimeout(this.timer);
1110
+ this.timer = null;
1111
+ this.listeners.clear();
1112
+ for (const resolve of this.idleResolvers) resolve();
1113
+ this.idleResolvers.clear();
1114
+ }
1115
+ async run(entryNode) {
1116
+ let restrictTo = entryNode ? this.collectDownstream(entryNode) : null;
1117
+ let pass = 0;
1118
+ let supersededCount = 0;
1119
+ let cutOffCount = 0;
1120
+ do {
1121
+ this.rerunRequested = false;
1122
+ this.invalidatedDuringRun = false;
1123
+ const stale = this.graph.nodes.filter(
1124
+ (node) => (node.cookState === "stale" || node.cookState === "queued") && (!restrictTo || restrictTo.has(node.id))
1125
+ );
1126
+ restrictTo = null;
1127
+ if (stale.length === 0) break;
1128
+ const staleIds = new Set(stale.map((node) => node.id));
1129
+ const ordered = this.graph.getTopologicalOrder().map((id) => this.graph.getNode(id)).filter((node) => !!node && staleIds.has(node.id));
1130
+ const orderedIds = new Set(ordered.map((node) => node.id));
1131
+ for (const node of stale) if (!orderedIds.has(node.id)) ordered.push(node);
1132
+ this.viewerFirst(ordered);
1133
+ for (const node of ordered) node.setCookState("queued");
1134
+ const startedAt = performance.now();
1135
+ pass += 1;
1136
+ this.setStatus({
1137
+ phase: "cooking",
1138
+ total: ordered.length,
1139
+ completed: 0,
1140
+ currentNode: null,
1141
+ pass,
1142
+ superseded: supersededCount,
1143
+ startedAt,
1144
+ elapsed: 0
1145
+ });
1146
+ const blocked = /* @__PURE__ */ new Set();
1147
+ const notReady = this.graph.pendingConnectionTargets();
1148
+ let superseded = false;
1149
+ for (const node of ordered) {
1150
+ if (notReady.has(node.id)) {
1151
+ node.setCookState("stale");
1152
+ blocked.add(node.id);
1153
+ this.setStatus({
1154
+ ...this._status,
1155
+ completed: this._status.completed + 1,
1156
+ currentNode: null,
1157
+ elapsed: performance.now() - startedAt
1158
+ });
1159
+ continue;
1160
+ }
1161
+ const upstreamBlocked = node.inputs.some((input) => input.connections.some(
1162
+ (connection) => staleIds.has(connection.from.nodeId) && (blocked.has(connection.from.nodeId) || this.graph.getNode(connection.from.nodeId)?.cookState === "error")
1163
+ ));
1164
+ if (upstreamBlocked) {
1165
+ node.setCookState("stale");
1166
+ blocked.add(node.id);
1167
+ this.setStatus({
1168
+ ...this._status,
1169
+ completed: this._status.completed + 1,
1170
+ currentNode: null,
1171
+ elapsed: performance.now() - startedAt
1172
+ });
1173
+ continue;
1174
+ }
1175
+ if (node.canSkipCook()) {
1176
+ node.skipCook();
1177
+ cutOffCount += 1;
1178
+ this.setStatus({
1179
+ ...this._status,
1180
+ completed: this._status.completed + 1,
1181
+ currentNode: null,
1182
+ cutOff: cutOffCount,
1183
+ elapsed: performance.now() - startedAt
1184
+ });
1185
+ continue;
1186
+ }
1187
+ node.setCookState("cooking");
1188
+ this.setStatus({ ...this._status, currentNode: node, elapsed: performance.now() - startedAt });
1189
+ await this.executeInteractive(node);
1190
+ if (node.cookState === "error") blocked.add(node.id);
1191
+ if (node.cookState === "stale") {
1192
+ blocked.add(node.id);
1193
+ superseded = true;
1194
+ supersededCount += 1;
1195
+ }
1196
+ this.setStatus({
1197
+ ...this._status,
1198
+ completed: this._status.completed + 1,
1199
+ currentNode: null,
1200
+ superseded: supersededCount,
1201
+ elapsed: performance.now() - startedAt
1202
+ });
1203
+ if (this.invalidatedDuringRun) {
1204
+ this.rerunRequested = true;
1205
+ break;
1206
+ }
1207
+ }
1208
+ if (superseded) this.rerunRequested = true;
1209
+ } while (this.rerunRequested);
1210
+ this.setStatus({ phase: "idle", total: 0, completed: 0, currentNode: null, pass: 0, superseded: 0, startedAt: null, elapsed: 0 });
1211
+ for (const resolve of this.idleResolvers) resolve();
1212
+ this.idleResolvers.clear();
1213
+ }
1214
+ /**
1215
+ * Cook one node with its signal published as the ambient cook signal, so a
1216
+ * stage it starts can be withdrawn when the cook is superseded even if the
1217
+ * node never passes `context.signal` along. Safe as a single global because
1218
+ * this lane runs one node at a time; restored rather than cleared in case a
1219
+ * host nests a run.
1220
+ */
1221
+ async executeInteractive(node) {
1222
+ const globals = globalThis;
1223
+ const previous = globals[COOK_SIGNAL_KEY];
1224
+ const read = () => node.inFlightCookSignal;
1225
+ globals[COOK_SIGNAL_KEY] = read;
1226
+ try {
1227
+ await node.execute();
1228
+ } finally {
1229
+ if (globals[COOK_SIGNAL_KEY] === read) globals[COOK_SIGNAL_KEY] = previous;
1230
+ }
1231
+ }
1232
+ // ============ The background lane ============
1233
+ /**
1234
+ * Work that must never delay the thing being looked at.
1235
+ *
1236
+ * Marcus's rule for an auto-updating series, 2026-09-09: *"i want the series
1237
+ * to auto-update ... however the single live node has priority."* That cannot
1238
+ * be expressed in the interactive queue, which is one FIFO with no priority
1239
+ * in it — measured by MW-OBSERVATORY-ART as 91 of 91 downstream cooks queued
1240
+ * behind their upstream with zero overlap and nothing ever superseded. A
1241
+ * series auto-updating through that queue would sit in front of the next
1242
+ * edit: twelve instances at ~220 ms each is 2.6 seconds between Marcus and
1243
+ * the slider he is dragging.
1244
+ *
1245
+ * So background is a second lane with three properties, and all three are
1246
+ * load-bearing:
1247
+ *
1248
+ * - it **starts only when the graph is quiet** — `phase === 'idle'` plus a
1249
+ * settling interval, because otherwise a drag becomes a chain of
1250
+ * started-and-cancelled passes, which is worse than not starting;
1251
+ * - it is **abandoned the moment interactive work arrives**, at `schedule`
1252
+ * rather than at the run, so the debounce window is not spent on it;
1253
+ * - it is abandoned **through a signal the work can see**, so an expensive
1254
+ * task can bail before committing rather than after. That is the fault
1255
+ * this lane exists to avoid rather than relocate: an abandoned cook on
1256
+ * `cloud-volumes` had already encoded and uploaded a 1.55 MB PNG before
1257
+ * anything noticed it was superseded.
1258
+ *
1259
+ * **The trap for whoever builds the first client.** Mutating the graph from
1260
+ * inside a background task — which is what applying a parameter set does —
1261
+ * reaches `markStale`, and `markStale` calls `schedule`, which preempts the
1262
+ * background lane. A task that invalidates the graph therefore cancels
1263
+ * itself. The way out is to drive nodes directly rather than through the
1264
+ * interactive queue, not to weaken the preemption; the preemption is the
1265
+ * only thing making the lane honest.
1266
+ */
1267
+ async runBackground(task, options = {}) {
1268
+ this.preemptBackground("superseded by a newer background task");
1269
+ await this.whenSettled(options.settleMs ?? this.debounceMs * 4);
1270
+ const abort = new AbortController();
1271
+ this.backgroundAbort = abort;
1272
+ try {
1273
+ await task(abort.signal);
1274
+ return abort.signal.aborted ? "preempted" : "completed";
1275
+ } finally {
1276
+ if (this.backgroundAbort === abort) this.backgroundAbort = null;
1277
+ }
1278
+ }
1279
+ /** Whether background work is in flight. Distinct from `isRunning`, which is
1280
+ * the interactive lane — a panel showing one as the other would report the
1281
+ * series as though Marcus's own edit were still cooking. */
1282
+ get isRunningBackground() {
1283
+ return this.backgroundAbort !== null;
1284
+ }
1285
+ /** @internal Abandon the background lane. Public so a client can stand down
1286
+ * without waiting for an invalidation to do it. */
1287
+ preemptBackground(reason) {
1288
+ const abort = this.backgroundAbort;
1289
+ if (!abort) return;
1290
+ this.backgroundAbort = null;
1291
+ if (!abort.signal.aborted) abort.abort(new Error(reason));
1292
+ }
1293
+ /**
1294
+ * Resolve once the graph has been idle for `settleMs` without interruption.
1295
+ *
1296
+ * Deliberately not `whenIdle()` plus a timer: `whenIdle` resolves the instant
1297
+ * the queue drains, and a drag drains it between every keystroke. The
1298
+ * settling interval is what turns *momentarily idle* into *finished*, and it
1299
+ * restarts whenever work reappears — so under sustained invalidation this
1300
+ * never resolves, which is correct. Background work has no business starting
1301
+ * while the graph is still moving.
1302
+ */
1303
+ whenSettled(settleMs) {
1304
+ return new Promise((resolve) => {
1305
+ let timer = null;
1306
+ const stop = this.subscribe((status) => {
1307
+ if (status.phase === "idle") {
1308
+ if (timer === null) {
1309
+ timer = setTimeout(() => {
1310
+ stop();
1311
+ resolve();
1312
+ }, settleMs);
1313
+ }
1314
+ return;
1315
+ }
1316
+ if (timer !== null) {
1317
+ clearTimeout(timer);
1318
+ timer = null;
1319
+ }
1320
+ });
1321
+ });
1322
+ }
1323
+ /** Stable-partition `ordered` in place: what a watched node needs, first. */
1324
+ viewerFirst(ordered) {
1325
+ if (this.watched.size === 0) return;
1326
+ const needed = /* @__PURE__ */ new Set();
1327
+ const visit = (id) => {
1328
+ if (needed.has(id)) return;
1329
+ const node = this.graph.getNode(id);
1330
+ if (!node) return;
1331
+ needed.add(id);
1332
+ for (const input of node.inputs) {
1333
+ for (const connection of input.connections) visit(connection.from.nodeId);
1334
+ }
1335
+ };
1336
+ for (const id of this.watched.keys()) visit(id);
1337
+ if (needed.size === 0) return;
1338
+ const first = ordered.filter((node) => needed.has(node.id));
1339
+ if (first.length === 0 || first.length === ordered.length) return;
1340
+ const rest = ordered.filter((node) => !needed.has(node.id));
1341
+ ordered.splice(0, ordered.length, ...first, ...rest);
1342
+ }
1343
+ collectDownstream(entryNode) {
1344
+ const result = /* @__PURE__ */ new Set();
1345
+ const visit = (node) => {
1346
+ if (result.has(node.id)) return;
1347
+ result.add(node.id);
1348
+ for (const output of node.outputs) {
1349
+ for (const connection of output.connections) {
1350
+ const downstream = this.graph.getNode(connection.to.nodeId);
1351
+ if (downstream) visit(downstream);
1352
+ }
1353
+ }
1354
+ };
1355
+ visit(entryNode);
1356
+ return result;
1357
+ }
1358
+ setStatus(status) {
1359
+ this._status = Object.freeze(status);
1360
+ this.emit();
1361
+ }
1362
+ emit() {
1363
+ for (const listener of this.listeners) listener(this._status);
1364
+ }
1365
+ };
1366
+
1367
+ // src/nodes/Graph.ts
1368
+ var GRAPH_FORMAT_VERSION = "0.2";
1369
+ function parsePortId(portId) {
1370
+ const parts = portId.split("_");
1371
+ if (parts.length < 3) {
1372
+ throw new Error(`Invalid port ID format: ${portId}`);
1373
+ }
1374
+ const portType = parts[parts.length - 2];
1375
+ const index = parseInt(parts[parts.length - 1], 10);
1376
+ if (isNaN(index)) {
1377
+ throw new Error(`Invalid port index in port ID: ${portId}`);
1378
+ }
1379
+ const elementId = parts.slice(0, -2).join("_");
1380
+ return { elementId, portType, index };
1381
+ }
1382
+ function createPortId(elementId, portType, index) {
1383
+ return `${elementId}_${portType}_${index}`;
1384
+ }
1385
+ var Graph = class _Graph {
1386
+ constructor(assetManager, packageManager, moduleResolver) {
1387
+ this._elements = [];
1388
+ this._elementMap = /* @__PURE__ */ new Map();
1389
+ // O(1) lookup cache
1390
+ this.connections = [];
1391
+ this._connectionMap = /* @__PURE__ */ new Map();
1392
+ // O(1) connection lookup
1393
+ this.connectionIdCounter = 0;
1394
+ // Project configuration (v0.2)
1395
+ this.project = { packages: [] };
1396
+ this.createdAt = (/* @__PURE__ */ new Date()).toISOString();
1397
+ // Execution Control
1398
+ this.cookingNodes = /* @__PURE__ */ new Set();
1399
+ // Execution state
1400
+ this.executionState = "idle";
1401
+ // Cached topological order (invalidated when graph structure changes)
1402
+ this.cachedTopologicalOrder = null;
1403
+ this.restoringHierarchy = false;
1404
+ /**
1405
+ * Whether the load-time connection restore has stopped making progress.
1406
+ *
1407
+ * Set by whoever drives the warm-up passes — Studio's `cookGraph`, the CLI's
1408
+ * runner — when the pending list stops shrinking. The pending list itself is
1409
+ * kept, so a connection can still bind later if a bypass is lifted or a
1410
+ * module finally compiles; what ends is treating it as a reason to hold a
1411
+ * node back.
1412
+ */
1413
+ this.connectionsSettled = false;
1414
+ this.assetManager = assetManager || new AssetManager();
1415
+ this.packageManager = packageManager || new PackageManager();
1416
+ this.moduleResolver = moduleResolver || createModuleResolver();
1417
+ this.scheduler = new CookScheduler(this);
1418
+ }
1419
+ get created() {
1420
+ return this.createdAt;
1421
+ }
1422
+ /**
1423
+ * Unified elements array (computations and annotations)
1424
+ */
1425
+ get elements() {
1426
+ return this._elements;
1427
+ }
1428
+ set elements(value) {
1429
+ this._elements = value;
1430
+ this._elementMap.clear();
1431
+ for (const el of value) {
1432
+ this._elementMap.set(el.id, el);
1433
+ }
1434
+ }
1435
+ /**
1436
+ * Helper to check if an element is an annotation
1437
+ * Uses marker property for reliability (survives HMR and serialization)
1438
+ */
1439
+ isAnnotation(element) {
1440
+ return element.isAnnotation === true;
1441
+ }
1442
+ /**
1443
+ * Get all computations (non-annotation nodes)
1444
+ */
1445
+ get nodes() {
1446
+ return this._elements.filter((e) => !this.isAnnotation(e));
1447
+ }
1448
+ /**
1449
+ * Set computations (triggers Svelte reactivity)
1450
+ */
1451
+ set nodes(value) {
1452
+ this._elements = [...this._elements.filter((e) => this.isAnnotation(e)), ...value];
1453
+ }
1454
+ /**
1455
+ * Get all annotations
1456
+ */
1457
+ get annotations() {
1458
+ return this._elements.filter((e) => this.isAnnotation(e));
1459
+ }
1460
+ /**
1461
+ * Set annotations (triggers Svelte reactivity)
1462
+ */
1463
+ set annotations(value) {
1464
+ this._elements = [...this._elements.filter((e) => !this.isAnnotation(e)), ...value];
1465
+ }
1466
+ /**
1467
+ * Generates a unique element ID based on a base ID.
1468
+ * If the base ID ends in a number (e.g., "Checkers1"), strips it and increments.
1469
+ * Always appends a number starting from 1 (e.g., "Checkers1", "Checkers2").
1470
+ * Ensures the ID has no spaces.
1471
+ * @param baseId The base ID to use (typically the element type or existing node ID)
1472
+ * @param excludeElementId Optional element ID to exclude from uniqueness check (useful when renaming)
1473
+ * @returns A unique element ID with no spaces
1474
+ */
1475
+ generateUniqueNodeId(baseId, excludeElementId) {
1476
+ const sanitizedBaseId = baseId.replace(/\s+/g, "");
1477
+ const match = sanitizedBaseId.match(/^(.+?)(\d+)?$/);
1478
+ const baseName = match ? match[1] : sanitizedBaseId;
1479
+ let counter = 1;
1480
+ let candidateId = `${baseName}${counter}`;
1481
+ while (this._elements.some(
1482
+ (element) => element.id === candidateId && (!excludeElementId || element.id !== excludeElementId)
1483
+ )) {
1484
+ counter++;
1485
+ candidateId = `${baseName}${counter}`;
1486
+ }
1487
+ return candidateId;
1488
+ }
1489
+ /**
1490
+ * Get any element by ID (O(1) using Map cache)
1491
+ */
1492
+ getElement(id) {
1493
+ return this._elementMap.get(id) || null;
1494
+ }
1495
+ /**
1496
+ * Get a computation by ID (type-safe)
1497
+ */
1498
+ getNode(nodeId) {
1499
+ const element = this.getElement(nodeId);
1500
+ return element && !this.isAnnotation(element) ? element : null;
1501
+ }
1502
+ /**
1503
+ * Get an annotation by ID (type-safe)
1504
+ */
1505
+ getAnnotation(id) {
1506
+ const element = this.getElement(id);
1507
+ return element && this.isAnnotation(element) ? element : null;
1508
+ }
1509
+ /**
1510
+ * Return the live members of a group in authored order.
1511
+ *
1512
+ * Group membership is an annotation relationship, separate from subnet
1513
+ * parenting. Only elements in the group's current network are visible, and
1514
+ * recursive traversal is guarded against duplicate references and malformed
1515
+ * cycles in loaded documents.
1516
+ */
1517
+ getGroupMembers(groupId, recursive = true) {
1518
+ const group = this.getElement(groupId);
1519
+ if (!this.isGroupAnnotation(group)) return [];
1520
+ const members = [];
1521
+ const seen = /* @__PURE__ */ new Set([group.id]);
1522
+ const visitingGroups = /* @__PURE__ */ new Set([group.id]);
1523
+ const visit = (owner) => {
1524
+ for (const memberId of owner.containedElements ?? []) {
1525
+ if (seen.has(memberId)) continue;
1526
+ const member = this.getElement(memberId);
1527
+ if (!member || member === owner || member.parent !== owner.parent) continue;
1528
+ seen.add(memberId);
1529
+ members.push(memberId);
1530
+ if (recursive && this.isGroupAnnotation(member) && !visitingGroups.has(member.id)) {
1531
+ visitingGroups.add(member.id);
1532
+ visit(member);
1533
+ visitingGroups.delete(member.id);
1534
+ }
1535
+ }
1536
+ };
1537
+ visit(group);
1538
+ return members;
1539
+ }
1540
+ /**
1541
+ * Assign existing elements to one group, or remove their group ownership.
1542
+ * Validation completes before any annotation is changed, so failures leave
1543
+ * every membership list untouched. Assignment never changes subnet parents.
1544
+ */
1545
+ assignToGroup(elementIds, groupId) {
1546
+ const ids = [...new Set(elementIds)];
1547
+ const elements = ids.map((id) => this.getElement(id));
1548
+ if (elements.some((element) => !element)) return false;
1549
+ const targetElement = groupId === null ? null : this.getElement(groupId);
1550
+ if (groupId !== null && !this.isGroupAnnotation(targetElement)) return false;
1551
+ const target = this.isGroupAnnotation(targetElement) ? targetElement : null;
1552
+ if (target) {
1553
+ for (const element of elements) {
1554
+ if (element === target || element.parent !== target.parent) return false;
1555
+ if (this.isGroupAnnotation(element) && this.groupContains(element, target.id)) return false;
1556
+ }
1557
+ }
1558
+ const selected = new Set(ids);
1559
+ for (const group of this.groupAnnotations()) {
1560
+ const current = group.containedElements ?? [];
1561
+ group.containedElements = current.filter((id) => !selected.has(id));
1562
+ }
1563
+ if (target) {
1564
+ target.containedElements = [
1565
+ .../* @__PURE__ */ new Set([...target.containedElements ?? [], ...ids])
1566
+ ];
1567
+ }
1568
+ return true;
1569
+ }
1570
+ /**
1571
+ * Get a node by absolute path (e.g., "/effects/blur1")
1572
+ */
1573
+ nodeByPath(path) {
1574
+ if (!path.startsWith("/")) return null;
1575
+ const segments = path.slice(1).split("/").filter((s) => s.length > 0);
1576
+ if (segments.length === 0) return null;
1577
+ let current = this.nodes.find((node) => !node.parent && node.id === segments[0]) ?? null;
1578
+ for (let index = 1; current && index < segments.length; index++) {
1579
+ if (!current.isNetwork()) return null;
1580
+ current = current.children().find((child) => child.id === segments[index]) ?? null;
1581
+ }
1582
+ return current;
1583
+ }
1584
+ /**
1585
+ * Add an element to the graph
1586
+ */
1587
+ addElement(element) {
1588
+ this._elements.push(element);
1589
+ this._elementMap.set(element.id, element);
1590
+ this.invalidateTopologicalOrder();
1591
+ }
1592
+ /**
1593
+ * Move an element between networks while keeping both sides of the parent
1594
+ * relationship consistent. Structural editor code must use this instead of
1595
+ * mutating `parent` or `_children` directly.
1596
+ */
1597
+ reparentElement(element, parent) {
1598
+ if (parent) {
1599
+ if (!parent.isNetwork() || parent === element) return false;
1600
+ for (let ancestor = parent; ancestor; ancestor = ancestor.parent) {
1601
+ if (ancestor === element) return false;
1602
+ }
1603
+ }
1604
+ if (element.parent === parent) return true;
1605
+ const previous = element.parent;
1606
+ if (previous) {
1607
+ const removeChild = previous.removeChild;
1608
+ if (typeof removeChild === "function") removeChild.call(previous, element);
1609
+ else {
1610
+ previous._children = previous.children().filter((child) => child !== element);
1611
+ element.parent = null;
1612
+ }
1613
+ }
1614
+ if (parent) {
1615
+ const addChild = parent.addChild;
1616
+ if (typeof addChild === "function") addChild.call(parent, element);
1617
+ else {
1618
+ element.parent = parent;
1619
+ parent._children.push(element);
1620
+ }
1621
+ }
1622
+ if (!this.restoringHierarchy) this.pruneInvalidGroupMemberships();
1623
+ this.invalidateTopologicalOrder();
1624
+ return true;
1625
+ }
1626
+ /**
1627
+ * Remove an element from the graph
1628
+ */
1629
+ removeElement(id) {
1630
+ const element = this._elementMap.get(id);
1631
+ if (!element) return;
1632
+ const removed = [];
1633
+ const visited = /* @__PURE__ */ new Set();
1634
+ const collect = (candidate) => {
1635
+ if (visited.has(candidate)) return;
1636
+ visited.add(candidate);
1637
+ candidate.children().forEach(collect);
1638
+ removed.push(candidate);
1639
+ };
1640
+ collect(element);
1641
+ const removedIds = new Set(removed.map((candidate) => candidate.id));
1642
+ this.connections.filter((connection) => removedIds.has(connection.from.nodeId) || removedIds.has(connection.to.nodeId)).forEach((connection) => this.disconnect(connection.id));
1643
+ const pending = this._connectionsToRestore;
1644
+ if (Array.isArray(pending)) {
1645
+ this._connectionsToRestore = pending.filter((connection) => {
1646
+ const fromId = Array.isArray(connection?.[0]) ? connection[0][0] : void 0;
1647
+ const toId = Array.isArray(connection?.[1]) ? connection[1][0] : void 0;
1648
+ return !removedIds.has(fromId) && !removedIds.has(toId);
1649
+ });
1650
+ }
1651
+ this._elements = this._elements.filter((candidate) => !removedIds.has(candidate.id));
1652
+ for (const candidate of removed) {
1653
+ this._elementMap.delete(candidate.id);
1654
+ this.cookingNodes.delete(candidate);
1655
+ }
1656
+ for (const group of this.groupAnnotations()) {
1657
+ group.containedElements = (group.containedElements ?? []).filter((memberId) => !removedIds.has(memberId));
1658
+ }
1659
+ for (const candidate of removed) {
1660
+ const parent = candidate.parent;
1661
+ if (parent) {
1662
+ const removeChild = parent.removeChild;
1663
+ if (typeof removeChild === "function") removeChild.call(parent, candidate);
1664
+ else candidate.parent = null;
1665
+ }
1666
+ }
1667
+ this.invalidateTopologicalOrder();
1668
+ for (const candidate of removed) candidate.onDestroy?.();
1669
+ }
1670
+ isGroupAnnotation(element) {
1671
+ return !!element && this.isAnnotation(element) && element.type === "Group";
1672
+ }
1673
+ groupAnnotations() {
1674
+ return this.annotations.filter((annotation) => this.isGroupAnnotation(annotation));
1675
+ }
1676
+ groupContains(group, soughtId) {
1677
+ const visited = /* @__PURE__ */ new Set();
1678
+ const visit = (candidate) => {
1679
+ if (visited.has(candidate.id)) return false;
1680
+ visited.add(candidate.id);
1681
+ for (const memberId of candidate.containedElements ?? []) {
1682
+ if (memberId === soughtId) return true;
1683
+ const member = this.getElement(memberId);
1684
+ if (this.isGroupAnnotation(member) && member.parent === candidate.parent && visit(member)) return true;
1685
+ }
1686
+ return false;
1687
+ };
1688
+ return visit(group);
1689
+ }
1690
+ pruneInvalidGroupMemberships() {
1691
+ for (const group of this.groupAnnotations()) {
1692
+ const seen = /* @__PURE__ */ new Set();
1693
+ group.containedElements = (group.containedElements ?? []).filter((memberId) => {
1694
+ const member = this.getElement(memberId);
1695
+ if (!member || member === group || member.parent !== group.parent || seen.has(memberId)) return false;
1696
+ seen.add(memberId);
1697
+ return true;
1698
+ });
1699
+ }
1700
+ }
1701
+ /**
1702
+ * Rename an element's ID, updating internal maps
1703
+ */
1704
+ renameElement(oldId, newId) {
1705
+ const element = this._elementMap.get(oldId);
1706
+ if (!element || !newId || oldId !== newId && this._elementMap.has(newId)) return false;
1707
+ if (oldId === newId) return true;
1708
+ const renamedPorts = element.renameId(newId);
1709
+ this._elementMap.delete(oldId);
1710
+ this._elementMap.set(newId, element);
1711
+ for (const connection of this.connections) {
1712
+ if (connection.from.nodeId === oldId) connection.from.nodeId = newId;
1713
+ if (connection.to.nodeId === oldId) connection.to.nodeId = newId;
1714
+ connection.from.portId = renamedPorts.get(connection.from.portId) ?? connection.from.portId;
1715
+ connection.to.portId = renamedPorts.get(connection.to.portId) ?? connection.to.portId;
1716
+ }
1717
+ const pending = this._connectionsToRestore;
1718
+ if (Array.isArray(pending)) {
1719
+ for (const connection of pending) {
1720
+ for (const endpoint of Array.isArray(connection) ? connection : []) {
1721
+ if (Array.isArray(endpoint) && endpoint[0] === oldId) endpoint[0] = newId;
1722
+ }
1723
+ }
1724
+ }
1725
+ for (const annotation of this.annotations) {
1726
+ if (Array.isArray(annotation.containedElements)) {
1727
+ annotation.containedElements = annotation.containedElements.map((id) => id === oldId ? newId : id);
1728
+ }
1729
+ }
1730
+ this.invalidateTopologicalOrder();
1731
+ return true;
1732
+ }
1733
+ addNode(type, position) {
1734
+ const displayName = getNodeDisplayName(type);
1735
+ const baseId = displayName.toLowerCase().replace(/\s+/g, "_");
1736
+ const id = this.generateUniqueNodeId(baseId);
1737
+ const NodeClass = getNodeClass(type);
1738
+ let node;
1739
+ if (NodeClass) {
1740
+ node = new NodeClass(id, this);
1741
+ } else {
1742
+ node = new Node(id, packagePathToType(type), this);
1743
+ }
1744
+ node.modulePath = type;
1745
+ if (isStandardLibraryNode(type)) {
1746
+ node.sourceType = "stdlib";
1747
+ } else if (type.startsWith("local.")) {
1748
+ node.sourceType = "embedded";
1749
+ } else {
1750
+ const sourceType = this.moduleResolver.getSourceType(type);
1751
+ node.sourceType = sourceType || "embedded";
1752
+ }
1753
+ node.position = position;
1754
+ this.addElement(node);
1755
+ return node;
1756
+ }
1757
+ /**
1758
+ * Point an existing node instance at a different module.
1759
+ *
1760
+ * Duplicating a built-in into an editable copy, and promoting an embedded
1761
+ * node to a file, both keep the node where it is on the canvas with its
1762
+ * connections and parameter values intact and only change which module
1763
+ * supplies `execute`. The compiled module is captured once when a node is
1764
+ * loaded, so swapping the path alone would leave the old code running.
1765
+ *
1766
+ * Ports and parameters are untouched on purpose: both callers move IDENTICAL
1767
+ * code, so the ports and settings the next cook declares are the ones already
1768
+ * there, and reconciling them would only be a chance to be wrong.
1769
+ *
1770
+ * Switching to a different VERSION of a definition is a different move — use
1771
+ * retargetDefinition, which reconciles and reports.
1772
+ */
1773
+ retargetModule(nodeId, modulePath, source, code) {
1774
+ return this.retargetDefinition(nodeId, modulePath, source, code, {
1775
+ preserveParameters: false
1776
+ }).ok;
1777
+ }
1778
+ /**
1779
+ * Point a node at a different definition and carry its settings across.
1780
+ *
1781
+ * The general form of retargetModule. One code path: the module swap is
1782
+ * identical, and what differs is whether a reconciliation is asked for.
1783
+ * retargetModule is the case where it is not — identical code drops nothing,
1784
+ * so its report would be empty anyway, and skipping it keeps a working path
1785
+ * working. Everything else goes through the reconciler.
1786
+ *
1787
+ * Policy (see ParameterCarryOverReport in Node.ts):
1788
+ * - a parameter surviving by NAME and TYPE keeps its value, and its
1789
+ * expression with it
1790
+ * - one the new definition adds takes its declared default
1791
+ * - one the new definition no longer has is dropped AND REPORTED, with the
1792
+ * value it held, so a caller can offer it back
1793
+ * - same name, different type is a conflict: the new type wins with its
1794
+ * default and the conflict is reported. No coercion is attempted
1795
+ *
1796
+ * Pass `options.parameters` when the new definition's declarations are known
1797
+ * (a Definition panel has them) and the result is complete on return. Without
1798
+ * them the result is `deferred: true` — a module's parameters are only known
1799
+ * once its code declares them, which happens on the next cook.
1800
+ */
1801
+ retargetDefinition(nodeId, modulePath, source, code, options = {}) {
1802
+ const blank = () => ({ kept: [], defaulted: [], dropped: [], retyped: [] });
1803
+ const node = this.getNode(nodeId);
1804
+ if (!node) {
1805
+ return { ok: false, deferred: false, ...blank(), error: `no node with id ${nodeId}` };
1806
+ }
1807
+ const preserve = options.preserveParameters !== false;
1808
+ if (preserve) node.beginParameterCarryOver();
1809
+ node.modulePath = modulePath;
1810
+ node.sourceType = source;
1811
+ if (source === "embedded" && code !== void 0) node.code = code;
1812
+ const modulePromise = source === "project" ? loadProjectModule(modulePath) : loadEmbeddedModule(code ?? node.code);
1813
+ node.setFunction(moduleNodeFunction(node, modulePath, modulePromise));
1814
+ modulePromise.catch((error) => {
1815
+ node.abandonParameterCarryOver();
1816
+ console.warn("Failed to compile " + modulePath + ":", error);
1817
+ });
1818
+ node.markDirty();
1819
+ if (preserve && options.parameters) {
1820
+ const report = node.applyParameterDeclarations(options.parameters) ?? blank();
1821
+ return { ok: true, deferred: false, ...report };
1822
+ }
1823
+ return { ok: true, deferred: preserve, ...blank() };
1824
+ }
1825
+ /** The report from a deferred retarget, once its cook has run. Null until
1826
+ * then, and null for a node that was never retargeted. */
1827
+ getRetargetReport(nodeId) {
1828
+ return this.getNode(nodeId)?.lastCarryOverReport ?? null;
1829
+ }
1830
+ removeNode(nodeId) {
1831
+ this.removeElement(nodeId);
1832
+ }
1833
+ /**
1834
+ * Validate a connection before creating it
1835
+ * Works with any element type (nodes and annotations)
1836
+ */
1837
+ validateConnection(fromPort, toPort) {
1838
+ let fromElementId;
1839
+ let toElementId;
1840
+ try {
1841
+ const fromParsed = parsePortId(fromPort.id);
1842
+ const toParsed = parsePortId(toPort.id);
1843
+ fromElementId = fromParsed.elementId;
1844
+ toElementId = toParsed.elementId;
1845
+ } catch (err) {
1846
+ return { valid: false, error: `Invalid port ID format: ${err}` };
1847
+ }
1848
+ const fromElement = this.getElement(fromElementId);
1849
+ const toElement = this.getElement(toElementId);
1850
+ if (!fromElement || !toElement) {
1851
+ return { valid: false, error: "Element not found" };
1852
+ }
1853
+ if (!this.isAnnotation(fromElement) && !this.isAnnotation(toElement)) {
1854
+ const error = GraphValidator.validateConnection(
1855
+ this,
1856
+ fromElementId,
1857
+ toElementId,
1858
+ fromPort.id,
1859
+ toPort.id
1860
+ );
1861
+ if (error) {
1862
+ return { valid: false, error: error.message };
1863
+ }
1864
+ }
1865
+ return { valid: true };
1866
+ }
1867
+ /**
1868
+ * Check data type compatibility between ports
1869
+ * Returns a warning message if types don't match, or null if compatible
1870
+ */
1871
+ checkDataTypeCompatibility(fromPort, toPort) {
1872
+ const fromType = normalizeType(fromPort.dataType);
1873
+ const toType = normalizeType(toPort.dataType);
1874
+ if (canConnect(fromType, toType)) {
1875
+ if (fromType === "bundle" && toType === "bundle" && !bundleSchemasCompatible(fromPort.options?.schema, toPort.options?.schema)) {
1876
+ return `Schema mismatch: connecting a ${fromPort.options.schema} bundle to an input that takes ${toPort.options.schema}`;
1877
+ }
1878
+ return null;
1879
+ }
1880
+ const legacy = {
1881
+ number: ["number[]", "string"],
1882
+ float: ["number[]"],
1883
+ int: ["number[]"],
1884
+ string: ["number", "float", "int"]
1885
+ };
1886
+ if (legacy[fromType]?.includes(toType)) return null;
1887
+ return `Type mismatch: connecting ${fromType} to ${toType}`;
1888
+ }
1889
+ /**
1890
+ * Update type mismatch warnings on a node based on its input connections
1891
+ */
1892
+ updateTypeMismatchWarnings(nodeId) {
1893
+ const node = this.getNode(nodeId);
1894
+ if (!node) return;
1895
+ const warnings = [];
1896
+ for (const inputPort of node.inputs) {
1897
+ for (const conn of inputPort.connections) {
1898
+ const fromNode = this.getNode(conn.from.nodeId);
1899
+ if (!fromNode) continue;
1900
+ const fromPort = fromNode.outputs.find((p) => p.id === conn.from.portId);
1901
+ if (!fromPort) continue;
1902
+ const warning = this.checkDataTypeCompatibility(fromPort, inputPort);
1903
+ if (warning) {
1904
+ warnings.push(warning);
1905
+ }
1906
+ }
1907
+ }
1908
+ if (warnings.length > 0) {
1909
+ node.warning = warnings.join("; ");
1910
+ } else if (node.warning?.startsWith("Type mismatch")) {
1911
+ node.warning = null;
1912
+ }
1913
+ }
1914
+ connect(fromPort, toPort) {
1915
+ const validation = this.validateConnection(fromPort, toPort);
1916
+ if (!validation.valid) {
1917
+ throw new Error(`Invalid connection: ${validation.error}`);
1918
+ }
1919
+ if (!toPort.variadic && toPort.connections && toPort.connections.length > 0) {
1920
+ const connectionsToRemove = [...toPort.connections];
1921
+ for (const conn of connectionsToRemove) {
1922
+ this.disconnect(conn.id);
1923
+ }
1924
+ }
1925
+ const fromParsed = parsePortId(fromPort.id);
1926
+ const toParsed = parsePortId(toPort.id);
1927
+ const connection = {
1928
+ id: `conn_${Date.now()}_${++this.connectionIdCounter}`,
1929
+ from: {
1930
+ nodeId: fromParsed.elementId,
1931
+ portId: fromPort.id
1932
+ },
1933
+ to: {
1934
+ nodeId: toParsed.elementId,
1935
+ portId: toPort.id
1936
+ },
1937
+ type: fromPort.portType
1938
+ };
1939
+ const existing = fromPort.connections.find(
1940
+ (c) => c.to.nodeId === connection.to.nodeId && c.to.portId === connection.to.portId
1941
+ );
1942
+ if (existing) {
1943
+ return existing;
1944
+ }
1945
+ this.connections.push(connection);
1946
+ this._connectionMap.set(connection.id, connection);
1947
+ fromPort.connections.push(connection);
1948
+ toPort.connections.push(connection);
1949
+ this.invalidateTopologicalOrder();
1950
+ if (fromPort.value !== void 0 && fromPort.value !== null) {
1951
+ toPort.value = fromPort.value;
1952
+ if (toPort.onChange) {
1953
+ try {
1954
+ toPort.onChange(fromPort.value);
1955
+ } catch (err) {
1956
+ console.error(`Error in onChange callback for port ${toPort.name}:`, err);
1957
+ }
1958
+ }
1959
+ }
1960
+ this.syncVariadicPortsOnNode(toParsed.elementId);
1961
+ this.updateTypeMismatchWarnings(toParsed.elementId);
1962
+ this.getNode(toParsed.elementId)?.markDirty();
1963
+ return connection;
1964
+ }
1965
+ disconnect(connectionId) {
1966
+ const conn = this._connectionMap.get(connectionId);
1967
+ if (!conn) return;
1968
+ const sourceNode = this.getElement(conn.from.nodeId);
1969
+ const targetNode = this.getElement(conn.to.nodeId);
1970
+ if (sourceNode) {
1971
+ const sourcePort = sourceNode.getPort(conn.from.portId);
1972
+ if (sourcePort) {
1973
+ sourcePort.connections = sourcePort.connections.filter((c) => c.id !== connectionId);
1974
+ }
1975
+ }
1976
+ if (targetNode) {
1977
+ const targetPort = targetNode.getPort(conn.to.portId);
1978
+ if (targetPort) {
1979
+ targetPort.connections = targetPort.connections.filter((c) => c.id !== connectionId);
1980
+ if (targetPort.connections.length === 0) {
1981
+ targetPort.value = targetPort.defaultValue;
1982
+ try {
1983
+ targetPort.onChange?.(targetPort.defaultValue);
1984
+ } catch (err) {
1985
+ console.error(`Error in onChange callback for port ${targetPort.name}:`, err);
1986
+ }
1987
+ }
1988
+ }
1989
+ }
1990
+ const index = this.connections.findIndex((c) => c.id === connectionId);
1991
+ if (index >= 0) {
1992
+ this.connections.splice(index, 1);
1993
+ }
1994
+ this._connectionMap.delete(connectionId);
1995
+ this.invalidateTopologicalOrder();
1996
+ this.syncVariadicPortsOnNode(conn.to.nodeId);
1997
+ this.updateTypeMismatchWarnings(conn.to.nodeId);
1998
+ this.getNode(conn.to.nodeId)?.markDirty();
1999
+ }
2000
+ /**
2001
+ * Sync variadic ports on a node after connection changes
2002
+ */
2003
+ syncVariadicPortsOnNode(nodeId) {
2004
+ const node = this.getNode(nodeId);
2005
+ if (node) {
2006
+ node.syncVariadicPorts();
2007
+ }
2008
+ }
2009
+ /**
2010
+ * Validate the entire graph
2011
+ */
2012
+ validate() {
2013
+ return GraphValidator.validateGraph(this);
2014
+ }
2015
+ /**
2016
+ * Execute graph using topological sort for proper ordering
2017
+ */
2018
+ async execute(entryNode) {
2019
+ this.executionState = "running";
2020
+ try {
2021
+ await this.scheduler.flush(entryNode);
2022
+ } finally {
2023
+ this.executionState = "idle";
2024
+ }
2025
+ }
2026
+ /**
2027
+ * Get or compute topological order for the entire graph
2028
+ * Caches the result until graph structure changes
2029
+ */
2030
+ getTopologicalOrder() {
2031
+ if (this.cachedTopologicalOrder) {
2032
+ return this.cachedTopologicalOrder;
2033
+ }
2034
+ this.cachedTopologicalOrder = GraphValidator.topologicalSort(this);
2035
+ return this.cachedTopologicalOrder;
2036
+ }
2037
+ /**
2038
+ * Invalidate cached topological order (call when graph structure changes)
2039
+ */
2040
+ invalidateTopologicalOrder() {
2041
+ this.cachedTopologicalOrder = null;
2042
+ }
2043
+ stop() {
2044
+ this.executionState = "stopped";
2045
+ }
2046
+ reset() {
2047
+ this.nodes.forEach((node) => {
2048
+ node.error = null;
2049
+ node.warning = null;
2050
+ node.markDirty();
2051
+ });
2052
+ this.cookingNodes.clear();
2053
+ this.executionState = "idle";
2054
+ }
2055
+ // Behavior Control (v1.2)
2056
+ clearCookingNodes(except) {
2057
+ if (except) {
2058
+ this.cookingNodes.forEach((comp) => {
2059
+ if (comp !== except) {
2060
+ comp.setCook(false);
2061
+ }
2062
+ });
2063
+ } else {
2064
+ this.cookingNodes.forEach((comp) => {
2065
+ comp.setCook(false);
2066
+ });
2067
+ }
2068
+ }
2069
+ isDownstreamOfCooking(node) {
2070
+ const visited = /* @__PURE__ */ new Set();
2071
+ const checkUpstream = (n) => {
2072
+ if (visited.has(n.id)) return false;
2073
+ visited.add(n.id);
2074
+ if (this.cookingNodes.has(n)) {
2075
+ return true;
2076
+ }
2077
+ for (const input of n.inputs) {
2078
+ for (const conn of input.connections) {
2079
+ const upstreamNode = this.getNode(conn.from.nodeId);
2080
+ if (upstreamNode && checkUpstream(upstreamNode)) {
2081
+ return true;
2082
+ }
2083
+ }
2084
+ }
2085
+ return false;
2086
+ };
2087
+ return checkUpstream(node);
2088
+ }
2089
+ addAnnotation(annotation) {
2090
+ let annotationInstance;
2091
+ if (this.isAnnotation(annotation)) {
2092
+ annotationInstance = annotation;
2093
+ } else {
2094
+ const annData = annotation;
2095
+ const normalizedType = annData.type.charAt(0).toUpperCase() + annData.type.slice(1).toLowerCase();
2096
+ switch (normalizedType) {
2097
+ case "Image":
2098
+ annotationInstance = new ImageAnnotation(annData.id, this);
2099
+ annotationInstance.src = annData.src;
2100
+ break;
2101
+ case "Text":
2102
+ annotationInstance = new TextAnnotation(annData.id, this);
2103
+ annotationInstance.content = annData.content;
2104
+ break;
2105
+ case "Group":
2106
+ annotationInstance = new GroupAnnotation(annData.id, this);
2107
+ annotationInstance.content = annData.content;
2108
+ break;
2109
+ case "Line":
2110
+ annotationInstance = new LineAnnotation(annData.id, this);
2111
+ if (annData.endPosition) {
2112
+ annotationInstance.endPosition = annData.endPosition;
2113
+ }
2114
+ break;
2115
+ case "Polyline":
2116
+ annotationInstance = new PolylineAnnotation(annData.id, this);
2117
+ if (annData.points) {
2118
+ annotationInstance.points = annData.points;
2119
+ }
2120
+ break;
2121
+ default:
2122
+ annotationInstance = new Annotation(annData.id, annData.type, this);
2123
+ if (annData.content !== void 0) annotationInstance.content = annData.content;
2124
+ if (annData.src !== void 0) annotationInstance.src = annData.src;
2125
+ if (annData.endPosition) annotationInstance.endPosition = annData.endPosition;
2126
+ if (annData.points) annotationInstance.points = annData.points;
2127
+ }
2128
+ annotationInstance.position = annData.position || { x: 0, y: 0 };
2129
+ if (annData.size) annotationInstance.size = annData.size;
2130
+ if (annData.style) annotationInstance.style = { ...annData.style };
2131
+ if (annData.caption !== void 0) annotationInstance.caption = annData.caption;
2132
+ if (annData.containedElements) annotationInstance.containedElements = annData.containedElements;
2133
+ }
2134
+ this.addElement(annotationInstance);
2135
+ this.initializeAnnotationPorts(annotationInstance).catch((err) => {
2136
+ console.error(`Failed to initialize ports for annotation ${annotationInstance.id}:`, err);
2137
+ });
2138
+ }
2139
+ removeAnnotation(id) {
2140
+ this.removeElement(id);
2141
+ }
2142
+ /**
2143
+ * Initialize output ports for an annotation based on its type
2144
+ * Annotations always have exactly one output at index 0
2145
+ */
2146
+ async initializeAnnotationPorts(annotation) {
2147
+ annotation.outputs = [];
2148
+ if (annotation.type === "Image") {
2149
+ const imageAnnotation = annotation;
2150
+ const portId = createPortId(annotation.id, "output", 0);
2151
+ const port = {
2152
+ id: portId,
2153
+ name: "image",
2154
+ portType: "param",
2155
+ dataType: "asset",
2156
+ value: null,
2157
+ connections: [],
2158
+ setValue: (value) => {
2159
+ port.value = value;
2160
+ port.connections.forEach((conn) => {
2161
+ const toParsed = parsePortId(conn.to.portId);
2162
+ const targetElement = this.getElement(toParsed.elementId);
2163
+ if (targetElement && !this.isAnnotation(targetElement)) {
2164
+ const targetPort = targetElement.getInputPort(toParsed.index);
2165
+ if (targetPort) {
2166
+ targetPort.value = value;
2167
+ targetPort.onChange?.(value);
2168
+ }
2169
+ }
2170
+ });
2171
+ },
2172
+ trigger: (props) => {
2173
+ port.connections.forEach((conn) => {
2174
+ const toParsed = parsePortId(conn.to.portId);
2175
+ const targetElement = this.getElement(toParsed.elementId);
2176
+ if (targetElement && !this.isAnnotation(targetElement)) {
2177
+ const targetPort = targetElement.getInputPort(toParsed.index);
2178
+ targetPort?.onTrigger?.(props);
2179
+ }
2180
+ });
2181
+ }
2182
+ };
2183
+ annotation.outputs.push(port);
2184
+ const imageSrc = imageAnnotation.src;
2185
+ if (imageSrc) {
2186
+ try {
2187
+ const img = new Image();
2188
+ img.crossOrigin = "anonymous";
2189
+ await new Promise((resolve, reject) => {
2190
+ img.onload = () => {
2191
+ port.setValue(img);
2192
+ resolve();
2193
+ };
2194
+ img.onerror = reject;
2195
+ img.src = imageSrc.startsWith("/") || imageSrc.startsWith("http") || imageSrc.startsWith("data:") ? imageSrc : `/${imageSrc}`;
2196
+ });
2197
+ } catch (error) {
2198
+ console.error(`Failed to load image for annotation ${annotation.id}:`, error);
2199
+ }
2200
+ }
2201
+ } else if (annotation.type === "Text") {
2202
+ const textAnnotation = annotation;
2203
+ if (textAnnotation.content !== void 0) {
2204
+ const portId = createPortId(annotation.id, "output", 0);
2205
+ const port = {
2206
+ id: portId,
2207
+ name: "text",
2208
+ portType: "param",
2209
+ dataType: "string",
2210
+ value: textAnnotation.content,
2211
+ connections: [],
2212
+ setValue: (value) => {
2213
+ port.value = value;
2214
+ port.connections.forEach((conn) => {
2215
+ const toParsed = parsePortId(conn.to.portId);
2216
+ const targetElement = this.getElement(toParsed.elementId);
2217
+ if (targetElement && !this.isAnnotation(targetElement)) {
2218
+ const targetPort = targetElement.getInputPort(toParsed.index);
2219
+ if (targetPort) {
2220
+ targetPort.value = value;
2221
+ targetPort.onChange?.(value);
2222
+ }
2223
+ }
2224
+ });
2225
+ },
2226
+ trigger: (props) => {
2227
+ port.connections.forEach((conn) => {
2228
+ const toParsed = parsePortId(conn.to.portId);
2229
+ const targetElement = this.getElement(toParsed.elementId);
2230
+ if (targetElement && !this.isAnnotation(targetElement)) {
2231
+ const targetPort = targetElement.getInputPort(toParsed.index);
2232
+ targetPort?.onTrigger?.(props);
2233
+ }
2234
+ });
2235
+ }
2236
+ };
2237
+ annotation.outputs.push(port);
2238
+ }
2239
+ }
2240
+ }
2241
+ // ============ Path Validation ============
2242
+ /**
2243
+ * Validate all expressions in the graph for broken paths
2244
+ * Returns warnings for any expressions referencing non-existent nodes or parameters
2245
+ */
2246
+ validateExpressionPaths() {
2247
+ const warnings = [];
2248
+ const pathRefPattern = /\b(?:ch|chs|chv)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
2249
+ for (const node of this.nodes) {
2250
+ for (const [propName, prop] of Object.entries(node.props)) {
2251
+ if (!prop.expression) continue;
2252
+ const location = `${node.path()}/${propName}`;
2253
+ const matches = prop.expression.matchAll(pathRefPattern);
2254
+ for (const match of matches) {
2255
+ const referencedPath = match[1];
2256
+ const bundleWarning = this.bundlePathWarning(node, referencedPath);
2257
+ if (bundleWarning !== void 0) {
2258
+ if (bundleWarning) warnings.push({ location, expression: prop.expression, brokenPath: referencedPath, suggestion: bundleWarning });
2259
+ continue;
2260
+ }
2261
+ const resolvedNode = this.resolvePathFromNode(node, referencedPath);
2262
+ if (!resolvedNode.node) {
2263
+ warnings.push({
2264
+ location,
2265
+ expression: prop.expression,
2266
+ brokenPath: referencedPath,
2267
+ suggestion: this.suggestPathFix(node, referencedPath)
2268
+ });
2269
+ } else if (resolvedNode.parmName && !resolvedNode.node.props[resolvedNode.parmName]) {
2270
+ warnings.push({
2271
+ location,
2272
+ expression: prop.expression,
2273
+ brokenPath: referencedPath,
2274
+ suggestion: `Parameter "${resolvedNode.parmName}" not found on node "${resolvedNode.node.id}"`
2275
+ });
2276
+ }
2277
+ }
2278
+ }
2279
+ }
2280
+ return {
2281
+ valid: warnings.length === 0,
2282
+ warnings
2283
+ };
2284
+ }
2285
+ /**
2286
+ * A bundle field path in an expression: undefined when the path is not one,
2287
+ * null when it is and reads a field the producer declares (or one that
2288
+ * cannot be checked), and the problem otherwise.
2289
+ */
2290
+ bundlePathWarning(node, path) {
2291
+ const remote = remoteBundlePath(path);
2292
+ if (remote) {
2293
+ const owner = this.getNode(remote.node);
2294
+ const output = owner?.outputs.find((port) => port.name === remote.port);
2295
+ if (output?.dataType !== "bundle") return void 0;
2296
+ return `Expressions read bundle fields through a wire: connect ${remote.node}.${remote.port} to a bundle input here and read "${remote.port}.${remote.field}"`;
2297
+ }
2298
+ const reference = parseBundleFieldPath(path);
2299
+ if (!reference) return void 0;
2300
+ const input = node.inputs.find((port) => port.name === reference.port);
2301
+ if (!input || input.dataType !== "bundle") return void 0;
2302
+ const known = this.bundleFieldsFeeding(input);
2303
+ if (!known || known.includes(reference.field)) return null;
2304
+ return unknownFieldMessage(reference.field, known, `${node.id}.${reference.port}`);
2305
+ }
2306
+ /** The field names declared by whatever feeds a bundle input, or null. */
2307
+ bundleFieldsFeeding(input) {
2308
+ const connection = input.connections[0];
2309
+ if (!connection) return null;
2310
+ const port = this.getNode(connection.from.nodeId)?.getPort(connection.from.portId);
2311
+ const fields = port?.options?.bundleFields;
2312
+ return fields ? Object.keys(fields) : null;
2313
+ }
2314
+ /**
2315
+ * Resolve a path from a node's context, returning both the node and optional parameter name
2316
+ */
2317
+ resolvePathFromNode(fromNode, path) {
2318
+ const lastSlash = path.lastIndexOf("/");
2319
+ let nodePath;
2320
+ let parmName;
2321
+ if (lastSlash > 0) {
2322
+ const possibleParm = path.substring(lastSlash + 1);
2323
+ const possibleNodePath = path.substring(0, lastSlash);
2324
+ const asNode = fromNode.node(path);
2325
+ if (asNode) {
2326
+ return { node: asNode };
2327
+ }
2328
+ const parentNode = fromNode.node(possibleNodePath);
2329
+ if (parentNode) {
2330
+ return { node: parentNode, parmName: possibleParm };
2331
+ }
2332
+ }
2333
+ return { node: fromNode.node(path) };
2334
+ }
2335
+ /**
2336
+ * Try to suggest a fix for a broken path
2337
+ */
2338
+ suggestPathFix(fromNode, brokenPath) {
2339
+ const segments = brokenPath.replace(/^\.?\.?\//, "").split("/");
2340
+ const targetName = segments[segments.length - 1];
2341
+ const allNodes = this.nodes;
2342
+ const similar = allNodes.filter(
2343
+ (n) => n.id.toLowerCase().includes(targetName.toLowerCase()) || targetName.toLowerCase().includes(n.id.toLowerCase())
2344
+ );
2345
+ if (similar.length > 0) {
2346
+ const suggestion = similar[0];
2347
+ return `Did you mean "${suggestion.path()}"?`;
2348
+ }
2349
+ return void 0;
2350
+ }
2351
+ toJSON() {
2352
+ const result = {
2353
+ version: GRAPH_FORMAT_VERSION,
2354
+ metadata: {
2355
+ name: this.project.name || "Cascade Graph",
2356
+ created: this.createdAt,
2357
+ modified: (/* @__PURE__ */ new Date()).toISOString(),
2358
+ ...this.project.description ? { description: this.project.description } : {},
2359
+ ...this.project.author ? { author: this.project.author } : {}
2360
+ }
2361
+ };
2362
+ if (this.project.packages.length > 0) {
2363
+ result.project = {
2364
+ packages: this.project.packages.map((pkg) => ({
2365
+ path: pkg.path,
2366
+ alias: pkg.alias
2367
+ }))
2368
+ };
2369
+ }
2370
+ const resolverConfig = this.moduleResolver.exportConfig();
2371
+ if (Object.keys(resolverConfig.embeddedModules).length > 0) {
2372
+ result.embeddedModules = resolverConfig.embeddedModules;
2373
+ }
2374
+ if (Object.keys(resolverConfig.externalModules).length > 0) {
2375
+ result.externalModules = resolverConfig.externalModules;
2376
+ }
2377
+ const packages = this.packageManager.getCachedPackages().map((pkg) => {
2378
+ const [name, version] = pkg.split("@");
2379
+ return { name, version: version || "latest" };
2380
+ });
2381
+ if (packages.length > 0) {
2382
+ result.packages = packages;
2383
+ }
2384
+ const assets = this.assetManager.list().map((asset) => ({
2385
+ id: asset.id,
2386
+ path: asset.path,
2387
+ type: asset.type,
2388
+ size: asset.size
2389
+ }));
2390
+ if (assets.length > 0) {
2391
+ result.assets = { manifest: assets };
2392
+ }
2393
+ const annotations = this.annotations;
2394
+ const nodes = this.nodes;
2395
+ if (annotations.length > 0) {
2396
+ result.annotations = annotations.map((ann) => {
2397
+ const serialized = {
2398
+ id: ann.id,
2399
+ type: ann.type,
2400
+ position: [ann.position.x, ann.position.y]
2401
+ };
2402
+ const annAny = ann;
2403
+ if (annAny.content !== void 0) serialized.content = annAny.content;
2404
+ if (annAny.src !== void 0) serialized.src = annAny.src;
2405
+ if (ann.size) serialized.size = ann.size;
2406
+ if (annAny.points) serialized.points = annAny.points.map((p) => [p.x, p.y]);
2407
+ if (annAny.endPosition) serialized.endPosition = [annAny.endPosition.x, annAny.endPosition.y];
2408
+ if (ann.style) serialized.style = ann.style;
2409
+ if (ann.caption !== void 0) serialized.caption = ann.caption;
2410
+ if (ann.containedElements) serialized.containedElements = [...ann.containedElements];
2411
+ if (ann.parent) serialized.parent = ann.parent.id;
2412
+ return serialized;
2413
+ });
2414
+ }
2415
+ result.nodes = nodes.map((n) => this.nodeToJSON(n));
2416
+ const connections = this.connections.map((conn) => {
2417
+ try {
2418
+ const fromParsed = parsePortId(conn.from.portId);
2419
+ const toParsed = parsePortId(conn.to.portId);
2420
+ const fromElement = this.getElement(fromParsed.elementId);
2421
+ const toElement = this.getElement(toParsed.elementId);
2422
+ if (!fromElement || !toElement) return null;
2423
+ const fromPort = fromElement.outputs?.[fromParsed.index];
2424
+ const toPort = toElement.inputs?.[toParsed.index];
2425
+ if (!fromPort || !toPort) return null;
2426
+ return [
2427
+ [fromParsed.elementId, fromParsed.index, fromPort.name],
2428
+ [toParsed.elementId, toParsed.index, toPort.name]
2429
+ ];
2430
+ } catch (err) {
2431
+ console.warn(`Failed to parse port ID in connection: ${err}`);
2432
+ return null;
2433
+ }
2434
+ }).filter((conn) => conn !== null);
2435
+ if (connections.length > 0) {
2436
+ result.connections = connections;
2437
+ }
2438
+ if (this.editor) result.editor = this.editor;
2439
+ if (this.seed !== void 0) result.seed = this.seed;
2440
+ if (this.timeline) result.timeline = this.timeline;
2441
+ return result;
2442
+ }
2443
+ /**
2444
+ * Serialize a node with v0.2 source information
2445
+ */
2446
+ nodeToJSON(node) {
2447
+ const nodeAny = node;
2448
+ const fullType = nodeAny.modulePath || (node.type.includes(".") ? node.type : `cascade.image.${node.type}`);
2449
+ let sourceType = nodeAny.sourceType;
2450
+ if (!sourceType) {
2451
+ if (isStandardLibraryNode(fullType)) {
2452
+ sourceType = "stdlib";
2453
+ } else if (fullType.startsWith("local.")) {
2454
+ sourceType = "embedded";
2455
+ } else {
2456
+ sourceType = this.moduleResolver.getSourceType(fullType) || "embedded";
2457
+ }
2458
+ }
2459
+ const result = {
2460
+ id: node.id,
2461
+ module: fullType,
2462
+ position: [node.position.x, node.position.y],
2463
+ source: sourceType
2464
+ };
2465
+ if (sourceType === "project") {
2466
+ const config = this.moduleResolver.exportConfig();
2467
+ const external = config.externalModules[fullType];
2468
+ if (external) {
2469
+ result.file = external.file;
2470
+ }
2471
+ }
2472
+ if (result.source === "embedded" && node.code) {
2473
+ result.code = node.code;
2474
+ }
2475
+ if (node.comment && node.comment.trim()) {
2476
+ result.comment = node.comment;
2477
+ }
2478
+ if (node.color) {
2479
+ result.color = node.color;
2480
+ }
2481
+ if (node.parent) {
2482
+ result.parent = node.parent.id;
2483
+ }
2484
+ const rawParameterValue = (p) => node.rawParameterValue(p.name);
2485
+ const changedParameters = (node.parameters ?? []).filter(
2486
+ (p) => p.promoted || node.preservesPlainProp(p.name) || JSON.stringify(rawParameterValue(p)) !== JSON.stringify(p.defaultValue)
2487
+ );
2488
+ const parameters = changedParameters.filter((p) => p.documentField !== "props").map((p) => p.promoted ? { name: p.name, value: rawParameterValue(p), promoted: true } : { name: p.name, value: rawParameterValue(p) });
2489
+ if (parameters.length > 0) {
2490
+ result.params = parameters;
2491
+ }
2492
+ if (node.promotions?.length) {
2493
+ result.promotions = node.promotions.map((entry) => ({
2494
+ nodeId: entry.nodeId,
2495
+ param: entry.param,
2496
+ ...entry.label ? { label: entry.label } : {}
2497
+ }));
2498
+ }
2499
+ const inputValues = node.inputs.filter((port) => (port.connections?.length ?? 0) === 0).filter((port) => port.value !== void 0 && port.value !== null).filter((port) => typeof port.value !== "function").map((port) => ({ name: port.name, defaultValue: port.value, dataType: port.dataType })).filter((port) => port.defaultValue !== "" || port.dataType === "string");
2500
+ if (inputValues.length > 0) {
2501
+ result.inputs = inputValues;
2502
+ }
2503
+ const props = Object.entries(node.props).reduce((acc, [key, prop]) => {
2504
+ const keyed = (prop.channel?.keys?.length ?? 0) > 0;
2505
+ if (prop.fromParameter && !prop.expression && !keyed) return acc;
2506
+ let value = prop.value;
2507
+ if (prop.type === "color" && isColorValue(value)) {
2508
+ const normalized = normalizeColor(value);
2509
+ value = [normalized.r, normalized.g, normalized.b, normalized.a ?? 1];
2510
+ }
2511
+ acc[key] = node.serializePropValue(key, value);
2512
+ return acc;
2513
+ }, {});
2514
+ for (const parameter of changedParameters) {
2515
+ if (parameter.documentField !== "props") continue;
2516
+ props[parameter.name] = node.serializePropValue(
2517
+ parameter.name,
2518
+ node.rawParameterValue(parameter.name)
2519
+ );
2520
+ }
2521
+ if (Object.keys(props).length > 0) {
2522
+ result.props = props;
2523
+ }
2524
+ if (node.bypass) {
2525
+ result.bypass = true;
2526
+ }
2527
+ if (node.cook) {
2528
+ result.cook = true;
2529
+ }
2530
+ if (typeof node.serialize === "function") {
2531
+ const nodeState = node.serialize();
2532
+ if (nodeState && Object.keys(nodeState).length > 0) {
2533
+ result.state = nodeState;
2534
+ }
2535
+ }
2536
+ return result;
2537
+ }
2538
+ static fromJSON(json, assetManager, packageManager) {
2539
+ const graph = new _Graph(assetManager, packageManager);
2540
+ const version = json.version || "0.1";
2541
+ if (typeof json.metadata?.created === "string") graph.createdAt = json.metadata.created;
2542
+ if (json.editor && typeof json.editor === "object") graph.editor = json.editor;
2543
+ if (Number.isInteger(json.seed)) graph.seed = json.seed;
2544
+ if (json.timeline && typeof json.timeline === "object") graph.timeline = json.timeline;
2545
+ graph.project = {
2546
+ ...graph.project,
2547
+ ...typeof json.metadata?.name === "string" ? { name: json.metadata.name } : {},
2548
+ ...typeof json.metadata?.description === "string" ? { description: json.metadata.description } : {},
2549
+ ...typeof json.metadata?.author === "string" ? { author: json.metadata.author } : {}
2550
+ };
2551
+ if (json.project) {
2552
+ graph.project = {
2553
+ ...graph.project,
2554
+ name: json.metadata?.name ?? graph.project.name,
2555
+ description: json.metadata?.description ?? graph.project.description,
2556
+ author: json.metadata?.author ?? graph.project.author,
2557
+ packages: json.project.packages || []
2558
+ };
2559
+ json.project.packages?.forEach((pkg) => {
2560
+ graph.moduleResolver.addProjectPackage(pkg);
2561
+ });
2562
+ }
2563
+ if (json.embeddedModules) {
2564
+ graph.moduleResolver.importConfig({
2565
+ embeddedModules: json.embeddedModules
2566
+ });
2567
+ }
2568
+ if (json.externalModules) {
2569
+ graph.moduleResolver.importConfig({
2570
+ externalModules: json.externalModules
2571
+ });
2572
+ }
2573
+ if (json.packages && Array.isArray(json.packages)) {
2574
+ }
2575
+ json.nodes.forEach((nodeData) => {
2576
+ let nodeId = nodeData.id;
2577
+ if (!nodeId) {
2578
+ const baseId = nodeData.name || nodeData.type || nodeData.module;
2579
+ nodeId = graph.generateUniqueNodeId(baseId);
2580
+ } else {
2581
+ let counter = 1;
2582
+ let candidateId = nodeId;
2583
+ while (graph.elements.some((e) => e.id === candidateId)) {
2584
+ counter++;
2585
+ candidateId = `${nodeId}${counter}`;
2586
+ }
2587
+ nodeId = candidateId;
2588
+ }
2589
+ const nodeType = nodeData.module || nodeData.type;
2590
+ if (!nodeType || !nodeType.includes(".")) {
2591
+ console.warn(`Invalid node type format: ${nodeType}. Expected package path (e.g., "cascade.image.Color"). Skipping node.`);
2592
+ return;
2593
+ }
2594
+ const shortType = packagePathToType(nodeType);
2595
+ const source = nodeData.source || (isStandardLibraryNode(nodeType) ? "stdlib" : "embedded");
2596
+ let nodeCode = "";
2597
+ if (source === "embedded") {
2598
+ const embeddedModule = json.embeddedModules?.[nodeType];
2599
+ nodeCode = embeddedModule?.code || nodeData.code || "";
2600
+ } else if (source === "project") {
2601
+ const externalModule = json.externalModules?.[nodeType];
2602
+ nodeCode = externalModule?.cachedCode || nodeData.code || "";
2603
+ }
2604
+ const NodeClass = getNodeClass(nodeType);
2605
+ let node;
2606
+ if (NodeClass) {
2607
+ node = new NodeClass(nodeId, graph);
2608
+ } else if (nodeType.startsWith("cascade.")) {
2609
+ throw new Error(`Unknown Cascade node type: ${nodeType}`);
2610
+ } else {
2611
+ node = new Node(nodeId, shortType, graph);
2612
+ }
2613
+ node.modulePath = nodeType;
2614
+ node.sourceType = source;
2615
+ if (Array.isArray(nodeData.position)) {
2616
+ node.position = { x: nodeData.position[0] || 0, y: nodeData.position[1] || 0 };
2617
+ } else if (nodeData.position && typeof nodeData.position === "object") {
2618
+ node.position = { x: nodeData.position.x || 0, y: nodeData.position.y || 0 };
2619
+ } else {
2620
+ node.position = { x: 0, y: 0 };
2621
+ }
2622
+ node.code = nodeCode;
2623
+ node.comment = nodeData.comment || "";
2624
+ if (nodeData.color) node.color = nodeData.color;
2625
+ if (Array.isArray(nodeData.params)) {
2626
+ nodeData.params.forEach((saved) => {
2627
+ if (!saved?.name) return;
2628
+ const declared = node.parameters.find((parameter) => parameter.name === saved.name);
2629
+ if (declared) {
2630
+ declared.value = saved.value;
2631
+ declared.promoted = Boolean(saved.promoted);
2632
+ } else {
2633
+ node.parameters.push({
2634
+ name: saved.name,
2635
+ value: saved.value,
2636
+ defaultValue: saved.value,
2637
+ dataType: "any",
2638
+ promoted: Boolean(saved.promoted),
2639
+ options: {}
2640
+ });
2641
+ }
2642
+ if (saved.promoted) {
2643
+ const port = node.in(saved.name, saved.value);
2644
+ port.fromParameter = saved.name;
2645
+ }
2646
+ });
2647
+ }
2648
+ if (Array.isArray(nodeData.promotions)) {
2649
+ node.promotions = nodeData.promotions.filter((entry) => entry?.nodeId && entry?.param).map((entry) => ({
2650
+ nodeId: String(entry.nodeId),
2651
+ param: String(entry.param),
2652
+ ...entry.label ? { label: String(entry.label) } : {}
2653
+ }));
2654
+ }
2655
+ graph.addElement(node);
2656
+ const restoreInput = (name, value, hasValue, saved = {}) => {
2657
+ if (!name) return;
2658
+ const declared = node.inputs.find((port2) => port2.name === name);
2659
+ const port = declared ?? node.in(
2660
+ name,
2661
+ hasValue ? value : void 0,
2662
+ saved.dataType ? { type: saved.dataType } : {}
2663
+ );
2664
+ if (declared && hasValue) declared.value = value;
2665
+ if (saved.id && port.id !== saved.id) port.id = saved.id;
2666
+ };
2667
+ if (Array.isArray(nodeData.inputs)) {
2668
+ nodeData.inputs.forEach((saved) => {
2669
+ if (!saved || typeof saved !== "object") return;
2670
+ const hasDefault = Object.prototype.hasOwnProperty.call(saved, "defaultValue");
2671
+ const hasValue = Object.prototype.hasOwnProperty.call(saved, "value");
2672
+ restoreInput(
2673
+ saved.name,
2674
+ hasDefault ? saved.defaultValue : saved.value,
2675
+ hasDefault || hasValue,
2676
+ saved
2677
+ );
2678
+ });
2679
+ } else if (nodeData.inputs && typeof nodeData.inputs === "object") {
2680
+ Object.entries(nodeData.inputs).forEach(([name, value]) => {
2681
+ restoreInput(name, value, true);
2682
+ });
2683
+ }
2684
+ if (nodeData.outputs && Array.isArray(nodeData.outputs)) {
2685
+ nodeData.outputs.forEach((portData) => {
2686
+ const port = node.out(portData.name, portData.portType || "param");
2687
+ if (portData.id && port.id !== portData.id) {
2688
+ port.id = portData.id;
2689
+ }
2690
+ });
2691
+ }
2692
+ if (nodeData.props) {
2693
+ Object.entries(nodeData.props).forEach(([key, propData]) => {
2694
+ let value;
2695
+ let expression;
2696
+ if (propData && typeof propData === "object" && "value" in propData) {
2697
+ value = propData.value;
2698
+ expression = propData.expression;
2699
+ } else {
2700
+ value = propData;
2701
+ }
2702
+ let normalizedValue = value;
2703
+ const declaredColor = node.props[key]?.type === "color";
2704
+ if (isColorValue(value) && (declaredColor || !Array.isArray(value))) {
2705
+ const normalized = normalizeColor(value);
2706
+ normalizedValue = normalized.a !== void 0 && normalized.a !== 1 ? { r: normalized.r, g: normalized.g, b: normalized.b, a: normalized.a } : { r: normalized.r, g: normalized.g, b: normalized.b };
2707
+ }
2708
+ const definitionProp = node.parameters.find(
2709
+ (parameter) => parameter.name === key && parameter.documentField === "props"
2710
+ );
2711
+ if (definitionProp) {
2712
+ definitionProp.value = value;
2713
+ } else if (!node.props[key]) {
2714
+ node.props[key] = { value: normalizedValue };
2715
+ } else {
2716
+ node.props[key].value = normalizedValue;
2717
+ }
2718
+ if (expression && node.props[key]) {
2719
+ node.props[key].expression = expression;
2720
+ } else {
2721
+ node.preservePlainProp(key);
2722
+ }
2723
+ if (!expression && node.props[key]?.expression) {
2724
+ delete node.props[key].expression;
2725
+ node.props[key] = { ...node.props[key] };
2726
+ }
2727
+ if (propData?.channel && node.props[key]) {
2728
+ node.restorePropChannel(key, propData.channel);
2729
+ }
2730
+ });
2731
+ }
2732
+ if (nodeData.bypass !== void 0) {
2733
+ node.setBypass(nodeData.bypass);
2734
+ }
2735
+ if (nodeData.cook !== void 0) {
2736
+ node.setCook(nodeData.cook);
2737
+ }
2738
+ if (!NodeClass && (source === "project" || node.code)) {
2739
+ const modulePromise = source === "project" ? loadProjectModule(nodeType) : loadEmbeddedModule(node.code);
2740
+ node.setFunction(moduleNodeFunction(node, nodeType, modulePromise));
2741
+ modulePromise.catch((err) => {
2742
+ console.warn("Failed to compile node " + node.id + ":", err);
2743
+ });
2744
+ }
2745
+ if (nodeData.state && typeof node.deserialize === "function") {
2746
+ try {
2747
+ node.deserialize(nodeData.state);
2748
+ } catch (err) {
2749
+ console.warn("Failed to deserialize state for node " + node.id + ":", err);
2750
+ }
2751
+ }
2752
+ });
2753
+ const connectionsToRestore = json.connections || [];
2754
+ if (json.annotations && Array.isArray(json.annotations)) {
2755
+ const annotations = json.annotations.map((annData) => {
2756
+ let annotation;
2757
+ const position = Array.isArray(annData.position) ? { x: annData.position[0] || 0, y: annData.position[1] || 0 } : { x: annData.position?.x || 0, y: annData.position?.y || 0 };
2758
+ const normalizedType = annData.type.charAt(0).toUpperCase() + annData.type.slice(1).toLowerCase();
2759
+ switch (normalizedType) {
2760
+ case "Image":
2761
+ annotation = new ImageAnnotation(annData.id, graph);
2762
+ annotation.src = annData.src;
2763
+ break;
2764
+ case "Text":
2765
+ annotation = new TextAnnotation(annData.id, graph);
2766
+ annotation.content = annData.content;
2767
+ break;
2768
+ case "Group":
2769
+ annotation = new GroupAnnotation(annData.id, graph);
2770
+ annotation.content = annData.content;
2771
+ break;
2772
+ case "Line":
2773
+ annotation = new LineAnnotation(annData.id, graph);
2774
+ if (annData.endPosition) {
2775
+ annotation.endPosition = Array.isArray(annData.endPosition) ? { x: annData.endPosition[0] || 0, y: annData.endPosition[1] || 0 } : { x: annData.endPosition.x || 0, y: annData.endPosition.y || 0 };
2776
+ }
2777
+ break;
2778
+ case "Polyline":
2779
+ annotation = new PolylineAnnotation(annData.id, graph);
2780
+ if (annData.points) {
2781
+ annotation.points = annData.points.map(
2782
+ (p) => Array.isArray(p) ? { x: p[0] || 0, y: p[1] || 0 } : { x: p.x || 0, y: p.y || 0 }
2783
+ );
2784
+ }
2785
+ break;
2786
+ default:
2787
+ annotation = new Annotation(annData.id, annData.type, graph);
2788
+ }
2789
+ annotation.position = position;
2790
+ if (annData.size) annotation.size = { width: annData.size.width, height: annData.size.height };
2791
+ if (annData.style) annotation.style = { ...annData.style };
2792
+ if (annData.caption !== void 0) annotation.caption = annData.caption;
2793
+ if (annData.containedElements) annotation.containedElements = annData.containedElements;
2794
+ return annotation;
2795
+ });
2796
+ annotations.forEach((ann) => graph.addElement(ann));
2797
+ graph._annotationPortsInitialized = Promise.all(
2798
+ annotations.map((ann) => graph.initializeAnnotationPorts(ann))
2799
+ ).catch((err) => {
2800
+ console.error("Failed to initialize some annotation ports:", err);
2801
+ });
2802
+ } else {
2803
+ graph._annotationPortsInitialized = Promise.resolve();
2804
+ }
2805
+ const authoredElements = [
2806
+ ...Array.isArray(json.nodes) ? json.nodes : [],
2807
+ ...Array.isArray(json.annotations) ? json.annotations : []
2808
+ ];
2809
+ graph.restoringHierarchy = true;
2810
+ for (const elementData of authoredElements) {
2811
+ if (typeof elementData?.parent !== "string") continue;
2812
+ const child = graph.getElement(elementData.id);
2813
+ const parent = graph.getElement(elementData.parent);
2814
+ const warn = (reason) => console.warn(
2815
+ `Parent link for "${elementData.id}" to "${elementData.parent}" ignored: ${reason}; element loaded at root.`
2816
+ );
2817
+ if (!child) continue;
2818
+ if (!parent) {
2819
+ warn("parent does not exist");
2820
+ continue;
2821
+ }
2822
+ if (child === parent) {
2823
+ warn("self-parenting is invalid");
2824
+ continue;
2825
+ }
2826
+ if (!parent.isNetwork()) {
2827
+ warn("parent is not a network");
2828
+ continue;
2829
+ }
2830
+ let ancestor = parent;
2831
+ let closesCycle = false;
2832
+ const visited = /* @__PURE__ */ new Set();
2833
+ while (ancestor && !visited.has(ancestor)) {
2834
+ if (ancestor === child) {
2835
+ closesCycle = true;
2836
+ break;
2837
+ }
2838
+ visited.add(ancestor);
2839
+ ancestor = ancestor.parent;
2840
+ }
2841
+ if (closesCycle) {
2842
+ warn("parent cycle detected");
2843
+ continue;
2844
+ }
2845
+ graph.reparentElement(child, parent);
2846
+ }
2847
+ graph.restoringHierarchy = false;
2848
+ graph.pruneInvalidGroupMemberships();
2849
+ const depth = (node) => {
2850
+ let value = 0;
2851
+ let parent = node.parent;
2852
+ const visited = /* @__PURE__ */ new Set();
2853
+ while (parent && !visited.has(parent)) {
2854
+ visited.add(parent);
2855
+ value++;
2856
+ parent = parent.parent;
2857
+ }
2858
+ return value;
2859
+ };
2860
+ graph.nodes.filter((node) => node.isNetwork()).sort((left, right) => depth(right) - depth(left)).forEach((node) => node.syncPorts?.());
2861
+ graph._connectionsToRestore = connectionsToRestore;
2862
+ connectionsToRestore.forEach((connData) => {
2863
+ graph.tryRestoreConnection(connData, false);
2864
+ });
2865
+ return graph;
2866
+ }
2867
+ async waitForAnnotationPorts() {
2868
+ const initPromise = this._annotationPortsInitialized;
2869
+ if (initPromise) {
2870
+ await initPromise;
2871
+ }
2872
+ }
2873
+ /**
2874
+ * Try to restore a single connection from serialized data
2875
+ * @internal
2876
+ */
2877
+ tryRestoreConnection(connData, checkDuplicates = false) {
2878
+ if (!Array.isArray(connData) || !Array.isArray(connData[0]) || !Array.isArray(connData[1])) {
2879
+ return false;
2880
+ }
2881
+ const [fromNodeId, fromPortIndex, fromPortName] = connData[0];
2882
+ const [toNodeId, toPortIndex, toPortName] = connData[1];
2883
+ if (typeof fromPortIndex !== "number" || typeof toPortIndex !== "number") {
2884
+ return false;
2885
+ }
2886
+ const fromElement = this.getElement(fromNodeId);
2887
+ const toElement = this.getElement(toNodeId);
2888
+ if (!fromElement || !toElement) return false;
2889
+ const byName = (ports, name) => typeof name === "string" ? ports?.find((p) => p.name === name) : void 0;
2890
+ const resolve = (ports, name, index) => typeof name === "string" ? byName(ports, name) : ports?.[index];
2891
+ const fromPort = resolve(fromElement.outputs, fromPortName, fromPortIndex);
2892
+ const toPort = resolve(toElement.inputs, toPortName, toPortIndex);
2893
+ if (!fromPort || !toPort) return false;
2894
+ if (checkDuplicates) {
2895
+ const exists = this.connections.some(
2896
+ (c) => c.from.nodeId === fromNodeId && c.from.portId === fromPort.id && c.to.nodeId === toNodeId && c.to.portId === toPort.id
2897
+ );
2898
+ if (exists) return true;
2899
+ }
2900
+ try {
2901
+ this.connect(fromPort, toPort);
2902
+ return true;
2903
+ } catch (err) {
2904
+ console.warn(`[connect] ${fromNodeId}[${fromPortIndex}] -> ${toNodeId}[${toPortIndex}] rejected:`, err);
2905
+ return false;
2906
+ }
2907
+ }
2908
+ /**
2909
+ * Restore connections that were stored during fromJSON
2910
+ */
2911
+ /**
2912
+ * Bind the connections a loaded file described, and KEEP the ones that could
2913
+ * not bind yet.
2914
+ *
2915
+ * A node's ports are created by running its code, so on load a connection
2916
+ * into a node that hasn't executed has no port to attach to. This used to
2917
+ * drop those connections on the floor — the pending list was deleted after
2918
+ * one pass — which meant a graph of code-defined nodes came up almost
2919
+ * entirely unwired, and nothing downstream of a source ever ran. Anything
2920
+ * still unresolved stays pending, so calling this again after another cook
2921
+ * finishes the job.
2922
+ */
2923
+ /**
2924
+ * Node ids that a loaded file wires an input to, and whose connection has
2925
+ * not bound yet.
2926
+ *
2927
+ * The reason this is public: **an input that is about to be connected is not
2928
+ * an input that is missing**, and until now nothing could tell the two
2929
+ * apart. On load a node's ports only exist once its code has run, so the
2930
+ * first cook passes reach a node whose upstream edge has no port to attach
2931
+ * to — the node runs with `undefined` on that input, throws whatever its
2932
+ * author wrote for the unwired case, and a later pass then succeeds.
2933
+ *
2934
+ * The graph ends up correct and the log does not: those warm-up throws are
2935
+ * real errors from the node's point of view, they are recorded, and they
2936
+ * never clear. A user reported `volume-field` as broken on exactly this,
2937
+ * having verified from the cache files that the output was in fact right.
2938
+ *
2939
+ * So the scheduler asks this before cooking, and treats a node with a
2940
+ * pending inbound edge as *not ready* rather than as failed.
2941
+ */
2942
+ pendingConnectionTargets() {
2943
+ const pending = this._connectionsToRestore;
2944
+ const targets = /* @__PURE__ */ new Set();
2945
+ if (this.connectionsSettled) return targets;
2946
+ if (!Array.isArray(pending)) return targets;
2947
+ for (const connection of pending) {
2948
+ const target = Array.isArray(connection?.[1]) ? connection[1][0] : void 0;
2949
+ if (typeof target === "string") targets.add(target);
2950
+ }
2951
+ return targets;
2952
+ }
2953
+ /** Called when the warm-up passes stop making progress. */
2954
+ markConnectionsSettled() {
2955
+ this.connectionsSettled = true;
2956
+ }
2957
+ restoreConnections() {
2958
+ const connectionsToRestore = this._connectionsToRestore || [];
2959
+ if (connectionsToRestore.length === 0) return;
2960
+ const stillPending = connectionsToRestore.filter(
2961
+ (connData) => !this.tryRestoreConnection(connData, true)
2962
+ );
2963
+ if (stillPending.length < connectionsToRestore.length) this.connectionsSettled = false;
2964
+ if (stillPending.length > 0) {
2965
+ this._connectionsToRestore = stillPending;
2966
+ } else {
2967
+ delete this._connectionsToRestore;
2968
+ }
2969
+ }
2970
+ };
2971
+ export {
2972
+ GRAPH_FORMAT_VERSION,
2973
+ Graph
2974
+ };
2975
+ //# sourceMappingURL=Graph-ZJJTRQTM.js.map