project-graph-mcp 2.3.1 → 2.4.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 (226) hide show
  1. package/package.json +3 -2
  2. package/src/analysis/analysis-cache.ctx +9 -0
  3. package/src/analysis/analysis-cache.js +1 -1
  4. package/src/analysis/complexity.ctx +6 -0
  5. package/src/analysis/complexity.js +1 -1
  6. package/src/analysis/custom-rules.ctx +14 -0
  7. package/src/analysis/custom-rules.js +1 -1
  8. package/src/analysis/db-analysis.ctx +7 -0
  9. package/src/analysis/db-analysis.js +1 -1
  10. package/src/analysis/dead-code.ctx +6 -0
  11. package/src/analysis/dead-code.js +1 -1
  12. package/src/analysis/full-analysis.ctx +9 -0
  13. package/src/analysis/full-analysis.js +1 -1
  14. package/src/analysis/jsdoc-checker.ctx +10 -0
  15. package/src/analysis/jsdoc-checker.js +1 -1
  16. package/src/analysis/jsdoc-generator.ctx +9 -0
  17. package/src/analysis/jsdoc-generator.js +1 -1
  18. package/src/analysis/large-files.ctx +6 -0
  19. package/src/analysis/large-files.js +1 -1
  20. package/src/analysis/outdated-patterns.ctx +7 -0
  21. package/src/analysis/outdated-patterns.js +1 -1
  22. package/src/analysis/similar-functions.ctx +6 -0
  23. package/src/analysis/similar-functions.js +1 -1
  24. package/src/analysis/test-annotations.ctx +11 -0
  25. package/src/analysis/test-annotations.js +1 -1
  26. package/src/analysis/type-checker.ctx +6 -0
  27. package/src/analysis/type-checker.js +1 -1
  28. package/src/analysis/undocumented.ctx +8 -0
  29. package/src/analysis/undocumented.js +1 -1
  30. package/src/cli/cli-handlers.ctx +7 -0
  31. package/src/cli/cli-handlers.js +1 -1
  32. package/src/cli/cli.ctx +6 -0
  33. package/src/cli/cli.js +1 -1
  34. package/src/compact/ai-context.ctx +6 -0
  35. package/src/compact/ai-context.js +1 -1
  36. package/src/compact/compact-migrate.ctx +8 -0
  37. package/src/compact/compact-migrate.js +1 -1
  38. package/src/compact/compact.ctx +11 -0
  39. package/src/compact/compact.js +1 -1
  40. package/src/compact/compress.ctx +7 -0
  41. package/src/compact/compress.js +1 -1
  42. package/src/compact/ctx-resolver.ctx +2 -0
  43. package/src/compact/ctx-resolver.js +1 -1
  44. package/src/compact/ctx-to-jsdoc.ctx +11 -0
  45. package/src/compact/ctx-to-jsdoc.js +1 -1
  46. package/src/compact/doc-dialect.ctx +11 -0
  47. package/src/compact/doc-dialect.js +2 -2
  48. package/src/compact/expand.ctx +14 -0
  49. package/src/compact/expand.js +1 -1
  50. package/src/compact/framework-references.ctx +7 -0
  51. package/src/compact/framework-references.js +1 -1
  52. package/src/compact/instructions.ctx +6 -0
  53. package/src/compact/instructions.js +1 -1
  54. package/src/compact/jsdoc-builder.ctx +4 -0
  55. package/src/compact/jsdoc-builder.js +1 -1
  56. package/src/compact/mode-config.ctx +8 -0
  57. package/src/compact/mode-config.js +1 -1
  58. package/src/compact/split-declarations.ctx +6 -0
  59. package/src/compact/split-declarations.js +1 -1
  60. package/src/compact/validate-pipeline.ctx +12 -0
  61. package/src/compact/validate-pipeline.js +1 -1
  62. package/src/core/event-bus.ctx +9 -0
  63. package/src/core/event-bus.js +1 -1
  64. package/src/core/file-walker.ctx +1 -0
  65. package/src/core/file-walker.js +1 -1
  66. package/src/core/filters.ctx +12 -0
  67. package/src/core/filters.js +1 -1
  68. package/src/core/graph-builder.ctx +7 -0
  69. package/src/core/graph-builder.js +1 -1
  70. package/src/core/parser.ctx +12 -0
  71. package/src/core/parser.js +1 -1
  72. package/src/core/utils.ctx +1 -0
  73. package/src/core/utils.js +1 -1
  74. package/src/core/workspace.ctx +7 -0
  75. package/src/core/workspace.js +1 -1
  76. package/src/lang/lang-go.ctx +8 -0
  77. package/src/lang/lang-go.js +1 -1
  78. package/src/lang/lang-python.ctx +5 -0
  79. package/src/lang/lang-python.js +1 -1
  80. package/src/lang/lang-sql.ctx +10 -0
  81. package/src/lang/lang-sql.js +1 -1
  82. package/src/lang/lang-typescript.ctx +6 -0
  83. package/src/lang/lang-typescript.js +1 -1
  84. package/src/lang/lang-utils.ctx +5 -0
  85. package/src/lang/lang-utils.js +1 -1
  86. package/src/mcp/mcp-server.ctx +6 -0
  87. package/src/mcp/mcp-server.js +1 -1
  88. package/src/mcp/tool-defs.ctx +2 -0
  89. package/src/mcp/tool-defs.js +1 -1
  90. package/src/mcp/tools.ctx +13 -0
  91. package/src/mcp/tools.js +1 -1
  92. package/src/network/backend-lifecycle.ctx +10 -0
  93. package/src/network/backend-lifecycle.js +1 -1
  94. package/src/network/backend.ctx +5 -0
  95. package/src/network/backend.js +1 -1
  96. package/src/network/local-gateway.ctx +9 -0
  97. package/src/network/local-gateway.js +1 -1
  98. package/src/network/mdns.ctx +6 -0
  99. package/src/network/mdns.js +1 -1
  100. package/src/network/server.ctx +2 -0
  101. package/src/network/server.js +2 -2
  102. package/src/network/web-server.ctx +17 -0
  103. package/src/network/web-server.js +2 -2
  104. package/web/follow-controller.js +94 -25
  105. package/web/panels/dep-graph.js +207 -21
  106. package/project-graph-mcp-2.3.0.tgz +0 -0
  107. package/vendor/symbiote-node/CHANGELOG.md +0 -31
  108. package/vendor/symbiote-node/LICENSE +0 -21
  109. package/vendor/symbiote-node/README.md +0 -206
  110. package/vendor/symbiote-node/canvas/AutoLayout.js +0 -725
  111. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.css.js +0 -73
  112. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.js +0 -93
  113. package/vendor/symbiote-node/canvas/Breadcrumb/Breadcrumb.tpl.js +0 -9
  114. package/vendor/symbiote-node/canvas/CanvasConnectionRenderer.js +0 -962
  115. package/vendor/symbiote-node/canvas/ConnectionRenderer.js +0 -1468
  116. package/vendor/symbiote-node/canvas/FlowSimulator.js +0 -323
  117. package/vendor/symbiote-node/canvas/ForceLayout.js +0 -189
  118. package/vendor/symbiote-node/canvas/ForceWorker.js +0 -1325
  119. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.css.js +0 -97
  120. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.js +0 -176
  121. package/vendor/symbiote-node/canvas/GraphTabs/GraphTabs.tpl.js +0 -12
  122. package/vendor/symbiote-node/canvas/LODManager.js +0 -88
  123. package/vendor/symbiote-node/canvas/Minimap/Minimap.css.js +0 -71
  124. package/vendor/symbiote-node/canvas/Minimap/Minimap.js +0 -207
  125. package/vendor/symbiote-node/canvas/Minimap/Minimap.tpl.js +0 -9
  126. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.css.js +0 -261
  127. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.js +0 -1840
  128. package/vendor/symbiote-node/canvas/NodeCanvas/NodeCanvas.tpl.js +0 -22
  129. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.css.js +0 -97
  130. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.js +0 -132
  131. package/vendor/symbiote-node/canvas/NodeSearch/NodeSearch.tpl.js +0 -21
  132. package/vendor/symbiote-node/canvas/NodeViewManager.js +0 -584
  133. package/vendor/symbiote-node/canvas/PinExpansion.js +0 -131
  134. package/vendor/symbiote-node/canvas/PseudoConnection.js +0 -80
  135. package/vendor/symbiote-node/canvas/SubgraphManager.js +0 -201
  136. package/vendor/symbiote-node/canvas/SubgraphRouter.js +0 -443
  137. package/vendor/symbiote-node/canvas/ViewportActions.js +0 -446
  138. package/vendor/symbiote-node/core/Connection.js +0 -45
  139. package/vendor/symbiote-node/core/Editor.js +0 -451
  140. package/vendor/symbiote-node/core/Frame.js +0 -31
  141. package/vendor/symbiote-node/core/GraphMermaid.js +0 -348
  142. package/vendor/symbiote-node/core/GraphText.js +0 -210
  143. package/vendor/symbiote-node/core/Node.js +0 -143
  144. package/vendor/symbiote-node/core/Portal.js +0 -104
  145. package/vendor/symbiote-node/core/Socket.js +0 -185
  146. package/vendor/symbiote-node/core/SubgraphNode.js +0 -125
  147. package/vendor/symbiote-node/index.js +0 -103
  148. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.css.js +0 -361
  149. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.js +0 -332
  150. package/vendor/symbiote-node/inspector/InspectorPanel/InspectorPanel.tpl.js +0 -96
  151. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.css.js +0 -104
  152. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.js +0 -133
  153. package/vendor/symbiote-node/inspector/TemplatePreview/TemplatePreview.tpl.js +0 -33
  154. package/vendor/symbiote-node/interactions/ConnectFlow.js +0 -307
  155. package/vendor/symbiote-node/interactions/Drag.js +0 -102
  156. package/vendor/symbiote-node/interactions/Selector.js +0 -132
  157. package/vendor/symbiote-node/interactions/SnapGrid.js +0 -65
  158. package/vendor/symbiote-node/interactions/Zoom.js +0 -140
  159. package/vendor/symbiote-node/layout/ActionZone/ActionZone.css.js +0 -88
  160. package/vendor/symbiote-node/layout/ActionZone/ActionZone.js +0 -254
  161. package/vendor/symbiote-node/layout/ActionZone/ActionZone.tpl.js +0 -11
  162. package/vendor/symbiote-node/layout/Layout/Layout.css.js +0 -88
  163. package/vendor/symbiote-node/layout/Layout/Layout.js +0 -622
  164. package/vendor/symbiote-node/layout/Layout/Layout.tpl.js +0 -25
  165. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.css.js +0 -293
  166. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.js +0 -467
  167. package/vendor/symbiote-node/layout/LayoutNode/LayoutNode.tpl.js +0 -33
  168. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.css.js +0 -46
  169. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.js +0 -102
  170. package/vendor/symbiote-node/layout/LayoutPreview/LayoutPreview.tpl.js +0 -6
  171. package/vendor/symbiote-node/layout/LayoutRouter/LayoutRouter.js +0 -156
  172. package/vendor/symbiote-node/layout/LayoutRouter/routerSync.js +0 -250
  173. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.css.js +0 -379
  174. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.js +0 -263
  175. package/vendor/symbiote-node/layout/LayoutSidebar/LayoutSidebar.tpl.js +0 -20
  176. package/vendor/symbiote-node/layout/LayoutSidebar/SidebarSection.js +0 -183
  177. package/vendor/symbiote-node/layout/LayoutTree.js +0 -246
  178. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.css.js +0 -43
  179. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.js +0 -89
  180. package/vendor/symbiote-node/layout/PanelMenu/PanelMenu.tpl.js +0 -14
  181. package/vendor/symbiote-node/layout/index.js +0 -16
  182. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.css.js +0 -61
  183. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.js +0 -79
  184. package/vendor/symbiote-node/menu/ContextMenu/ContextMenu.tpl.js +0 -19
  185. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.css.js +0 -41
  186. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.js +0 -24
  187. package/vendor/symbiote-node/node/CtrlItem/CtrlItem.tpl.js +0 -16
  188. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.css.js +0 -65
  189. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.js +0 -29
  190. package/vendor/symbiote-node/node/GraphFrame/GraphFrame.tpl.js +0 -13
  191. package/vendor/symbiote-node/node/GraphNode/GraphNode.css.js +0 -683
  192. package/vendor/symbiote-node/node/GraphNode/GraphNode.js +0 -92
  193. package/vendor/symbiote-node/node/GraphNode/GraphNode.tpl.js +0 -17
  194. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.js +0 -25
  195. package/vendor/symbiote-node/node/NodeSocket/NodeSocket.tpl.js +0 -7
  196. package/vendor/symbiote-node/node/PortItem/PortItem.css.js +0 -90
  197. package/vendor/symbiote-node/node/PortItem/PortItem.js +0 -87
  198. package/vendor/symbiote-node/node/PortItem/PortItem.tpl.js +0 -10
  199. package/vendor/symbiote-node/package.json +0 -59
  200. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.css.js +0 -143
  201. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.js +0 -131
  202. package/vendor/symbiote-node/palette/PaletteBrowser/PaletteBrowser.tpl.js +0 -16
  203. package/vendor/symbiote-node/plugins/History.js +0 -384
  204. package/vendor/symbiote-node/plugins/Readonly.js +0 -59
  205. package/vendor/symbiote-node/shapes/CircleShape.js +0 -80
  206. package/vendor/symbiote-node/shapes/CommentShape.js +0 -35
  207. package/vendor/symbiote-node/shapes/DiamondShape.js +0 -115
  208. package/vendor/symbiote-node/shapes/NodeShape.js +0 -80
  209. package/vendor/symbiote-node/shapes/PillShape.js +0 -91
  210. package/vendor/symbiote-node/shapes/RectShape.js +0 -72
  211. package/vendor/symbiote-node/shapes/SVGShape.js +0 -494
  212. package/vendor/symbiote-node/shapes/index.js +0 -53
  213. package/vendor/symbiote-node/themes/Palette.js +0 -32
  214. package/vendor/symbiote-node/themes/Skin.js +0 -113
  215. package/vendor/symbiote-node/themes/Theme.js +0 -84
  216. package/vendor/symbiote-node/themes/carbon.js +0 -137
  217. package/vendor/symbiote-node/themes/dark.js +0 -137
  218. package/vendor/symbiote-node/themes/ebook.js +0 -138
  219. package/vendor/symbiote-node/themes/grey.js +0 -137
  220. package/vendor/symbiote-node/themes/light.js +0 -137
  221. package/vendor/symbiote-node/themes/neon.js +0 -138
  222. package/vendor/symbiote-node/themes/pcb.js +0 -273
  223. package/vendor/symbiote-node/themes/synthwave.js +0 -137
  224. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.css.js +0 -86
  225. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.js +0 -128
  226. package/vendor/symbiote-node/toolbar/QuickToolbar/QuickToolbar.tpl.js +0 -29
@@ -1,443 +0,0 @@
1
- export class SubgraphRouter {
2
- #canvas = null;
3
- #config = {};
4
- #isAutoRouting = false;
5
- #canvasDepth = 0;
6
- #listeners = [];
7
- #destroyed = false;
8
-
9
- /**
10
- * @param {HTMLElement} canvas - NodeCanvas instance
11
- * @param {Object} config - Configuration options
12
- * @param {String} [config.hashPrefix='graph'] - URL hash routing prefix
13
- * @param {Map} [config.fileMap] - Map of file paths to node IDs
14
- * @param {Map} [config.dirNodeMap] - Map of directory paths to node IDs
15
- * @param {Map} [config.symbolMap] - Map of symbol IDs to { name, file }
16
- * @param {Set} [config.drillableFiles] - Set of file paths that contain symbol subgraphs
17
- * @param {Function} [config.onNavigate] - Callback after successful navigation
18
- */
19
- constructor(canvas, config = {}) {
20
- this.#canvas = canvas;
21
- this.updateConfig(config);
22
- this.#bindListeners();
23
- }
24
-
25
- updateConfig(config = {}) {
26
- this.#config = {
27
- hashPrefix: 'graph',
28
- fileMap: new Map(),
29
- dirNodeMap: new Map(),
30
- symbolMap: new Map(),
31
- drillableFiles: new Set(),
32
- onNavigate: () => {},
33
- ...this.#config,
34
- ...config
35
- };
36
- }
37
-
38
- /**
39
- * Internal router depth tracker
40
- * @returns {number}
41
- */
42
- get depth() {
43
- return this.#canvasDepth;
44
- }
45
-
46
- /**
47
- * Prevent hash rewriting during automatic routing across layers
48
- */
49
- #runAutoRouting(fn) {
50
- this.#isAutoRouting = true;
51
- fn();
52
- this.#isAutoRouting = false;
53
- }
54
-
55
- #bindListeners() {
56
- const handleEnter = (e) => {
57
- this.#canvasDepth++;
58
- if (this.#isAutoRouting) return;
59
-
60
- const nodeId = e.detail?.nodeId;
61
- if (!nodeId) return;
62
-
63
- // Find the path string for this node ID
64
- let path = null;
65
- for (const [key, id] of this.#config.dirNodeMap.entries()) {
66
- if (id === nodeId) { path = key; break; }
67
- }
68
- if (!path) {
69
- for (const [key, id] of this.#config.fileMap.entries()) {
70
- if (id === nodeId) { path = key; break; }
71
- }
72
- }
73
-
74
- if (path) {
75
- const hash = window.location.hash;
76
- const [base, queryStr] = hash.split('?');
77
- const params = new URLSearchParams(queryStr || '');
78
-
79
- params.set('in', '1');
80
-
81
- // Preserve symbol if it exists and path is drillable
82
- if (!params.has('symbol') || !this.#config.drillableFiles.has(path)) {
83
- params.delete('symbol');
84
- }
85
-
86
- const newQuery = params.toString();
87
- history.replaceState(null, '', `#${this.#config.hashPrefix}/${path}?${newQuery}`);
88
- }
89
- };
90
-
91
- const handleExit = (e) => {
92
- const level = e.detail?.level;
93
- this.#canvasDepth = (typeof level === 'number') ? level : Math.max(0, this.#canvasDepth - 1);
94
- if (this.#isAutoRouting) return; // Prevent erasing URL when popping out to find hidden nested paths
95
-
96
- // Extract the path we were drilled into BEFORE modifying the URL
97
- const hashPath = window.location.hash.replace(`#${this.#config.hashPrefix}/`, '').split('?')[0].split('&')[0];
98
-
99
- // Find the directory path we just exited from (to focus on it)
100
- let exitedDirPath = hashPath;
101
- if (this.#config.fileMap?.has(hashPath)) {
102
- const parts = hashPath.split('/');
103
- parts.pop();
104
- exitedDirPath = parts.join('/') + '/';
105
- }
106
- // Walk up to find the nearest known directory
107
- if (exitedDirPath && !this.#config.dirNodeMap?.has(exitedDirPath)) {
108
- const segments = exitedDirPath.replace(/\/$/, '').split('/');
109
- while (segments.length > 0) {
110
- const candidate = segments.join('/') + '/';
111
- if (this.#config.dirNodeMap?.has(candidate)) {
112
- exitedDirPath = candidate;
113
- break;
114
- }
115
- segments.pop();
116
- }
117
- }
118
-
119
- const updateUrl = (newPath, setIn = false, setFocus = null) => {
120
- const hash = window.location.hash;
121
- const [base, queryStr] = hash.split('?');
122
- const params = new URLSearchParams(queryStr || '');
123
-
124
- let newBase = `#${this.#config.hashPrefix}`;
125
- if (newPath) newBase += `/${newPath}`;
126
-
127
- if (setIn) params.set('in', '1');
128
- else params.delete('in');
129
-
130
- if (setFocus) params.set('focus', setFocus);
131
- else params.delete('focus');
132
-
133
- params.delete('symbol'); // always clear symbol on exit
134
-
135
- const newQuery = params.toString();
136
- const newHash = newQuery ? `${newBase}?${newQuery}` : newBase;
137
- history.replaceState(null, '', newHash);
138
- };
139
-
140
- if (this.#canvasDepth > 0) {
141
- // Still inside a subgraph — update URL to parent context
142
- if (this.#config.dirNodeMap?.has(exitedDirPath)) {
143
- updateUrl(exitedDirPath, true, null);
144
- } else if (exitedDirPath) {
145
- updateUrl(exitedDirPath, false, null);
146
- }
147
- } else {
148
- // Back at root
149
- if (exitedDirPath) {
150
- updateUrl(null, false, exitedDirPath);
151
- } else {
152
- updateUrl(null, false, null);
153
- }
154
- }
155
-
156
- // Fly to the exited group node at ANY level
157
- if (exitedDirPath) {
158
- requestAnimationFrame(() => {
159
- const nodeId = this.#config.dirNodeMap?.get(exitedDirPath) ||
160
- this.#config.fileMap?.get(exitedDirPath);
161
- if (nodeId && this.#canvas.flyToNode) {
162
- this.#canvas.flyToNode(nodeId, { zoom: 0.8 });
163
- } else if (this.#canvas.fitView) {
164
- this.#canvas.fitView();
165
- }
166
- });
167
- } else if (this.#canvas.fitView) {
168
- requestAnimationFrame(() => this.#canvas.fitView());
169
- }
170
- };
171
-
172
- this.#canvas.addEventListener('subgraph-enter', handleEnter);
173
- this.#canvas.addEventListener('subgraph-exit', handleExit);
174
-
175
- this.#listeners.push(
176
- { name: 'subgraph-enter', fn: handleEnter },
177
- { name: 'subgraph-exit', fn: handleExit }
178
- );
179
- }
180
-
181
- /**
182
- * Reads URL hash and triggers initial drill down + focus sequence.
183
- *
184
- * Universal URL semantics:
185
- * - `#graph` → root, fit view
186
- * - `#graph?focus=src/analysis/` → root, fly to analysis node
187
- * - `#graph/src/analysis/?in=1` → drill into analysis
188
- * - `#graph/src/analysis/?in=1&focus=file.js` → drill into analysis, focus file.js
189
- * - `#graph/src/analysis/file.js?in=1` → drill into analysis, drill into file
190
- * - `#graph/src/analysis/file.js?in=1&symbol=name` → drill into file, focus symbol
191
- * - `#graph/src/analysis/` → (legacy) focus analysis at root
192
- *
193
- * @param {NodeEditor} editor
194
- */
195
- restoreFromHash(editor) {
196
- if (this.#destroyed || !this.#canvas) return;
197
-
198
- const hash = window.location.hash;
199
- const prefix = `#${this.#config.hashPrefix}`;
200
- if (!hash.startsWith(prefix)) return;
201
-
202
- const afterPrefix = hash.slice(prefix.length); // e.g. '/src/analysis/?in=1&focus=file.js' or '?focus=src/analysis/'
203
-
204
- // Parse query parameters from the hash
205
- const qIdx = afterPrefix.indexOf('?');
206
- const pathPart = qIdx >= 0 ? afterPrefix.slice(0, qIdx) : afterPrefix; // '/src/analysis/' or ''
207
- const queryStr = qIdx >= 0 ? afterPrefix.slice(qIdx + 1) : '';
208
- const params = new URLSearchParams(queryStr);
209
-
210
- const drillPath = pathPart.replace(/^\//, ''); // strip leading /
211
- const hasDrillFlag = params.get('in') === '1';
212
- const focusParam = params.get('focus');
213
- const symbolParam = params.get('symbol');
214
-
215
- // Case 0: bare #graph — pop all subgraph layers and reset to root view
216
- if (!drillPath && !focusParam && !hasDrillFlag && !symbolParam) {
217
- // Event-driven pop: wait for each subgraph-exit before the next drillUp.
218
- // rAF-polling was unreliable because canvasDepth updates only AFTER the exit
219
- // event fires — which happens asynchronously during the canvas animation.
220
- this.#isAutoRouting = true;
221
- let safetyCounter = 10;
222
- const doPopStep = () => {
223
- if (this.#canvasDepth <= 0 || safetyCounter-- <= 0) {
224
- this.#isAutoRouting = false;
225
- this.#canvas.fitView?.();
226
- return;
227
- }
228
- // Register exit listener FIRST, then trigger drillUp
229
- const onExit = () => {
230
- this.#canvas.removeEventListener('subgraph-exit', onExit);
231
- // canvasDepth is now decremented by the main handler; recurse
232
- requestAnimationFrame(doPopStep);
233
- };
234
- this.#canvas.addEventListener('subgraph-exit', onExit);
235
- this.#canvas.drillUp?.();
236
- };
237
- doPopStep();
238
- return;
239
- }
240
-
241
- // Case 1: #graph?focus=src/analysis/ (root-level focus, no path)
242
- if (!drillPath && focusParam) {
243
- this.navigateTo(decodeURIComponent(focusParam), 0, false);
244
- return;
245
- }
246
-
247
- // Case 2: #graph/path?in=1 (drill into path)
248
- if (drillPath && hasDrillFlag) {
249
- const drilled = this.#restoreDrillDown(drillPath, editor, true);
250
-
251
- // After drilling, handle &focus= (select node inside group)
252
- if (drilled && focusParam) {
253
- const fullFocusPath = drillPath + decodeURIComponent(focusParam);
254
- requestAnimationFrame(() => {
255
- this.navigateTo(fullFocusPath, 0, false);
256
- });
257
- }
258
-
259
- // Handle &symbol= (focus symbol inside file subgraph)
260
- if (drilled && symbolParam) {
261
- requestAnimationFrame(() => {
262
- this.restoreSymbolFocus(drillPath);
263
- });
264
- }
265
- return;
266
- }
267
-
268
- // Case 3: #graph/path (legacy — just focus the node at root)
269
- if (drillPath) {
270
- this.navigateTo(drillPath, 0, false);
271
- }
272
- }
273
-
274
- #restoreDrillDown(targetPath, editor, autoDrill = false) {
275
- if (!this.#canvas) return false;
276
-
277
- // Try to find a directory SubgraphNode matching the path
278
- for (const node of editor.getNodes()) {
279
- if (!node._isSubgraph) continue;
280
- const nodePath = node.params?.path;
281
- if (!nodePath) continue;
282
-
283
- // Exact directory match (e.g. 'src/core/')
284
- if (nodePath === targetPath) {
285
- if (autoDrill) {
286
- this.#runAutoRouting(() => {
287
- this.#canvas.drillDown(node.id);
288
- });
289
- if (this.#canvas.fitView) {
290
- requestAnimationFrame(() => this.#canvas.fitView());
291
- }
292
- } else {
293
- // Just focus on the directory node without drilling in
294
- this.#canvas.flyToNode?.(node.id, { zoom: 0.8 }) ||
295
- this.#canvas.selectNode?.(node.id);
296
- }
297
- return true;
298
- }
299
-
300
- // File inside this directory — drill into dir, then focus file
301
- if (targetPath.startsWith(nodePath)) {
302
- this.#runAutoRouting(() => {
303
- this.#canvas.drillDown(node.id);
304
- });
305
- // After transition, specifically focus the exact nested file node
306
- requestAnimationFrame(() => {
307
- this.navigateTo(targetPath, 0, autoDrill);
308
- });
309
- return true;
310
- }
311
- }
312
-
313
- return false;
314
- }
315
-
316
- /**
317
- * Restore visual symbol focus from &symbol= URL parameter.
318
- * Called after autoDrill into a file subgraph to select the target function/class node.
319
- * @param {string} filePath - the file we drilled into
320
- */
321
- restoreSymbolFocus(filePath) {
322
- const hashParts = window.location.hash.split('&symbol=');
323
- if (hashParts.length < 2) return;
324
- const symbolName = decodeURIComponent(hashParts[1].split('&')[0]);
325
- if (!symbolName || !this.#config.symbolMap) return;
326
-
327
- for (const [nodeId, params] of this.#config.symbolMap) {
328
- if (params.name === symbolName && params.file === filePath) {
329
- this.#canvas?.selectNode(nodeId);
330
- return;
331
- }
332
- }
333
- }
334
-
335
- /**
336
- * Focus viewport on a specific node by path
337
- * @param {string} targetPath - e.g. 'src/core/event-bus.js'
338
- * @param {number} depth - Internal recursion depth limit
339
- * @param {boolean} autoDrill - Attempt to drill into target if it is a Subgraph
340
- * @returns {boolean} true if node found and focused
341
- */
342
- navigateTo(targetPath, depth = 0, autoDrill = false) {
343
- if (this.#destroyed || !this.#canvas || !this.#config.fileMap || depth > 5) return false;
344
-
345
- // Find node ID by file path string or directory path string
346
- let targetId = null;
347
- let isFile = true;
348
- if (this.#config.fileMap.has(targetPath)) {
349
- targetId = this.#config.fileMap.get(targetPath);
350
- } else if (this.#config.dirNodeMap && this.#config.dirNodeMap.has(targetPath)) {
351
- targetId = this.#config.dirNodeMap.get(targetPath);
352
- isFile = false;
353
- }
354
-
355
- if (!targetId) return false;
356
-
357
- const positions = typeof this.#canvas.getPositions === 'function' ? this.#canvas.getPositions() : {};
358
- const pos = positions[targetId];
359
-
360
- // Auto-traversal engine: if target is not visible on current canvas layer
361
- if (!pos && typeof this.#canvas.drillDown === 'function') {
362
- if (this.#config.dirNodeMap) {
363
- // Case 1: Target is a file or directory hidden inside a deeper subgraph.
364
- // Walk up the path hierarchy to find the nearest visible ancestor directory.
365
- let searchPath = targetPath;
366
- if (isFile) {
367
- // Start from the file's parent directory
368
- const parts = targetPath.split('/');
369
- parts.pop();
370
- searchPath = parts.join('/') + '/';
371
- if (searchPath === '/') searchPath = './';
372
- }
373
-
374
- // Walk UP the directory tree to find the nearest visible ancestor
375
- let segments = searchPath.replace(/\/$/, '').split('/');
376
- while (segments.length > 0) {
377
- const candidateDir = segments.join('/') + '/';
378
- const dirId = this.#config.dirNodeMap.get(candidateDir);
379
- if (dirId && positions[dirId]) {
380
- // Found a visible ancestor — drill into it
381
- this.#runAutoRouting(() => {
382
- this.#canvas.drillDown(dirId);
383
- });
384
- requestAnimationFrame(() => this.navigateTo(targetPath, depth + 1, autoDrill));
385
- return true;
386
- }
387
- segments.pop();
388
- }
389
- // Also try "./" as root
390
- const rootId = this.#config.dirNodeMap.get('./');
391
- if (rootId && positions[rootId]) {
392
- this.#runAutoRouting(() => {
393
- this.#canvas.drillDown(rootId);
394
- });
395
- requestAnimationFrame(() => this.navigateTo(targetPath, depth + 1, autoDrill));
396
- return true;
397
- }
398
-
399
- // Case 2: Target is completely off-scope (we are inside wrong group). Drill UP to root.
400
- if (this.#canvasDepth > 0) {
401
- this.#runAutoRouting(() => {
402
- this.#canvas.drillUp?.();
403
- });
404
- requestAnimationFrame(() => this.navigateTo(targetPath, depth + 1, autoDrill));
405
- return true;
406
- }
407
- }
408
- return false; // Unable to locate on any layer
409
- }
410
-
411
- // We found the node on the current layer. If target is a subgraph file and we are commanded to drill into it, do it.
412
- if (autoDrill && isFile && this.#config.drillableFiles?.has(targetPath)) {
413
- this.#runAutoRouting(() => {
414
- this.#canvas.drillDown?.(targetId);
415
- });
416
- requestAnimationFrame(() => {
417
- if (this.#canvas.fitView) this.#canvas.fitView();
418
- // Restore &symbol= focus from deep-link URL
419
- this.restoreSymbolFocus(targetPath);
420
- });
421
- return true;
422
- }
423
-
424
- // SubgraphRouter delegates raw center/fly animations up to Canvas if possible
425
- if (this.#canvas.flyToNode) {
426
- this.#canvas.flyToNode(targetId, { zoom: 0.8 });
427
- } else {
428
- // Safe fallback just in case
429
- this.#canvas.selectNode?.(targetId);
430
- }
431
-
432
- this.#config.onNavigate(targetPath);
433
- return true;
434
- }
435
-
436
- destroy() {
437
- this.#destroyed = true;
438
- for (const listener of this.#listeners) {
439
- this.#canvas.removeEventListener(listener.name, listener.fn);
440
- }
441
- this.#listeners = [];
442
- }
443
- }