@esankhan3/code-search-mcp 0.0.2 → 0.1.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 +203 -263
- package/dist/resources/resources.js +3 -3
- package/dist/server.js +2 -2
- package/dist/tools/graph.js +1 -1
- package/dist/tools/index-tools.js +1 -1
- package/dist/tools/profile.js +2 -2
- package/dist/tools/search.js +1 -1
- package/package.json +16 -9
- package/dist/core/ast-graph-builder.js +0 -800
- package/dist/core/chunker.js +0 -368
- package/dist/core/claude-runner.js +0 -173
- package/dist/core/config.js +0 -59
- package/dist/core/cross-repo-detector.js +0 -887
- package/dist/core/embedder.js +0 -334
- package/dist/core/file-walker.js +0 -186
- package/dist/core/git-diff.js +0 -160
- package/dist/core/graph-metrics.js +0 -233
- package/dist/core/indexer.js +0 -552
- package/dist/core/project-graph-builder.js +0 -202
- package/dist/core/query-classifier.js +0 -144
- package/dist/core/query-router.js +0 -210
- package/dist/core/rag-evaluator.js +0 -213
- package/dist/core/repo-profiler.js +0 -383
- package/dist/core/reranker.js +0 -218
- package/dist/core/retriever.js +0 -242
- package/dist/core/semantic-edge-detector.js +0 -147
- package/dist/core/service-mesh-inferrer.js +0 -241
- package/dist/core/structural-hasher.js +0 -316
- package/dist/core/tree-sitter-parser.js +0 -539
- package/dist/core/types.js +0 -0
- package/dist/core/vector-store.js +0 -259
- package/dist/core/workspace-detector.js +0 -481
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { relative, extname, dirname, resolve, basename, join } from "node:path";
|
|
3
|
-
import { walkDir, langFromExt, extractImports, extractNamedImports } from "./file-walker.js";
|
|
4
|
-
function tsEntityPatterns() {
|
|
5
|
-
return [
|
|
6
|
-
{ regex: /^export\s+function\s+/, type: "function", nameExtractor: (l) => l.match(/^export\s+function\s+(\w+)/)?.[1] },
|
|
7
|
-
{ regex: /^export\s+async\s+function\s+/, type: "function", nameExtractor: (l) => l.match(/^export\s+async\s+function\s+(\w+)/)?.[1] },
|
|
8
|
-
{ regex: /^function\s+/, type: "function", nameExtractor: (l) => l.match(/^function\s+(\w+)/)?.[1] },
|
|
9
|
-
{ regex: /^async\s+function\s+/, type: "function", nameExtractor: (l) => l.match(/^async\s+function\s+(\w+)/)?.[1] },
|
|
10
|
-
{ regex: /^export\s+class\s+/, type: "class", nameExtractor: (l) => l.match(/^export\s+class\s+(\w+)/)?.[1] },
|
|
11
|
-
{ regex: /^class\s+/, type: "class", nameExtractor: (l) => l.match(/^class\s+(\w+)/)?.[1] },
|
|
12
|
-
{ regex: /^export\s+interface\s+/, type: "interface", nameExtractor: (l) => l.match(/^export\s+interface\s+(\w+)/)?.[1] },
|
|
13
|
-
{ regex: /^interface\s+/, type: "interface", nameExtractor: (l) => l.match(/^interface\s+(\w+)/)?.[1] },
|
|
14
|
-
{ regex: /^export\s+type\s+/, type: "type", nameExtractor: (l) => l.match(/^export\s+type\s+(\w+)/)?.[1] },
|
|
15
|
-
{ regex: /^type\s+/, type: "type", nameExtractor: (l) => l.match(/^type\s+(\w+)/)?.[1] },
|
|
16
|
-
{ regex: /^export\s+const\s+/, type: "const", nameExtractor: (l) => l.match(/^export\s+const\s+(\w+)/)?.[1] }
|
|
17
|
-
];
|
|
18
|
-
}
|
|
19
|
-
function pyEntityPatterns() {
|
|
20
|
-
return [
|
|
21
|
-
{ regex: /^def\s+/, type: "function", nameExtractor: (l) => l.match(/^def\s+(\w+)/)?.[1] },
|
|
22
|
-
{ regex: /^async\s+def\s+/, type: "function", nameExtractor: (l) => l.match(/^async\s+def\s+(\w+)/)?.[1] },
|
|
23
|
-
{ regex: /^class\s+/, type: "class", nameExtractor: (l) => l.match(/^class\s+(\w+)/)?.[1] }
|
|
24
|
-
];
|
|
25
|
-
}
|
|
26
|
-
function goEntityPatterns() {
|
|
27
|
-
return [
|
|
28
|
-
{ regex: /^func\s+/, type: "function", nameExtractor: (l) => l.match(/^func\s+(?:\([^)]*\)\s+)?(\w+)/)?.[1] },
|
|
29
|
-
{ regex: /^type\s+\w+\s+struct/, type: "struct", nameExtractor: (l) => l.match(/^type\s+(\w+)\s+struct/)?.[1] },
|
|
30
|
-
{ regex: /^type\s+\w+\s+interface/, type: "interface", nameExtractor: (l) => l.match(/^type\s+(\w+)\s+interface/)?.[1] }
|
|
31
|
-
];
|
|
32
|
-
}
|
|
33
|
-
function rsEntityPatterns() {
|
|
34
|
-
return [
|
|
35
|
-
{ regex: /^(?:pub\s+)?fn\s+/, type: "function", nameExtractor: (l) => l.match(/fn\s+(\w+)/)?.[1] },
|
|
36
|
-
{ regex: /^(?:pub\s+)?struct\s+/, type: "struct", nameExtractor: (l) => l.match(/struct\s+(\w+)/)?.[1] },
|
|
37
|
-
{ regex: /^impl\s+/, type: "impl", nameExtractor: (l) => l.match(/^impl\s+(?:<[^>]+>\s+)?(\w+)/)?.[1] },
|
|
38
|
-
{ regex: /^(?:pub\s+)?trait\s+/, type: "trait", nameExtractor: (l) => l.match(/trait\s+(\w+)/)?.[1] },
|
|
39
|
-
{ regex: /^(?:pub\s+)?enum\s+/, type: "enum", nameExtractor: (l) => l.match(/enum\s+(\w+)/)?.[1] }
|
|
40
|
-
];
|
|
41
|
-
}
|
|
42
|
-
function javaEntityPatterns() {
|
|
43
|
-
return [
|
|
44
|
-
{ regex: /(?:public|private|protected)\s+class\s+/, type: "class", nameExtractor: (l) => l.match(/class\s+(\w+)/)?.[1] },
|
|
45
|
-
{ regex: /(?:public|private|protected)\s+interface\s+/, type: "interface", nameExtractor: (l) => l.match(/interface\s+(\w+)/)?.[1] },
|
|
46
|
-
{ regex: /(?:public|private|protected)\s+(?:static\s+)?(?:void|int|long|boolean|String|[A-Z]\w*)\s+\w+\s*\(/, type: "method", nameExtractor: (l) => l.match(/(?:void|int|long|boolean|String|[A-Z]\w*)\s+(\w+)\s*\(/)?.[1] }
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
function phpEntityPatterns() {
|
|
50
|
-
return [
|
|
51
|
-
{ regex: /^(?:abstract\s+)?class\s+/, type: "class", nameExtractor: (l) => l.match(/class\s+(\w+)/)?.[1] },
|
|
52
|
-
{ regex: /^interface\s+/, type: "interface", nameExtractor: (l) => l.match(/interface\s+(\w+)/)?.[1] },
|
|
53
|
-
{ regex: /^trait\s+/, type: "trait", nameExtractor: (l) => l.match(/trait\s+(\w+)/)?.[1] },
|
|
54
|
-
{ regex: /^(?:public|private|protected)\s+(?:static\s+)?function\s+/, type: "method", nameExtractor: (l) => l.match(/function\s+(\w+)/)?.[1] },
|
|
55
|
-
{ regex: /^function\s+/, type: "function", nameExtractor: (l) => l.match(/^function\s+(\w+)/)?.[1] }
|
|
56
|
-
];
|
|
57
|
-
}
|
|
58
|
-
function entityPatternsForLang(lang) {
|
|
59
|
-
switch (lang) {
|
|
60
|
-
case "typescript":
|
|
61
|
-
case "javascript":
|
|
62
|
-
return tsEntityPatterns();
|
|
63
|
-
case "python":
|
|
64
|
-
return pyEntityPatterns();
|
|
65
|
-
case "go":
|
|
66
|
-
return goEntityPatterns();
|
|
67
|
-
case "rust":
|
|
68
|
-
return rsEntityPatterns();
|
|
69
|
-
case "java":
|
|
70
|
-
return javaEntityPatterns();
|
|
71
|
-
case "php":
|
|
72
|
-
return phpEntityPatterns();
|
|
73
|
-
default:
|
|
74
|
-
return tsEntityPatterns();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function extractInheritance(line, lang) {
|
|
78
|
-
const parents = [];
|
|
79
|
-
if (lang === "typescript" || lang === "javascript") {
|
|
80
|
-
const ext = line.match(/class\s+\w+\s+extends\s+(\w+)/);
|
|
81
|
-
if (ext) parents.push(ext[1]);
|
|
82
|
-
const impl = line.match(/class\s+\w+(?:\s+extends\s+\w+)?\s+implements\s+([\w,\s]+)/);
|
|
83
|
-
if (impl) parents.push(...impl[1].split(",").map((s) => s.trim()).filter(Boolean));
|
|
84
|
-
} else if (lang === "python") {
|
|
85
|
-
const m = line.match(/^class\s+\w+\(([^)]+)\)/);
|
|
86
|
-
if (m) parents.push(...m[1].split(",").map((s) => s.trim()).filter((s) => s && s !== "object"));
|
|
87
|
-
} else if (lang === "rust") {
|
|
88
|
-
const m = line.match(/^impl\s+(\w+)\s+for\s+(\w+)/);
|
|
89
|
-
if (m) parents.push(m[1]);
|
|
90
|
-
} else if (lang === "java" || lang === "php") {
|
|
91
|
-
const ext = line.match(/class\s+\w+\s+extends\s+(\w+)/);
|
|
92
|
-
if (ext) parents.push(ext[1]);
|
|
93
|
-
const impl = line.match(/class\s+\w+(?:\s+extends\s+\w+)?\s+implements\s+([\w,\s]+)/);
|
|
94
|
-
if (impl) parents.push(...impl[1].split(",").map((s) => s.trim()).filter(Boolean));
|
|
95
|
-
}
|
|
96
|
-
return parents;
|
|
97
|
-
}
|
|
98
|
-
function extractTypeReferences(body, lang) {
|
|
99
|
-
const refs = /* @__PURE__ */ new Set();
|
|
100
|
-
if (lang === "typescript" || lang === "javascript") {
|
|
101
|
-
for (const m of body.matchAll(/:\s*([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
102
|
-
for (const m of body.matchAll(/<([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
103
|
-
for (const m of body.matchAll(/\bas\s+([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
104
|
-
for (const m of body.matchAll(/\bnew\s+([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
105
|
-
} else if (lang === "python") {
|
|
106
|
-
for (const m of body.matchAll(/:\s*([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
107
|
-
for (const m of body.matchAll(/->\s*([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
108
|
-
} else if (lang === "go") {
|
|
109
|
-
for (const m of body.matchAll(/[*\[\]]\s*([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
110
|
-
for (const m of body.matchAll(/\w+\s+([A-Z][A-Za-z0-9_]*)[,)\s]/g)) refs.add(m[1]);
|
|
111
|
-
for (const m of body.matchAll(/\)\s*(?:\()?([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
112
|
-
} else if (lang === "java" || lang === "php") {
|
|
113
|
-
for (const m of body.matchAll(/\b([A-Z][A-Za-z0-9_]*)\s+\$?\w+/g)) refs.add(m[1]);
|
|
114
|
-
for (const m of body.matchAll(/\bnew\s+([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
115
|
-
} else if (lang === "rust") {
|
|
116
|
-
for (const m of body.matchAll(/[-:]\s*(?:&\s*(?:mut\s+)?)?([A-Z][A-Za-z0-9_]*)/g)) refs.add(m[1]);
|
|
117
|
-
}
|
|
118
|
-
const builtins = /* @__PURE__ */ new Set([
|
|
119
|
-
"String",
|
|
120
|
-
"Number",
|
|
121
|
-
"Boolean",
|
|
122
|
-
"Object",
|
|
123
|
-
"Array",
|
|
124
|
-
"Map",
|
|
125
|
-
"Set",
|
|
126
|
-
"Promise",
|
|
127
|
-
"Error",
|
|
128
|
-
"Date",
|
|
129
|
-
"RegExp",
|
|
130
|
-
"Function",
|
|
131
|
-
"Symbol",
|
|
132
|
-
"Buffer",
|
|
133
|
-
"Console",
|
|
134
|
-
"JSON",
|
|
135
|
-
"Math",
|
|
136
|
-
"Uint8Array",
|
|
137
|
-
"Int32Array",
|
|
138
|
-
"Float64Array",
|
|
139
|
-
"None",
|
|
140
|
-
"True",
|
|
141
|
-
"False",
|
|
142
|
-
"Self",
|
|
143
|
-
"Vec",
|
|
144
|
-
"Box",
|
|
145
|
-
"Option",
|
|
146
|
-
"Result",
|
|
147
|
-
"HashMap",
|
|
148
|
-
"HashSet",
|
|
149
|
-
"Iterator",
|
|
150
|
-
"Exception",
|
|
151
|
-
"Throwable"
|
|
152
|
-
]);
|
|
153
|
-
return [...refs].filter((r) => r.length >= 3 && !builtins.has(r));
|
|
154
|
-
}
|
|
155
|
-
const RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"];
|
|
156
|
-
function extractPackageName(importPath) {
|
|
157
|
-
if (importPath.startsWith("@")) {
|
|
158
|
-
const parts = importPath.split("/");
|
|
159
|
-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
|
|
160
|
-
}
|
|
161
|
-
const slashIdx = importPath.indexOf("/");
|
|
162
|
-
return slashIdx > 0 ? importPath.slice(0, slashIdx) : importPath;
|
|
163
|
-
}
|
|
164
|
-
function findOwningPackage(relPath, workspaceMap) {
|
|
165
|
-
let best = null;
|
|
166
|
-
let bestLen = -1;
|
|
167
|
-
for (const pkg of workspaceMap.packages) {
|
|
168
|
-
if (pkg.relativePath === ".") continue;
|
|
169
|
-
const prefix = pkg.relativePath + "/";
|
|
170
|
-
if (relPath.startsWith(prefix) && prefix.length > bestLen) {
|
|
171
|
-
best = pkg;
|
|
172
|
-
bestLen = prefix.length;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return best;
|
|
176
|
-
}
|
|
177
|
-
function resolveImportPath(importPath, currentFile, knownFiles, workspaceMap) {
|
|
178
|
-
if (importPath.startsWith(".")) {
|
|
179
|
-
const currentDir = dirname(currentFile);
|
|
180
|
-
const base = resolve("/", currentDir, importPath).slice(1);
|
|
181
|
-
for (const ext of ["", ...RESOLVE_EXTENSIONS]) {
|
|
182
|
-
const candidate = base + ext;
|
|
183
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
184
|
-
}
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
if (!workspaceMap) return null;
|
|
188
|
-
const pkgName = extractPackageName(importPath);
|
|
189
|
-
const pkg = workspaceMap.nameToPackage.get(pkgName);
|
|
190
|
-
if (pkg) {
|
|
191
|
-
if (importPath !== pkgName) {
|
|
192
|
-
const subPath = importPath.slice(pkgName.length + 1);
|
|
193
|
-
const fullSub = join(pkg.relativePath, subPath);
|
|
194
|
-
for (const ext of ["", ...RESOLVE_EXTENSIONS]) {
|
|
195
|
-
if (knownFiles.has(fullSub + ext)) return fullSub + ext;
|
|
196
|
-
}
|
|
197
|
-
const srcSub = join(pkg.relativePath, "src", subPath);
|
|
198
|
-
for (const ext of ["", ...RESOLVE_EXTENSIONS]) {
|
|
199
|
-
if (knownFiles.has(srcSub + ext)) return srcSub + ext;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return `pkg::${pkg.name}`;
|
|
203
|
-
}
|
|
204
|
-
for (const [alias, resolvedBase] of workspaceMap.pathAliases) {
|
|
205
|
-
if (importPath === alias || importPath.startsWith(alias + "/")) {
|
|
206
|
-
const subPath = importPath === alias ? "" : importPath.slice(alias.length + 1);
|
|
207
|
-
const mapped = subPath ? join(resolvedBase, subPath) : resolvedBase;
|
|
208
|
-
for (const ext of ["", ...RESOLVE_EXTENSIONS]) {
|
|
209
|
-
if (knownFiles.has(mapped + ext)) return mapped + ext;
|
|
210
|
-
}
|
|
211
|
-
for (const wsPkg of workspaceMap.packages) {
|
|
212
|
-
if (resolvedBase.startsWith(wsPkg.relativePath)) {
|
|
213
|
-
return `pkg::${wsPkg.name}`;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
function parseGoModulePath(repoPath) {
|
|
221
|
-
try {
|
|
222
|
-
const gomod = readFileSync(join(repoPath, "go.mod"), "utf-8");
|
|
223
|
-
const m = gomod.match(/^module\s+(\S+)/m);
|
|
224
|
-
return m?.[1] ?? null;
|
|
225
|
-
} catch {
|
|
226
|
-
return null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function buildGoPackageMap(repoPath, goModulePath, knownFiles) {
|
|
230
|
-
const pkgMap = /* @__PURE__ */ new Map();
|
|
231
|
-
const goDirs = /* @__PURE__ */ new Set();
|
|
232
|
-
for (const f of knownFiles) {
|
|
233
|
-
if (f.endsWith(".go")) {
|
|
234
|
-
goDirs.add(dirname(f));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
for (const dir of goDirs) {
|
|
238
|
-
const importPath = dir === "." ? goModulePath : `${goModulePath}/${dir}`;
|
|
239
|
-
pkgMap.set(importPath, dir);
|
|
240
|
-
}
|
|
241
|
-
return pkgMap;
|
|
242
|
-
}
|
|
243
|
-
function resolveGoImport(importPath, goModulePath, goPackageMap, knownFiles) {
|
|
244
|
-
if (!goModulePath || !importPath.startsWith(goModulePath)) return null;
|
|
245
|
-
const dir = goPackageMap.get(importPath);
|
|
246
|
-
if (!dir) return null;
|
|
247
|
-
for (const f of knownFiles) {
|
|
248
|
-
if (f.startsWith(dir + "/") && f.endsWith(".go") && !f.endsWith("_test.go")) {
|
|
249
|
-
return dir;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return dir;
|
|
253
|
-
}
|
|
254
|
-
function parseComposerAutoload(repoPath) {
|
|
255
|
-
const nsMap = /* @__PURE__ */ new Map();
|
|
256
|
-
try {
|
|
257
|
-
const composer = JSON.parse(readFileSync(join(repoPath, "composer.json"), "utf-8"));
|
|
258
|
-
const psr4 = composer?.autoload?.["psr-4"] ?? {};
|
|
259
|
-
for (const [ns, path] of Object.entries(psr4)) {
|
|
260
|
-
nsMap.set(ns, path.replace(/\/+$/, ""));
|
|
261
|
-
}
|
|
262
|
-
const devPsr4 = composer?.["autoload-dev"]?.["psr-4"] ?? {};
|
|
263
|
-
for (const [ns, path] of Object.entries(devPsr4)) {
|
|
264
|
-
nsMap.set(ns, path.replace(/\/+$/, ""));
|
|
265
|
-
}
|
|
266
|
-
} catch {
|
|
267
|
-
}
|
|
268
|
-
return nsMap;
|
|
269
|
-
}
|
|
270
|
-
function resolvePhpImport(importPath, phpNamespaceMap, knownFiles) {
|
|
271
|
-
for (const [prefix, basePath] of phpNamespaceMap) {
|
|
272
|
-
if (importPath.startsWith(prefix)) {
|
|
273
|
-
const relPart = importPath.slice(prefix.length).replace(/\\/g, "/");
|
|
274
|
-
const candidate = basePath + "/" + relPart + ".php";
|
|
275
|
-
if (knownFiles.has(candidate)) return candidate;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
function resolvePythonImport(importPath, currentFile, knownFiles) {
|
|
281
|
-
if (importPath.startsWith(".")) {
|
|
282
|
-
const dots = importPath.match(/^(\.+)/)?.[1].length ?? 1;
|
|
283
|
-
let baseDir = dirname(currentFile);
|
|
284
|
-
for (let i = 1; i < dots; i++) baseDir = dirname(baseDir);
|
|
285
|
-
const modulePart = importPath.slice(dots);
|
|
286
|
-
if (!modulePart) return null;
|
|
287
|
-
const candidate2 = join(baseDir, modulePart.replace(/\./g, "/"));
|
|
288
|
-
for (const ext of [".py", "/__init__.py"]) {
|
|
289
|
-
if (knownFiles.has(candidate2 + ext)) return candidate2 + ext;
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
const pathParts = importPath.split(".");
|
|
294
|
-
const candidate = pathParts.join("/");
|
|
295
|
-
for (const ext of [".py", "/__init__.py"]) {
|
|
296
|
-
if (knownFiles.has(candidate + ext)) return candidate + ext;
|
|
297
|
-
}
|
|
298
|
-
if (pathParts.length > 1) {
|
|
299
|
-
const withoutPkg = pathParts.slice(1).join("/");
|
|
300
|
-
for (const ext of [".py", "/__init__.py"]) {
|
|
301
|
-
if (knownFiles.has(withoutPkg + ext)) return withoutPkg + ext;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
function resolveRustImport(importPath, knownFiles) {
|
|
307
|
-
if (!importPath.startsWith("crate::")) return null;
|
|
308
|
-
const parts = importPath.slice(7).split("::");
|
|
309
|
-
for (let len = parts.length; len > 0; len--) {
|
|
310
|
-
const candidate = "src/" + parts.slice(0, len).join("/");
|
|
311
|
-
if (knownFiles.has(candidate + ".rs")) return candidate + ".rs";
|
|
312
|
-
if (knownFiles.has(candidate + "/mod.rs")) return candidate + "/mod.rs";
|
|
313
|
-
}
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
function resolveJavaImport(importPath, knownFiles) {
|
|
317
|
-
const filePath = importPath.replace(/\./g, "/") + ".java";
|
|
318
|
-
for (const prefix of ["src/main/java/", "src/", ""]) {
|
|
319
|
-
if (knownFiles.has(prefix + filePath)) return prefix + filePath;
|
|
320
|
-
}
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
const SKIP_CALL_NAMES = /* @__PURE__ */ new Set([
|
|
324
|
-
"if",
|
|
325
|
-
"else",
|
|
326
|
-
"for",
|
|
327
|
-
"while",
|
|
328
|
-
"switch",
|
|
329
|
-
"case",
|
|
330
|
-
"return",
|
|
331
|
-
"new",
|
|
332
|
-
"throw",
|
|
333
|
-
"catch",
|
|
334
|
-
"try",
|
|
335
|
-
"await",
|
|
336
|
-
"yield",
|
|
337
|
-
"import",
|
|
338
|
-
"export",
|
|
339
|
-
"from",
|
|
340
|
-
"const",
|
|
341
|
-
"let",
|
|
342
|
-
"var",
|
|
343
|
-
"function",
|
|
344
|
-
"class",
|
|
345
|
-
"interface",
|
|
346
|
-
"type",
|
|
347
|
-
"enum",
|
|
348
|
-
"struct",
|
|
349
|
-
"impl",
|
|
350
|
-
"trait",
|
|
351
|
-
"fn",
|
|
352
|
-
"pub",
|
|
353
|
-
"use",
|
|
354
|
-
"mod",
|
|
355
|
-
"def",
|
|
356
|
-
"self",
|
|
357
|
-
"super",
|
|
358
|
-
"this",
|
|
359
|
-
"true",
|
|
360
|
-
"false",
|
|
361
|
-
"null",
|
|
362
|
-
"undefined",
|
|
363
|
-
"void",
|
|
364
|
-
"async",
|
|
365
|
-
"static",
|
|
366
|
-
"print",
|
|
367
|
-
"println",
|
|
368
|
-
"console",
|
|
369
|
-
"log",
|
|
370
|
-
"warn",
|
|
371
|
-
"error",
|
|
372
|
-
"info",
|
|
373
|
-
"debug",
|
|
374
|
-
"require",
|
|
375
|
-
"module",
|
|
376
|
-
"exports",
|
|
377
|
-
"default",
|
|
378
|
-
"extends",
|
|
379
|
-
"implements",
|
|
380
|
-
"package",
|
|
381
|
-
"public",
|
|
382
|
-
"private",
|
|
383
|
-
"protected",
|
|
384
|
-
"abstract",
|
|
385
|
-
"final",
|
|
386
|
-
"override",
|
|
387
|
-
"virtual",
|
|
388
|
-
"delete",
|
|
389
|
-
"typeof",
|
|
390
|
-
"instanceof",
|
|
391
|
-
"len",
|
|
392
|
-
"str",
|
|
393
|
-
"int",
|
|
394
|
-
"float",
|
|
395
|
-
"bool",
|
|
396
|
-
"list",
|
|
397
|
-
"dict",
|
|
398
|
-
"set",
|
|
399
|
-
"tuple",
|
|
400
|
-
"range",
|
|
401
|
-
"map",
|
|
402
|
-
"filter",
|
|
403
|
-
"reduce",
|
|
404
|
-
"append",
|
|
405
|
-
"push",
|
|
406
|
-
"pop",
|
|
407
|
-
"shift",
|
|
408
|
-
"fmt",
|
|
409
|
-
"err",
|
|
410
|
-
"nil",
|
|
411
|
-
"make",
|
|
412
|
-
"close",
|
|
413
|
-
"open",
|
|
414
|
-
"read",
|
|
415
|
-
"write"
|
|
416
|
-
]);
|
|
417
|
-
function resolveCallTarget(calledName, callerFile, callerEntityId, symbolToIds, fileImportTargets) {
|
|
418
|
-
const candidates = symbolToIds.get(calledName);
|
|
419
|
-
if (!candidates || candidates.length === 0) return null;
|
|
420
|
-
if (candidates.length === 1) {
|
|
421
|
-
if (candidates[0] === callerEntityId) return null;
|
|
422
|
-
const targetFile = candidates[0].split("::")[0];
|
|
423
|
-
const confidence = targetFile === callerFile ? 0.9 : 0.8;
|
|
424
|
-
return { targetId: candidates[0], confidence };
|
|
425
|
-
}
|
|
426
|
-
const sameFile = candidates.find((c) => c.startsWith(callerFile + "::") && c !== callerEntityId);
|
|
427
|
-
if (sameFile) return { targetId: sameFile, confidence: 0.9 };
|
|
428
|
-
const callerImports = fileImportTargets.get(callerFile);
|
|
429
|
-
if (callerImports) {
|
|
430
|
-
const importedCandidate = candidates.find((c) => {
|
|
431
|
-
const candidateFile = c.split("::")[0];
|
|
432
|
-
return callerImports.has(candidateFile);
|
|
433
|
-
});
|
|
434
|
-
if (importedCandidate) return { targetId: importedCandidate, confidence: 0.85 };
|
|
435
|
-
}
|
|
436
|
-
const callerDir = dirname(callerFile);
|
|
437
|
-
const sameDirCandidates = candidates.filter((c) => {
|
|
438
|
-
const candidateFile = c.split("::")[0];
|
|
439
|
-
return dirname(candidateFile) === callerDir && c !== callerEntityId;
|
|
440
|
-
});
|
|
441
|
-
if (sameDirCandidates.length === 1) return { targetId: sameDirCandidates[0], confidence: 0.65 };
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
function resolveImportFull(importPath, currentFile, lang, knownFiles, workspaceMap, goModulePath, goPackageMap, phpNamespaceMap) {
|
|
445
|
-
const standard = resolveImportPath(importPath, currentFile, knownFiles, workspaceMap);
|
|
446
|
-
if (standard) return standard;
|
|
447
|
-
if (lang === "go") {
|
|
448
|
-
return resolveGoImport(importPath, goModulePath, goPackageMap, knownFiles);
|
|
449
|
-
}
|
|
450
|
-
if (lang === "php") {
|
|
451
|
-
return resolvePhpImport(importPath, phpNamespaceMap, knownFiles);
|
|
452
|
-
}
|
|
453
|
-
if (lang === "python") {
|
|
454
|
-
return resolvePythonImport(importPath, currentFile, knownFiles);
|
|
455
|
-
}
|
|
456
|
-
if (lang === "rust") {
|
|
457
|
-
return resolveRustImport(importPath, knownFiles);
|
|
458
|
-
}
|
|
459
|
-
if (lang === "java") {
|
|
460
|
-
return resolveJavaImport(importPath, knownFiles);
|
|
461
|
-
}
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
async function buildAstGraph(repoPath, opts) {
|
|
465
|
-
const files = [];
|
|
466
|
-
walkDir(repoPath, files);
|
|
467
|
-
const maxFiles = opts?.maxFiles ?? 5e3;
|
|
468
|
-
const sourceFiles = files.slice(0, maxFiles);
|
|
469
|
-
const relPaths = new Set(sourceFiles.map((f) => relative(repoPath, f)));
|
|
470
|
-
const nodes = [];
|
|
471
|
-
const edges = [];
|
|
472
|
-
const entities = [];
|
|
473
|
-
const symbolToIds = /* @__PURE__ */ new Map();
|
|
474
|
-
function registerSymbol(name, entityId) {
|
|
475
|
-
const existing = symbolToIds.get(name);
|
|
476
|
-
if (existing) {
|
|
477
|
-
existing.push(entityId);
|
|
478
|
-
} else {
|
|
479
|
-
symbolToIds.set(name, [entityId]);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
const wsMap = opts?.workspaceMap;
|
|
483
|
-
if (wsMap) {
|
|
484
|
-
for (const pkg of wsMap.packages) {
|
|
485
|
-
nodes.push({
|
|
486
|
-
id: `pkg::${pkg.name}`,
|
|
487
|
-
label: pkg.name,
|
|
488
|
-
type: "package",
|
|
489
|
-
file: pkg.relativePath
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
const goModulePath = parseGoModulePath(repoPath);
|
|
494
|
-
const goPackageMap = goModulePath ? buildGoPackageMap(repoPath, goModulePath, relPaths) : /* @__PURE__ */ new Map();
|
|
495
|
-
const phpNamespaceMap = parseComposerAutoload(repoPath);
|
|
496
|
-
const fileImportTargets = /* @__PURE__ */ new Map();
|
|
497
|
-
for (const filePath of sourceFiles) {
|
|
498
|
-
let content;
|
|
499
|
-
try {
|
|
500
|
-
content = readFileSync(filePath, "utf-8");
|
|
501
|
-
} catch {
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
const relPath = relative(repoPath, filePath);
|
|
505
|
-
const ext = extname(filePath);
|
|
506
|
-
const lang = langFromExt(ext);
|
|
507
|
-
const lines = content.split("\n");
|
|
508
|
-
const patterns = entityPatternsForLang(lang);
|
|
509
|
-
nodes.push({ id: relPath, label: basename(relPath), type: "module", file: relPath });
|
|
510
|
-
let currentClassId = null;
|
|
511
|
-
let currentClassIndent = -1;
|
|
512
|
-
for (let i = 0; i < lines.length; i++) {
|
|
513
|
-
const line = lines[i];
|
|
514
|
-
const trimmed = line.trimStart();
|
|
515
|
-
const indent = line.length - trimmed.length;
|
|
516
|
-
if (currentClassId && indent <= currentClassIndent) {
|
|
517
|
-
currentClassId = null;
|
|
518
|
-
currentClassIndent = -1;
|
|
519
|
-
}
|
|
520
|
-
for (const pat of patterns) {
|
|
521
|
-
if (!pat.regex.test(trimmed)) continue;
|
|
522
|
-
const name = pat.nameExtractor(trimmed);
|
|
523
|
-
if (!name) break;
|
|
524
|
-
const entityId = `${relPath}::${name}`;
|
|
525
|
-
const parentNames = extractInheritance(trimmed, lang);
|
|
526
|
-
nodes.push({ id: entityId, label: name, type: pat.type, file: relPath });
|
|
527
|
-
const entity = {
|
|
528
|
-
id: entityId,
|
|
529
|
-
label: name,
|
|
530
|
-
type: pat.type,
|
|
531
|
-
file: relPath,
|
|
532
|
-
line: i,
|
|
533
|
-
indent,
|
|
534
|
-
parents: parentNames
|
|
535
|
-
};
|
|
536
|
-
entities.push(entity);
|
|
537
|
-
registerSymbol(name, entityId);
|
|
538
|
-
if (currentClassId && indent > currentClassIndent && (pat.type === "function" || pat.type === "method")) {
|
|
539
|
-
edges.push({ source: currentClassId, target: entityId, type: "contains", confidence: 1 });
|
|
540
|
-
const className = currentClassId.split("::").pop();
|
|
541
|
-
if (className) registerSymbol(`${className}.${name}`, entityId);
|
|
542
|
-
} else {
|
|
543
|
-
edges.push({ source: relPath, target: entityId, type: "contains", confidence: 1 });
|
|
544
|
-
}
|
|
545
|
-
if (pat.type === "class" || pat.type === "struct" || pat.type === "interface" || pat.type === "trait") {
|
|
546
|
-
currentClassId = entityId;
|
|
547
|
-
currentClassIndent = indent;
|
|
548
|
-
}
|
|
549
|
-
break;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
const importSpecs = extractImports(lines, lang);
|
|
553
|
-
const importTargetSet = /* @__PURE__ */ new Set();
|
|
554
|
-
for (const spec of importSpecs) {
|
|
555
|
-
const resolved = resolveImportFull(spec, relPath, lang, relPaths, wsMap, goModulePath, goPackageMap, phpNamespaceMap);
|
|
556
|
-
if (resolved) {
|
|
557
|
-
edges.push({ source: relPath, target: resolved, type: "imports", confidence: 1 });
|
|
558
|
-
importTargetSet.add(resolved);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
const namedImports = extractNamedImports(lines, lang);
|
|
562
|
-
for (const ni of namedImports) {
|
|
563
|
-
const resolved = resolveImportFull(ni.specifier, relPath, lang, relPaths, wsMap, goModulePath, goPackageMap, phpNamespaceMap);
|
|
564
|
-
if (!resolved) continue;
|
|
565
|
-
importTargetSet.add(resolved);
|
|
566
|
-
for (const name of ni.names) {
|
|
567
|
-
const targetEntityId = `${resolved}::${name}`;
|
|
568
|
-
const candidates = symbolToIds.get(name);
|
|
569
|
-
if (candidates?.includes(targetEntityId)) {
|
|
570
|
-
edges.push({ source: relPath, target: targetEntityId, type: "uses", confidence: 0.95 });
|
|
571
|
-
} else if (candidates && candidates.length === 1) {
|
|
572
|
-
edges.push({ source: relPath, target: candidates[0], type: "uses", confidence: 0.85 });
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
fileImportTargets.set(relPath, importTargetSet);
|
|
577
|
-
}
|
|
578
|
-
const entityByFile = /* @__PURE__ */ new Map();
|
|
579
|
-
for (const e of entities) {
|
|
580
|
-
const list = entityByFile.get(e.file) || [];
|
|
581
|
-
list.push(e);
|
|
582
|
-
entityByFile.set(e.file, list);
|
|
583
|
-
}
|
|
584
|
-
for (const filePath of sourceFiles) {
|
|
585
|
-
const relPath = relative(repoPath, filePath);
|
|
586
|
-
const fileEntities = entityByFile.get(relPath);
|
|
587
|
-
if (!fileEntities || fileEntities.length === 0) continue;
|
|
588
|
-
let content;
|
|
589
|
-
try {
|
|
590
|
-
content = readFileSync(filePath, "utf-8");
|
|
591
|
-
} catch {
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
const lines = content.split("\n");
|
|
595
|
-
const lang = langFromExt(extname(filePath));
|
|
596
|
-
for (let ei = 0; ei < fileEntities.length; ei++) {
|
|
597
|
-
const entity = fileEntities[ei];
|
|
598
|
-
const startLine = entity.line;
|
|
599
|
-
const endLine = ei + 1 < fileEntities.length ? fileEntities[ei + 1].line : lines.length;
|
|
600
|
-
const body = lines.slice(startLine, Math.min(startLine + 30, endLine)).join("\n");
|
|
601
|
-
const typeRefs = extractTypeReferences(body, lang);
|
|
602
|
-
for (const typeName of typeRefs) {
|
|
603
|
-
const resolved = resolveCallTarget(typeName, entity.file, entity.id, symbolToIds, fileImportTargets);
|
|
604
|
-
if (resolved) {
|
|
605
|
-
edges.push({ source: entity.id, target: resolved.targetId, type: "type-ref", confidence: Math.min(resolved.confidence, 0.85) });
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
for (const filePath of sourceFiles) {
|
|
611
|
-
const relPath = relative(repoPath, filePath);
|
|
612
|
-
const fileEntities = entityByFile.get(relPath);
|
|
613
|
-
if (!fileEntities || fileEntities.length === 0) continue;
|
|
614
|
-
let content;
|
|
615
|
-
try {
|
|
616
|
-
content = readFileSync(filePath, "utf-8");
|
|
617
|
-
} catch {
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
const lines = content.split("\n");
|
|
621
|
-
for (let ei = 0; ei < fileEntities.length; ei++) {
|
|
622
|
-
const entity = fileEntities[ei];
|
|
623
|
-
const startLine = entity.line;
|
|
624
|
-
const endLine = ei + 1 < fileEntities.length ? fileEntities[ei + 1].line : lines.length;
|
|
625
|
-
const body = lines.slice(startLine, endLine).join("\n");
|
|
626
|
-
const callRegex = /\b([a-zA-Z_]\w*)\s*\(/g;
|
|
627
|
-
let match;
|
|
628
|
-
const seenCalls = /* @__PURE__ */ new Set();
|
|
629
|
-
while ((match = callRegex.exec(body)) !== null) {
|
|
630
|
-
const calledName = match[1];
|
|
631
|
-
if (calledName.length < 3) continue;
|
|
632
|
-
if (SKIP_CALL_NAMES.has(calledName)) continue;
|
|
633
|
-
if (seenCalls.has(calledName)) continue;
|
|
634
|
-
seenCalls.add(calledName);
|
|
635
|
-
const resolved = resolveCallTarget(calledName, entity.file, entity.id, symbolToIds, fileImportTargets);
|
|
636
|
-
if (resolved) {
|
|
637
|
-
edges.push({ source: entity.id, target: resolved.targetId, type: "calls", confidence: resolved.confidence });
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
for (const entity of entities) {
|
|
643
|
-
for (const parent of entity.parents) {
|
|
644
|
-
const candidates = symbolToIds.get(parent);
|
|
645
|
-
if (!candidates || candidates.length === 0) continue;
|
|
646
|
-
if (candidates.length === 1) {
|
|
647
|
-
edges.push({ source: entity.id, target: candidates[0], type: "inherits", confidence: 0.95 });
|
|
648
|
-
} else {
|
|
649
|
-
const resolved = resolveCallTarget(parent, entity.file, entity.id, symbolToIds, fileImportTargets);
|
|
650
|
-
if (resolved) {
|
|
651
|
-
edges.push({ source: entity.id, target: resolved.targetId, type: "inherits", confidence: resolved.confidence });
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (wsMap) {
|
|
657
|
-
for (const relPath of relPaths) {
|
|
658
|
-
const owning = findOwningPackage(relPath, wsMap);
|
|
659
|
-
if (owning) {
|
|
660
|
-
edges.push({ source: `pkg::${owning.name}`, target: relPath, type: "contains", confidence: 1 });
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
return { nodes, links: edges };
|
|
665
|
-
}
|
|
666
|
-
function generateGraphReport(repoName, graph) {
|
|
667
|
-
const sections = [];
|
|
668
|
-
sections.push(`# Knowledge Graph Report: ${repoName}`);
|
|
669
|
-
sections.push(`
|
|
670
|
-
> Auto-generated by Anvil AST graph builder.
|
|
671
|
-
`);
|
|
672
|
-
const nodeCount = graph.nodes.length;
|
|
673
|
-
const edgeCount = graph.links.length;
|
|
674
|
-
const fileNodes = graph.nodes.filter((n) => n.type === "module");
|
|
675
|
-
sections.push(`## Overview
|
|
676
|
-
`);
|
|
677
|
-
sections.push(`- **Files:** ${fileNodes.length}`);
|
|
678
|
-
sections.push(`- **Entities:** ${nodeCount - fileNodes.length}`);
|
|
679
|
-
sections.push(`- **Total nodes:** ${nodeCount}`);
|
|
680
|
-
sections.push(`- **Total edges:** ${edgeCount}`);
|
|
681
|
-
const edgeTypeCounts = /* @__PURE__ */ new Map();
|
|
682
|
-
for (const e of graph.links) {
|
|
683
|
-
const t = e.type || "unknown";
|
|
684
|
-
edgeTypeCounts.set(t, (edgeTypeCounts.get(t) || 0) + 1);
|
|
685
|
-
}
|
|
686
|
-
sections.push(`
|
|
687
|
-
### Edge Types
|
|
688
|
-
`);
|
|
689
|
-
sections.push("| Type | Count | Avg Confidence |");
|
|
690
|
-
sections.push("|------|-------|----------------|");
|
|
691
|
-
for (const [type, count] of [...edgeTypeCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
692
|
-
const edgesOfType = graph.links.filter((e) => e.type === type);
|
|
693
|
-
const avgConf = edgesOfType.reduce((sum, e) => sum + (e.confidence ?? 0), 0) / edgesOfType.length;
|
|
694
|
-
sections.push(`| ${type} | ${count} | ${avgConf.toFixed(2)} |`);
|
|
695
|
-
}
|
|
696
|
-
const entityTypeCounts = /* @__PURE__ */ new Map();
|
|
697
|
-
for (const n of graph.nodes) {
|
|
698
|
-
const t = n.type || "unknown";
|
|
699
|
-
entityTypeCounts.set(t, (entityTypeCounts.get(t) || 0) + 1);
|
|
700
|
-
}
|
|
701
|
-
sections.push(`
|
|
702
|
-
### Entity Types
|
|
703
|
-
`);
|
|
704
|
-
sections.push("| Type | Count |");
|
|
705
|
-
sections.push("|------|-------|");
|
|
706
|
-
for (const [type, count] of [...entityTypeCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
707
|
-
sections.push(`| ${type} | ${count} |`);
|
|
708
|
-
}
|
|
709
|
-
const degreeMap = /* @__PURE__ */ new Map();
|
|
710
|
-
for (const e of graph.links) {
|
|
711
|
-
degreeMap.set(e.source, (degreeMap.get(e.source) || 0) + 1);
|
|
712
|
-
degreeMap.set(e.target, (degreeMap.get(e.target) || 0) + 1);
|
|
713
|
-
}
|
|
714
|
-
const topNodes = [...degreeMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
715
|
-
if (topNodes.length > 0) {
|
|
716
|
-
sections.push(`
|
|
717
|
-
### Hotspots (Top 10 by edge degree)
|
|
718
|
-
`);
|
|
719
|
-
sections.push("| Node | Degree | Type |");
|
|
720
|
-
sections.push("|------|--------|------|");
|
|
721
|
-
for (const [nodeId, degree] of topNodes) {
|
|
722
|
-
const node = graph.nodes.find((n) => n.id === nodeId);
|
|
723
|
-
sections.push(`| ${nodeId} | ${degree} | ${node?.type || "-"} |`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const importEdges = graph.links.filter((e) => e.type === "imports" || e.type === "uses");
|
|
727
|
-
const importCounts = /* @__PURE__ */ new Map();
|
|
728
|
-
for (const e of importEdges) {
|
|
729
|
-
importCounts.set(e.source, (importCounts.get(e.source) || 0) + 1);
|
|
730
|
-
}
|
|
731
|
-
const topImporters = [...importCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
732
|
-
if (topImporters.length > 0) {
|
|
733
|
-
sections.push(`
|
|
734
|
-
### Top Importers
|
|
735
|
-
`);
|
|
736
|
-
sections.push("| File | Import Count |");
|
|
737
|
-
sections.push("|------|-------------|");
|
|
738
|
-
for (const [file, count] of topImporters) {
|
|
739
|
-
sections.push(`| ${file} | ${count} |`);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
return sections.join("\n");
|
|
743
|
-
}
|
|
744
|
-
async function incrementalGraphUpdate(existingGraph, changedFiles, deletedFiles, repoPath, opts) {
|
|
745
|
-
const totalFiles = new Set(existingGraph.nodes.filter((n) => n.file).map((n) => n.file)).size;
|
|
746
|
-
const changedCount = changedFiles.length + deletedFiles.length;
|
|
747
|
-
if (totalFiles === 0 || changedCount / totalFiles > 0.3) {
|
|
748
|
-
return buildAstGraph(repoPath, opts);
|
|
749
|
-
}
|
|
750
|
-
try {
|
|
751
|
-
const filesToRemove = /* @__PURE__ */ new Set([...changedFiles, ...deletedFiles]);
|
|
752
|
-
const removedNodeIds = /* @__PURE__ */ new Set();
|
|
753
|
-
for (const node of existingGraph.nodes) {
|
|
754
|
-
if (node.file && filesToRemove.has(node.file)) {
|
|
755
|
-
removedNodeIds.add(node.id);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
for (const node of existingGraph.nodes) {
|
|
759
|
-
if (node.type === "module" && node.file && filesToRemove.has(node.file)) {
|
|
760
|
-
removedNodeIds.add(node.id);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
const keptNodes = existingGraph.nodes.filter((n) => !removedNodeIds.has(n.id));
|
|
764
|
-
const keptEdges = existingGraph.links.filter(
|
|
765
|
-
(e) => !removedNodeIds.has(e.source) && !removedNodeIds.has(e.target)
|
|
766
|
-
);
|
|
767
|
-
if (changedFiles.length === 0) {
|
|
768
|
-
return { nodes: keptNodes, links: keptEdges };
|
|
769
|
-
}
|
|
770
|
-
const freshGraph = await buildAstGraph(repoPath, opts);
|
|
771
|
-
const changedFileSet = new Set(changedFiles);
|
|
772
|
-
const newNodes = freshGraph.nodes.filter((n) => n.file && changedFileSet.has(n.file));
|
|
773
|
-
const newNodeIds = new Set(newNodes.map((n) => n.id));
|
|
774
|
-
const keptNodeIds = new Set(keptNodes.map((n) => n.id));
|
|
775
|
-
const newEdges = freshGraph.links.filter((e) => {
|
|
776
|
-
const sourceIsNew = newNodeIds.has(e.source);
|
|
777
|
-
const targetIsNew = newNodeIds.has(e.target);
|
|
778
|
-
const sourceExists = keptNodeIds.has(e.source) || newNodeIds.has(e.source);
|
|
779
|
-
const targetExists = keptNodeIds.has(e.target) || newNodeIds.has(e.target);
|
|
780
|
-
return sourceExists && targetExists && (sourceIsNew || targetIsNew);
|
|
781
|
-
});
|
|
782
|
-
const updatedEdges = freshGraph.links.filter((e) => {
|
|
783
|
-
const sourceKept = keptNodeIds.has(e.source);
|
|
784
|
-
const targetNew = newNodeIds.has(e.target);
|
|
785
|
-
return sourceKept && targetNew;
|
|
786
|
-
});
|
|
787
|
-
return {
|
|
788
|
-
nodes: [...keptNodes, ...newNodes],
|
|
789
|
-
links: [...keptEdges, ...newEdges, ...updatedEdges]
|
|
790
|
-
};
|
|
791
|
-
} catch (err) {
|
|
792
|
-
console.warn(`[ast-graph] Incremental update failed, falling back to full rebuild: ${err}`);
|
|
793
|
-
return buildAstGraph(repoPath, opts);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
export {
|
|
797
|
-
buildAstGraph,
|
|
798
|
-
generateGraphReport,
|
|
799
|
-
incrementalGraphUpdate
|
|
800
|
-
};
|