project-graph-mcp 2.2.4 → 2.3.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 (151) hide show
  1. package/ARCHITECTURE.md +81 -0
  2. package/CHANGELOG.md +57 -0
  3. package/README.md +9 -4
  4. package/package.json +6 -13
  5. package/src/compact/expand.js +2 -4
  6. package/src/core/graph-builder.js +2 -2
  7. package/src/core/parser.js +2 -2
  8. package/src/network/server.js +1 -2
  9. package/src/network/web-server.js +4 -1
  10. package/vendor/symbiote-node/CHANGELOG.md +31 -0
  11. package/vendor/symbiote-node/LICENSE +21 -0
  12. package/vendor/symbiote-node/README.md +206 -0
  13. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  14. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  16. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  17. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  18. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  19. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  20. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  21. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  22. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  24. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  25. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  26. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  28. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  29. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  31. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  32. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  34. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  35. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  36. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  37. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  38. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  39. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  40. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  41. package/vendor/symbiote-node/core/Connection.js +45 -0
  42. package/vendor/symbiote-node/core/Editor.js +451 -0
  43. package/vendor/symbiote-node/core/Frame.js +31 -0
  44. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  45. package/vendor/symbiote-node/core/GraphText.js +210 -0
  46. package/vendor/symbiote-node/core/Node.js +143 -0
  47. package/vendor/symbiote-node/core/Portal.js +104 -0
  48. package/vendor/symbiote-node/core/Socket.js +185 -0
  49. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  50. package/vendor/symbiote-node/index.js +103 -0
  51. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  53. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  54. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  56. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  57. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  58. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  59. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  60. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  61. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  62. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  64. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  65. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  67. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  68. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  70. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  71. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  73. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  74. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  75. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  76. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  79. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  80. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  81. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  83. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  84. package/vendor/symbiote-node/layout/index.js +16 -0
  85. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  87. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  88. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  90. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  91. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  93. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  94. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  96. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  97. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  98. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  99. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  101. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  102. package/vendor/symbiote-node/package.json +59 -0
  103. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  105. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  106. package/vendor/symbiote-node/plugins/History.js +384 -0
  107. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  108. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  109. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  110. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  111. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  112. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  113. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  114. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  115. package/vendor/symbiote-node/shapes/index.js +53 -0
  116. package/vendor/symbiote-node/themes/Palette.js +32 -0
  117. package/vendor/symbiote-node/themes/Skin.js +113 -0
  118. package/vendor/symbiote-node/themes/Theme.js +84 -0
  119. package/vendor/symbiote-node/themes/carbon.js +137 -0
  120. package/vendor/symbiote-node/themes/dark.js +137 -0
  121. package/vendor/symbiote-node/themes/ebook.js +138 -0
  122. package/vendor/symbiote-node/themes/grey.js +137 -0
  123. package/vendor/symbiote-node/themes/light.js +137 -0
  124. package/vendor/symbiote-node/themes/neon.js +138 -0
  125. package/vendor/symbiote-node/themes/pcb.js +273 -0
  126. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  127. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  129. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  130. package/web/app.js +6 -5
  131. package/web/components/canvas-graph.js +1666 -0
  132. package/web/components/event-feed/CodeWidget.js +32 -0
  133. package/web/components/event-feed/EventWidget.js +97 -0
  134. package/web/components/event-feed/ListWidget.js +57 -0
  135. package/web/components/event-feed/MiniGraphWidget.js +69 -0
  136. package/web/dashboard.js +1 -1
  137. package/web/index.html +4 -0
  138. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  139. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  140. package/web/panels/code-viewer.js +50 -15
  141. package/web/panels/dep-graph.js +2712 -7
  142. package/web/panels/file-tree.js +5 -2
  143. package/web/panels/live-monitor.js +75 -3
  144. package/web/style.css +33 -0
  145. package/docs/img/explorer-compact.jpg +0 -0
  146. package/docs/img/explorer-expanded.jpg +0 -0
  147. package/src/.contextignore +0 -22
  148. package/src/.project-graph-cache.json +0 -1
  149. package/src/compact/.project-graph-cache.json +0 -1
  150. package/web/.project-graph-cache.json +0 -1
  151. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,348 @@
1
+ /**
2
+ * GraphMermaid — bidirectional Mermaid ↔ graph serialization
3
+ *
4
+ * Converts NodeEditor state to Mermaid flowchart syntax and back.
5
+ * Supports shapes, labeled connections, and subgraphs (frames).
6
+ *
7
+ * Mermaid shape mapping:
8
+ * circle → ((label))
9
+ * diamond → {label}
10
+ * pill → ([label])
11
+ * rect → [label]
12
+ * comment → >label]
13
+ *
14
+ * @module symbiote-node/core/GraphMermaid
15
+ */
16
+
17
+ // --- Shape mapping ---
18
+
19
+ const SHAPE_TO_MERMAID = {
20
+ circle: (id, label) => `${id}((${label}))`,
21
+ diamond: (id, label) => `${id}{${label}}`,
22
+ pill: (id, label) => `${id}([${label}])`,
23
+ rect: (id, label) => `${id}[${label}]`,
24
+ comment: (id, label) => `${id}>${label}]`,
25
+ };
26
+
27
+ /**
28
+ * Pattern matchers for Mermaid node shapes.
29
+ * Order matters — more specific patterns first.
30
+ * @type {Array<{re: RegExp, shape: string}>}
31
+ */
32
+ const MERMAID_SHAPE_PATTERNS = [
33
+ { re: /^(\w+)\(\((.+?)\)\)$/, shape: 'circle' },
34
+ { re: /^(\w+)\(\[(.+?)\]\)$/, shape: 'pill' },
35
+ { re: /^(\w+)\{(.+?)\}$/, shape: 'diamond' },
36
+ { re: /^(\w+)>(.+?)\]$/, shape: 'comment' },
37
+ { re: /^(\w+)\[(.+?)\]$/, shape: 'rect' },
38
+ ];
39
+
40
+ // --- Mermaid Arrow patterns ---
41
+
42
+ /**
43
+ * Arrow patterns with optional label.
44
+ * Supports: -->, --->, -->|label|, -- label -->
45
+ * @type {Array<{re: RegExp}>}
46
+ */
47
+ const ARROW_PATTERNS = [
48
+ // nodeA -->|label| nodeB
49
+ /^(.+?)\s*-->\|([^|]*)\|\s*(.+)$/,
50
+ // nodeA -- label --> nodeB
51
+ /^(.+?)\s*--\s+(.+?)\s+-->\s*(.+)$/,
52
+ // nodeA --> nodeB (no label)
53
+ /^(.+?)\s*-->\s*(.+)$/,
54
+ ];
55
+
56
+ /**
57
+ * Parse a node reference that might include inline shape definition.
58
+ *
59
+ * @param {string} raw - e.g. "trigger((Job Event))" or just "trigger"
60
+ * @returns {{ id: string, label: string|null, shape: string|null }}
61
+ */
62
+ function parseNodeRef(raw) {
63
+ const trimmed = raw.trim();
64
+ for (const { re, shape } of MERMAID_SHAPE_PATTERNS) {
65
+ const m = trimmed.match(re);
66
+ if (m) return { id: m[1], label: m[2], shape };
67
+ }
68
+ // Plain id reference
69
+ return { id: trimmed, label: null, shape: null };
70
+ }
71
+
72
+ /**
73
+ * Convert a NodeEditor to Mermaid flowchart syntax
74
+ *
75
+ * @param {import('./Editor.js').NodeEditor} editor
76
+ * @param {object} [options]
77
+ * @param {'LR'|'TB'|'RL'|'BT'} [options.direction='LR']
78
+ * @returns {string}
79
+ */
80
+ export function editorToMermaid(editor, options = {}) {
81
+ const { direction = 'LR' } = options;
82
+ const lines = [];
83
+
84
+ lines.push(`graph ${direction}`);
85
+
86
+ // Collect which nodes belong to which frame (by spatial containment)
87
+ const frames = editor.getFrames();
88
+ const nodeToFrame = new Map();
89
+
90
+ // We don't have positions in editor, so we rely on frame data
91
+ // Frames are matched by checking if any connection links nodes in the frame
92
+ // For simplicity, use frame label as subgraph name
93
+
94
+ // Build node declarations grouped by frame
95
+ const framedNodes = new Map(); // frameId -> [node]
96
+ const freeNodes = [];
97
+
98
+ // If frames exist, check node positions (stored in frame data)
99
+ // Since we don't have positions here, we serialize frames
100
+ // and let the user define membership via subgraph
101
+
102
+ // Collect all nodes
103
+ const allNodes = editor.getNodes();
104
+ const allConnections = editor.getConnections();
105
+
106
+ // Render nodes inside subgraphs (frames)
107
+ if (frames.length) {
108
+ // Frames without spatial data — output as subgraphs with all nodes listed
109
+ // In practice, frame membership is defined externally
110
+ for (const frame of frames) {
111
+ const nodeIds = frame._nodeIds || [];
112
+ if (nodeIds.length) {
113
+ for (const nid of nodeIds) {
114
+ nodeToFrame.set(nid, frame);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // Separate framed vs free nodes
121
+ for (const node of allNodes) {
122
+ if (nodeToFrame.has(node.id)) {
123
+ const frame = nodeToFrame.get(node.id);
124
+ if (!framedNodes.has(frame.id)) framedNodes.set(frame.id, []);
125
+ framedNodes.get(frame.id).push(node);
126
+ } else {
127
+ freeNodes.push(node);
128
+ }
129
+ }
130
+
131
+ // Render free nodes first
132
+ for (const node of freeNodes) {
133
+ lines.push(' ' + nodeToMermaid(node));
134
+ }
135
+
136
+ // If no frame membership data, put all nodes as free and create
137
+ // empty subgraphs as comments
138
+ if (framedNodes.size === 0 && frames.length > 0) {
139
+ // Output all nodes first
140
+ if (freeNodes.length === 0) {
141
+ for (const node of allNodes) {
142
+ lines.push(' ' + nodeToMermaid(node));
143
+ }
144
+ }
145
+ // Frames as subgraphs with node references
146
+ for (const frame of frames) {
147
+ lines.push('');
148
+ lines.push(` subgraph ${sanitizeId(frame.label)}["${frame.label}"]`);
149
+ lines.push(' direction TB');
150
+ lines.push(' end');
151
+ }
152
+ } else {
153
+ // Render subgraphs with their nodes
154
+ for (const [frameId, nodes] of framedNodes) {
155
+ const frame = frames.find(f => f.id === frameId);
156
+ if (!frame) continue;
157
+ lines.push('');
158
+ lines.push(` subgraph ${sanitizeId(frame.label)}["${frame.label}"]`);
159
+ lines.push(' direction TB');
160
+ for (const node of nodes) {
161
+ lines.push(' ' + nodeToMermaid(node));
162
+ }
163
+ lines.push(' end');
164
+ }
165
+ }
166
+
167
+ // Render connections
168
+ lines.push('');
169
+ for (const conn of allConnections) {
170
+ const label = conn.out === 'exec' ? '' : conn.out;
171
+ if (label) {
172
+ lines.push(` ${conn.from} -->|${label}| ${conn.to}`);
173
+ } else {
174
+ lines.push(` ${conn.from} --> ${conn.to}`);
175
+ }
176
+ }
177
+
178
+ return lines.join('\n');
179
+ }
180
+
181
+ /**
182
+ * Convert a single node to Mermaid declaration
183
+ * @param {import('./Node.js').Node} node
184
+ * @returns {string}
185
+ */
186
+ function nodeToMermaid(node) {
187
+ const shapeFn = SHAPE_TO_MERMAID[node.shape] || SHAPE_TO_MERMAID.rect;
188
+ return shapeFn(node.id, node.label);
189
+ }
190
+
191
+ /**
192
+ * Sanitize a string for use as Mermaid subgraph ID
193
+ * @param {string} str
194
+ * @returns {string}
195
+ */
196
+ function sanitizeId(str) {
197
+ return str.replace(/[^a-zA-Z0-9_]/g, '_');
198
+ }
199
+
200
+ /**
201
+ * Parse Mermaid flowchart text into graph data structure.
202
+ * Supports: node shapes, labeled arrows, subgraphs.
203
+ *
204
+ * @param {string} text
205
+ * @returns {{ nodes: Array, connections: Array, frames: Array, direction: string }}
206
+ */
207
+ export function mermaidToGraph(text) {
208
+ const nodes = new Map(); // id -> { id, name, shape, category }
209
+ const connections = [];
210
+ const frames = [];
211
+ const frameStack = []; // for nested subgraphs
212
+
213
+ let direction = 'LR';
214
+
215
+ /**
216
+ * Register a node from a parsed reference
217
+ * @param {{ id: string, label: string|null, shape: string|null }} ref
218
+ */
219
+ function registerNode(ref) {
220
+ if (!nodes.has(ref.id)) {
221
+ nodes.set(ref.id, {
222
+ id: ref.id,
223
+ name: ref.label || ref.id,
224
+ type: 'default',
225
+ shape: ref.shape || 'rect',
226
+ category: 'default',
227
+ });
228
+ } else if (ref.label && !nodes.get(ref.id).name) {
229
+ // Update label if first seen was bare reference
230
+ const existing = nodes.get(ref.id);
231
+ if (existing.name === existing.id) {
232
+ existing.name = ref.label;
233
+ }
234
+ if (ref.shape) existing.shape = ref.shape;
235
+ }
236
+ // Track frame membership
237
+ if (frameStack.length > 0) {
238
+ const currentFrame = frameStack[frameStack.length - 1];
239
+ if (!currentFrame._nodeIds) currentFrame._nodeIds = [];
240
+ if (!currentFrame._nodeIds.includes(ref.id)) {
241
+ currentFrame._nodeIds.push(ref.id);
242
+ }
243
+ }
244
+ }
245
+
246
+ for (const rawLine of text.split('\n')) {
247
+ const line = rawLine.trim();
248
+ if (!line || line.startsWith('%%')) continue; // skip empty and comments
249
+
250
+ // Graph direction
251
+ const dirMatch = line.match(/^graph\s+(LR|RL|TB|BT|TD)\s*$/);
252
+ if (dirMatch) {
253
+ direction = dirMatch[1] === 'TD' ? 'TB' : dirMatch[1];
254
+ continue;
255
+ }
256
+
257
+ // Flowchart direction (alias)
258
+ const flowMatch = line.match(/^flowchart\s+(LR|RL|TB|BT|TD)\s*$/);
259
+ if (flowMatch) {
260
+ direction = flowMatch[1] === 'TD' ? 'TB' : flowMatch[1];
261
+ continue;
262
+ }
263
+
264
+ // Subgraph start
265
+ const subMatch = line.match(/^subgraph\s+(\w+)(?:\["(.+?)"\])?\s*$/);
266
+ if (subMatch) {
267
+ const frame = {
268
+ label: subMatch[2] || subMatch[1],
269
+ color: '#4a9eff',
270
+ x: 0, y: 0,
271
+ width: 400, height: 300,
272
+ _nodeIds: [],
273
+ };
274
+ frameStack.push(frame);
275
+ frames.push(frame);
276
+ continue;
277
+ }
278
+
279
+ // Subgraph end
280
+ if (line === 'end') {
281
+ frameStack.pop();
282
+ continue;
283
+ }
284
+
285
+ // Direction inside subgraph
286
+ if (line.match(/^direction\s+(LR|RL|TB|BT|TD)$/)) continue;
287
+
288
+ // Try arrow patterns (connection lines)
289
+ let matched = false;
290
+ for (const pattern of ARROW_PATTERNS) {
291
+ const m = line.match(pattern);
292
+ if (m) {
293
+ matched = true;
294
+ if (m.length === 4) {
295
+ // With label: source, label, target
296
+ const source = parseNodeRef(m[1]);
297
+ const label = m[2].trim();
298
+ const target = parseNodeRef(m[3]);
299
+ registerNode(source);
300
+ registerNode(target);
301
+ connections.push({
302
+ from: source.id,
303
+ out: label || 'exec',
304
+ to: target.id,
305
+ in: 'exec',
306
+ });
307
+ } else if (m.length === 3) {
308
+ // No label: source, target
309
+ const source = parseNodeRef(m[1]);
310
+ const target = parseNodeRef(m[2]);
311
+ registerNode(source);
312
+ registerNode(target);
313
+ connections.push({
314
+ from: source.id,
315
+ out: 'exec',
316
+ to: target.id,
317
+ in: 'exec',
318
+ });
319
+ }
320
+ break;
321
+ }
322
+ }
323
+
324
+ // If not a connection, try as standalone node declaration
325
+ if (!matched) {
326
+ // Handle "nodeA & nodeB & nodeC" syntax
327
+ const parts = line.split(/\s*&\s*/);
328
+ for (const part of parts) {
329
+ const ref = parseNodeRef(part.trim());
330
+ if (ref.id && ref.id !== 'end' && !ref.id.startsWith('style') && !ref.id.startsWith('class')) {
331
+ registerNode(ref);
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ return {
338
+ nodes: [...nodes.values()],
339
+ connections,
340
+ frames: frames.map(f => ({
341
+ label: f.label,
342
+ color: f.color,
343
+ x: f.x, y: f.y,
344
+ width: f.width, height: f.height,
345
+ })),
346
+ direction,
347
+ };
348
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * GraphText — bidirectional text ↔ graph serialization
3
+ *
4
+ * Converts NodeEditor state to human-readable text and back.
5
+ * Useful for debugging, LLM-based generation, and quick iteration.
6
+ *
7
+ * Text format:
8
+ * NODES:
9
+ * [○ trigger] Job Event: RU (queue/job-event) shape=circle
10
+ * [◇ switch_status] Status? (flow/switch) shape=diamond
11
+ *
12
+ * CONNECTIONS:
13
+ * trigger.exec --> switch_status.exec
14
+ * switch_status.created --> fmt_created.exec
15
+ *
16
+ * FRAMES:
17
+ * [Formatters] color=#5cd87a x=490 y=-10 w=260 h=520
18
+ *
19
+ * @module symbiote-node/core/GraphText
20
+ */
21
+
22
+ const SHAPE_ICONS = {
23
+ circle: '○',
24
+ diamond: '◇',
25
+ pill: '⊃',
26
+ rect: '□',
27
+ comment: '✎',
28
+ };
29
+
30
+ const ICON_SHAPES = Object.fromEntries(
31
+ Object.entries(SHAPE_ICONS).map(([k, v]) => [v, k])
32
+ );
33
+
34
+ /**
35
+ * Convert a NodeEditor to human-readable text
36
+ *
37
+ * @param {import('./Editor.js').NodeEditor} editor
38
+ * @param {Object<string, number[]>} [positions] - {nodeId: [x, y]}
39
+ * @returns {string}
40
+ */
41
+ export function editorToText(editor, positions = {}) {
42
+ const lines = [];
43
+
44
+ // --- NODES ---
45
+ lines.push('NODES:');
46
+ for (const node of editor.getNodes()) {
47
+ const icon = SHAPE_ICONS[node.shape] || '□';
48
+ const ins = Object.keys(node.inputs);
49
+ const outs = Object.keys(node.outputs);
50
+ let line = `[${icon} ${node.id}] ${node.label} (${node.type})`;
51
+ if (node.shape !== 'rect') line += ` shape=${node.shape}`;
52
+ if (node.category !== 'default') line += ` cat=${node.category}`;
53
+ if (ins.length) line += ` in=[${ins.join(',')}]`;
54
+ if (outs.length) line += ` out=[${outs.join(',')}]`;
55
+ const pos = positions[node.id];
56
+ if (pos) line += ` @${pos[0]},${pos[1]}`;
57
+ lines.push(' ' + line);
58
+ }
59
+
60
+ // --- CONNECTIONS ---
61
+ lines.push('');
62
+ lines.push('CONNECTIONS:');
63
+ for (const conn of editor.getConnections()) {
64
+ lines.push(` ${conn.from}.${conn.out} --> ${conn.to}.${conn.in}`);
65
+ }
66
+
67
+ // --- FRAMES ---
68
+ const frames = editor.getFrames();
69
+ if (frames.length) {
70
+ lines.push('');
71
+ lines.push('FRAMES:');
72
+ for (const frame of frames) {
73
+ lines.push(` [${frame.label}] color=${frame.color} x=${frame.x} y=${frame.y} w=${frame.width} h=${frame.height}`);
74
+ }
75
+ }
76
+
77
+ return lines.join('\n');
78
+ }
79
+
80
+ /**
81
+ * Parse text representation back into graph data structure.
82
+ * Returns plain objects suitable for building a NodeEditor.
83
+ *
84
+ * @param {string} text
85
+ * @returns {{ nodes: Array, connections: Array, frames: Array, positions: Object }}
86
+ */
87
+ export function textToGraph(text) {
88
+ const nodes = [];
89
+ const connections = [];
90
+ const frames = [];
91
+ const positions = {};
92
+
93
+ let section = '';
94
+ for (const raw of text.split('\n')) {
95
+ const line = raw.trim();
96
+ if (!line) continue;
97
+
98
+ if (line === 'NODES:') { section = 'nodes'; continue; }
99
+ if (line === 'CONNECTIONS:') { section = 'connections'; continue; }
100
+ if (line === 'FRAMES:') { section = 'frames'; continue; }
101
+
102
+ if (section === 'nodes') {
103
+ // [○ trigger] Job Event: RU (queue/job-event) shape=circle cat=server in=[exec] out=[exec,data] @50,200
104
+ const m = line.match(/^\[(.)\s+(\S+)\]\s+(.+?)\s+\(([^)]+)\)(.*)$/);
105
+ if (!m) continue;
106
+
107
+ const [, shapeIcon, id, name, type, rest] = m;
108
+ const shape = ICON_SHAPES[shapeIcon] || 'rect';
109
+ const category = rest.match(/cat=(\S+)/)?.[1] || 'default';
110
+ const posMatch = rest.match(/@(-?\d+),(-?\d+)/);
111
+
112
+ nodes.push({ id, name, type, shape, category });
113
+ if (posMatch) {
114
+ positions[id] = [parseInt(posMatch[1]), parseInt(posMatch[2])];
115
+ }
116
+ }
117
+
118
+ if (section === 'connections') {
119
+ // trigger.exec --> switch_status.exec
120
+ const m = line.match(/^(\S+)\.(\S+)\s+-->\s+(\S+)\.(\S+)$/);
121
+ if (!m) continue;
122
+ const [, from, out, to, inp] = m;
123
+ connections.push({ from, out, to, in: inp });
124
+ }
125
+
126
+ if (section === 'frames') {
127
+ // [Formatters] color=#5cd87a x=490 y=-10 w=260 h=520
128
+ const m = line.match(/^\[([^\]]+)\]\s+(.*)$/);
129
+ if (!m) continue;
130
+ const [, label, rest] = m;
131
+ const color = rest.match(/color=(\S+)/)?.[1] || '#4a9eff';
132
+ const x = parseInt(rest.match(/x=(-?\d+)/)?.[1] || '0');
133
+ const y = parseInt(rest.match(/y=(-?\d+)/)?.[1] || '0');
134
+ const w = parseInt(rest.match(/w=(\d+)/)?.[1] || '400');
135
+ const h = parseInt(rest.match(/h=(\d+)/)?.[1] || '300');
136
+ frames.push({ label, color, x, y, width: w, height: h });
137
+ }
138
+ }
139
+
140
+ return { nodes, connections, frames, positions };
141
+ }
142
+
143
+ /**
144
+ * Build a NodeEditor from text representation
145
+ *
146
+ * @param {string} text
147
+ * @param {import('./Editor.js').NodeEditor} editor
148
+ * @param {{ Socket: Function, Node: Function, Input: Function, Output: Function, Connection: Function, Frame: Function }} classes
149
+ * @returns {{ editor: import('./Editor.js').NodeEditor, positions: Object }}
150
+ */
151
+ export function textToEditor(text, editor, classes) {
152
+ const { Node, Connection, Socket, Input, Output, Frame } = classes;
153
+ const { nodes, connections, frames, positions } = textToGraph(text);
154
+
155
+ const execSocket = new Socket('exec', { color: '#ffffff' });
156
+ const dataSocket = new Socket('data', { color: '#5cb8ff' });
157
+ const nodeMap = new Map();
158
+
159
+ // Collect ports from connections
160
+ const inPorts = {};
161
+ const outPorts = {};
162
+ for (const conn of connections) {
163
+ if (!inPorts[conn.to]) inPorts[conn.to] = new Set();
164
+ inPorts[conn.to].add(conn.in);
165
+ if (!outPorts[conn.from]) outPorts[conn.from] = new Set();
166
+ outPorts[conn.from].add(conn.out);
167
+ }
168
+
169
+ for (const n of nodes) {
170
+ const node = new Node(n.name, {
171
+ id: n.id,
172
+ type: n.type,
173
+ category: n.category,
174
+ shape: n.shape,
175
+ });
176
+
177
+ const ins = inPorts[n.id] || new Set();
178
+ const outs = outPorts[n.id] || new Set();
179
+
180
+ for (const port of ins) {
181
+ const isExec = port === 'exec' || port === 'trigger';
182
+ node.addInput(port, new Input(isExec ? execSocket : dataSocket, port === 'exec' ? '' : port));
183
+ }
184
+ for (const port of outs) {
185
+ const isExec = port === 'exec' || port === 'trigger';
186
+ node.addOutput(port, new Output(isExec ? execSocket : dataSocket, port === 'exec' ? '' : port));
187
+ }
188
+
189
+ editor.addNode(node);
190
+ nodeMap.set(n.id, node);
191
+ }
192
+
193
+ for (const conn of connections) {
194
+ const fromNode = nodeMap.get(conn.from);
195
+ const toNode = nodeMap.get(conn.to);
196
+ if (fromNode && toNode) {
197
+ editor.addConnection(new Connection(fromNode, conn.out, toNode, conn.in));
198
+ }
199
+ }
200
+
201
+ for (const f of frames) {
202
+ editor.addFrame(new Frame(f.label, {
203
+ x: f.x, y: f.y,
204
+ width: f.width, height: f.height,
205
+ color: f.color,
206
+ }));
207
+ }
208
+
209
+ return { editor, positions };
210
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Node — graph node with typed ports and controls
3
+ *
4
+ * Compatible with symbiote-node GraphNode structure.
5
+ * Ports are explicit objects (unlike symbiote-node where they're implicit).
6
+ *
7
+ * @module symbiote-node/core/Node
8
+ */
9
+
10
+ import { uid, Input, Output } from './Socket.js';
11
+
12
+ export class Node {
13
+ /**
14
+ * @param {string} label - Display name
15
+ * @param {object} [options]
16
+ * @param {string} [options.id] - Custom ID (default: auto-generated)
17
+ * @param {string} [options.type] - Node type identifier (e.g. 'ai/llm')
18
+ * @param {string} [options.category] - Category for styling (server/instance/control)
19
+ * @param {string} [options.shape] - Shape name (rect/pill/circle/diamond/comment)
20
+ * @param {string} [options.icon] - Material icon name for visual rendering
21
+ */
22
+ constructor(label, options = {}) {
23
+ /** @type {string} */
24
+ this.id = options.id || uid('nd');
25
+
26
+ /** @type {string} */
27
+ this.label = label;
28
+
29
+ /** @type {string} */
30
+ this.type = options.type || 'default';
31
+
32
+ /** @type {string} */
33
+ this.category = options.category || 'default';
34
+
35
+ /** @type {string} */
36
+ this.shape = options.shape || 'rect';
37
+
38
+ /** @type {string} */
39
+ this.icon = options.icon || '';
40
+
41
+ /** @type {Object<string, Input>} */
42
+ this.inputs = {};
43
+
44
+ /** @type {Object<string, Output>} */
45
+ this.outputs = {};
46
+
47
+ /** @type {Object<string, import('./Socket.js').Control>} */
48
+ this.controls = {};
49
+
50
+ /** @type {Object<string, *>} */
51
+ this.params = {};
52
+
53
+ /** @type {boolean} */
54
+ this.selected = false;
55
+
56
+ /** @type {boolean} */
57
+ this.collapsed = false;
58
+
59
+ /** @type {boolean} */
60
+ this.muted = false;
61
+ }
62
+
63
+ /**
64
+ * Check if input exists
65
+ * @param {string} key
66
+ * @returns {boolean}
67
+ */
68
+ hasInput(key) {
69
+ return key in this.inputs;
70
+ }
71
+
72
+ /**
73
+ * Add input port
74
+ * @param {string} key - Port key
75
+ * @param {Input} input - Input instance
76
+ */
77
+ addInput(key, input) {
78
+ if (this.hasInput(key)) throw new Error(`input '${key}' already exists`);
79
+ this.inputs[key] = input;
80
+ }
81
+
82
+ /**
83
+ * Remove input port
84
+ * @param {string} key
85
+ */
86
+ removeInput(key) {
87
+ delete this.inputs[key];
88
+ }
89
+
90
+ /**
91
+ * Check if output exists
92
+ * @param {string} key
93
+ * @returns {boolean}
94
+ */
95
+ hasOutput(key) {
96
+ return key in this.outputs;
97
+ }
98
+
99
+ /**
100
+ * Add output port
101
+ * @param {string} key - Port key
102
+ * @param {Output} output - Output instance
103
+ */
104
+ addOutput(key, output) {
105
+ if (this.hasOutput(key)) throw new Error(`output '${key}' already exists`);
106
+ this.outputs[key] = output;
107
+ }
108
+
109
+ /**
110
+ * Remove output port
111
+ * @param {string} key
112
+ */
113
+ removeOutput(key) {
114
+ delete this.outputs[key];
115
+ }
116
+
117
+ /**
118
+ * Check if control exists
119
+ * @param {string} key
120
+ * @returns {boolean}
121
+ */
122
+ hasControl(key) {
123
+ return key in this.controls;
124
+ }
125
+
126
+ /**
127
+ * Add control widget
128
+ * @param {string} key
129
+ * @param {import('./Socket.js').Control} control
130
+ */
131
+ addControl(key, control) {
132
+ if (this.hasControl(key)) throw new Error(`control '${key}' already exists`);
133
+ this.controls[key] = control;
134
+ }
135
+
136
+ /**
137
+ * Remove control widget
138
+ * @param {string} key
139
+ */
140
+ removeControl(key) {
141
+ delete this.controls[key];
142
+ }
143
+ }