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,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.4",
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": {
@@ -1,10 +1,8 @@
1
1
  // @ctx .context/src/compact/expand.ctx
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
- function y(t){if(!t)return null;const e=t.match(/^→([A-Z][\w<>\[\]|]*)/);return e?e[1]:null}
5
-
6
- function w(t,e,n,s,o){const r=new Map,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});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}))}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}))}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 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 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)}}),e}(t.body)){const t=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;let i=null;for(const e of c)t.start>=e.s&&t.end<=e.e&&(!i||e.e-e.s<i.e-i.s)&&(i=e);i&&i.p.has(o)?i.r.has(o)&&d.push({s:t.start,e:t.end,n:i.r.get(o)}):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}
7
- 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}}try{i=w(i,l,a,c,p),l=d(i,{ecmaVersion:"latest",sourceType:"module",locations:!0})}catch{}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}}
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}}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}}
8
6
 
9
7
 
10
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)}}
@@ -5,4 +5,7 @@ function g(e,n){const a=o.normalize(e).replace(/^(\.\.[/\\])+/,""),s=a.match(/^[
5
5
  function u(e){return n.createHash("sha1").update(e+"258EAFA5-E914-47DA-95CA-5AB5ADF35C70").digest("base64")}
6
6
  function y(e){const t=Buffer.from(e,"utf8"),o=t.length;let n;return o<126?(n=Buffer.alloc(2),n[0]=129,n[1]=o):o<65536?(n=Buffer.alloc(4),n[0]=129,n[1]=126,n.writeUInt16BE(o,2)):(n=Buffer.alloc(10),n[0]=129,n[1]=127,n.writeBigUInt64BE(BigInt(o),2)),Buffer.concat([n,t])}
7
7
  function w(e){if(e.length<2)return null;const t=15&e[0],o=!!(128&e[1]);let n=127&e[1],a=2;if(126===n){if(e.length<4)return null;n=e.readUInt16BE(2),a=4}else if(127===n){if(e.length<10)return null;n=Number(e.readBigUInt64BE(2)),a=10}if(o){if(e.length<a+4+n)return null;const o=e.slice(a,a+4);a+=4;const s=e.slice(a,a+n);for(let e=0;e<s.length;e++)s[e]^=o[e%4];return{opcode:t,data:s.toString("utf8"),totalLen:a+n}}return e.length<a+n?null:{opcode:t,data:e.slice(a,a+n).toString("utf8"),totalLen:a+n}}
8
- export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),_ext=s(e).toLowerCase(),_jsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts.has(_ext)){const p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else{try{const raw=i(d,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n,extname:_ex2}=await import("path"),{readFileSync:_rf2}=await import("fs"),s=n(t,e),_ext2=_ex2(e).toLowerCase(),_jsExts2=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts2.has(_ext2)){const i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else{try{const raw=_rf2(s,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,original:tok,compressed:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/expand-file":{const e=a.get("path");if(e){try{const _pt2=await import("path"),_fs2=await import("fs"),_abs=_pt2.resolve(t,e),_ext3=_pt2.extname(e).toLowerCase(),_jsExts3=new Set([".js",".mjs",".ts",".tsx"]);if(!_jsExts3.has(_ext3)){const raw=_fs2.readFileSync(_abs,"utf-8");c={code:raw,file:e,injected:0};break}let _ctxContent=null;const _ctxName=_pt2.basename(e,_pt2.extname(e))+".ctx";const _ctxPath1=_pt2.join(t,_pt2.dirname(e),_ctxName);const _ctxPath2=_pt2.join(t,".context",_pt2.dirname(e),_ctxName);if(_fs2.existsSync(_ctxPath1))_ctxContent=_fs2.readFileSync(_ctxPath1,"utf-8");else if(_fs2.existsSync(_ctxPath2))_ctxContent=_fs2.readFileSync(_ctxPath2,"utf-8");const _result=await _ef(_abs,_ctxContent);c={code:_result.code,file:e,injected:_result.injected,original:_result.original,decompiled:_result.decompiled}}catch(err){c={code:`// Expand failed: ${err.message}`,file:e}}}else c={code:"// No file specified",file:""};break}case"/api/image":{const e=a.get("path");if(e){try{const _fs=await import("fs"),_pt=await import("path"),_imgPath=_pt.resolve(t,e);if(!_fs.existsSync(_imgPath)){i.writeHead(404);i.end("Not Found");return}const _imgTypes={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".webp":"image/webp",".ico":"image/x-icon",".bmp":"image/bmp"};const ct=_imgTypes[_pt.extname(e).toLowerCase()]||"application/octet-stream";const buf=_fs.readFileSync(_imgPath);i.writeHead(200,{"Content-Type":ct,"Cache-Control":"public, max-age=3600","Content-Length":buf.length});i.end(buf);return}catch(err){i.writeHead(500);i.end(err.message);return}}i.writeHead(400);i.end("No path");return}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),_wsExt=o.extname(s.path).toLowerCase(),_wsJsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_wsJsExts.has(_wsExt)){const n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else{try{const{readFileSync:rf}=await import("fs");const raw=rf(e,"utf-8"),tok=Math.ceil(raw.length/4);t={code:raw,file:s.path,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){t={code:`// Cannot read: ${err.message}`,file:s.path}}}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
8
+ export function startWebServer(t,a){_setRoots([{uri:"file://"+o.resolve(t)}]);const d=i(()=>{}),p=o.basename(o.resolve(t))||"root";let m=1;const _startedAt=Date.now();const h=o.resolve(t),f=n.createHash("md5").update(h).digest("hex"),v=parseInt(f.slice(0,4),16)%360,j={project:{name:p,path:h,color:`hsl(${v}, 65%, 55%)`,agents:0,pid:process.pid},skeleton:null,events:[]};function x(e,t){const o=JSON.stringify({jsonrpc:"2.0",method:e,params:t});for(const e of T)try{e.send(o)}catch{T.delete(e)}}function S(e,t){const o=e.split(".");let n=j;for(let e=0;e<o.length-1;e++)n=n[o[e]];n[o[o.length-1]]=t,x("patch",{path:e,value:t})}async function k(){if(!j.skeleton)try{j.skeleton=await d.executeTool("get_skeleton",{path:t})}catch(e){console.error("[project-graph] Failed to load skeleton:",e.message)}return j.skeleton}const b=new Map,T=new Set;let C=null;const _cache={cs:null,cst:0,as:null,ast:0,fa:null,fat:0};function _clearCache(){_cache.cs=null;_cache.cst=0;_cache.as=null;_cache.ast=0;_cache.fa=null;_cache.fat=0;j.skeleton=null}function N(){return b.size>0||T.size>0}function O(){C&&(clearTimeout(C),C=null);_shutdownAt=0}let _shutdownAt=0;function P(){N()||(O(),_shutdownAt=Date.now()+9e5,C=setTimeout(()=>{N()||(console.log("[project-graph] No clients for 15 min — shutting down."),process.exit(0))},9e5))}function z(){O(),P()}async function A(e,a,s,i){try{let c;const r=a.get("path")||t;switch(e){case"/api/skeleton":c=await d.executeTool("get_skeleton",{path:r});break;case"/api/file":{const e=a.get("path");if(e){const{resolve:n,basename:a,extname:s}=await import("path"),{readFileSync:i,existsSync:r}=await import("fs"),d=n(t,e),_ext=s(e).toLowerCase(),_jsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts.has(_ext)){const p=a(e,s(e))+".ctx",m=o.resolve(t,".context",o.dirname(e),p),f=await _cf(d,{beautify:false,legend:false}),y=r(m)?Math.ceil(i(m,"utf-8").length/4):0,w=f.compressed+y;c={code:f.code,file:e,codeTok:f.compressed,ctxTok:y,totalTok:w,expanded:f.expanded||f.original,savings:f.savings}}else{try{const raw=i(d,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/compact-file":{const e=a.get("path");if(e){const{resolve:n,extname:_ex2}=await import("path"),{readFileSync:_rf2}=await import("fs"),s=n(t,e),_ext2=_ex2(e).toLowerCase(),_jsExts2=new Set([".js",".mjs",".ts",".tsx"]);if(_jsExts2.has(_ext2)){const i=await _cf(s,{beautify:!1,legend:!1});c={code:i.code,file:e,original:i.original,compressed:i.compressed,savings:i.savings}}else{try{const raw=_rf2(s,"utf-8"),tok=Math.ceil(raw.length/4);c={code:raw,file:e,original:tok,compressed:tok,savings:"0%",raw:true}}catch(err){c={code:`// Cannot read: ${err.message}`,file:e}}}}else c={code:"// No file specified",file:""};break}case"/api/raw-file":{const e=a.get("path");try{const{readFileSync:o}=await import("fs"),{resolve:n,relative:a}=await import("path");c={content:o(n(t,e),"utf-8"),file:e}}catch(t){c={content:`// Cannot read: ${t.message}`,file:e}}break}case"/api/expand-file":{const e=a.get("path");if(e){try{const _pt2=await import("path"),_fs2=await import("fs"),_abs=_pt2.resolve(t,e),_ext3=_pt2.extname(e).toLowerCase(),_jsExts3=new Set([".js",".mjs",".ts",".tsx"]);if(!_jsExts3.has(_ext3)){const raw=_fs2.readFileSync(_abs,"utf-8");c={code:raw,file:e,injected:0};break}let _ctxContent=null;const _ctxName=_pt2.basename(e,_pt2.extname(e))+".ctx";const _ctxPath1=_pt2.join(t,_pt2.dirname(e),_ctxName);const _ctxPath2=_pt2.join(t,".context",_pt2.dirname(e),_ctxName);if(_fs2.existsSync(_ctxPath1))_ctxContent=_fs2.readFileSync(_ctxPath1,"utf-8");else if(_fs2.existsSync(_ctxPath2))_ctxContent=_fs2.readFileSync(_ctxPath2,"utf-8");const _result=await _ef(_abs,_ctxContent);
9
+ // Validation removed: we enforce a zero-fallback policy. Code must be generated correctly or fail loudly.
10
+ let _expandedCode=_result.code;
11
+ c={code:_expandedCode,file:e,injected:_result.injected,original:_result.original,decompiled:_result.decompiled}}catch(err){c={code:`// Expand failed: ${err.message}`,file:e}}}else c={code:"// No file specified",file:""};break}case"/api/image":{const e=a.get("path");if(e){try{const _fs=await import("fs"),_pt=await import("path"),_imgPath=_pt.resolve(t,e);if(!_fs.existsSync(_imgPath)){i.writeHead(404);i.end("Not Found");return}const _imgTypes={".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".gif":"image/gif",".svg":"image/svg+xml",".webp":"image/webp",".ico":"image/x-icon",".bmp":"image/bmp"};const ct=_imgTypes[_pt.extname(e).toLowerCase()]||"application/octet-stream";const buf=_fs.readFileSync(_imgPath);i.writeHead(200,{"Content-Type":ct,"Cache-Control":"public, max-age=3600","Content-Length":buf.length});i.end(buf);return}catch(err){i.writeHead(500);i.end(err.message);return}}i.writeHead(400);i.end("No path");return}case"/api/compression-stats":if(_cache.cs&&Date.now()-_cache.cst<6e4){c=_cache.cs;break}{const{readdirSync:e,statSync:n,readFileSync:a,existsSync:s}=await import("fs"),{join:i,extname:r,basename:d,dirname:p,relative:m}=await import("path"),h=new Set([".js",".mjs"]),f=[],g=["node_modules",".git","vendor",".context",".expanded","web"];!function t(o){try{for(const a of e(o)){if(a.startsWith("."))continue;const e=i(o,a);n(e).isDirectory()?g.includes(a)||t(e):h.has(r(a))&&f.push(e)}}catch{}}(o.resolve(t,"src"));let u=0,y=0,w=0,v=0,_og=0;const j=o.resolve(t);for(const e of f){try{const t=m(j,e),i=d(t,".js"),c=o.resolve(j,".context",p(t),i+".ctx");let r=0;s(c)&&(r=n(c).size,w+=r);try{const t=s(c)?a(c,"utf-8"):null;{const _r=await _cf(e,{beautify:false,legend:false});v+=(_r.expanded||_r.original);u+=_r.compressed;_og+=_r.original}}catch{v+=Math.ceil(1.3*n(e).size/4);_og+=Math.ceil(n(e).size/4)}}catch{continue}y++}c={files:y,codeTok:u,ctxTok:Math.ceil(w/4),totalTok:u+Math.ceil(w/4),expanded:v,original:_og};_cache.cs=c;_cache.cst=Date.now();break}case"/api/docs":{const e=a.get("file");if(e){try{const{readFileSync:n,existsSync:a}=await import("fs"),{basename:s,extname:i,dirname:r}=await import("path"),d=s(e,i(e))+".ctx",p=o.resolve(t,".context",r(e),d);c=a(p)?{docs:n(p,"utf-8"),file:e}:{docs:"",file:e}}catch(t){c={docs:"",file:e}}}else{c=await d.executeTool("docs",{action:"get",path:r})}break}case"/api/analysis":if(_cache.fa&&Date.now()-_cache.fat<12e4){c=_cache.fa}else{c=await d.executeTool("analyze",{action:"full_analysis",path:r});_cache.fa=c;_cache.fat=Date.now()}break;case"/api/analysis-summary":if(_cache.as&&Date.now()-_cache.ast<12e4){c=_cache.as}else{c=await d.executeTool("analyze",{action:"analysis_summary",path:r});_cache.as=c;_cache.ast=Date.now()}break;case"/api/deps":c=await d.executeTool("navigate",{action:"deps",symbol:a.get("symbol")});break;case"/api/usages":c=await d.executeTool("navigate",{action:"usages",symbol:a.get("symbol")});break;case"/api/expand":c=await d.executeTool("navigate",{action:"expand",symbol:a.get("symbol")});break;case"/api/chain":c=await d.executeTool("navigate",{action:"call_chain",from:a.get("from"),to:a.get("to")});break;case"/api/server-status":{const _now=Date.now();c={version:_pkgVersion,uptime:Math.round((_now-_startedAt)/1e3),agents:b.size,monitors:T.size,shutdownAt:_shutdownAt?Math.max(0,Math.round((_shutdownAt-_now)/1e3)):null};break}case"/api/stop":i.writeHead(200,{"Content-Type":"application/json"});i.end(JSON.stringify({ok:true}));setTimeout(()=>process.exit(0),200);return;case"/api/project-info":{const e=o.resolve(t),a=n.createHash("md5").update(e).digest("hex"),s=parseInt(a.slice(0,4),16)%360;c={name:p,path:e,color:`hsl(${s}, 65%, 55%)`,version:_pkgVersion,agents:b.size,pid:process.pid};break}case"/api/instances":try{const{listBackends:e}=await import("./backend-lifecycle.js");c=e()}catch{c=[{name:p,path:o.resolve(t),agents:b.size}]}break;default:return"POST"===s&&"/api/restart"===e?(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache"}),i.end(JSON.stringify({ok:!0,message:"Server restarting..."})),void setTimeout(async()=>{const{spawn:e}=await import("child_process"),{removePortFile:n}=await import("./backend-lifecycle.js"),{fileURLToPath:a}=await import("url"),s=o.join(o.dirname(a(import.meta.url)),"backend.js");n(t),e(process.execPath,[s,o.resolve(t)],{detached:!0,stdio:"ignore",env:{...process.env,PROJECT_GRAPH_BACKEND:"1"}}).unref(),setTimeout(()=>process.exit(0),300)},200)):(i.writeHead(200,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),void i.end(JSON.stringify({error:"Unknown API endpoint"})))}i.writeHead(200,{"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Cache-Control":"no-cache"}),i.end(JSON.stringify(c))}catch(e){i.writeHead(500,{"Content-Type":"application/json","Cache-Control":"no-cache, no-store, must-revalidate"}),i.end(JSON.stringify({error:e.message}))}}P(),k(),c.on("tool:call",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e);const _n=e.tool||e.name||"";if("invalidate_cache"===_n||"set_custom_rule"===_n||"delete_custom_rule"===_n)_clearCache();else if("docs"===_n||"compact"===_n||"filters"===_n){const _a=e.args?.action||"";if("generate"===_a||"set_mode"===_a||"set"===_a||"reset"===_a)_clearCache()}}),c.on("tool:result",e=>{j.events.push(e),j.events.length>500&&j.events.shift(),x("event",e)});const B=e.createServer((e,t)=>{const o=new URL(e.url,`http://localhost:${a||0}`);return"OPTIONS"===e.method?(t.writeHead(204,{"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET, POST","Access-Control-Allow-Headers":"Content-Type"}),void t.end()):o.pathname.startsWith("/api/")?(z(),void A(o.pathname,o.searchParams,e.method,t)):("/ws/monitor"===o.pathname&&console.log("UNEXPECTED HTTP /ws/monitor",e.headers),void g(o.pathname,t))}),M=new s({noServer:!0});M.on("connection",async e=>{T.add(e),z();const t={project:j.project};try{e.send(JSON.stringify({jsonrpc:"2.0",method:"snapshot",params:{state:t}}))}catch{}e.on("message",async t=>{let n;z();try{n=JSON.parse(t.toString())}catch{return}if(n.jsonrpc&&n.id&&n.method)if("tool"===n.method){const{name:a,args:s}=n.params||{};try{let t;if("compact"===a&&"compact_file"===s?.action&&s?.path){const e=o.resolve(h,s.path),_wsExt=o.extname(s.path).toLowerCase(),_wsJsExts=new Set([".js",".mjs",".ts",".tsx"]);if(_wsJsExts.has(_wsExt)){const n=o.basename(s.path,o.extname(s.path))+".ctx",a=o.resolve(h,".context",o.dirname(s.path),n);let i=null;try{const{readFileSync:e,existsSync:t}=await import("fs");t(a)&&(i=e(a,"utf-8"))}catch{}const c=await _cf(e,{beautify:false,legend:false}),p=i?Math.ceil(i.length/4):0,m=c.compressed+p;t={code:c.code,file:s.path,codeTok:c.compressed,ctxTok:p,totalTok:m,expanded:c.expanded||c.original,savings:c.savings}}else{try{const{readFileSync:rf}=await import("fs");const raw=rf(e,"utf-8"),tok=Math.ceil(raw.length/4);t={code:raw,file:s.path,codeTok:tok,ctxTok:0,totalTok:tok,expanded:tok,savings:"0%",raw:true}}catch(err){t={code:`// Cannot read: ${err.message}`,file:s.path}}}}else t=await d.executeTool(a,s||{});e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,result:t}))}catch(t){e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32e3,message:t.message}}))}}else e.send(JSON.stringify({jsonrpc:"2.0",id:n.id,error:{code:-32601,message:`Unknown method: ${n.method}`}}))}),e.on("close",()=>{T.delete(e),P()}),e.on("error",()=>{T.delete(e),P()})}),B.on("upgrade",(e,t,o)=>{const n=e.headers["sec-websocket-key"];if(n)if("/ws/monitor"!==e.url){if("/mcp-ws"===e.url){const e=u(n);t.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${e}\r\n\r\n`);const o="agent-"+m++,a=i(e=>{try{t.write(y(JSON.stringify(e)))}catch{}});b.set(t,{agentId:o,mcpServer:a,connectedAt:Date.now()}),O(),S("project.agents",b.size),x("event",{type:"agent_connect",agentId:o,agents:b.size,ts:Date.now()});let s=Buffer.alloc(0);return t.on("data",e=>{for(s=Buffer.concat([s,e]);s.length>=2;){const n=w(s);if(!n)break;if(s=s.slice(n.totalLen),8===n.opcode)return b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),t.end(),void(0===b.size&&P());if(9===n.opcode){const o=Buffer.from(e);o[0]=240&o[0]|10,t.write(o);continue}1===n.opcode&&(async()=>{try{const e=JSON.parse(n.data),o=await a.handleMessage(e);null!==o&&t.write(y(JSON.stringify(o)))}catch(e){t.write(y(JSON.stringify({jsonrpc:"2.0",error:{code:-32700,message:"Parse error"}})))}})()}}),t.on("close",()=>{b.delete(t),S("project.agents",b.size),x("event",{type:"agent_disconnect",agentId:o,agents:b.size,ts:Date.now()}),0===b.size&&P()}),void t.on("error",()=>{b.delete(t),0===b.size&&P()})}t.destroy()}else M.handleUpgrade(e,t,o,t=>{M.emit("connection",t,e)});else t.destroy()});const H=!a,J=a||0;return B.listen(J,"127.0.0.1",()=>{const e=B.address().port;if(H){const n=r("project-graph",e,{projectPath:o.resolve(t),projectName:p});setTimeout(()=>{const a=n.url;console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → ${a}`),console.log(` → ${n.directUrl} (direct)`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)},200)}else console.log(`\n ⬡ project-graph-mcp v${_pkgVersion}`),console.log(" ─────────────────────────────"),console.log(` → http://localhost:${e}/`),console.log(` → Project: ${o.resolve(t)}`),console.log(` → MCP WebSocket: ws://127.0.0.1:${e}/mcp-ws\n`)}),B}
@@ -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.