project-graph-mcp 2.2.6 → 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 (150) 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 +1 -1
  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/vendor/symbiote-node/CHANGELOG.md +31 -0
  10. package/vendor/symbiote-node/LICENSE +21 -0
  11. package/vendor/symbiote-node/README.md +206 -0
  12. package/vendor/symbiote-node/canvas/AutoLayout.js +725 -0
  13. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +73 -0
  14. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +93 -0
  15. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +9 -0
  16. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +962 -0
  17. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +1468 -0
  18. package/vendor/symbiote-node/canvas/FlowSimulator.js +323 -0
  19. package/vendor/symbiote-node/canvas/ForceLayout.js +189 -0
  20. package/vendor/symbiote-node/canvas/ForceWorker.js +1325 -0
  21. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +97 -0
  22. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +176 -0
  23. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +12 -0
  24. package/vendor/symbiote-node/canvas/LODManager.js +88 -0
  25. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +71 -0
  26. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +207 -0
  27. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +9 -0
  28. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +261 -0
  29. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +1840 -0
  30. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +22 -0
  31. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +97 -0
  32. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +132 -0
  33. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +21 -0
  34. package/vendor/symbiote-node/canvas/NodeViewManager.js +584 -0
  35. package/vendor/symbiote-node/canvas/PinExpansion.js +131 -0
  36. package/vendor/symbiote-node/canvas/PseudoConnection.js +80 -0
  37. package/vendor/symbiote-node/canvas/SubgraphManager.js +201 -0
  38. package/vendor/symbiote-node/canvas/SubgraphRouter.js +443 -0
  39. package/vendor/symbiote-node/canvas/ViewportActions.js +446 -0
  40. package/vendor/symbiote-node/core/Connection.js +45 -0
  41. package/vendor/symbiote-node/core/Editor.js +451 -0
  42. package/vendor/symbiote-node/core/Frame.js +31 -0
  43. package/vendor/symbiote-node/core/GraphMermaid.js +348 -0
  44. package/vendor/symbiote-node/core/GraphText.js +210 -0
  45. package/vendor/symbiote-node/core/Node.js +143 -0
  46. package/vendor/symbiote-node/core/Portal.js +104 -0
  47. package/vendor/symbiote-node/core/Socket.js +185 -0
  48. package/vendor/symbiote-node/core/SubgraphNode.js +125 -0
  49. package/vendor/symbiote-node/index.js +103 -0
  50. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +361 -0
  51. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +332 -0
  52. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +96 -0
  53. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +104 -0
  54. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +133 -0
  55. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +33 -0
  56. package/vendor/symbiote-node/interactions/ConnectFlow.js +307 -0
  57. package/vendor/symbiote-node/interactions/Drag.js +102 -0
  58. package/vendor/symbiote-node/interactions/Selector.js +132 -0
  59. package/vendor/symbiote-node/interactions/SnapGrid.js +65 -0
  60. package/vendor/symbiote-node/interactions/Zoom.js +140 -0
  61. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +88 -0
  62. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +254 -0
  63. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +11 -0
  64. package/vendor/symbiote-node/layout/Layout/Layout.css.js +88 -0
  65. package/vendor/symbiote-node/layout/Layout/Layout.js +622 -0
  66. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +25 -0
  67. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +293 -0
  68. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +467 -0
  69. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +33 -0
  70. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +46 -0
  71. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +102 -0
  72. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +6 -0
  73. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +156 -0
  74. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +250 -0
  75. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +379 -0
  76. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +263 -0
  77. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +20 -0
  78. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +183 -0
  79. package/vendor/symbiote-node/layout/LayoutTree.js +246 -0
  80. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +43 -0
  81. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +89 -0
  82. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +14 -0
  83. package/vendor/symbiote-node/layout/index.js +16 -0
  84. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +61 -0
  85. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +79 -0
  86. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +19 -0
  87. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +41 -0
  88. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +24 -0
  89. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +16 -0
  90. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +65 -0
  91. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +29 -0
  92. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +13 -0
  93. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +683 -0
  94. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +92 -0
  95. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +17 -0
  96. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +25 -0
  97. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +7 -0
  98. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +90 -0
  99. package/vendor/symbiote-node/node/PortItem/PortItem.js +87 -0
  100. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +10 -0
  101. package/vendor/symbiote-node/package.json +59 -0
  102. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +143 -0
  103. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +131 -0
  104. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +16 -0
  105. package/vendor/symbiote-node/plugins/History.js +384 -0
  106. package/vendor/symbiote-node/plugins/Readonly.js +59 -0
  107. package/vendor/symbiote-node/shapes/CircleShape.js +80 -0
  108. package/vendor/symbiote-node/shapes/CommentShape.js +35 -0
  109. package/vendor/symbiote-node/shapes/DiamondShape.js +115 -0
  110. package/vendor/symbiote-node/shapes/NodeShape.js +80 -0
  111. package/vendor/symbiote-node/shapes/PillShape.js +91 -0
  112. package/vendor/symbiote-node/shapes/RectShape.js +72 -0
  113. package/vendor/symbiote-node/shapes/SVGShape.js +494 -0
  114. package/vendor/symbiote-node/shapes/index.js +53 -0
  115. package/vendor/symbiote-node/themes/Palette.js +32 -0
  116. package/vendor/symbiote-node/themes/Skin.js +113 -0
  117. package/vendor/symbiote-node/themes/Theme.js +84 -0
  118. package/vendor/symbiote-node/themes/carbon.js +137 -0
  119. package/vendor/symbiote-node/themes/dark.js +137 -0
  120. package/vendor/symbiote-node/themes/ebook.js +138 -0
  121. package/vendor/symbiote-node/themes/grey.js +137 -0
  122. package/vendor/symbiote-node/themes/light.js +137 -0
  123. package/vendor/symbiote-node/themes/neon.js +138 -0
  124. package/vendor/symbiote-node/themes/pcb.js +273 -0
  125. package/vendor/symbiote-node/themes/synthwave.js +137 -0
  126. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +86 -0
  127. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +128 -0
  128. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +29 -0
  129. package/web/app.js +6 -5
  130. package/web/components/canvas-graph.js +1666 -0
  131. package/web/components/event-feed/CodeWidget.js +32 -0
  132. package/web/components/event-feed/EventWidget.js +97 -0
  133. package/web/components/event-feed/ListWidget.js +57 -0
  134. package/web/components/event-feed/MiniGraphWidget.js +69 -0
  135. package/web/dashboard.js +1 -1
  136. package/web/index.html +4 -0
  137. package/web/panels/ActionBoard/ActionBoard.js +1 -1
  138. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -1
  139. package/web/panels/code-viewer.js +50 -15
  140. package/web/panels/dep-graph.js +2712 -7
  141. package/web/panels/file-tree.js +5 -2
  142. package/web/panels/live-monitor.js +75 -3
  143. package/web/style.css +33 -0
  144. package/docs/img/explorer-compact.jpg +0 -0
  145. package/docs/img/explorer-expanded.jpg +0 -0
  146. package/src/.contextignore +0 -22
  147. package/src/.project-graph-cache.json +0 -1
  148. package/src/compact/.project-graph-cache.json +0 -1
  149. package/web/.project-graph-cache.json +0 -1
  150. package/web/panels/SettingsPanel/.project-graph-cache.json +0 -1
@@ -0,0 +1,81 @@
1
+ ## Architecture
2
+
3
+ ```
4
+ project-graph-mcp/
5
+ ├── src/
6
+ │ ├── core/ # Foundation
7
+ │ │ ├── parser.js # AST parser (Acorn) + language routing
8
+ │ │ ├── graph-builder.js # Minified graph + legend
9
+ │ │ ├── filters.js # Exclude patterns, .gitignore
10
+ │ │ ├── workspace.js # Path resolution + traversal protection
11
+ │ │ └── event-bus.js # Tool call/result events for web UI
12
+ │ ├── analysis/ # Code quality analysis
13
+ │ │ ├── dead-code.js # Unused code detection
14
+ │ │ ├── complexity.js # Cyclomatic complexity
15
+ │ │ ├── similar-functions.js # Duplicate detection
16
+ │ │ ├── large-files.js # File size analysis
17
+ │ │ ├── outdated-patterns.js # Legacy pattern detection
18
+ │ │ ├── full-analysis.js # Health Score (0–100) + streaming + summary
19
+ │ │ ├── jsdoc-checker.js # JSDoc ↔ AST consistency validator
20
+ │ │ ├── jsdoc-generator.js # JSDoc template generation
21
+ │ │ ├── type-checker.js # Optional tsc wrapper (async)
22
+ │ │ ├── undocumented.js # Missing JSDoc finder
23
+ │ │ ├── custom-rules.js # Configurable lint rules
24
+ │ │ ├── test-annotations.js # .ctx.md test checklist parsing
25
+ │ │ ├── db-analysis.js # SQL schema + table usage
26
+ │ │ └── analysis-cache.js # Incremental cache (.context/.cache/)
27
+ │ ├── compact/ # AI context compression
28
+ │ │ ├── compress.js # Terser minification + export legend
29
+ │ │ ├── compact.js # Project-wide compact/beautify (mangle: false)
30
+ │ │ ├── expand.js # Decompile: name restoration from .ctx
31
+ │ │ ├── doc-dialect.js # Doc Dialect (.context/ format)
32
+ │ │ ├── ctx-to-jsdoc.js # .ctx → JSDoc injection + stripping
33
+ │ │ ├── ai-context.js # AI boot aggregator
34
+ │ │ ├── mode-config.js # Compact mode config (1/2/3/4)
35
+ │ │ ├── validate-pipeline.js # .ctx ↔ source contract validation
36
+ │ │ ├── framework-references.js # Framework-specific docs
37
+ │ │ └── instructions.js # Agent guidelines
38
+ │ ├── lang/ # Multi-language parsers
39
+ │ │ ├── lang-typescript.js # TypeScript/TSX regex parser
40
+ │ │ ├── lang-python.js # Python regex parser
41
+ │ │ ├── lang-go.js # Go regex parser
42
+ │ │ ├── lang-sql.js # SQL extraction (tables, columns)
43
+ │ │ └── lang-utils.js # Shared: stripStringsAndComments
44
+ │ ├── cli/ # CLI interface
45
+ │ │ ├── cli.js # CLI entry point + help
46
+ │ │ └── cli-handlers.js # CLI command handlers
47
+ │ ├── mcp/ # MCP protocol
48
+ │ │ ├── mcp-server.js # MCP server + response hints
49
+ │ │ ├── tool-defs.js # MCP tool schemas (18 grouped tools)
50
+ │ │ └── tools.js # Graph tools (skeleton, expand, deps)
51
+ │ └── network/ # Server & networking
52
+ │ ├── server.js # Entry point (CLI/MCP/Serve mode switch)
53
+ │ ├── backend.js # Background backend process
54
+ │ ├── backend-lifecycle.js # Port file management + stdio proxy
55
+ │ ├── web-server.js # HTTP + WebSocket server for web UI
56
+ │ ├── local-gateway.js # Multi-project gateway registry
57
+ │ └── mdns.js # mDNS/DNS-SD service advertisement
58
+ ├── web/ # Web dashboard
59
+ │ ├── index.html / dashboard.html
60
+ │ ├── app.js / dashboard.js # Application entry points
61
+ │ ├── state.js # WebSocket state management
62
+ │ └── panels/ # UI components (Symbiote.js)
63
+ ├── rules/ # Pre-built rule sets (JSON)
64
+ ├── vendor/
65
+ │ ├── acorn.mjs # AST parser (MIT, vendored)
66
+ │ ├── walk.mjs # AST walker (MIT, vendored)
67
+ │ └── terser.mjs # JS minifier (BSD, vendored)
68
+ ├── tests/
69
+ │ ├── parser.test.js # AST parser + graph builder
70
+ │ ├── mcp.test.js # MCP tool integration
71
+ │ ├── compact.test.js # Compact/beautify, ctx-to-jsdoc
72
+ │ ├── consolidated.test.js # Analysis tools tests
73
+ │ └── orm.test.js # ORM/SQL test cases
74
+ ├── docs/ # Public documentation
75
+ │ ├── ROADMAP.md
76
+ │ ├── examples/ # AGENT_ROLE templates
77
+ │ └── references/ # Framework reference docs
78
+ └── dev-docs/ # Internal R&D (not public)
79
+ ├── ideas/ # Feature research & design docs
80
+ └── prototypes/ # Experimental code
81
+ ```
package/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [2.2.8] — 2026-04-24
6
+
7
+ ### Fixed
8
+ - **Skeleton exports regression**: `createSkeleton()` was pushing `{id, l}` objects instead of plain string IDs into the `X` (exports) field. This broke `ctx-panel`, `dep-graph`, and integration tests. Exports now consistently contain string legend keys.
9
+ - **Expand pipeline crash**: `expandFile()` would crash with `Unexpected token` when scope-aware variable renaming produced invalid JS (e.g., callback parameter shadowing). Now falls back to beautified code without rename when re-parse fails.
10
+ - **Console.log pollution**: Removed debug `console.log` and `console.time` calls from `dep-graph.js` and `ActionBoard.js` that leaked into the browser console.
11
+
12
+ ### Added
13
+ - **`.npmignore`**: Excludes debug artifacts (`web/debug-*`, `web/test-*`, `web/tune-*`), unused vendor directories (`engine/`, `demo/`, `tests/`), `.context/`, and internal tooling from the published npm package. Reduces package from 301 to ~237 files.
14
+ - **`.gitignore` patterns**: Added patterns for debug/test files to prevent re-committing.
15
+
16
+ ### Removed
17
+ - 9 debug/temp files removed from repository: `patch.js`, `temp_output.txt`, `temp_output_rects.txt`, `test-graph.js`, `web/debug-patch.js`, `web/debug-sleep.js`, `web/test-force-sim.html`, `web/test-graph-data.json`, `web/tune-physics.js`.
18
+ - `"files"` whitelist from `package.json` (replaced by `.npmignore` for granular exclusion control).
19
+
20
+ ## [2.2.7] — 2026-04-22
21
+
22
+ ### Added
23
+ - Force-directed graph layout with ForceWorker (continuous mode, live tick feedback)
24
+ - Hierarchical directory nesting with universal URL routing (`#graph/path/to/dir`)
25
+ - Focus-driven graph exploration mode (radial layout with imports/dependents hemispheres)
26
+ - File tree sidebar with bidirectional sync to graph view
27
+ - Path style toggle (PCB/orthogonal, straight, bezier)
28
+ - Mode toggle URL routing (`?mode=flat|tree`)
29
+ - Code viewer two-way linking with file tree and graph
30
+ - Depth-of-field effect during node drag interaction
31
+
32
+ ### Fixed
33
+ - Graph navigation routing stability (URL path corruption, deep-linking)
34
+ - Force layout convergence and phantom position sync
35
+ - Node overlap inside subgraphs on page refresh
36
+ - LOD phantom promotion and text clipping
37
+ - PCB obstacle avoidance threshold at >200 nodes
38
+ - Severe lag when toggling TREE/FLAT inside subgraph
39
+
40
+ ### Changed
41
+ - Migrated from d3-force to pure force layout engine (zero external dependencies)
42
+ - Centralized fitView, flyToNode, LOD, and PinExpansion logic in symbiote-node
43
+ - `symbiote-node` updated to v0.3.0 with 41/41 force layout tests passing
44
+
45
+ ## [2.2.5] — 2026-04-08
46
+
47
+ ### Added
48
+ - Expand pipeline with JSDoc injection from `.ctx` documentation
49
+ - Scope-aware identifier deduplication in expand renaming
50
+ - Validate pipeline for compact ↔ expand round-trip integrity
51
+
52
+ ## [2.2.4] — 2026-03-28
53
+
54
+ ### Added
55
+ - Web explorer with interactive dependency graph visualization
56
+ - SubgraphRouter for hierarchical graph navigation
57
+ - Canvas-based graph rendering with LOD system
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  **Maximize your AI agent's context window.** An MCP server that lets agents read and edit your codebase in **compact mode** — minified source with all variable names preserved. Code tokens drop **↓40%**, and `.ctx` documentation is injected only in the focus zone. Fewer tokens per file → more files fit in context → **deeper understanding of your codebase**.
8
8
 
9
- ![Expanded view — formatted code with JSDoc, 28+ lines per function](docs/img/explorer-expanded.jpg)
9
+ ![Expanded view — formatted code with JSDoc, 28+ lines per function](https://raw.githubusercontent.com/rnd-pro/project-graph-mcp/main/docs/img/explorer-expanded.jpg)
10
10
 
11
- ![Compact mode — same file, 14 lines total, ↓40% tokens. Agents read and edit this directly.](docs/img/explorer-compact.jpg)
11
+ ![Compact mode — same file, 14 lines total, ↓40% tokens. Agents read and edit this directly.](https://raw.githubusercontent.com/rnd-pro/project-graph-mcp/main/docs/img/explorer-compact.jpg)
12
12
 
13
13
  Includes a built-in [Web Dashboard](#web-dashboard) (`npx project-graph-mcp serve`) to visualize token metrics and compact ⇄ raw code in real-time.
14
14
 
@@ -328,12 +328,17 @@ See **[CONFIGURATION.md](CONFIGURATION.md)** for all supported IDEs (Antigravity
328
328
  <summary>Alternative: from source</summary>
329
329
 
330
330
  ```bash
331
- git clone https://github.com/rnd-pro/project-graph-mcp
331
+ git clone --recursive https://github.com/rnd-pro/project-graph-mcp
332
332
  cd project-graph-mcp
333
- # No npm install needed — zero dependencies
333
+ npm install
334
334
  # Use "node /path/to/project-graph-mcp/src/network/server.js" as the command in MCP config
335
335
  ```
336
336
 
337
+ > **Note:** The `--recursive` flag is required to fetch the `vendor/symbiote-node` submodule. If you already cloned without it, run:
338
+ > ```bash
339
+ > git submodule update --init --recursive
340
+ > ```
341
+
337
342
  </details>
338
343
 
339
344
  ## CLI
package/package.json CHANGED
@@ -1,23 +1,16 @@
1
1
  {
2
2
  "name": "project-graph-mcp",
3
- "version": "2.2.6",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "MCP server for AI agents — project graph, code quality analysis, visual web explorer. JS, TS, Python, Go.",
6
6
  "main": "src/network/server.js",
7
7
  "bin": {
8
8
  "project-graph-mcp": "src/network/server.js"
9
9
  },
10
- "files": [
11
- "src/",
12
- "web/",
13
- "vendor/",
14
- "rules/",
15
- "docs/",
16
- "GUIDE.md",
17
- "CONFIGURATION.md",
18
- "README.md",
19
- "LICENSE"
20
- ],
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+
21
14
  "scripts": {
22
15
  "start": "node src/network/server.js",
23
16
  "test": "node --test tests/*.test.js"
@@ -47,7 +40,7 @@
47
40
  "license": "MIT",
48
41
  "dependencies": {
49
42
  "@symbiotejs/symbiote": "^3.2.1",
50
- "symbiote-node": "^0.2.0",
43
+ "symbiote-node": "file:vendor/symbiote-node",
51
44
  "ws": "^8.20.0"
52
45
  },
53
46
  "engines": {
@@ -2,7 +2,7 @@
2
2
  import{parseCtxParams as h,buildJSDocBlock as g}from"./jsdoc-builder.js";import{readCtxFile as $}from"./ctx-resolver.js";import{walkJSFiles}from"../core/file-walker.js";import{readFileSync as t,writeFileSync as e,mkdirSync as n,existsSync as s}from"fs";import{join as i,basename as a,extname as c,dirname as p,relative as l}from"path";import{minify as m}from"../../vendor/terser.mjs";import{parse as d}from"../../vendor/acorn.mjs";import{simple as f,ancestor as u}from"../../vendor/walk.mjs";
3
3
 
4
4
  function y(t){if(!t)return null;const e=t.match(/^→([A-Z][\w<>\[\]|]*)/);return e?e[1]:null}function w(t,e,n,s,o){const r=new Map;let i=[];for(const t of e.body)if("ImportDeclaration"===t.type)for(const e of t.specifiers)if("ImportSpecifier"===e.type&&e.imported.name!==e.local.name)r.set(e.local.name,e.imported.name),i.push({s:e.start,e:e.end,n:e.imported.name,k:e.local.name});else if("ImportDefaultSpecifier"===e.type){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:n,k:e.local.name}))}else if("ImportNamespaceSpecifier"===e.type&&e.local.name.length<=2){const n=t.source.value.replace(/^node:/,"").split("/").pop().replace(/\.\\w+$/,"").replace(/-(\w)/g,(t,e)=>e.toUpperCase());n&&/^[a-zA-Z_$][\w$]*$/.test(n)&&n!==e.local.name&&(r.set(e.local.name,n),i.push({s:e.start,e:e.end,n:"* as "+n,k:e.local.name}))}for(const[t,e]of s)r.set(t,e);if(o.has("__top__"))for(const[t,e]of o.get("__top__"))r.set(t,e);const a=new Set(r.values());for(const[t]of[...r])a.has(t)&&r.delete(t);{const _u=new Set;for(const[k,v]of[...r])_u.has(v)?(r.delete(k),i=i.filter(x=>x.k!==k)):_u.add(v)}const c=[],p=[],l=[];const m=t=>{const e=t.params.map(t=>"Identifier"===t.type?t.name:"AssignmentPattern"===t.type&&"Identifier"===t.left?.type?t.left.name:"RestElement"===t.type&&"Identifier"===t.argument?.type?t.argument.name:null).filter(Boolean),s=new Map;if(t.id?.name){const r=n.get(t.id.name);if(r?.params)for(let t=0;t<Math.min(e.length,r.params.length);t++)e[t]!==r.params[t].name&&s.set(e[t],r.params[t].name);const i=o.get(t.id.name);if(i)for(const[t,e]of i)s.has(t)||s.set(t,e);if(s.size>0)for(const e of t.params){const t="Identifier"===e.type?e:"AssignmentPattern"===e.type&&"Identifier"===e.left?.type?e.left:null;t&&s.has(t.name)&&p.push({s:t.start,e:t.end,n:s.get(t.name)})}}const r=function(t){const e=new Set;return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.add(t.id.name)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.add(t.param.name)}}),e}(t.body);for(const t of e)r.add(t);const _tgts=Array.from(s.values());for(const t of _tgts){if(r.has(t)&&!s.has(t)){let n=t+"_";while(r.has(n)||_tgts.includes(n))n+="_";s.set(t,n)}}const i=t.id?.name&&o.get(t.id.name);for(const e of function(t){const e=[];return f(t,{VariableDeclarator(t){t.id&&"Identifier"===t.id.type&&e.push(t.id)},CatchClause(t){t.param&&"Identifier"===t.param.type&&e.push(t.param)}}),e}(t.body)){const t=s.has(e.name)?s.get(e.name):i?.get(e.name);t&&l.push({s:e.start,e:e.end,n:t})}c.push({s:t.params.length>0?t.params[0].start:t.body.start,e:t.body.end,p:r,r:s})};f(e,{FunctionDeclaration:m,FunctionExpression:m,ArrowFunctionExpression:m});for(const t of e.body)if("VariableDeclaration"===t.type)for(const e of t.declarations)e.id&&"Identifier"===e.id.type&&r.has(e.id.name)&&l.push({s:e.id.start,e:e.id.end,n:r.get(e.id.name)});const d=[...i,...p,...l];u(e,{Identifier(t,e,n){const s=n[n.length-2];if("MemberExpression"===s?.type&&s.property===t&&!s.computed)return;if("Property"===s?.type&&s.key===t&&!s.computed&&s.value!==t)return;if("ExportSpecifier"===s?.type)return;const o=t.name;const scopes=c.filter(e=>t.start>=e.s&&t.end<=e.e).sort((a,b)=>(a.e-a.s)-(b.e-b.s));let mapped=!1,isLocal=!1;for(const e of scopes){if(e.r.has(o)){d.push({s:t.start,e:t.end,n:e.r.get(o)});mapped=!0;break}if(e.p.has(o)){isLocal=!0;break}}if(!mapped&&!isLocal&&r.has(o)){d.push({s:t.start,e:t.end,n:r.get(o)})}}});d.sort((t,e)=>e.s-t.s);let h=t;for(const t of d)h=h.slice(0,t.s)+t.n+h.slice(t.e);return h}
5
- export async function expandFile(e,n,s={}){const{indentLevel:o=2}=s,r=t(e,"utf-8");if(!r.trim())return{code:"",injected:0,original:0,decompiled:0};let i;try{i=(await m(r,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:o,semicolons:!0}})).code||r}catch{i=r}{const t=i.split("\n"),e=[];for(let n=0;n<t.length;n++){const s=t[n];if(""===s.trim()){let s=n+1;for(;s<t.length&&""===t[s].trim();)s++;if(n>0&&e.length>0&&e[e.length-1].startsWith("import ")&&s<t.length&&t[s].startsWith("import "))continue}e.push(s)}i=e.join("\n")}i=i.replace(/^( +)/gm,t=>{const e=Math.floor(t.length/o);return"\t".repeat(e)+" ".repeat(t.length%o)});const a=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(!t||t.startsWith("---")||t.startsWith("@")||t.startsWith("CALLS")||t.startsWith("R→")||t.startsWith("W→")||t.startsWith("PATTERNS:")||t.startsWith("EDGE_CASES:")||t.startsWith("Rules:")||t.startsWith("Save this"))continue;const s=t.match(/^class\s+([\w]+)([^|]*)\|([^|]*)\|?(.*)$/);if(s){e.set(s[1],{type:"class",extends:s[2].replace(/\s*extends\s*/,"").trim()||null,meta:s[3].trim(),description:s[4]?.trim()||"",exported:!1});continue}const o=t.match(/^\s+\.(\w+)\(([^)]*)\)\|?(.*)$/);if(o){e.set(o[1],{type:"method",params:h(o[2]),description:o[3]?.trim()||""});continue}const r=t.match(/^(export\s+)?(\w+)\(([^)]*)\)(→[^|]*)?\|(.*)$/);if(r){const t=r[2],n=r[3],s=r[4]||"",o=(r[5]||"").split("|"),i=o[0]?.trim()||"";e.set(t,{type:"function",params:h(n),returnType:y(s),description:i,exported:!!r[1]});continue}}return e}(n),c=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@vars ")){const n=t.slice(6).split(",");for(const t of n){const n=t.trim().split("=");2===n.length&&e.set(n[0].trim(),n[1].trim())}}}return e}(n),p=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@names ")){const n=t.slice(7).split(/\s+/);for(const t of n){const n=t.indexOf(":");if(-1===n)continue;const s=t.slice(0,n),o=t.slice(n+1),r=new Map;for(const t of o.split(",")){const e=t.trim().split("=");2===e.length&&r.set(e[0].trim(),e[1].trim())}r.size>0&&e.set(s,r)}}}return e}(n);let l;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}i=w(i,l,a,c,p);l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0});if(0===a.size)return{code:i,injected:0,original:r.length,decompiled:i.length};const u=[];f(l,{ExportNamedDeclaration(t){const e=t.declaration;if(e){if("FunctionDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&u.push({pos:t.start,jsdoc:g(n)})}if("ClassDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&n.description&&u.push({pos:t.start,jsdoc:`/**\n * ${n.description}\n */`})}}},FunctionDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&u.push({pos:t.start,jsdoc:g(e)})},ClassDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&e.description&&u.push({pos:t.start,jsdoc:`/**\n * ${e.description}\n */`})}}),u.sort((t,e)=>e.pos-t.pos);let x=i,j=0;for(const{pos:t,jsdoc:e}of u){const n=x.lastIndexOf("\n",t-1),s=-1===n?0:n+1,o=x.slice(s,t).match(/^(\s*)/)?.[1]||"",r=e.split("\n").map(t=>o+t).join("\n");x=x.slice(0,t)+r+"\n"+x.slice(t),j++}return{code:x,injected:j,original:r.length,decompiled:x.length}}
5
+ export async function expandFile(e,n,s={}){const{indentLevel:o=2}=s,r=t(e,"utf-8");if(!r.trim())return{code:"",injected:0,original:0,decompiled:0};let i;try{i=(await m(r,{compress:!1,mangle:!1,module:!0,output:{beautify:!0,comments:!1,indent_level:o,semicolons:!0}})).code||r}catch{i=r}{const t=i.split("\n"),e=[];for(let n=0;n<t.length;n++){const s=t[n];if(""===s.trim()){let s=n+1;for(;s<t.length&&""===t[s].trim();)s++;if(n>0&&e.length>0&&e[e.length-1].startsWith("import ")&&s<t.length&&t[s].startsWith("import "))continue}e.push(s)}i=e.join("\n")}i=i.replace(/^( +)/gm,t=>{const e=Math.floor(t.length/o);return"\t".repeat(e)+" ".repeat(t.length%o)});const a=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(!t||t.startsWith("---")||t.startsWith("@")||t.startsWith("CALLS")||t.startsWith("R→")||t.startsWith("W→")||t.startsWith("PATTERNS:")||t.startsWith("EDGE_CASES:")||t.startsWith("Rules:")||t.startsWith("Save this"))continue;const s=t.match(/^class\s+([\w]+)([^|]*)\|([^|]*)\|?(.*)$/);if(s){e.set(s[1],{type:"class",extends:s[2].replace(/\s*extends\s*/,"").trim()||null,meta:s[3].trim(),description:s[4]?.trim()||"",exported:!1});continue}const o=t.match(/^\s+\.(\w+)\(([^)]*)\)\|?(.*)$/);if(o){e.set(o[1],{type:"method",params:h(o[2]),description:o[3]?.trim()||""});continue}const r=t.match(/^(export\s+)?(\w+)\(([^)]*)\)(→[^|]*)?\|(.*)$/);if(r){const t=r[2],n=r[3],s=r[4]||"",o=(r[5]||"").split("|"),i=o[0]?.trim()||"";e.set(t,{type:"function",params:h(n),returnType:y(s),description:i,exported:!!r[1]});continue}}return e}(n),c=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@vars ")){const n=t.slice(6).split(",");for(const t of n){const n=t.trim().split("=");2===n.length&&e.set(n[0].trim(),n[1].trim())}}}return e}(n),p=function(t){const e=new Map;if(!t)return e;for(const n of t.split("\n")){const t=n.trim();if(t.startsWith("@names ")){const n=t.slice(7).split(/\s+/);for(const t of n){const n=t.indexOf(":");if(-1===n)continue;const s=t.slice(0,n),o=t.slice(n+1),r=new Map;for(const t of o.split(",")){const e=t.trim().split("=");2===e.length&&r.set(e[0].trim(),e[1].trim())}r.size>0&&e.set(s,r)}}}return e}(n);let l;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}const _preRename=i;i=w(i,l,a,c,p);try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{i=_preRename;try{l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{return{code:i,injected:0,original:r.length,decompiled:i.length}}};if(0===a.size)return{code:i,injected:0,original:r.length,decompiled:i.length};const u=[];f(l,{ExportNamedDeclaration(t){const e=t.declaration;if(e){if("FunctionDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&u.push({pos:t.start,jsdoc:g(n)})}if("ClassDeclaration"===e.type&&e.id?.name){const n=a.get(e.id.name);n&&n.description&&u.push({pos:t.start,jsdoc:`/**\n * ${n.description}\n */`})}}},FunctionDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&u.push({pos:t.start,jsdoc:g(e)})},ClassDeclaration(t){if(!t.id?.name)return;const e=a.get(t.id.name);e&&!e.exported&&e.description&&u.push({pos:t.start,jsdoc:`/**\n * ${e.description}\n */`})}}),u.sort((t,e)=>e.pos-t.pos);let x=i,j=0;for(const{pos:t,jsdoc:e}of u){const n=x.lastIndexOf("\n",t-1),s=-1===n?0:n+1,o=x.slice(s,t).match(/^(\s*)/)?.[1]||"",r=e.split("\n").map(t=>o+t).join("\n");x=x.slice(0,t)+r+"\n"+x.slice(t),j++}return{code:x,injected:j,original:r.length,decompiled:x.length}}
6
6
 
7
7
 
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @ctx .context/src/core/graph-builder.ctx
2
2
  export function minifyLegend(s){const t={},o=new Set;for(const n of s){let s=e(n),c=1;for(;o.has(s);)s=e(n)+c,c++;o.add(s),t[n]=s}return t}
3
3
  function e(e){const s=e.replace(/[a-z]/g,"");if(s.length>=2)return s.slice(0,3);const t=e.match(/[A-Z]/g);return t&&t.length>0?e[0].toLowerCase()+t[0]:e.slice(0,2)}
4
- export function buildGraph(e){const s=e.classes||[],t=e.functions||[],o=[...s.map(e=>e.name),...t.map(e=>e.name),...s.flatMap(e=>e.methods||[])],n=minifyLegend([...new Set(o)]),c=Object.fromEntries(Object.entries(n).map(([e,s])=>[s,e])),f={v:1,legend:n,reverseLegend:c,stats:{files:(e.files||[]).length,classes:s.length,functions:t.length,tables:(e.tables||[]).length},nodes:{},edges:[],orphans:[],duplicates:{},files:e.files||[]};for(const e of s){const s=n[e.name];f.nodes[s]={t:"C",x:e.extends||void 0,m:(e.methods||[]).map(e=>n[e]||e),$:(e.properties||[]).length?e.properties:void 0,i:e.imports?.length?e.imports:void 0,f:e.file||void 0};for(const t of e.calls||[])if(t.includes(".")){const[e,o]=t.split(".");if(n[e]){const t=[s,"→",`${n[e]}.${n[o]||o}`];f.edges.push(t)}}else if(n[t]){const e=[s,"→",n[t]];f.edges.push(e)}}for(const e of t){const s=n[e.name];f.nodes[s]={t:"F",e:e.exported,f:e.file||void 0};for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const e of s){const s=n[e.name];for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const s of e.tables||[])f.nodes[s.name]={t:"T",cols:s.columns.map(e=>e.name),f:s.file||void 0};const l=new Set;for(const e of f.edges){const s=e[2].split(".")[0];l.add(s)}for(const e of Object.keys(f.nodes))l.has(e)||"F"!==f.nodes[e].t||f.nodes[e].e||f.orphans.push(c[e]);const i=Object.create(null);for(const e of s)for(const s of e.methods||[])i[s]||(i[s]=[]),i[s].push(`${e.name}:${e.line}`);for(const[e,s]of Object.entries(i))s.length>1&&(f.duplicates[e]=s);return f}
5
- export function createSkeleton(e,s=null){const t={},o={};for(const[s,n]of Object.entries(e.legend)){const c=e.nodes[n];if(c&&"C"===c.t){const e=c.m?.length||0,f=c.$?.length||0;if(0===e&&0===f)continue;t[n]=s;const l={m:e};f>0&&(l.$=f),c.f&&(l.f=c.f),o[n]=l}}const n={};for(const[s,o]of Object.entries(e.legend)){const c=e.nodes[o];if("F"===c?.t&&c.e){t[o]=s;const e=c.f||"?";n[e]||(n[e]=[]),n[e].push(o)}}const c=new Set;for(const e of Object.values(o))e.f&&c.add(e.f);for(const e of Object.keys(n))c.add(e);const f={};for(const s of e.files||[]){if(c.has(s))continue;const e=s.lastIndexOf("/"),t=e>=0?s.slice(0,e+1):"./",o=e>=0?s.slice(e+1):s;f[t]||(f[t]=[]),f[t].push(o)}const l={v:e.v,L:t,s:e.stats,n:o,X:n,e:e.edges.length,o:e.orphans.length,d:Object.keys(e.duplicates).length};if(Object.keys(f).length>0&&(l.f=f),s&&s.length>0){const t=new Set(e.files||[]),o=s.filter(e=>!t.has(e));if(o.length>0){const e={};for(const s of o){const t=s.lastIndexOf("/"),o=t>=0?s.slice(0,t+1):"./",n=t>=0?s.slice(t+1):s;e[o]||(e[o]=[]),e[o].push(n)}l.a=e}}return l}
4
+ export function buildGraph(e){const s=e.classes||[],t=e.functions||[],o=[...s.map(e=>e.name),...t.map(e=>e.name),...s.flatMap(e=>e.methods||[])],n=minifyLegend([...new Set(o)]),c=Object.fromEntries(Object.entries(n).map(([e,s])=>[s,e])),f={v:1,legend:n,reverseLegend:c,stats:{files:(e.files||[]).length,classes:s.length,functions:t.length,tables:(e.tables||[]).length},nodes:{},edges:[],orphans:[],duplicates:{},files:e.files||[],fileImports:e.fileImports||{}};for(const e of s){const s=n[e.name];f.nodes[s]={t:"C",x:e.extends||void 0,m:(e.methods||[]).map(e=>n[e]||e),$:(e.properties||[]).length?e.properties:void 0,i:e.imports?.length?e.imports:void 0,f:e.file||void 0,l:e.line||void 0};for(const t of e.calls||[])if(t.includes(".")){const[e,o]=t.split(".");if(n[e]){const t=[s,"→",`${n[e]}.${n[o]||o}`];f.edges.push(t)}}else if(n[t]){const e=[s,"→",n[t]];f.edges.push(e)}}for(const e of t){const s=n[e.name];f.nodes[s]={t:"F",e:e.exported,f:e.file||void 0,l:e.line||void 0};for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const e of s){const s=n[e.name];for(const t of e.dbReads||[])f.edges.push([s,"R→",t]);for(const t of e.dbWrites||[])f.edges.push([s,"W→",t])}for(const s of e.tables||[])f.nodes[s.name]={t:"T",cols:s.columns.map(e=>e.name),f:s.file||void 0};const l=new Set;for(const e of f.edges){const s=e[2].split(".")[0];l.add(s)}for(const e of Object.keys(f.nodes))l.has(e)||"F"!==f.nodes[e].t||f.nodes[e].e||f.orphans.push(c[e]);const i=Object.create(null);for(const e of s)for(const s of e.methods||[])i[s]||(i[s]=[]),i[s].push(`${e.name}:${e.line}`);for(const[e,s]of Object.entries(i))s.length>1&&(f.duplicates[e]=s);return f}
5
+ export function createSkeleton(e,s=null){const t={},o={};for(const[s,n]of Object.entries(e.legend)){const c=e.nodes[n];if(c&&"C"===c.t){const e=c.m?.length||0,f=c.$?.length||0;if(0===e&&0===f)continue;t[n]=s;const l={m:e};f>0&&(l.$=f),c.f&&(l.f=c.f),c.l&&(l.l=c.l),o[n]=l}}const n={};for(const[s,o]of Object.entries(e.legend)){const c=e.nodes[o];if("F"===c?.t&&c.e){t[o]=s;const f=c.f||"?";n[f]||(n[f]=[]);n[f].push(o)}}const c=new Set;for(const e of Object.values(o))e.f&&c.add(e.f);for(const e of Object.keys(n))c.add(e);const f={};for(const s of e.files||[]){if(c.has(s))continue;const e=s.lastIndexOf("/"),t=e>=0?s.slice(0,e+1):"./",o=e>=0?s.slice(e+1):s;f[t]||(f[t]=[]),f[t].push(o)}const l={v:e.v,L:t,s:e.stats,n:o,X:n,e:e.edges.length,o:e.orphans.length,d:Object.keys(e.duplicates).length};if(Object.keys(f).length>0&&(l.f=f),s&&s.length>0){const t=new Set(e.files||[]),o=s.filter(e=>!t.has(e));if(o.length>0){const e={};for(const s of o){const t=s.lastIndexOf("/"),o=t>=0?s.slice(0,t+1):"./",n=t>=0?s.slice(t+1):s;e[o]||(e[o]=[]),e[o].push(n)}l.a=e}}const _fi=e.fileImports||{};if(Object.keys(_fi).length>0){const _I={};for(const[_file,_sources]of Object.entries(_fi)){const _compact=_sources.map(s=>s.s);if(_compact.length>0)_I[_file]=_compact}if(Object.keys(_I).length>0)l.I=_I}return l}
@@ -9,12 +9,12 @@ import{parsePython as d}from"../lang/lang-python.js";
9
9
  import{parseGo as m}from"../lang/lang-go.js";
10
10
  import{parseSQL as h,extractSQLFromString as y,isSQLString as g}from"../lang/lang-sql.js";
11
11
  const x=[".js",".ts",".tsx",".py",".go",".sql"];
12
- export async function parseFile(e,s){const t={file:s,classes:[],functions:[],imports:[],exports:[]},n=[];let r;try{r=a(e,{ecmaVersion:"latest",sourceType:"module",locations:!0,onComment:n})}catch(e){return console.warn(`Parse error in ${s}:`,e.message),t}const o=function(e,s){const t=new Map;for(const n of e){if("Block"!==n.type||!n.value.startsWith("*"))continue;const e="/*"+n.value+"*/",r=s.slice(0,n.end).split("\n").length,o=[],i=/@param\s+\{/g;let a;for(;null!==(a=i.exec(e));){let s=1,t=a.index+a[0].length;for(;t<e.length&&s>0;)"{"===e[t]?s++:"}"===e[t]&&s--,t++;if(0!==s)continue;const n=e.slice(a.index+a[0].length,t-1),r=e.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!r)continue;let i=r[1];i.startsWith("[")&&(i=i.slice(1)),i.endsWith("]")&&(i=i.slice(0,-1)),i.includes(".")||o.push({name:i,type:n})}let c=null;const l=e.match(/@returns?\s+\{([^}]+)\}/);l&&(c=l[1]),(o.length>0||c)&&t.set(r,{params:o,returns:c})}return t}(n,e),i=new Set;c.simple(r,{ImportDeclaration(e){for(const s of e.specifiers)"ImportDefaultSpecifier"===s.type?t.imports.push(s.local.name):"ImportSpecifier"===s.type&&t.imports.push(s.imported.name)},ExportNamedDeclaration(e){if(e.declaration)if(e.declaration.id)i.add(e.declaration.id.name);else if(e.declaration.declarations)for(const s of e.declaration.declarations)i.add(s.id.name);if(e.specifiers)for(const s of e.specifiers)i.add(s.exported.name)},ExportDefaultDeclaration(e){e.declaration&&e.declaration.id&&i.add(e.declaration.id.name)},ClassDeclaration(e){const n={name:e.id.name,extends:e.superClass?e.superClass.name:null,methods:[],properties:[],calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};for(const s of e.body.body)if("MethodDefinition"===s.type&&"constructor"!==s.key.name)n.methods.push(s.key.name),j(s.value.body,n.calls,n.dbReads,n.dbWrites);else if("PropertyDefinition"===s.type&&"init$"===s.key.name&&s.value&&"ObjectExpression"===s.value.type)for(const e of s.value.properties)e.key&&e.key.name&&n.properties.push(e.key.name);t.classes.push(n)},FunctionDeclaration(e){if(e.id){const n=e.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"RestElement"===e.type&&"Identifier"===e.argument.type?"..."+e.argument.name:"ObjectPattern"===e.type?"options":"?"),r=function(e,s){for(let t=1;t<=3;t++){const n=e.get(s-t);if(n)return n}return null}(o,e.loc.start.line),i=function(e,s){if(!s||0===s.params.length)return e;const t=new Map;for(const e of s.params)t.set(e.name,e.type);return e.map(e=>{const s=e.startsWith("..."),n=e.endsWith("=");let r=e;s&&(r=r.slice(3)),n&&(r=r.slice(0,-1));let o=t.get(r);return o?(o.startsWith("...")&&(o=o.slice(3)),`${s?"...":""}${r}:${o}${n?"=":""}`):e})}(n,r),a={name:e.id.name,exported:!1,params:i,async:e.async||!1,returns:r?.returns||null,calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};j(e.body,a.calls,a.dbReads,a.dbWrites),t.functions.push(a)}}});for(const e of t.functions)e.exported=i.has(e.name);return t.exports=[...i],t}
12
+ export async function parseFile(e,s){const t={file:s,classes:[],functions:[],imports:[],exports:[],importSources:[]},n=[];let r;try{r=a(e,{ecmaVersion:"latest",sourceType:"module",locations:!0,onComment:n})}catch(e){return console.warn(`Parse error in ${s}:`,e.message),t}const o=function(e,s){const t=new Map;for(const n of e){if("Block"!==n.type||!n.value.startsWith("*"))continue;const e="/*"+n.value+"*/",r=s.slice(0,n.end).split("\n").length,o=[],i=/@param\s+\{/g;let a;for(;null!==(a=i.exec(e));){let s=1,t=a.index+a[0].length;for(;t<e.length&&s>0;)"{"===e[t]?s++:"}"===e[t]&&s--,t++;if(0!==s)continue;const n=e.slice(a.index+a[0].length,t-1),r=e.slice(t).match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);if(!r)continue;let i=r[1];i.startsWith("[")&&(i=i.slice(1)),i.endsWith("]")&&(i=i.slice(0,-1)),i.includes(".")||o.push({name:i,type:n})}let c=null;const l=e.match(/@returns?\s+\{([^}]+)\}/);l&&(c=l[1]),(o.length>0||c)&&t.set(r,{params:o,returns:c})}return t}(n,e),i=new Set;c.simple(r,{ImportDeclaration(e){const _names=[];for(const s of e.specifiers)"ImportDefaultSpecifier"===s.type?(t.imports.push(s.local.name),_names.push(s.local.name)):"ImportSpecifier"===s.type&&(t.imports.push(s.imported.name),_names.push(s.imported.name));if(e.source&&e.source.value){t.importSources.push({s:e.source.value,n:_names})}},ExportNamedDeclaration(e){if(e.declaration)if(e.declaration.id)i.add(e.declaration.id.name);else if(e.declaration.declarations)for(const s of e.declaration.declarations)i.add(s.id.name);if(e.specifiers)for(const s of e.specifiers)i.add(s.exported.name)},ExportDefaultDeclaration(e){e.declaration&&e.declaration.id&&i.add(e.declaration.id.name)},ClassDeclaration(e){const n={name:e.id.name,extends:e.superClass?e.superClass.name:null,methods:[],properties:[],calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};for(const s of e.body.body)if("MethodDefinition"===s.type&&"constructor"!==s.key.name)n.methods.push(s.key.name),j(s.value.body,n.calls,n.dbReads,n.dbWrites);else if("PropertyDefinition"===s.type&&"init$"===s.key.name&&s.value&&"ObjectExpression"===s.value.type)for(const e of s.value.properties)e.key&&e.key.name&&n.properties.push(e.key.name);t.classes.push(n)},FunctionDeclaration(e){if(e.id){const n=e.params.map(e=>"Identifier"===e.type?e.name:"AssignmentPattern"===e.type&&"Identifier"===e.left.type?e.left.name+"=":"RestElement"===e.type&&"Identifier"===e.argument.type?"..."+e.argument.name:"ObjectPattern"===e.type?"options":"?"),r=function(e,s){for(let t=1;t<=3;t++){const n=e.get(s-t);if(n)return n}return null}(o,e.loc.start.line),i=function(e,s){if(!s||0===s.params.length)return e;const t=new Map;for(const e of s.params)t.set(e.name,e.type);return e.map(e=>{const s=e.startsWith("..."),n=e.endsWith("=");let r=e;s&&(r=r.slice(3)),n&&(r=r.slice(0,-1));let o=t.get(r);return o?(o.startsWith("...")&&(o=o.slice(3)),`${s?"...":""}${r}:${o}${n?"=":""}`):e})}(n,r),a={name:e.id.name,exported:!1,params:i,async:e.async||!1,returns:r?.returns||null,calls:[],dbReads:[],dbWrites:[],file:s,line:e.loc.start.line};j(e.body,a.calls,a.dbReads,a.dbWrites),t.functions.push(a)}}});for(const e of t.functions)e.exported=i.has(e.name);return t.exports=[...i],t}
13
13
  const b=new Set(["query","execute","raw","exec","queryFile","none","one","many","any","oneOrNone","manyOrNone","result"]);
14
14
  function j(e,s,t,n){e&&c.simple(e,{CallExpression(e){const r=e.callee;if("MemberExpression"===r.type){const e=r.object,t=r.property;if("Identifier"===t.type)if("Identifier"===e.type){const n=`${e.name}.${t.name}`;s.includes(n)||s.push(n)}else if("MemberExpression"===e.type&&"Identifier"===e.property.type){const n=`${e.property.name}.${t.name}`;s.includes(n)||s.push(n)}else if("ThisExpression"===e.type){const e=t.name;s.includes(e)||s.push(e)}}else if("Identifier"===r.type){const e=r.name;s.includes(e)||s.push(e)}if(t&&n){const s=function(e){const s=e.callee;return"MemberExpression"===s.type&&"Identifier"===s.property.type?s.property.name:null}(e);if(s&&b.has(s)&&e.arguments.length>0){const s=function(e){return e?"Literal"===e.type&&"string"==typeof e.value?e.value:"TemplateLiteral"===e.type?S(e):null:null}(e.arguments[0]);if(s&&g(s)){const e=y(s);e.reads.forEach(e=>{t.includes(e)||t.push(e)}),e.writes.forEach(e=>{n.includes(e)||n.push(e)})}}}},TaggedTemplateExpression(e){if(!t||!n)return;const s=function(e){return"Identifier"===e.type?e.name:"MemberExpression"===e.type&&"Identifier"===e.property.type?e.property.name:null}(e.tag);if(s&&/sql/i.test(s)){const s=S(e.quasi);if(s){const e=y(s);e.reads.forEach(e=>{t.includes(e)||t.push(e)}),e.writes.forEach(e=>{n.includes(e)||n.push(e)})}}},TemplateLiteral(e){if(!t||!n)return;const s=S(e);if(s&&g(s)){const e=y(s);e.reads.forEach(e=>{t.includes(e)||t.push(e)}),e.writes.forEach(e=>{n.includes(e)||n.push(e)})}},Literal(e){if(t&&n&&"string"==typeof e.value&&g(e.value)){const s=y(e.value);s.reads.forEach(e=>{t.includes(e)||t.push(e)}),s.writes.forEach(e=>{n.includes(e)||n.push(e)})}}})}
15
15
  function S(e){if(!e||!e.quasis)return"";let s="";for(let t=0;t<e.quasis.length;t++)s+=e.quasis[t].value.cooked||e.quasis[t].value.raw||"",t<e.expressions?.length&&(s+="$"+(t+1));return s}
16
16
  export function discoverSubProjects(a){const c=i(a),l=[],p=["packages","apps","services","modules","libs","plugins"];for(const i of p){const a=r(c,i);if(n(a))try{for(const i of s(a)){const s=r(a,i),p=r(s,"package.json");if(t(s).isDirectory()&&n(p))try{const t=JSON.parse(e(p,"utf-8"));l.push({name:t.name||i,path:o(c,s),absolutePath:s})}catch{l.push({name:i,path:o(c,s),absolutePath:s})}}}catch{}}return l}
17
- export async function parseProject(s,t={}){const n={files:[],classes:[],functions:[],imports:[],exports:[],tables:[]},a=i(s),c=findJSFiles(s);for(const s of c)try{const t=e(s,"utf-8"),r=o(a,s),i=await v(t,r);n.files.push(r),n.classes.push(...i.classes),n.functions.push(...i.functions),n.imports.push(...i.imports),n.exports.push(...i.exports),i.tables?.length&&n.tables.push(...i.tables)}catch(e){}if(t.recursive){const e=discoverSubProjects(s);n.subProjects=[];for(const s of e)try{const e=await parseProject(s.absolutePath);for(const t of e.files)n.files.push(r(s.path,t));for(const t of e.classes)t.file=r(s.path,t.file),n.classes.push(t);for(const t of e.functions)t.file=r(s.path,t.file),n.functions.push(t);n.imports.push(...e.imports),n.exports.push(...e.exports),e.tables?.length&&n.tables.push(...e.tables),n.subProjects.push({name:s.name,path:s.path,files:e.files.length})}catch{}}return n.imports=[...new Set(n.imports)],n.exports=[...new Set(n.exports)],n}
17
+ export async function parseProject(s,t={}){const n={files:[],classes:[],functions:[],imports:[],exports:[],tables:[],fileImports:{}},a=i(s),c=findJSFiles(s);for(const s of c)try{const t=e(s,"utf-8"),r=o(a,s),i=await v(t,r);n.files.push(r),n.classes.push(...i.classes),n.functions.push(...i.functions),n.imports.push(...i.imports),n.exports.push(...i.exports),i.tables?.length&&n.tables.push(...i.tables);if(i.importSources?.length)n.fileImports[r]=i.importSources}catch(e){}if(t.recursive){const e=discoverSubProjects(s);n.subProjects=[];for(const s of e)try{const e=await parseProject(s.absolutePath);for(const t of e.files)n.files.push(r(s.path,t));for(const t of e.classes)t.file=r(s.path,t.file),n.classes.push(t);for(const t of e.functions)t.file=r(s.path,t.file),n.functions.push(t);n.imports.push(...e.imports),n.exports.push(...e.exports),e.tables?.length&&n.tables.push(...e.tables),n.subProjects.push({name:s.name,path:s.path,files:e.files.length})}catch{}}return n.imports=[...new Set(n.imports)],n.exports=[...new Set(n.exports)],n}
18
18
  async function v(e,s){return s.endsWith(".sql")?h(e,s):s.endsWith(".py")?d(e,s):s.endsWith(".go")?m(e,s):s.endsWith(".ts")||s.endsWith(".tsx")?f(e,s):parseFile(e,s)}
19
19
  function E(e){return!e.endsWith(".css.js")&&!e.endsWith(".tpl.js")&&x.some(s=>e.endsWith(s))}
20
20
  export function findJSFiles(e,n=e){e===n&&u(n);const i=[];try{for(const a of s(e)){const s=r(e,a),c=t(s),u=o(n,e);c.isDirectory()?l(a,u)||i.push(...findJSFiles(s,n)):E(a)&&(p(a,u)||i.push(s))}}catch(s){console.warn(`Cannot read directory ${e}:`,s.message)}return i}
@@ -5,5 +5,4 @@ const c=i({input:process.stdin,terminal:!1}),a=[];
5
5
  let l=!1,p=null,d=null;
6
6
  const startProxy=async e=>{if(!l){l=!0,c.removeAllListeners("line"),c.close(),n.write(`RESOLVED: ${e}\n`),n.end();try{const t=await r(e);console.error(`[project-graph] Connected to backend on port ${t} (project: ${e})`),s(t,a)}catch(e){console.error(`[project-graph] Singleton failed (${e.message}), falling back to direct stdio`);const{startStdioServer:t}=await import("../mcp/mcp-server.js");t(a)}}};c.on("line",t=>{try{const r=JSON.parse(t);if(n.write(`IN: ${r.method||`response:${r.id}`}\n`),"initialize"===r.method){d=r.id,r.params?.roots?.length>0&&(e(r.params.roots),n.write("ROOTS from initialize.params\n"));
7
7
  const t=JSON.stringify({jsonrpc:"2.0",id:r.id,result:{protocolVersion:"2025-06-18",capabilities:{tools:{},resources:{}},serverInfo:{name:"project-graph",version:_v}}});return n.write("OUT: initialize response\n"),void process.stdout.write(t+"\n")}if("initialized"===r.method||"notifications/initialized"===r.method){n.write("IN: initialized notification\n"),p=999999;
8
- const e=JSON.stringify({jsonrpc:"2.0",id:p,method:"roots/list"});return n.write(`OUT: roots/list request id=${p}\n`),process.stdout.write(e+"\n"),void setTimeout(()=>{if(!l){const e=o();n.write(`ROOTS timeout, using: ${e}\n`),startProxy(e)}},2e3)}if(void 0!==r.id&&r.id===p&&(n.write(`IN: roots/list response: ${JSON.stringify(r.result)}\n`),r.result?.roots?.length>0)){e(r.result.roots);
9
- const t=o();return n.write(`ROOTS resolved: ${t}\n`),void startProxy(t)}a.push(t)}catch{a.push(t)}}),setTimeout(()=>{if(!l){const e=o();n.write(`TIMEOUT: fallback to ${e}\n`),console.error(`[project-graph] No roots received in 5s, using fallback: ${e}`),startProxy(e)}},5e3)}}
8
+ const e=JSON.stringify({jsonrpc:"2.0",id:p,method:"roots/list"});return n.write(`OUT: roots/list request id=${p}\n`),process.stdout.write(e+"\n"),void setTimeout(()=>{if(!l){const e=o();n.write(`ROOTS timeout, using: ${e}\n`),startProxy(e)}},2e3)}if(void 0!==r.id&&r.id===p){n.write(`IN: roots/list response: ${JSON.stringify(r.result)}\n`);if(r.result?.roots?.length>0){e(r.result.roots)}const t=o();return n.write(`ROOTS resolved: ${t}\n`),void startProxy(t)}if(r.method&&void 0!==r.id){n.write(`BUFFERED: ${r.method} id=${r.id}\n`),a.push(t)}else{a.push(t)}}catch{a.push(t)}}),setTimeout(()=>{if(!l){const e=o();n.write(`TIMEOUT: fallback to ${e}\n`),console.error(`[project-graph] No roots received in 5s, using fallback: ${e}`),startProxy(e)}},5e3)}}
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to symbiote-node will be documented in this file.
4
+
5
+ ## [0.3.0-alpha.0] — 2026-04-18
6
+
7
+ ### Fixed
8
+ - **Memory leak**: zombie `setTimeout` loops in SubgraphNode preview rendering — replaced with on-demand redraws
9
+ - **Memory leak**: event listener accumulation in `NodeCanvas.setEditor()` — added explicit unsubscribe on context switch
10
+ - **Memory leak**: incorrect `cancelAnimationFrame` cleanup for `setTimeout` IDs in `NodeViewManager.removeView()`
11
+ - **Layout overlap**: nodes measured as 4px height (DOM not ready) caused overlap — enforced minimum `nodeHeight` floor in `getSize()`
12
+ - **Inspector z-index**: panel header overlapped toolbar buttons — removed header, added toolbar-aware padding
13
+
14
+ ### Added
15
+ - `Editor.removeAllListeners()` — clean teardown method for editor event system
16
+ - `computeTreeLayout()` — directory-hierarchy-aware tree layout with indent levels
17
+ - Shape primitives: `CircleShape`, `DiamondShape`, `PillShape`, `RectShape`
18
+ - PCB dark theme enhancements: improved node styling, copper trace connections
19
+
20
+ ### Breaking
21
+ - `InspectorPanel` no longer renders a title header bar — consumers relying on `.insp-header` CSS should update
22
+
23
+ ## [0.2.1] — 2026-04-13
24
+
25
+ - Initial open-source release
26
+ - Node graph editor with Symbiote.js web components
27
+ - Sugiyama-based auto layout (`computeAutoLayout`)
28
+ - PCB/Carbon theming system
29
+ - Inspector panel with resize handle
30
+ - Subgraph navigation (drill-down/drill-up)
31
+ - Execution engine with topological sorting
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RND-PRO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,206 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+ [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-339933?logo=node.js&logoColor=white)](https://nodejs.org)
3
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](#)
4
+ [![Web Components](https://img.shields.io/badge/Web_Components-native-blue?logo=webcomponents.org&logoColor=white)](#)
5
+
6
+ # symbiote-node
7
+
8
+ A **visual node graph editor** and **execution engine** built on [Symbiote.js](https://github.com/symbiotejs/symbiote.js) — extensible, themeable, zero-dependency. Pure Web Components, works anywhere: vanilla HTML, React, Vue, Svelte, or any framework that supports custom elements.
9
+
10
+ > [!TIP]
11
+ > **22K lines, 150 files, zero external dependencies.** 70+ public API exports. Clone, serve, and start building node graphs in under a minute.
12
+
13
+ ### Graph Editor
14
+
15
+ The editor constructs visual node graphs from a data model. Nodes have typed input/output ports with compatibility validation, color-coded category headers, inline controls, and drag & drop with snap-to-grid. Connections render as SVG Bézier curves with gradient coloring.
16
+
17
+ ```javascript
18
+ import { NodeEditor, Node, Socket, Input, Output, NodeCanvas } from 'symbiote-node';
19
+
20
+ const editor = new NodeEditor();
21
+ const socket = new Socket('data', { color: '#4a9eff' });
22
+
23
+ const node1 = new Node('Source', { category: 'server' });
24
+ node1.addOutput('out', new Output(socket, 'Output'));
25
+
26
+ const node2 = new Node('Target', { category: 'control' });
27
+ node2.addInput('in', new Input(socket, 'Input'));
28
+
29
+ editor.addNode(node1);
30
+ editor.addNode(node2);
31
+
32
+ const canvas = document.querySelector('node-canvas');
33
+ canvas.setEditor(editor);
34
+ ```
35
+
36
+ ### Execution Engine
37
+
38
+ Server-side graph runtime with handler packs for data flow, control flow, I/O, and transforms. Graphs serialize to JSON and execute with topological ordering, retry logic, and parallel barriers.
39
+
40
+ ```javascript
41
+ import { Graph, Executor, Registry } from 'symbiote-node/engine';
42
+
43
+ const registry = new Registry();
44
+ await registry.loadDir('./engine/packs');
45
+
46
+ const graph = Graph.fromFile('workflow.json');
47
+ const executor = new Executor(graph, registry);
48
+ const results = await executor.run();
49
+ ```
50
+
51
+ ### Node Shapes
52
+
53
+ Two coexisting rendering modes on the same canvas — **HTML nodes** (CSS-styled rectangles) and **SVG nodes** (arbitrary vector shapes with perimeter-aware connector positioning). Built-in presets: `hexagon`, `star`, `cloud`, `shield`, `heart`, `rect`, `pill`, `circle`, `diamond`, `comment`.
54
+
55
+ ```javascript
56
+ import { createSVGShape, registerShape } from 'symbiote-node';
57
+
58
+ const myShape = createSVGShape('myshape', 'M12 2L22 8V16L12 22L2 16V8Z');
59
+ registerShape('myshape', myShape);
60
+
61
+ const node = new Node('Custom', { shape: 'myshape' });
62
+ ```
63
+
64
+ ### Theme System
65
+
66
+ Separate **Palette** (colors), **Skin** (geometry), and **Theme** (combined) layers — all driven by CSS custom properties. Switch at runtime without page reload.
67
+
68
+ | Theme | Description |
69
+ |-------|-------------|
70
+ | `GREY_NEUTRAL` | Balanced grey UI (default) |
71
+ | `DARK_DEFAULT` | Professional dark interface |
72
+ | `LIGHT_CLEAN` | Light mode |
73
+ | `SYNTHWAVE` | Neon retro aesthetic |
74
+ | `NEON_GLOW` | Vivid glow effects |
75
+ | `CARBON` | IBM Carbon-inspired |
76
+ | `EBOOK` | Warm paper-like reading theme |
77
+
78
+ ```javascript
79
+ import { applyTheme, CARBON, applyPalette, SYNTHWAVE_PALETTE } from 'symbiote-node';
80
+
81
+ applyTheme(canvasElement, CARBON); // Full theme
82
+ applyPalette(canvasElement, SYNTHWAVE_PALETTE); // Colors only
83
+ ```
84
+
85
+ ### Layout System (BSP)
86
+
87
+ Binary Space Partitioning layout engine for IDE-style panel workspaces. Panels resize by dragging dividers, sections split horizontally or vertically. Sidebar navigation with section switching and panel routing.
88
+
89
+ ```javascript
90
+ import { Layout, LayoutTree, LayoutSidebar } from 'symbiote-node';
91
+ ```
92
+
93
+ ### Plugins & Interactions
94
+
95
+ - **History** — undo/redo with keyboard bindings (Ctrl+Z / Ctrl+Shift+Z)
96
+ - **Readonly** — toggle read-only mode, blocks all mutations
97
+ - **FlowSimulator** — topological sort-based data flow animation with marching ants
98
+ - **SnapGrid** — configurable grid snapping
99
+ - **Selector** — rubber-band multi-select
100
+ - **ConnectFlow** — socket highlighting during connection drag
101
+ - **AutoLayout** — Sugiyama-based automatic node arrangement
102
+ - **Minimap** — viewport minimap with live position tracking
103
+ - **NodeSearch** — search/omnibox for quick node insertion
104
+ - **GraphTabs** — multi-page graph management
105
+ - **Subgraphs** — drill-down with breadcrumb navigation
106
+ - **Portals** — named reroutes for cross-graph connections
107
+ - **InspectorPanel** — property inspector sidebar
108
+ - **QuickToolbar** — floating action toolbar above selected node
109
+
110
+ ## Quick Start
111
+
112
+ ```bash
113
+ git clone https://github.com/RND-PRO/symbiote-node.git
114
+ cd symbiote-node
115
+ npx -y serve -l 3000 .
116
+ # Open http://localhost:3000/demo/
117
+ ```
118
+
119
+ ### As ES Module (CDN)
120
+
121
+ ```html
122
+ <script type="importmap">
123
+ {
124
+ "imports": {
125
+ "@symbiotejs/symbiote": "https://esm.sh/@symbiotejs/symbiote@3.2.1",
126
+ "symbiote-node": "./index.js"
127
+ }
128
+ }
129
+ </script>
130
+ <script type="module">
131
+ import { NodeEditor, Node, Socket, Input, Output, NodeCanvas } from 'symbiote-node';
132
+ import 'symbiote-node/canvas/NodeCanvas/NodeCanvas.js';
133
+ import 'symbiote-node/node/GraphNode/GraphNode.js';
134
+ // ...
135
+ </script>
136
+ ```
137
+
138
+ ## CLI (Engine)
139
+
140
+ ```bash
141
+ node engine/cli.js run <workflow.json> # Execute graph
142
+ node engine/cli.js validate <workflow.json> # Validate graph
143
+ node engine/cli.js list # List available node types
144
+ node engine/cli.js inspect <workflow.json> # Inspect graph structure
145
+ ```
146
+
147
+ ## Engine Handler Packs
148
+
149
+ Built-in node types organized by domain:
150
+
151
+ | Pack | Handlers | Description |
152
+ |------|----------|-------------|
153
+ | `flow` | `if`, `switch`, `loop`, `merge`, `retry`, `wait-all`, `agent` | Control flow and branching |
154
+ | `data` | `db-query`, `prompt-loader`, `rss-feed` | Data sources |
155
+ | `transform` | `json-parse`, `set`, `template`, `template-builder` | Data transformation |
156
+ | `io` | `http-request`, `read-file`, `write-file` | External I/O |
157
+ | `util` | `delay`, `log` | Utility nodes |
158
+ | `debug` | `inject` | Testing and debugging |
159
+
160
+ Custom handler packs can be loaded from any directory via `registry.loadDir()`.
161
+
162
+ ## Project Structure
163
+
164
+ ```
165
+ symbiote-node/
166
+ ├── index.js — public API (70+ exports)
167
+ ├── core/ — Editor, Node, Connection, Socket, Portal
168
+ ├── canvas/ — NodeCanvas, ConnectionRenderer, FlowSimulator, AutoLayout
169
+ ├── node/ — GraphNode, PortItem, CtrlItem, NodeSocket
170
+ ├── menu/ — ContextMenu
171
+ ├── interactions/ — Drag, Zoom, Selector, SnapGrid, ConnectFlow
172
+ ├── shapes/ — SVG shape system with 10 presets
173
+ ├── themes/ — Theme, Palette, Skin (7 themes)
174
+ ├── layout/ — BSP layout engine + LayoutSidebar + LayoutRouter
175
+ ├── toolbar/ — QuickToolbar
176
+ ├── inspector/ — InspectorPanel
177
+ ├── palette/ — PaletteBrowser
178
+ ├── plugins/ — Readonly, History
179
+ ├── engine/ — Server-side graph runtime
180
+ │ ├── packs/ — Built-in handler packs (flow, data, transform, io, util)
181
+ │ └── cli.js — CLI runner
182
+ ├── demo/ — Interactive demo
183
+ └── tests/ — 45 tests (geometry, serialization, topology)
184
+ ```
185
+
186
+ ## Tests
187
+
188
+ ```bash
189
+ node --test tests/*.test.js
190
+ # 45 tests: geometry, Bézier paths, serialization, socket compatibility,
191
+ # collapse/mute, subgraphs, execution, duck-typing
192
+ ```
193
+
194
+ ## Related Projects
195
+
196
+ - [project-graph-mcp](https://github.com/rnd-pro/project-graph-mcp) — MCP server for AI agents: project graph, code analysis, 18 tools
197
+ - [agent-pool-mcp](https://github.com/rnd-pro/agent-pool-mcp) — Multi-agent orchestration via Gemini CLI
198
+ - [Symbiote.js](https://github.com/symbiotejs/symbiote.js) — Isomorphic Reactive Web Components framework
199
+
200
+ ## License
201
+
202
+ MIT © [RND-PRO.com](https://rnd-pro.com)
203
+
204
+ ---
205
+
206
+ **Made with ❤️ by the RND-PRO team**