project-graph-mcp 2.2.6 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +4 -13
  5. package/project-graph-mcp-2.3.0.tgz +0 -0
  6. package/src/compact/expand.js +1 -1
  7. package/src/core/graph-builder.js +2 -2
  8. package/src/core/parser.js +2 -2
  9. package/src/network/server.js +1 -2
  10. package/src/network/web-server.js +1 -1
  11. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  12. package/vendor/symbiote-node/LICENSE +21 -0
  13. package/vendor/symbiote-node/README.md +206 -0
  14. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  16. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  17. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  18. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  19. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  20. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  21. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  22. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  24. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  25. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  26. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  28. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  29. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  31. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  32. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  34. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  35. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  36. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  37. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  38. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  39. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  40. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  41. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  42. package/vendor/symbiote-node/core/Connection.js +45 -0
  43. package/vendor/symbiote-node/core/Editor.js +451 -0
  44. package/vendor/symbiote-node/core/Frame.js +31 -0
  45. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  46. package/vendor/symbiote-node/core/GraphText.js +210 -0
  47. package/vendor/symbiote-node/core/Node.js +143 -0
  48. package/vendor/symbiote-node/core/Portal.js +104 -0
  49. package/vendor/symbiote-node/core/Socket.js +185 -0
  50. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  51. package/vendor/symbiote-node/index.js +103 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  53. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  54. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  56. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  57. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  58. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  59. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  60. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  61. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  62. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  64. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  65. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  67. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  68. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  70. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  71. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  73. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  74. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  75. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  76. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  79. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  80. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  81. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  83. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  84. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  85. package/vendor/symbiote-node/layout/index.js +16 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  87. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  88. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  90. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  91. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  93. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  94. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  96. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  97. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  98. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  99. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  101. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  102. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  103. package/vendor/symbiote-node/package.json +59 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  105. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  106. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  107. package/vendor/symbiote-node/plugins/History.js +384 -0
  108. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  109. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  110. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  111. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  112. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  113. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  114. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  115. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  116. package/vendor/symbiote-node/shapes/index.js +53 -0
  117. package/vendor/symbiote-node/themes/Palette.js +32 -0
  118. package/vendor/symbiote-node/themes/Skin.js +113 -0
  119. package/vendor/symbiote-node/themes/Theme.js +84 -0
  120. package/vendor/symbiote-node/themes/carbon.js +137 -0
  121. package/vendor/symbiote-node/themes/dark.js +137 -0
  122. package/vendor/symbiote-node/themes/ebook.js +138 -0
  123. package/vendor/symbiote-node/themes/grey.js +137 -0
  124. package/vendor/symbiote-node/themes/light.js +137 -0
  125. package/vendor/symbiote-node/themes/neon.js +138 -0
  126. package/vendor/symbiote-node/themes/pcb.js +273 -0
  127. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  129. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  130. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  131. package/web/app.js +9 -5
  132. package/web/components/canvas-graph.js +1705 -0
  133. package/web/components/code-block.js +1 -1
  134. package/web/components/event-feed/CodeWidget.js +32 -0
  135. package/web/components/event-feed/EventWidget.js +97 -0
  136. package/web/components/event-feed/ListWidget.js +57 -0
  137. package/web/components/event-feed/MiniGraphWidget.js +159 -0
  138. package/web/components/follow-ribbon.js +134 -0
  139. package/web/dashboard.js +1 -1
  140. package/web/follow-controller.js +241 -0
  141. package/web/index.html +4 -0
  142. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  143. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  144. package/web/panels/code-viewer.js +50 -15
  145. package/web/panels/dep-graph.js +2691 -7
  146. package/web/panels/file-tree.js +5 -2
  147. package/web/panels/live-monitor.js +75 -3
  148. package/web/style.css +39 -0
  149. package/docs/img/explorer-compact.jpg +0 -0
  150. package/docs/img/explorer-expanded.jpg +0 -0
  151. package/src/.contextignore +0 -22
  152. package/src/.project-graph-cache.json +0 -1
  153. package/src/compact/.project-graph-cache.json +0 -1
  154. package/web/.project-graph-cache.json +0 -1
  155. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,451 @@
1
+ /**
2
+ * NodeEditor — central graph manager with event system
3
+ *
4
+ * Provides CRUD operations for nodes and connections with
5
+ * pre/post event hooks via callback-based event emitter.
6
+ * Replaces Rete.js Scope/Signal with simpler emit pattern.
7
+ *
8
+ * @module symbiote-node/core/Editor
9
+ */
10
+
11
+ import { Connection } from './Connection.js';
12
+ import { Node } from './Node.js';
13
+ import { Socket, Input, Output, InputControl } from './Socket.js';
14
+ import { Frame } from './Frame.js';
15
+
16
+ /**
17
+ * @typedef {'nodecreate'|'nodecreated'|'noderemove'|'noderemoved'|
18
+ * 'connectioncreate'|'connectioncreated'|'connectionremove'|'connectionremoved'|
19
+ * 'framecreate'|'framecreated'|'frameremove'|'frameremoved'|
20
+ * 'clear'|'cleared'|'nodeselect'|'nodedeselect'} EditorEvent
21
+ */
22
+
23
+ export class NodeEditor {
24
+ constructor() {
25
+ /** @type {Map<string, import('./Node.js').Node>} */
26
+ this.nodes = new Map();
27
+
28
+ /** @type {Map<string, Connection>} */
29
+ this.connections = new Map();
30
+
31
+ /** @type {Map<string, import('./Frame.js').Frame>} */
32
+ this.frames = new Map();
33
+
34
+ /** @type {Object<string, Set<function>>} */
35
+ this._listeners = {};
36
+ }
37
+
38
+ // --- Event System ---
39
+
40
+ /**
41
+ * Subscribe to editor event
42
+ * @param {EditorEvent} event
43
+ * @param {function} handler
44
+ * @returns {function} Unsubscribe function
45
+ */
46
+ on(event, handler) {
47
+ if (!this._listeners[event]) {
48
+ this._listeners[event] = new Set();
49
+ }
50
+ this._listeners[event].add(handler);
51
+ return () => this._listeners[event].delete(handler);
52
+ }
53
+
54
+ /**
55
+ * Emit event to all listeners
56
+ * @param {EditorEvent} event
57
+ * @param {*} data
58
+ * @returns {boolean} - false if any listener returned false (cancel)
59
+ */
60
+ emit(event, data) {
61
+ const handlers = this._listeners[event];
62
+ if (!handlers) return true;
63
+ for (const handler of handlers) {
64
+ if (handler(data) === false) return false;
65
+ }
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * Remove all event listeners (for clean teardown)
71
+ */
72
+ removeAllListeners() {
73
+ this._listeners = {};
74
+ }
75
+
76
+ // --- Node CRUD ---
77
+
78
+ /**
79
+ * Get node by ID
80
+ * @param {string} id
81
+ * @returns {import('./Node.js').Node|undefined}
82
+ */
83
+ getNode(id) {
84
+ return this.nodes.get(id);
85
+ }
86
+
87
+ /**
88
+ * Get all nodes
89
+ * @returns {import('./Node.js').Node[]}
90
+ */
91
+ getNodes() {
92
+ return [...this.nodes.values()];
93
+ }
94
+
95
+ /**
96
+ * Add node to editor
97
+ * @param {import('./Node.js').Node} node
98
+ * @returns {boolean}
99
+ */
100
+ addNode(node) {
101
+ if (this.nodes.has(node.id)) throw new Error('node already added');
102
+ if (!this.emit('nodecreate', node)) return false;
103
+ this.nodes.set(node.id, node);
104
+ this.emit('nodecreated', node);
105
+ return true;
106
+ }
107
+
108
+ /**
109
+ * Remove node and all its connections
110
+ * @param {string} id
111
+ * @returns {boolean}
112
+ */
113
+ removeNode(id) {
114
+ const node = this.nodes.get(id);
115
+ if (!node) throw new Error('node not found');
116
+ if (!this.emit('noderemove', node)) return false;
117
+
118
+ // Remove all connections to/from this node
119
+ for (const [connId, conn] of this.connections) {
120
+ if (conn.from === id || conn.to === id) {
121
+ this.removeConnection(connId);
122
+ }
123
+ }
124
+
125
+ this.nodes.delete(id);
126
+ this.emit('noderemoved', node);
127
+ return true;
128
+ }
129
+
130
+ // --- Connection CRUD ---
131
+
132
+ /**
133
+ * Get connection by ID
134
+ * @param {string} id
135
+ * @returns {Connection|undefined}
136
+ */
137
+ getConnection(id) {
138
+ return this.connections.get(id);
139
+ }
140
+
141
+ /**
142
+ * Get all connections
143
+ * @returns {Connection[]}
144
+ */
145
+ getConnections() {
146
+ return [...this.connections.values()];
147
+ }
148
+
149
+ /**
150
+ * Get connections for a specific node
151
+ * @param {string} nodeId
152
+ * @returns {Connection[]}
153
+ */
154
+ getNodeConnections(nodeId) {
155
+ return this.getConnections().filter(c => c.from === nodeId || c.to === nodeId);
156
+ }
157
+
158
+ /**
159
+ * Add connection
160
+ * @param {Connection} connection
161
+ * @returns {boolean}
162
+ */
163
+ addConnection(connection) {
164
+ if (this.connections.has(connection.id)) throw new Error('connection already added');
165
+ if (!this.emit('connectioncreate', connection)) return false;
166
+ this.connections.set(connection.id, connection);
167
+ this.emit('connectioncreated', connection);
168
+ return true;
169
+ }
170
+
171
+ /**
172
+ * Remove connection
173
+ * @param {string} id
174
+ * @returns {boolean}
175
+ */
176
+ removeConnection(id) {
177
+ const conn = this.connections.get(id);
178
+ if (!conn) return false;
179
+ if (!this.emit('connectionremove', conn)) return false;
180
+ this.connections.delete(id);
181
+ this.emit('connectionremoved', conn);
182
+ return true;
183
+ }
184
+
185
+ // --- Bulk Operations ---
186
+
187
+ /**
188
+ * Clear all nodes and connections
189
+ * @returns {boolean}
190
+ */
191
+ clear() {
192
+ if (!this.emit('clear', null)) return false;
193
+ for (const id of [...this.connections.keys()]) {
194
+ this.removeConnection(id);
195
+ }
196
+ for (const id of [...this.nodes.keys()]) {
197
+ this.removeNode(id);
198
+ }
199
+ this.emit('cleared', null);
200
+ return true;
201
+ }
202
+
203
+ // --- Frame CRUD ---
204
+
205
+ /**
206
+ * Get frame by ID
207
+ * @param {string} id
208
+ * @returns {import('./Frame.js').Frame|undefined}
209
+ */
210
+ getFrame(id) {
211
+ return this.frames.get(id);
212
+ }
213
+
214
+ /**
215
+ * Get all frames
216
+ * @returns {import('./Frame.js').Frame[]}
217
+ */
218
+ getFrames() {
219
+ return [...this.frames.values()];
220
+ }
221
+
222
+ /**
223
+ * Add frame
224
+ * @param {import('./Frame.js').Frame} frame
225
+ * @returns {boolean}
226
+ */
227
+ addFrame(frame) {
228
+ if (this.frames.has(frame.id)) throw new Error('frame already added');
229
+ if (!this.emit('framecreate', frame)) return false;
230
+ this.frames.set(frame.id, frame);
231
+ this.emit('framecreated', frame);
232
+ return true;
233
+ }
234
+
235
+ /**
236
+ * Remove frame
237
+ * @param {string} id
238
+ * @returns {boolean}
239
+ */
240
+ removeFrame(id) {
241
+ const frame = this.frames.get(id);
242
+ if (!frame) return false;
243
+ if (!this.emit('frameremove', frame)) return false;
244
+ this.frames.delete(id);
245
+ this.emit('frameremoved', frame);
246
+ return true;
247
+ }
248
+
249
+ // --- Serialization (agi-graph isomorphic bridge) ---
250
+
251
+ /**
252
+ * Serialize editor state to agi-graph workflow JSON format.
253
+ * Output is directly compatible with engine/Graph.fromJSON().
254
+ * @param {Object<string, number[]>} [positions] - Node positions {nodeId: [x, y]}
255
+ * @returns {object} Workflow JSON
256
+ */
257
+ toJSON(positions = {}) {
258
+ return {
259
+ version: 1,
260
+ nodes: this.getNodes().map((n) => {
261
+ const obj = {
262
+ id: n.id,
263
+ type: n.type,
264
+ name: n.label,
265
+ params: { ...n.params },
266
+ };
267
+ if (n.category && n.category !== 'default') obj.category = n.category;
268
+ if (n.shape && n.shape !== 'rect') obj.shape = n.shape;
269
+ if (n.icon) obj.icon = n.icon;
270
+ if (n.cacheMode && n.cacheMode !== 'auto') obj.cacheMode = n.cacheMode;
271
+
272
+ // Serialize port definitions for round-trip
273
+ const inputs = Object.entries(n.inputs);
274
+ if (inputs.length > 0) {
275
+ obj.inputs = inputs.map(([key, inp]) => ({
276
+ name: key,
277
+ type: inp.socket?.name || 'any',
278
+ label: inp.label || '',
279
+ }));
280
+ }
281
+ const outputs = Object.entries(n.outputs);
282
+ if (outputs.length > 0) {
283
+ obj.outputs = outputs.map(([key, out]) => ({
284
+ name: key,
285
+ type: out.socket?.name || 'any',
286
+ label: out.label || '',
287
+ }));
288
+ }
289
+ return obj;
290
+ }),
291
+ connections: this.getConnections().map((c) => ({
292
+ id: c.id,
293
+ from: c.from,
294
+ out: c.out,
295
+ to: c.to,
296
+ in: c.in,
297
+ })),
298
+ frames: this.getFrames().map((f) => ({
299
+ id: f.id,
300
+ label: f.label,
301
+ x: f.x,
302
+ y: f.y,
303
+ width: f.width,
304
+ height: f.height,
305
+ color: f.color,
306
+ })),
307
+ ui: {
308
+ positions: { ...positions },
309
+ },
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Reconstruct editor state from agi-graph workflow JSON.
315
+ * Enables round-trip: Editor → toJSON → fromJSON → Editor.
316
+ * Also accepts output from engine/Graph.toJSON().
317
+ * @param {object} data - Workflow JSON
318
+ * @param {Object<string, number[]>} [positionsOut] - Optional object to populate with positions
319
+ * @returns {NodeEditor} this
320
+ */
321
+ fromJSON(data, positionsOut) {
322
+ // Clear existing state (bypass events for bulk load)
323
+ this.nodes.clear();
324
+ this.connections.clear();
325
+ this.frames.clear();
326
+
327
+ // Restore nodes
328
+ for (const nd of (data.nodes || [])) {
329
+ const node = new Node(nd.name || nd.type, {
330
+ id: nd.id,
331
+ type: nd.type,
332
+ category: nd.category || 'default',
333
+ shape: nd.shape || 'rect',
334
+ icon: nd.icon || '',
335
+ });
336
+ node.params = { ...nd.params };
337
+ if (nd.cacheMode) node.cacheMode = nd.cacheMode;
338
+
339
+ // Auto-create InputControls from params for Inspector display
340
+ // Merge driver defaults into params (fills missing keys from handler definitions)
341
+ const driverParams = nd.driver?.params;
342
+ if (driverParams && !nd.params) nd.params = {};
343
+ if (driverParams) {
344
+ for (const [key, def] of Object.entries(driverParams)) {
345
+ if (!(key in nd.params) && def.default !== undefined) {
346
+ nd.params[key] = def.default;
347
+ }
348
+ }
349
+ }
350
+
351
+ if (nd.params) {
352
+ for (const [key, value] of Object.entries(nd.params)) {
353
+ /** @type {'text'|'number'|'textarea'|'select'|'boolean'} */
354
+ let controlType = 'text';
355
+ let displayValue = value;
356
+ let controlOptions = [];
357
+
358
+ // Use driver metadata for richer control type detection
359
+ const paramMeta = driverParams?.[key];
360
+ if (paramMeta?.type === 'boolean' || typeof value === 'boolean') {
361
+ controlType = 'boolean';
362
+ } else if (paramMeta?.type === 'number' || typeof value === 'number') {
363
+ controlType = 'number';
364
+ } else if (typeof value === 'string' && value.includes('\n')) {
365
+ controlType = 'textarea';
366
+ } else if (typeof value === 'object') {
367
+ controlType = 'textarea';
368
+ displayValue = JSON.stringify(value, null, 2);
369
+ }
370
+
371
+ // Select type from driver options
372
+ if (paramMeta?.options) {
373
+ controlType = 'select';
374
+ controlOptions = paramMeta.options;
375
+ }
376
+
377
+ node.addControl(key, new InputControl(controlType, {
378
+ initial: displayValue,
379
+ label: paramMeta?.description || key,
380
+ options: controlOptions,
381
+ }));
382
+ }
383
+ }
384
+
385
+ // Restore ports from serialized definitions
386
+ if (nd.inputs) {
387
+ for (const inp of nd.inputs) {
388
+ node.addInput(inp.name, new Input(new Socket(inp.type || 'any'), inp.label || ''));
389
+ }
390
+ }
391
+ if (nd.outputs) {
392
+ for (const out of nd.outputs) {
393
+ node.addOutput(out.name, new Output(new Socket(out.type || 'any'), out.label || ''));
394
+ }
395
+ }
396
+
397
+ this.nodes.set(node.id, node);
398
+ }
399
+
400
+ // Restore connections
401
+ for (const cd of (data.connections || [])) {
402
+ const srcNode = this.nodes.get(cd.from);
403
+ const tgtNode = this.nodes.get(cd.to);
404
+ if (!srcNode || !tgtNode) continue;
405
+
406
+ // Ensure ports exist (auto-create if coming from engine format without port defs)
407
+ if (!srcNode.outputs[cd.out]) {
408
+ srcNode.addOutput(cd.out, new Output(new Socket('any'), cd.out));
409
+ }
410
+ if (!tgtNode.inputs[cd.in]) {
411
+ tgtNode.addInput(cd.in, new Input(new Socket('any'), cd.in));
412
+ }
413
+
414
+ const conn = new Connection(srcNode, cd.out, tgtNode, cd.in);
415
+ if (cd.id) conn.id = cd.id;
416
+ this.connections.set(conn.id, conn);
417
+ }
418
+
419
+ // Restore frames
420
+ for (const fd of (data.frames || [])) {
421
+ const frame = new Frame(fd.label, {
422
+ id: fd.id,
423
+ x: fd.x,
424
+ y: fd.y,
425
+ width: fd.width,
426
+ height: fd.height,
427
+ color: fd.color,
428
+ });
429
+ this.frames.set(frame.id, frame);
430
+ }
431
+
432
+ // Extract positions
433
+ if (positionsOut && data.ui?.positions) {
434
+ Object.assign(positionsOut, data.ui.positions);
435
+ }
436
+
437
+ return this;
438
+ }
439
+
440
+ /**
441
+ * Convert editor state to an engine Graph instance for server-side execution.
442
+ * The Graph can be passed directly to Executor.run().
443
+ * @param {Object<string, number[]>} [positions] - Node positions
444
+ * @returns {import('../engine/Graph.js').Graph}
445
+ */
446
+ async toGraph(positions = {}) {
447
+ const { Graph } = await import('../engine/Graph.js');
448
+ const json = this.toJSON(positions);
449
+ return new Graph(json);
450
+ }
451
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Frame — visual grouping rectangle for nodes
3
+ *
4
+ * Pure data class (like Node). Stores position, size, color, label.
5
+ * Frame does not own nodes — containment is determined by spatial overlap.
6
+ *
7
+ * @module symbiote-node/core/Frame
8
+ */
9
+
10
+ import { uid } from './Socket.js';
11
+
12
+ export class Frame {
13
+ /**
14
+ * @param {string} label - Display label
15
+ * @param {object} [options]
16
+ * @param {string} [options.color='#4a9eff'] - Border/header color
17
+ * @param {number} [options.x=0] - X position
18
+ * @param {number} [options.y=0] - Y position
19
+ * @param {number} [options.width=400] - Width
20
+ * @param {number} [options.height=300] - Height
21
+ */
22
+ constructor(label, options = {}) {
23
+ this.id = options.id || uid();
24
+ this.label = label;
25
+ this.color = options.color || '#4a9eff';
26
+ this.x = options.x || 0;
27
+ this.y = options.y || 0;
28
+ this.width = options.width || 400;
29
+ this.height = options.height || 300;
30
+ }
31
+ }