project-graph-mcp 1.3.0 → 2.0.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.
- package/README.md +223 -17
- package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +87 -30
- package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +23 -8
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -0
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +13 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/vendor/terser.mjs +49 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +16 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/cli-handlers.js +0 -140
- package/src/cli.js +0 -83
- package/src/complexity.js +0 -223
- package/src/custom-rules.js +0 -583
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -174
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -175
- package/src/jsdoc-generator.js +0 -214
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -162
- package/src/mcp-server.js +0 -468
- package/src/outdated-patterns.js +0 -295
- package/src/parser.js +0 -452
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -278
- package/src/test-annotations.js +0 -301
- package/src/tool-defs.js +0 -525
- package/src/tools.js +0 -470
- package/src/undocumented.js +0 -260
- package/src/workspace.js +0 -70
package/src/tools.js
DELETED
|
@@ -1,470 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MCP Tools for Project Graph
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { parseProject, parseFile, findJSFiles } from './parser.js';
|
|
6
|
-
import { buildGraph, createSkeleton } from './graph-builder.js';
|
|
7
|
-
import { readFileSync, statSync, writeFileSync, existsSync, unlinkSync } from 'fs';
|
|
8
|
-
import { execSync } from 'child_process';
|
|
9
|
-
import { join } from 'path';
|
|
10
|
-
|
|
11
|
-
/** @type {import('./graph-builder.js').Graph|null} */
|
|
12
|
-
let cachedGraph = null;
|
|
13
|
-
|
|
14
|
-
/** @type {string|null} */
|
|
15
|
-
let cachedPath = null;
|
|
16
|
-
|
|
17
|
-
/** @type {Map<string, number>} file path -> mtimeMs */
|
|
18
|
-
let cachedMtimes = new Map();
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Save cache to disk
|
|
22
|
-
* @param {string} path
|
|
23
|
-
* @param {import('./graph-builder.js').Graph} graph
|
|
24
|
-
*/
|
|
25
|
-
function saveDiskCache(path, graph) {
|
|
26
|
-
try {
|
|
27
|
-
const cachePath = join(path, '.project-graph-cache.json');
|
|
28
|
-
const cacheData = {
|
|
29
|
-
version: 1,
|
|
30
|
-
path: path,
|
|
31
|
-
mtimes: Object.fromEntries(cachedMtimes),
|
|
32
|
-
graph: graph
|
|
33
|
-
};
|
|
34
|
-
writeFileSync(cachePath, JSON.stringify(cacheData), 'utf-8');
|
|
35
|
-
} catch (e) {
|
|
36
|
-
// Ignore cache save errors
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Load cache from disk
|
|
42
|
-
* @param {string} path
|
|
43
|
-
* @returns {boolean} true if cache was successfully loaded and is valid
|
|
44
|
-
*/
|
|
45
|
-
function loadDiskCache(path) {
|
|
46
|
-
try {
|
|
47
|
-
const cachePath = join(path, '.project-graph-cache.json');
|
|
48
|
-
if (!existsSync(cachePath)) return false;
|
|
49
|
-
|
|
50
|
-
const content = readFileSync(cachePath, 'utf-8');
|
|
51
|
-
const data = JSON.parse(content);
|
|
52
|
-
|
|
53
|
-
if (data.version !== 1 || data.path !== path) return false;
|
|
54
|
-
|
|
55
|
-
cachedMtimes.clear();
|
|
56
|
-
for (const [file, mtime] of Object.entries(data.mtimes)) {
|
|
57
|
-
cachedMtimes.set(file, mtime);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
cachedGraph = data.graph;
|
|
61
|
-
cachedPath = path;
|
|
62
|
-
|
|
63
|
-
const changed = detectChanges(path);
|
|
64
|
-
if (changed) {
|
|
65
|
-
cachedGraph = null;
|
|
66
|
-
cachedPath = null;
|
|
67
|
-
cachedMtimes.clear();
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return true;
|
|
72
|
-
} catch (e) {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get or build graph with smart mtime-based caching.
|
|
79
|
-
* On first call: full parse + build.
|
|
80
|
-
* On subsequent calls: check file mtimes, rebuild only if changes detected.
|
|
81
|
-
* @param {string} path
|
|
82
|
-
* @returns {Promise<import('./graph-builder.js').Graph>}
|
|
83
|
-
*/
|
|
84
|
-
async function getGraph(path) {
|
|
85
|
-
// Different path = full rebuild
|
|
86
|
-
if (cachedGraph && cachedPath === path) {
|
|
87
|
-
// Check for file changes via mtime
|
|
88
|
-
const changed = detectChanges(path);
|
|
89
|
-
if (!changed) {
|
|
90
|
-
return cachedGraph;
|
|
91
|
-
}
|
|
92
|
-
// Files changed - full rebuild (incremental would need graph-builder changes)
|
|
93
|
-
} else if (!cachedGraph) {
|
|
94
|
-
if (loadDiskCache(path)) {
|
|
95
|
-
return cachedGraph;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const parsed = await parseProject(path);
|
|
100
|
-
cachedGraph = buildGraph(parsed);
|
|
101
|
-
cachedPath = path;
|
|
102
|
-
|
|
103
|
-
// Snapshot mtimes for all parsed files
|
|
104
|
-
snapshotMtimes(path);
|
|
105
|
-
saveDiskCache(path, cachedGraph);
|
|
106
|
-
|
|
107
|
-
return cachedGraph;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Detect if any JS files changed since last snapshot.
|
|
112
|
-
* Checks: new files, deleted files, modified files (via mtimeMs).
|
|
113
|
-
* @param {string} path
|
|
114
|
-
* @returns {boolean} true if changes detected
|
|
115
|
-
*/
|
|
116
|
-
function detectChanges(path) {
|
|
117
|
-
if (cachedMtimes.size === 0) return true;
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
const currentFiles = findJSFiles(path);
|
|
121
|
-
const currentSet = new Set(currentFiles);
|
|
122
|
-
const cachedSet = new Set(cachedMtimes.keys());
|
|
123
|
-
|
|
124
|
-
// New or deleted files
|
|
125
|
-
if (currentFiles.length !== cachedMtimes.size) return true;
|
|
126
|
-
for (const f of currentFiles) {
|
|
127
|
-
if (!cachedSet.has(f)) return true;
|
|
128
|
-
}
|
|
129
|
-
for (const f of cachedSet) {
|
|
130
|
-
if (!currentSet.has(f)) return true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Check mtimes
|
|
134
|
-
for (const file of currentFiles) {
|
|
135
|
-
try {
|
|
136
|
-
const mtime = statSync(file).mtimeMs;
|
|
137
|
-
if (mtime !== cachedMtimes.get(file)) return true;
|
|
138
|
-
} catch {
|
|
139
|
-
return true; // File gone or unreadable
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return false;
|
|
144
|
-
} catch {
|
|
145
|
-
return true; // Safety: rebuild on error
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Snapshot current mtimes for all JS files in path.
|
|
151
|
-
* @param {string} path
|
|
152
|
-
*/
|
|
153
|
-
function snapshotMtimes(path) {
|
|
154
|
-
cachedMtimes.clear();
|
|
155
|
-
try {
|
|
156
|
-
const files = findJSFiles(path);
|
|
157
|
-
for (const file of files) {
|
|
158
|
-
try {
|
|
159
|
-
cachedMtimes.set(file, statSync(file).mtimeMs);
|
|
160
|
-
} catch {
|
|
161
|
-
// Skip unreadable files
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} catch {
|
|
165
|
-
// Ignore errors
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Get compact project skeleton
|
|
171
|
-
* @param {string} path
|
|
172
|
-
* @returns {Promise<Object>}
|
|
173
|
-
*/
|
|
174
|
-
export async function getSkeleton(path) {
|
|
175
|
-
const graph = await getGraph(path);
|
|
176
|
-
return createSkeleton(graph);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get enriched focus zone based on recent activity
|
|
181
|
-
* @param {Object} options
|
|
182
|
-
* @param {string[]} [options.recentFiles]
|
|
183
|
-
* @param {boolean} [options.useGitDiff]
|
|
184
|
-
* @returns {Promise<Object>}
|
|
185
|
-
*/
|
|
186
|
-
export async function getFocusZone(options = {}) {
|
|
187
|
-
const path = options.path || 'src/components';
|
|
188
|
-
const graph = await getGraph(path);
|
|
189
|
-
|
|
190
|
-
let focusFiles = options.recentFiles || [];
|
|
191
|
-
|
|
192
|
-
// Auto-detect from git diff
|
|
193
|
-
if (options.useGitDiff) {
|
|
194
|
-
try {
|
|
195
|
-
const diff = execSync('git diff --name-only HEAD~5', { encoding: 'utf-8' });
|
|
196
|
-
focusFiles = diff.split('\n').filter(f => f.endsWith('.js'));
|
|
197
|
-
} catch (e) {
|
|
198
|
-
// Git not available or not a repo
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const expanded = {};
|
|
203
|
-
|
|
204
|
-
for (const file of focusFiles) {
|
|
205
|
-
// Find classes in this file
|
|
206
|
-
const content = readFileSync(file, 'utf-8');
|
|
207
|
-
const parsed = await parseFile(content, file);
|
|
208
|
-
|
|
209
|
-
for (const cls of parsed.classes) {
|
|
210
|
-
const shortName = graph.legend[cls.name];
|
|
211
|
-
if (shortName && graph.nodes[shortName]) {
|
|
212
|
-
expanded[shortName] = {
|
|
213
|
-
...graph.nodes[shortName],
|
|
214
|
-
methods: cls.methods,
|
|
215
|
-
properties: cls.properties,
|
|
216
|
-
file: cls.file,
|
|
217
|
-
line: cls.line,
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return {
|
|
224
|
-
focusFiles,
|
|
225
|
-
expanded,
|
|
226
|
-
expandable: Object.keys(graph.nodes).filter(k => !expanded[k]),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Expand a symbol to full details
|
|
232
|
-
* @param {string} symbol - Minified symbol like 'SN' or 'SN.tP'
|
|
233
|
-
* @returns {Promise<Object>}
|
|
234
|
-
*/
|
|
235
|
-
export async function expand(symbol) {
|
|
236
|
-
const path = cachedPath || 'src/components';
|
|
237
|
-
const graph = await getGraph(path);
|
|
238
|
-
|
|
239
|
-
const [nodeKey, methodKey] = symbol.split('.');
|
|
240
|
-
const fullName = graph.reverseLegend[nodeKey];
|
|
241
|
-
|
|
242
|
-
if (!fullName) {
|
|
243
|
-
return { error: `Unknown symbol: ${symbol}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.` };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Find the source file
|
|
247
|
-
const parsed = await parseProject(path);
|
|
248
|
-
const cls = parsed.classes.find(c => c.name === fullName);
|
|
249
|
-
const fn = parsed.functions.find(f => f.name === fullName);
|
|
250
|
-
|
|
251
|
-
if (!cls && !fn) {
|
|
252
|
-
return { error: `Symbol not found: ${fullName}` };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (fn && !methodKey) {
|
|
256
|
-
return {
|
|
257
|
-
symbol,
|
|
258
|
-
fullName,
|
|
259
|
-
type: 'function',
|
|
260
|
-
file: fn.file,
|
|
261
|
-
line: fn.line,
|
|
262
|
-
exported: fn.exported,
|
|
263
|
-
calls: fn.calls,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// If method specified, extract method code
|
|
268
|
-
if (methodKey && cls) {
|
|
269
|
-
const methodName = graph.reverseLegend[methodKey] || methodKey;
|
|
270
|
-
const content = readFileSync(cls.file, 'utf-8');
|
|
271
|
-
const methodCode = extractMethod(content, methodName);
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
symbol,
|
|
275
|
-
fullName: `${fullName}.${methodName}`,
|
|
276
|
-
file: cls.file,
|
|
277
|
-
line: cls.line, // TODO: get actual method line
|
|
278
|
-
code: methodCode,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Return full class info
|
|
283
|
-
return {
|
|
284
|
-
symbol,
|
|
285
|
-
fullName,
|
|
286
|
-
file: cls.file,
|
|
287
|
-
line: cls.line,
|
|
288
|
-
extends: cls.extends,
|
|
289
|
-
methods: cls.methods,
|
|
290
|
-
properties: cls.properties,
|
|
291
|
-
calls: cls.calls,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Get dependency tree for a symbol
|
|
297
|
-
* @param {string} symbol
|
|
298
|
-
* @returns {Promise<Object>}
|
|
299
|
-
*/
|
|
300
|
-
export async function deps(symbol) {
|
|
301
|
-
const path = cachedPath || 'src/components';
|
|
302
|
-
const graph = await getGraph(path);
|
|
303
|
-
|
|
304
|
-
const node = graph.nodes[symbol];
|
|
305
|
-
if (!node) {
|
|
306
|
-
return { error: `Unknown symbol: ${symbol}. Run get_skeleton on your project first, then use symbols from the L (Legend) field.` };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Find incoming edges (usedBy)
|
|
310
|
-
const usedBy = graph.edges
|
|
311
|
-
.filter(e => e[2].startsWith(symbol))
|
|
312
|
-
.map(e => e[0]);
|
|
313
|
-
|
|
314
|
-
// Find outgoing edges (calls)
|
|
315
|
-
const calls = graph.edges
|
|
316
|
-
.filter(e => e[0] === symbol)
|
|
317
|
-
.map(e => e[2]);
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
symbol,
|
|
321
|
-
imports: node.i || [],
|
|
322
|
-
usedBy: [...new Set(usedBy)],
|
|
323
|
-
calls: [...new Set(calls)],
|
|
324
|
-
};
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Find all usages of a symbol
|
|
329
|
-
* @param {string} symbol
|
|
330
|
-
* @returns {Promise<Array>}
|
|
331
|
-
*/
|
|
332
|
-
export async function usages(symbol) {
|
|
333
|
-
const path = cachedPath || 'src/components';
|
|
334
|
-
const graph = await getGraph(path);
|
|
335
|
-
const parsed = await parseProject(path);
|
|
336
|
-
|
|
337
|
-
const fullName = graph.reverseLegend[symbol] || symbol;
|
|
338
|
-
const results = [];
|
|
339
|
-
|
|
340
|
-
for (const cls of parsed.classes) {
|
|
341
|
-
if (cls.calls?.includes(fullName) || cls.calls?.some(c => c.includes(fullName))) {
|
|
342
|
-
results.push({
|
|
343
|
-
file: cls.file,
|
|
344
|
-
line: cls.line,
|
|
345
|
-
context: `${cls.name} calls ${fullName}`,
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
return results;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* Extract method code from file content
|
|
355
|
-
* @param {string} content
|
|
356
|
-
* @param {string} methodName
|
|
357
|
-
* @returns {string}
|
|
358
|
-
*/
|
|
359
|
-
function extractMethod(content, methodName) {
|
|
360
|
-
const regex = new RegExp(`((?:\\/\\*\\*[\\s\\S]*?\\*\\/\\s*)?)(?:async\\s+)?${methodName}\\s*\\([^)]*\\)\\s*{`, 'g');
|
|
361
|
-
const match = regex.exec(content);
|
|
362
|
-
|
|
363
|
-
if (!match) return '';
|
|
364
|
-
|
|
365
|
-
const start = match.index;
|
|
366
|
-
let depth = 0;
|
|
367
|
-
let i = match.index + match[0].length - 1;
|
|
368
|
-
|
|
369
|
-
while (i < content.length) {
|
|
370
|
-
if (content[i] === '{') depth++;
|
|
371
|
-
else if (content[i] === '}') {
|
|
372
|
-
depth--;
|
|
373
|
-
if (depth === 0) {
|
|
374
|
-
return content.slice(start, i + 1);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
i++;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return content.slice(start);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Find call chain from one symbol to another
|
|
385
|
-
* @param {Object} options
|
|
386
|
-
* @param {string} options.from - Starting symbol (full or minified)
|
|
387
|
-
* @param {string} options.to - Target symbol (full or minified)
|
|
388
|
-
* @param {string} [options.path] - Project path
|
|
389
|
-
* @returns {Promise<string[]|Object>}
|
|
390
|
-
*/
|
|
391
|
-
export async function getCallChain(options = {}) {
|
|
392
|
-
const { from, to, path } = options;
|
|
393
|
-
if (!from || !to) {
|
|
394
|
-
return { error: 'Both "from" and "to" parameters are required' };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const projectPath = path || cachedPath || 'src/components';
|
|
398
|
-
const graph = await getGraph(projectPath);
|
|
399
|
-
|
|
400
|
-
const fromSym = graph.legend[from] || from;
|
|
401
|
-
const toSym = graph.legend[to] || to;
|
|
402
|
-
|
|
403
|
-
// Build adjacency list for fast lookup
|
|
404
|
-
const adj = {};
|
|
405
|
-
for (const [caller, _, target] of graph.edges) {
|
|
406
|
-
if (!adj[caller]) adj[caller] = [];
|
|
407
|
-
adj[caller].push(target);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Queue stores { current: string, path: string[] }
|
|
411
|
-
const queue = [{ current: fromSym, path: [fromSym] }];
|
|
412
|
-
const visitedNodes = new Set();
|
|
413
|
-
const expandedBases = new Set();
|
|
414
|
-
visitedNodes.add(fromSym);
|
|
415
|
-
|
|
416
|
-
while (queue.length > 0) {
|
|
417
|
-
const { current, path: currentPath } = queue.shift();
|
|
418
|
-
|
|
419
|
-
const currentBase = current.split('.')[0];
|
|
420
|
-
const currentMethod = current.split('.')[1];
|
|
421
|
-
|
|
422
|
-
if (current === toSym || currentBase === toSym || currentMethod === toSym) {
|
|
423
|
-
const fullPath = currentPath.map(sym => {
|
|
424
|
-
const parts = sym.split('.');
|
|
425
|
-
const base = graph.reverseLegend[parts[0]] || parts[0];
|
|
426
|
-
if (parts.length === 2) {
|
|
427
|
-
const method = graph.reverseLegend[parts[1]] || parts[1];
|
|
428
|
-
return `${base}.${method}`;
|
|
429
|
-
}
|
|
430
|
-
return base;
|
|
431
|
-
});
|
|
432
|
-
return fullPath;
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (expandedBases.has(currentBase)) {
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
expandedBases.add(currentBase);
|
|
439
|
-
|
|
440
|
-
const neighbors = adj[currentBase] || [];
|
|
441
|
-
for (const neighbor of neighbors) {
|
|
442
|
-
if (!visitedNodes.has(neighbor)) {
|
|
443
|
-
visitedNodes.add(neighbor);
|
|
444
|
-
queue.push({
|
|
445
|
-
current: neighbor,
|
|
446
|
-
path: [...currentPath, neighbor]
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
return { error: `No call path found from "${from}" to "${to}"` };
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* Invalidate cache
|
|
457
|
-
*/
|
|
458
|
-
export function invalidateCache() {
|
|
459
|
-
if (cachedPath) {
|
|
460
|
-
try {
|
|
461
|
-
const cachePath = join(cachedPath, '.project-graph-cache.json');
|
|
462
|
-
if (existsSync(cachePath)) {
|
|
463
|
-
unlinkSync(cachePath);
|
|
464
|
-
}
|
|
465
|
-
} catch (e) {}
|
|
466
|
-
}
|
|
467
|
-
cachedGraph = null;
|
|
468
|
-
cachedPath = null;
|
|
469
|
-
cachedMtimes.clear();
|
|
470
|
-
}
|