ctxloom-pro 1.0.4 → 1.0.6
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/LICENSE +61 -207
- package/README.md +55 -5
- package/dist/VectorStore-G6RNAVQC.js +8 -0
- package/dist/chunk-KPNCYRWW.js +8409 -0
- package/dist/{chunk-ZYDVY7VZ.js → chunk-MZRK5LFU.js} +35 -30
- package/dist/{chunk-XNKTZGDX.js → chunk-NC4L5MCD.js} +32 -13
- package/dist/{chunk-IHXVD5SO.js → chunk-YXMXRVFH.js} +3 -4
- package/dist/{embedder-RECRKXTB.js → embedder-MUH5U4NC.js} +3 -3
- package/dist/index.js +456 -4820
- package/dist/logger-SVRJYSFC.js +7 -0
- package/dist/src-GYVQEDV5.js +163 -0
- package/dist/workers/indexerWorker.js +8 -5
- package/package.json +8 -5
- package/dist/ASTParser-3QJ637L6.js +0 -8
- package/dist/DependencyGraph-FGTNORTW.js +0 -10
- package/dist/VectorStore-UQNBYPBV.js +0 -8
- package/dist/chunk-5CJIMX6D.js +0 -1599
- package/dist/chunk-PSLPRDPL.js +0 -139
- package/dist/chunk-RE2V3FRF.js +0 -1052
- package/dist/logger-2FVN3AGZ.js +0 -7
- package/dist/rules-OMDOX7IJ.js +0 -15
package/dist/chunk-RE2V3FRF.js
DELETED
|
@@ -1,1052 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
collectFiles
|
|
3
|
-
} from "./chunk-ZYDVY7VZ.js";
|
|
4
|
-
import {
|
|
5
|
-
ASTParser,
|
|
6
|
-
extractNotebookPythonSource
|
|
7
|
-
} from "./chunk-5CJIMX6D.js";
|
|
8
|
-
import {
|
|
9
|
-
logger
|
|
10
|
-
} from "./chunk-IHXVD5SO.js";
|
|
11
|
-
|
|
12
|
-
// src/graph/DependencyGraph.js
|
|
13
|
-
import fs3 from "fs";
|
|
14
|
-
import path3 from "path";
|
|
15
|
-
|
|
16
|
-
// src/utils/importExtractor.js
|
|
17
|
-
import fs2 from "fs";
|
|
18
|
-
import path2 from "path";
|
|
19
|
-
|
|
20
|
-
// src/utils/GoModuleResolver.js
|
|
21
|
-
import fs from "fs";
|
|
22
|
-
import path from "path";
|
|
23
|
-
var GoModuleResolver = class {
|
|
24
|
-
rootDir;
|
|
25
|
-
modulePath = null;
|
|
26
|
-
initialized = false;
|
|
27
|
-
constructor(rootDir) {
|
|
28
|
-
this.rootDir = rootDir;
|
|
29
|
-
this.init();
|
|
30
|
-
}
|
|
31
|
-
init() {
|
|
32
|
-
if (this.initialized)
|
|
33
|
-
return;
|
|
34
|
-
this.initialized = true;
|
|
35
|
-
const goModPath = path.join(this.rootDir, "go.mod");
|
|
36
|
-
if (!fs.existsSync(goModPath))
|
|
37
|
-
return;
|
|
38
|
-
try {
|
|
39
|
-
const content = fs.readFileSync(goModPath, "utf-8");
|
|
40
|
-
const match = content.match(/^module\s+(\S+)/m);
|
|
41
|
-
if (match?.[1]) {
|
|
42
|
-
this.modulePath = match[1];
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
/** Returns the module path declared in go.mod, or null if no go.mod found. */
|
|
48
|
-
getModulePath() {
|
|
49
|
-
return this.modulePath;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Resolve a module-path import (e.g. `github.com/myorg/myapp/internal/auth`)
|
|
53
|
-
* to a relative project path (e.g. `internal/auth/auth.go`).
|
|
54
|
-
*
|
|
55
|
-
* Returns null for:
|
|
56
|
-
* - Third-party imports (different module prefix)
|
|
57
|
-
* - Relative imports (use resolveRelative() instead)
|
|
58
|
-
* - Imports where no .go files are found
|
|
59
|
-
*/
|
|
60
|
-
resolve(importPath) {
|
|
61
|
-
if (!this.modulePath)
|
|
62
|
-
return null;
|
|
63
|
-
if (importPath.startsWith("."))
|
|
64
|
-
return null;
|
|
65
|
-
if (!importPath.startsWith(this.modulePath))
|
|
66
|
-
return null;
|
|
67
|
-
const suffix = importPath.slice(this.modulePath.length);
|
|
68
|
-
if (!suffix)
|
|
69
|
-
return null;
|
|
70
|
-
const subPath = suffix.startsWith("/") ? suffix.slice(1) : suffix;
|
|
71
|
-
const absDir = path.join(this.rootDir, subPath);
|
|
72
|
-
return this.firstGoFileInDir(absDir, subPath);
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Resolve a relative import (`./config`, `../pkg`) from a given Go source file.
|
|
76
|
-
* Returns the relative project path to the first .go file found, or null.
|
|
77
|
-
*/
|
|
78
|
-
resolveRelative(fromFile, importSpec) {
|
|
79
|
-
const fromDir = path.dirname(fromFile);
|
|
80
|
-
const absTarget = path.resolve(fromDir, importSpec);
|
|
81
|
-
const subPath = path.relative(this.rootDir, absTarget);
|
|
82
|
-
return this.firstGoFileInDir(absTarget, subPath);
|
|
83
|
-
}
|
|
84
|
-
firstGoFileInDir(absDir, relDir) {
|
|
85
|
-
if (!fs.existsSync(absDir))
|
|
86
|
-
return null;
|
|
87
|
-
let entries;
|
|
88
|
-
try {
|
|
89
|
-
entries = fs.readdirSync(absDir);
|
|
90
|
-
} catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const goFiles = entries.filter((f) => f.endsWith(".go")).sort((a, b) => {
|
|
94
|
-
const aTest = a.endsWith("_test.go") ? 1 : 0;
|
|
95
|
-
const bTest = b.endsWith("_test.go") ? 1 : 0;
|
|
96
|
-
return aTest - bTest || a.localeCompare(b);
|
|
97
|
-
});
|
|
98
|
-
if (goFiles.length === 0)
|
|
99
|
-
return null;
|
|
100
|
-
return path.join(relDir, goFiles[0]).replace(/\\/g, "/");
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// src/utils/importExtractor.js
|
|
105
|
-
var goResolverCache = /* @__PURE__ */ new Map();
|
|
106
|
-
function getGoResolver(rootDir) {
|
|
107
|
-
let resolver = goResolverCache.get(rootDir);
|
|
108
|
-
if (!resolver) {
|
|
109
|
-
resolver = new GoModuleResolver(rootDir);
|
|
110
|
-
goResolverCache.set(rootDir, resolver);
|
|
111
|
-
}
|
|
112
|
-
return resolver;
|
|
113
|
-
}
|
|
114
|
-
function extractImports(filePath, content) {
|
|
115
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
116
|
-
switch (ext) {
|
|
117
|
-
case ".py":
|
|
118
|
-
return extractPythonImports(content);
|
|
119
|
-
case ".rs":
|
|
120
|
-
return extractRustModules(content);
|
|
121
|
-
case ".go":
|
|
122
|
-
return extractGoImports(content);
|
|
123
|
-
case ".java":
|
|
124
|
-
return extractJavaImports(content);
|
|
125
|
-
case ".cs":
|
|
126
|
-
return extractCSharpImports(content);
|
|
127
|
-
case ".rb":
|
|
128
|
-
return extractRubyImports(content);
|
|
129
|
-
case ".kt":
|
|
130
|
-
case ".kts":
|
|
131
|
-
return extractKotlinImports(content);
|
|
132
|
-
case ".swift":
|
|
133
|
-
return extractSwiftImports(content);
|
|
134
|
-
case ".php":
|
|
135
|
-
return extractPhpImports(content);
|
|
136
|
-
case ".dart":
|
|
137
|
-
return extractDartImports(content);
|
|
138
|
-
case ".ipynb":
|
|
139
|
-
return extractNotebookImports(filePath, content);
|
|
140
|
-
case ".vue":
|
|
141
|
-
return extractVueImports(content);
|
|
142
|
-
default:
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function resolveImport(fromAbs, raw, rootDir) {
|
|
147
|
-
const ext = path2.extname(fromAbs).toLowerCase();
|
|
148
|
-
const fromDir = path2.dirname(fromAbs);
|
|
149
|
-
if (ext === ".py")
|
|
150
|
-
return resolvePythonImport(fromAbs, fromDir, raw, rootDir);
|
|
151
|
-
if (ext === ".rs")
|
|
152
|
-
return resolveRustModule(fromDir, raw, rootDir);
|
|
153
|
-
if (ext === ".go")
|
|
154
|
-
return resolveGoImportFull(fromAbs, fromDir, raw, rootDir);
|
|
155
|
-
if (ext === ".java")
|
|
156
|
-
return resolveJavaImport(fromDir, raw, rootDir);
|
|
157
|
-
if (ext === ".cs")
|
|
158
|
-
return resolveCSharpImport(fromDir, raw, rootDir);
|
|
159
|
-
if (ext === ".rb")
|
|
160
|
-
return resolveRubyImport(fromDir, raw, rootDir);
|
|
161
|
-
if (ext === ".kt" || ext === ".kts")
|
|
162
|
-
return resolveKotlinImport(fromDir, raw, rootDir);
|
|
163
|
-
if (ext === ".swift")
|
|
164
|
-
return resolveSwiftImport(fromDir, raw, rootDir);
|
|
165
|
-
if (ext === ".php")
|
|
166
|
-
return resolvePhpImport(fromAbs, fromDir, raw, rootDir);
|
|
167
|
-
if (ext === ".dart")
|
|
168
|
-
return resolveDartImport(fromAbs, fromDir, raw, rootDir);
|
|
169
|
-
if (ext === ".ipynb")
|
|
170
|
-
return resolvePythonImport(fromAbs, fromDir, raw, rootDir);
|
|
171
|
-
if (ext === ".vue")
|
|
172
|
-
return resolveVueImport(fromAbs, fromDir, raw, rootDir);
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
function extractPythonImports(content) {
|
|
176
|
-
const results = [];
|
|
177
|
-
const relFrom = /^from\s+(\.+[\w.]*)\s+import/gm;
|
|
178
|
-
let m;
|
|
179
|
-
while ((m = relFrom.exec(content)) !== null) {
|
|
180
|
-
results.push({ specifier: m[1], isRelative: true });
|
|
181
|
-
}
|
|
182
|
-
return results;
|
|
183
|
-
}
|
|
184
|
-
function resolvePythonImport(fromAbs, fromDir, raw, rootDir) {
|
|
185
|
-
const dotsMatch = raw.specifier.match(/^(\.+)/);
|
|
186
|
-
const dots = dotsMatch?.[1] ?? ".";
|
|
187
|
-
const modulePart = raw.specifier.slice(dots.length);
|
|
188
|
-
let baseDir = fromDir;
|
|
189
|
-
for (let i = 1; i < dots.length; i++) {
|
|
190
|
-
baseDir = path2.dirname(baseDir);
|
|
191
|
-
}
|
|
192
|
-
const modulePath = modulePart.replace(/\./g, path2.sep);
|
|
193
|
-
const candidates = modulePath ? [
|
|
194
|
-
path2.join(baseDir, modulePath + ".py"),
|
|
195
|
-
path2.join(baseDir, modulePath, "__init__.py")
|
|
196
|
-
] : [
|
|
197
|
-
path2.join(fromDir, "__init__.py"),
|
|
198
|
-
fromAbs
|
|
199
|
-
// self — skip below
|
|
200
|
-
];
|
|
201
|
-
for (const c of candidates) {
|
|
202
|
-
if (c !== fromAbs && fs2.existsSync(c)) {
|
|
203
|
-
return path2.relative(rootDir, c);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
function extractRustModules(content) {
|
|
209
|
-
const results = [];
|
|
210
|
-
const modDecl = /^\s*(?:pub(?:\([\w:]+\))?\s+)?mod\s+(\w+)\s*;/gm;
|
|
211
|
-
let m;
|
|
212
|
-
while ((m = modDecl.exec(content)) !== null) {
|
|
213
|
-
results.push({ specifier: m[1], isRelative: true });
|
|
214
|
-
}
|
|
215
|
-
return results;
|
|
216
|
-
}
|
|
217
|
-
function resolveRustModule(fromDir, raw, rootDir) {
|
|
218
|
-
const candidates = [
|
|
219
|
-
path2.join(fromDir, raw.specifier + ".rs"),
|
|
220
|
-
path2.join(fromDir, raw.specifier, "mod.rs")
|
|
221
|
-
];
|
|
222
|
-
for (const c of candidates) {
|
|
223
|
-
if (fs2.existsSync(c)) {
|
|
224
|
-
return path2.relative(rootDir, c);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
function extractGoImports(content) {
|
|
230
|
-
const results = [];
|
|
231
|
-
const singleImport = /^import\s+(?:\w+\s+)?"([^"]+)"/gm;
|
|
232
|
-
let m;
|
|
233
|
-
while ((m = singleImport.exec(content)) !== null) {
|
|
234
|
-
const spec = m[1];
|
|
235
|
-
results.push({ specifier: spec, isRelative: spec.startsWith(".") });
|
|
236
|
-
}
|
|
237
|
-
const MAX_BLOCK_LEN = 512 * 1024;
|
|
238
|
-
const safeContent = content.length > MAX_BLOCK_LEN ? content.slice(0, MAX_BLOCK_LEN) : content;
|
|
239
|
-
const blockImport = /import\s*\(([^)]{0,4096})\)/gs;
|
|
240
|
-
while ((m = blockImport.exec(safeContent)) !== null) {
|
|
241
|
-
const block = m[1];
|
|
242
|
-
const lineRe = /(?:\w+\s+)?"([^"]+)"/g;
|
|
243
|
-
let lm;
|
|
244
|
-
while ((lm = lineRe.exec(block)) !== null) {
|
|
245
|
-
const spec = lm[1];
|
|
246
|
-
results.push({ specifier: spec, isRelative: spec.startsWith(".") });
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return results;
|
|
250
|
-
}
|
|
251
|
-
function resolveGoImportFull(fromAbs, fromDir, raw, rootDir) {
|
|
252
|
-
if (raw.isRelative) {
|
|
253
|
-
const resolver2 = getGoResolver(rootDir);
|
|
254
|
-
return resolver2.resolveRelative(fromAbs, raw.specifier);
|
|
255
|
-
}
|
|
256
|
-
const resolver = getGoResolver(rootDir);
|
|
257
|
-
return resolver.resolve(raw.specifier);
|
|
258
|
-
}
|
|
259
|
-
function extractJavaImports(content) {
|
|
260
|
-
const results = [];
|
|
261
|
-
const importStmt = /^import\s+(?:static\s+)?([\w.]+)\s*;/gm;
|
|
262
|
-
let m;
|
|
263
|
-
while ((m = importStmt.exec(content)) !== null) {
|
|
264
|
-
results.push({ specifier: m[1], isRelative: false });
|
|
265
|
-
}
|
|
266
|
-
return results;
|
|
267
|
-
}
|
|
268
|
-
function resolveJavaImport(fromDir, raw, rootDir) {
|
|
269
|
-
const filePath = path2.join(rootDir, raw.specifier.replace(/\./g, path2.sep) + ".java");
|
|
270
|
-
if (fs2.existsSync(filePath)) {
|
|
271
|
-
return path2.relative(rootDir, filePath);
|
|
272
|
-
}
|
|
273
|
-
const localPath = path2.join(fromDir, raw.specifier.split(".").pop() + ".java");
|
|
274
|
-
if (fs2.existsSync(localPath)) {
|
|
275
|
-
return path2.relative(rootDir, localPath);
|
|
276
|
-
}
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
function extractCSharpImports(content) {
|
|
280
|
-
const results = [];
|
|
281
|
-
const usingRe = /^using\s+(?:static\s+)?([\w.]+)\s*;/gm;
|
|
282
|
-
let m;
|
|
283
|
-
while ((m = usingRe.exec(content)) !== null) {
|
|
284
|
-
results.push({ specifier: m[1], isRelative: false });
|
|
285
|
-
}
|
|
286
|
-
return results;
|
|
287
|
-
}
|
|
288
|
-
function resolveCSharpImport(fromDir, raw, rootDir) {
|
|
289
|
-
const filePath = path2.join(rootDir, raw.specifier.replace(/\./g, path2.sep) + ".cs");
|
|
290
|
-
if (fs2.existsSync(filePath))
|
|
291
|
-
return path2.relative(rootDir, filePath);
|
|
292
|
-
const className = raw.specifier.split(".").pop() ?? raw.specifier;
|
|
293
|
-
const local = path2.join(fromDir, className + ".cs");
|
|
294
|
-
if (fs2.existsSync(local))
|
|
295
|
-
return path2.relative(rootDir, local);
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
function extractRubyImports(content) {
|
|
299
|
-
const results = [];
|
|
300
|
-
const relRe = /require_relative\s+['"]([^'"]+)['"]/gm;
|
|
301
|
-
let m;
|
|
302
|
-
while ((m = relRe.exec(content)) !== null) {
|
|
303
|
-
results.push({ specifier: m[1], isRelative: true });
|
|
304
|
-
}
|
|
305
|
-
return results;
|
|
306
|
-
}
|
|
307
|
-
function resolveRubyImport(fromDir, raw, rootDir) {
|
|
308
|
-
const candidates = [
|
|
309
|
-
path2.join(fromDir, raw.specifier + ".rb"),
|
|
310
|
-
path2.join(fromDir, raw.specifier)
|
|
311
|
-
];
|
|
312
|
-
for (const c of candidates) {
|
|
313
|
-
if (fs2.existsSync(c))
|
|
314
|
-
return path2.relative(rootDir, c);
|
|
315
|
-
}
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
function extractKotlinImports(content) {
|
|
319
|
-
const results = [];
|
|
320
|
-
const importRe = /^import\s+([\w.]+)/gm;
|
|
321
|
-
let m;
|
|
322
|
-
while ((m = importRe.exec(content)) !== null) {
|
|
323
|
-
results.push({ specifier: m[1], isRelative: false });
|
|
324
|
-
}
|
|
325
|
-
return results;
|
|
326
|
-
}
|
|
327
|
-
function resolveKotlinImport(fromDir, raw, rootDir) {
|
|
328
|
-
const asPath = raw.specifier.replace(/\./g, path2.sep);
|
|
329
|
-
for (const ext of [".kt", ".kts"]) {
|
|
330
|
-
const candidate = path2.join(rootDir, asPath + ext);
|
|
331
|
-
if (fs2.existsSync(candidate))
|
|
332
|
-
return path2.relative(rootDir, candidate);
|
|
333
|
-
}
|
|
334
|
-
const className = raw.specifier.split(".").pop() ?? raw.specifier;
|
|
335
|
-
const local = path2.join(fromDir, className + ".kt");
|
|
336
|
-
if (fs2.existsSync(local))
|
|
337
|
-
return path2.relative(rootDir, local);
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
function extractSwiftImports(_content) {
|
|
341
|
-
return [];
|
|
342
|
-
}
|
|
343
|
-
function resolveSwiftImport(_fromDir, _raw, _rootDir) {
|
|
344
|
-
return null;
|
|
345
|
-
}
|
|
346
|
-
function extractPhpImports(content) {
|
|
347
|
-
const results = [];
|
|
348
|
-
const requireRe = /(?:require|require_once|include|include_once)\s*\(?['"](\.[^'"]+\.php)['"]\)?/gm;
|
|
349
|
-
let m;
|
|
350
|
-
while ((m = requireRe.exec(content)) !== null) {
|
|
351
|
-
results.push({ specifier: m[1], isRelative: true });
|
|
352
|
-
}
|
|
353
|
-
const useRe = /^use\s+([\w\\]+)(?:\s+as\s+\w+)?\s*;/gm;
|
|
354
|
-
while ((m = useRe.exec(content)) !== null) {
|
|
355
|
-
results.push({ specifier: m[1], isRelative: false });
|
|
356
|
-
}
|
|
357
|
-
const groupedRe = /^use\s+([\w\\]+)\\{([^}]+)}/gm;
|
|
358
|
-
while ((m = groupedRe.exec(content)) !== null) {
|
|
359
|
-
const prefix = m[1];
|
|
360
|
-
for (const part of m[2].split(",")) {
|
|
361
|
-
const name = part.trim().replace(/\s+as\s+\w+$/, "");
|
|
362
|
-
if (name)
|
|
363
|
-
results.push({ specifier: `${prefix}\\${name}`, isRelative: false });
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return results;
|
|
367
|
-
}
|
|
368
|
-
function resolvePhpImport(fromAbs, fromDir, raw, rootDir) {
|
|
369
|
-
void fromAbs;
|
|
370
|
-
if (raw.isRelative) {
|
|
371
|
-
const candidate = path2.resolve(fromDir, raw.specifier);
|
|
372
|
-
const rootResolved = path2.resolve(rootDir);
|
|
373
|
-
if (!candidate.startsWith(rootResolved + path2.sep) && candidate !== rootResolved)
|
|
374
|
-
return null;
|
|
375
|
-
if (fs2.existsSync(candidate))
|
|
376
|
-
return path2.relative(rootDir, candidate);
|
|
377
|
-
return null;
|
|
378
|
-
}
|
|
379
|
-
const asPath = raw.specifier.replace(/\\/g, path2.sep);
|
|
380
|
-
const candidates = [
|
|
381
|
-
path2.join(rootDir, "src", asPath + ".php"),
|
|
382
|
-
path2.join(rootDir, asPath + ".php"),
|
|
383
|
-
path2.join(fromDir, asPath.split(path2.sep).pop() + ".php")
|
|
384
|
-
];
|
|
385
|
-
for (const c of candidates) {
|
|
386
|
-
if (fs2.existsSync(c))
|
|
387
|
-
return path2.relative(rootDir, c);
|
|
388
|
-
}
|
|
389
|
-
return null;
|
|
390
|
-
}
|
|
391
|
-
function extractDartImports(content) {
|
|
392
|
-
const results = [];
|
|
393
|
-
const importRe = /^import\s+['"](\.[^'"]+\.dart)['"]/gm;
|
|
394
|
-
let m;
|
|
395
|
-
while ((m = importRe.exec(content)) !== null) {
|
|
396
|
-
results.push({ specifier: m[1], isRelative: true });
|
|
397
|
-
}
|
|
398
|
-
return results;
|
|
399
|
-
}
|
|
400
|
-
function resolveDartImport(fromAbs, fromDir, raw, rootDir) {
|
|
401
|
-
void fromAbs;
|
|
402
|
-
const candidate = path2.resolve(fromDir, raw.specifier);
|
|
403
|
-
const rootResolved = path2.resolve(rootDir);
|
|
404
|
-
if (!candidate.startsWith(rootResolved + path2.sep) && candidate !== rootResolved)
|
|
405
|
-
return null;
|
|
406
|
-
if (fs2.existsSync(candidate))
|
|
407
|
-
return path2.relative(rootDir, candidate);
|
|
408
|
-
const withoutExt = path2.resolve(fromDir, raw.specifier.replace(/\.dart$/, ""));
|
|
409
|
-
const withExt = withoutExt + ".dart";
|
|
410
|
-
if (fs2.existsSync(withExt))
|
|
411
|
-
return path2.relative(rootDir, withExt);
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
function extractNotebookImports(filePath, content) {
|
|
415
|
-
void filePath;
|
|
416
|
-
const pythonSource = extractNotebookPythonSource(content);
|
|
417
|
-
if (!pythonSource)
|
|
418
|
-
return [];
|
|
419
|
-
return extractPythonImports(pythonSource);
|
|
420
|
-
}
|
|
421
|
-
function extractVueScriptContent(content) {
|
|
422
|
-
const match = content.match(/<script(?:\s[^>]*)?>([^]*?)<\/script>/i);
|
|
423
|
-
return match?.[1] ?? "";
|
|
424
|
-
}
|
|
425
|
-
function extractVueImports(content) {
|
|
426
|
-
const scriptContent = extractVueScriptContent(content);
|
|
427
|
-
if (!scriptContent.trim())
|
|
428
|
-
return [];
|
|
429
|
-
const results = [];
|
|
430
|
-
const staticImport = /import\s+(?:[^'"]*from\s+)?['"](\.[^'"]+)['"]/gm;
|
|
431
|
-
let m;
|
|
432
|
-
while ((m = staticImport.exec(scriptContent)) !== null) {
|
|
433
|
-
results.push({ specifier: m[1], isRelative: true });
|
|
434
|
-
}
|
|
435
|
-
return results;
|
|
436
|
-
}
|
|
437
|
-
function resolveVueImport(fromAbs, fromDir, raw, rootDir) {
|
|
438
|
-
void fromAbs;
|
|
439
|
-
const direct = path2.resolve(fromDir, raw.specifier);
|
|
440
|
-
const rootResolved = path2.resolve(rootDir);
|
|
441
|
-
if (!direct.startsWith(rootResolved + path2.sep) && direct !== rootResolved)
|
|
442
|
-
return null;
|
|
443
|
-
if (fs2.existsSync(direct))
|
|
444
|
-
return path2.relative(rootDir, direct);
|
|
445
|
-
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".vue", "/index.ts", "/index.js"]) {
|
|
446
|
-
const candidate = path2.resolve(fromDir, raw.specifier.replace(/\.js$/, "") + ext);
|
|
447
|
-
if (!candidate.startsWith(rootResolved + path2.sep))
|
|
448
|
-
continue;
|
|
449
|
-
if (fs2.existsSync(candidate))
|
|
450
|
-
return path2.relative(rootDir, candidate);
|
|
451
|
-
}
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// src/graph/CallGraphIndex.js
|
|
456
|
-
var CallGraphIndex = class _CallGraphIndex {
|
|
457
|
-
/** calleeSymbol → Map<"callerFile:callerSymbol", EdgeConfidence> (reverse lookup) */
|
|
458
|
-
bySite = /* @__PURE__ */ new Map();
|
|
459
|
-
/** "callerFile:callerSymbol" → Set<calleeSymbol> (forward lookup) */
|
|
460
|
-
byCallerKey = /* @__PURE__ */ new Map();
|
|
461
|
-
addEdge(edge) {
|
|
462
|
-
const { callerFile, callerSymbol, calleeSymbol } = edge;
|
|
463
|
-
const confidence = edge.confidence ?? "extracted";
|
|
464
|
-
if (!this.bySite.has(calleeSymbol)) {
|
|
465
|
-
this.bySite.set(calleeSymbol, /* @__PURE__ */ new Map());
|
|
466
|
-
}
|
|
467
|
-
this.bySite.get(calleeSymbol).set(`${callerFile}:${callerSymbol}`, confidence);
|
|
468
|
-
const callerKey = `${callerFile}:${callerSymbol}`;
|
|
469
|
-
if (!this.byCallerKey.has(callerKey)) {
|
|
470
|
-
this.byCallerKey.set(callerKey, /* @__PURE__ */ new Set());
|
|
471
|
-
}
|
|
472
|
-
this.byCallerKey.get(callerKey).add(calleeSymbol);
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* Returns all symbols called by the given function in the given file.
|
|
476
|
-
*/
|
|
477
|
-
getCallees(callerFile, callerSymbol) {
|
|
478
|
-
const key = `${callerFile}:${callerSymbol}`;
|
|
479
|
-
return Array.from(this.byCallerKey.get(key) ?? []);
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Returns all files that contain the symbol as a caller (i.e., files where
|
|
483
|
-
* this symbol has outgoing call edges). Used to resolve callee definition
|
|
484
|
-
* file when the symbol index has no entry (e.g. in test graphs).
|
|
485
|
-
*/
|
|
486
|
-
findFilesForCallerSymbol(symbol) {
|
|
487
|
-
const suffix = `:${symbol}`;
|
|
488
|
-
const files = [];
|
|
489
|
-
for (const key of this.byCallerKey.keys()) {
|
|
490
|
-
if (key.endsWith(suffix)) {
|
|
491
|
-
files.push(key.slice(0, key.length - suffix.length));
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return files;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Returns all callers of the given symbol across all indexed files.
|
|
498
|
-
* Optionally filters by confidence tier.
|
|
499
|
-
*/
|
|
500
|
-
getCallers(symbol, confidenceFilter) {
|
|
501
|
-
const callerMap = this.bySite.get(symbol);
|
|
502
|
-
if (!callerMap)
|
|
503
|
-
return [];
|
|
504
|
-
const results = [];
|
|
505
|
-
for (const [key, confidence] of callerMap.entries()) {
|
|
506
|
-
if (confidenceFilter !== void 0 && confidence !== confidenceFilter) {
|
|
507
|
-
continue;
|
|
508
|
-
}
|
|
509
|
-
const idx = key.indexOf(":");
|
|
510
|
-
const file = idx >= 0 ? key.slice(0, idx) : key;
|
|
511
|
-
const symbol_ = idx >= 0 ? key.slice(idx + 1) : "";
|
|
512
|
-
results.push({ file, symbol: symbol_, callerSymbol: symbol_, confidence });
|
|
513
|
-
}
|
|
514
|
-
return results;
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Remove all call edges where callerFile is the given file.
|
|
518
|
-
* Called before re-indexing a file to prevent stale edges.
|
|
519
|
-
*/
|
|
520
|
-
removeEdgesForFile(callerFile) {
|
|
521
|
-
const prefix = callerFile + ":";
|
|
522
|
-
for (const [callee, callerMap] of this.bySite.entries()) {
|
|
523
|
-
for (const key of callerMap.keys()) {
|
|
524
|
-
if (key === callerFile || key.startsWith(prefix)) {
|
|
525
|
-
callerMap.delete(key);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
if (callerMap.size === 0) {
|
|
529
|
-
this.bySite.delete(callee);
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
for (const key of this.byCallerKey.keys()) {
|
|
533
|
-
if (key === callerFile || key.startsWith(prefix)) {
|
|
534
|
-
this.byCallerKey.delete(key);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
/** Total number of distinct caller→callee edges. */
|
|
539
|
-
size() {
|
|
540
|
-
let n = 0;
|
|
541
|
-
for (const m of this.bySite.values())
|
|
542
|
-
n += m.size;
|
|
543
|
-
return n;
|
|
544
|
-
}
|
|
545
|
-
toJSON() {
|
|
546
|
-
return {
|
|
547
|
-
bySite: Object.fromEntries(Array.from(this.bySite.entries()).map(([callee, callerMap]) => [
|
|
548
|
-
callee,
|
|
549
|
-
Array.from(callerMap.entries()).map(([callerKey, confidence]) => ({ callerKey, confidence }))
|
|
550
|
-
]))
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
static fromJSON(data) {
|
|
554
|
-
const idx = new _CallGraphIndex();
|
|
555
|
-
if (!data || typeof data !== "object")
|
|
556
|
-
return idx;
|
|
557
|
-
const { bySite } = data;
|
|
558
|
-
if (!bySite || typeof bySite !== "object")
|
|
559
|
-
return idx;
|
|
560
|
-
for (const [callee, edges] of Object.entries(bySite)) {
|
|
561
|
-
if (!Array.isArray(edges))
|
|
562
|
-
continue;
|
|
563
|
-
for (const edge of edges) {
|
|
564
|
-
if (typeof edge === "string") {
|
|
565
|
-
if (!idx.bySite.has(callee)) {
|
|
566
|
-
idx.bySite.set(callee, /* @__PURE__ */ new Map());
|
|
567
|
-
}
|
|
568
|
-
idx.bySite.get(callee).set(edge, "extracted");
|
|
569
|
-
if (!idx.byCallerKey.has(edge)) {
|
|
570
|
-
idx.byCallerKey.set(edge, /* @__PURE__ */ new Set());
|
|
571
|
-
}
|
|
572
|
-
idx.byCallerKey.get(edge).add(callee);
|
|
573
|
-
} else if (edge && typeof edge === "object" && "callerKey" in edge) {
|
|
574
|
-
const { callerKey, confidence } = edge;
|
|
575
|
-
if (typeof callerKey !== "string")
|
|
576
|
-
continue;
|
|
577
|
-
const resolvedConfidence = confidence === "inferred" || confidence === "ambiguous" ? confidence : "extracted";
|
|
578
|
-
if (!idx.bySite.has(callee)) {
|
|
579
|
-
idx.bySite.set(callee, /* @__PURE__ */ new Map());
|
|
580
|
-
}
|
|
581
|
-
idx.bySite.get(callee).set(callerKey, resolvedConfidence);
|
|
582
|
-
if (!idx.byCallerKey.has(callerKey)) {
|
|
583
|
-
idx.byCallerKey.set(callerKey, /* @__PURE__ */ new Set());
|
|
584
|
-
}
|
|
585
|
-
idx.byCallerKey.get(callerKey).add(callee);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return idx;
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
|
|
593
|
-
// src/graph/DependencyGraph.js
|
|
594
|
-
var TS_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".vue"]);
|
|
595
|
-
var AST_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".rs", ".java", ".cs", ".rb", ".kt", ".kts", ".swift", ".ipynb", ".php", ".dart"]);
|
|
596
|
-
var DependencyGraph = class {
|
|
597
|
-
/** file → set of files it imports (forward edges) */
|
|
598
|
-
forwardEdges = /* @__PURE__ */ new Map();
|
|
599
|
-
/** file → set of files that import it (reverse edges) */
|
|
600
|
-
reverseEdges = /* @__PURE__ */ new Map();
|
|
601
|
-
/** Symbol index: symbolName → { filePath, type, signature, startLine?, endLine? } */
|
|
602
|
-
symbolIndex = /* @__PURE__ */ new Map();
|
|
603
|
-
callGraphIndex = new CallGraphIndex();
|
|
604
|
-
parser = null;
|
|
605
|
-
rootDir = "";
|
|
606
|
-
snapshotDir = "";
|
|
607
|
-
/**
|
|
608
|
-
* Build the graph from all supported files in rootDir using AST parsing.
|
|
609
|
-
*/
|
|
610
|
-
async buildFromDirectory(rootDir) {
|
|
611
|
-
this.rootDir = rootDir;
|
|
612
|
-
this.snapshotDir = path3.join(rootDir, ".ctxloom");
|
|
613
|
-
const files = collectFiles(rootDir);
|
|
614
|
-
if (await this.loadSnapshot(files.length)) {
|
|
615
|
-
logger.info("Loaded graph from snapshot", { edges: this.edgeCount() });
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
if (!this.parser) {
|
|
619
|
-
this.parser = new ASTParser();
|
|
620
|
-
await this.parser.init();
|
|
621
|
-
}
|
|
622
|
-
for (const absPath of files) {
|
|
623
|
-
const relPath = path3.relative(rootDir, absPath);
|
|
624
|
-
const ext = path3.extname(absPath).toLowerCase();
|
|
625
|
-
try {
|
|
626
|
-
if (!this.forwardEdges.has(relPath)) {
|
|
627
|
-
this.forwardEdges.set(relPath, /* @__PURE__ */ new Set());
|
|
628
|
-
}
|
|
629
|
-
if (AST_EXTENSIONS.has(ext)) {
|
|
630
|
-
const nodes = await this.parser.parse(absPath);
|
|
631
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
632
|
-
const importNodes = nodes.filter((n) => n.type === "import");
|
|
633
|
-
for (const imp of importNodes) {
|
|
634
|
-
const src = imp.source ?? "";
|
|
635
|
-
if (!src.startsWith("."))
|
|
636
|
-
continue;
|
|
637
|
-
const resolved = this.resolveImport(absPath, src, rootDir);
|
|
638
|
-
if (resolved)
|
|
639
|
-
this.addEdge(relPath, resolved);
|
|
640
|
-
}
|
|
641
|
-
} else if (ext === ".go") {
|
|
642
|
-
const goResolver = new GoModuleResolver(rootDir);
|
|
643
|
-
const importNodes = nodes.filter((n) => n.type === "import");
|
|
644
|
-
for (const imp of importNodes) {
|
|
645
|
-
const spec = imp.source ?? imp.name;
|
|
646
|
-
const isRelative = spec.startsWith(".");
|
|
647
|
-
const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
|
|
648
|
-
if (resolved)
|
|
649
|
-
this.addEdge(relPath, resolved);
|
|
650
|
-
}
|
|
651
|
-
} else {
|
|
652
|
-
const importNodes = nodes.filter((n) => n.type === "import");
|
|
653
|
-
if (importNodes.length > 0) {
|
|
654
|
-
for (const imp of importNodes) {
|
|
655
|
-
const specifier = imp.source ?? imp.name;
|
|
656
|
-
const isRelative = specifier.startsWith(".");
|
|
657
|
-
const resolved = resolveImport(absPath, { specifier, isRelative }, rootDir);
|
|
658
|
-
if (resolved)
|
|
659
|
-
this.addEdge(relPath, resolved);
|
|
660
|
-
}
|
|
661
|
-
} else {
|
|
662
|
-
const content = fs3.readFileSync(absPath, "utf-8");
|
|
663
|
-
const rawImports = extractImports(absPath, content);
|
|
664
|
-
for (const raw of rawImports) {
|
|
665
|
-
const resolved = resolveImport(absPath, raw, rootDir);
|
|
666
|
-
if (resolved)
|
|
667
|
-
this.addEdge(relPath, resolved);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
for (const node of nodes) {
|
|
672
|
-
if (node.type === "function" || node.type === "class" || node.type === "interface") {
|
|
673
|
-
const existing = this.symbolIndex.get(node.name) ?? [];
|
|
674
|
-
existing.push({
|
|
675
|
-
filePath: relPath,
|
|
676
|
-
type: node.type,
|
|
677
|
-
signature: node.signature ?? `${node.type} ${node.name}`,
|
|
678
|
-
startLine: node.startLine,
|
|
679
|
-
endLine: node.endLine
|
|
680
|
-
});
|
|
681
|
-
this.symbolIndex.set(node.name, existing);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
685
|
-
const callEdges = await this.parser.parseAllCallEdges(absPath);
|
|
686
|
-
for (const edge of callEdges) {
|
|
687
|
-
this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
} else {
|
|
691
|
-
const content = fs3.readFileSync(absPath, "utf-8");
|
|
692
|
-
const rawImports = extractImports(absPath, content);
|
|
693
|
-
for (const raw of rawImports) {
|
|
694
|
-
const resolved = resolveImport(absPath, raw, rootDir);
|
|
695
|
-
if (resolved)
|
|
696
|
-
this.addEdge(relPath, resolved);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
} catch (err) {
|
|
700
|
-
logger.error("Failed to parse", { file: relPath, detail: err instanceof Error ? err.message : String(err) });
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
await this.saveSnapshot();
|
|
704
|
-
logger.info("Graph built", { files: files.length, edges: this.edgeCount() });
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Set the ASTParser instance (avoids re-initialization).
|
|
708
|
-
*/
|
|
709
|
-
setParser(parser) {
|
|
710
|
-
this.parser = parser;
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Get files that the given file directly imports.
|
|
714
|
-
*/
|
|
715
|
-
getImports(fileRel) {
|
|
716
|
-
return Array.from(this.forwardEdges.get(fileRel) ?? []);
|
|
717
|
-
}
|
|
718
|
-
/**
|
|
719
|
-
* Get files that import the given file.
|
|
720
|
-
*/
|
|
721
|
-
getImporters(fileRel) {
|
|
722
|
-
return Array.from(this.reverseEdges.get(fileRel) ?? []);
|
|
723
|
-
}
|
|
724
|
-
/**
|
|
725
|
-
* Look up a symbol by name. Returns all definitions across files.
|
|
726
|
-
*/
|
|
727
|
-
lookupSymbol(name) {
|
|
728
|
-
return this.symbolIndex.get(name) ?? [];
|
|
729
|
-
}
|
|
730
|
-
/**
|
|
731
|
-
* Return all symbol names defined in a given file.
|
|
732
|
-
*/
|
|
733
|
-
lookupSymbolsByFile(fileRel) {
|
|
734
|
-
const results = [];
|
|
735
|
-
for (const [name, entries] of this.symbolIndex.entries()) {
|
|
736
|
-
if (entries.some((e) => e.filePath === fileRel)) {
|
|
737
|
-
results.push(name);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
return results;
|
|
741
|
-
}
|
|
742
|
-
/**
|
|
743
|
-
* Iterate all symbol entries. Used by ctx_find_large_functions.
|
|
744
|
-
*/
|
|
745
|
-
symbolEntries() {
|
|
746
|
-
return this.symbolIndex.entries();
|
|
747
|
-
}
|
|
748
|
-
/** Return the pre-built call graph index (TypeScript/TSX only). */
|
|
749
|
-
getCallGraphIndex() {
|
|
750
|
-
return this.callGraphIndex;
|
|
751
|
-
}
|
|
752
|
-
/**
|
|
753
|
-
* Traverse the call graph bidirectionally with configurable depth.
|
|
754
|
-
* direction: 'callers' = reverse edges (who imports me)
|
|
755
|
-
* 'callees' = forward edges (who do I import)
|
|
756
|
-
*/
|
|
757
|
-
traverse(startFile, direction, depth = 1) {
|
|
758
|
-
const MAX_DEPTH = 10;
|
|
759
|
-
depth = Math.min(depth, MAX_DEPTH);
|
|
760
|
-
const visited = /* @__PURE__ */ new Set();
|
|
761
|
-
const queue = [{ file: startFile, currentDepth: 0 }];
|
|
762
|
-
while (queue.length > 0) {
|
|
763
|
-
const { file, currentDepth } = queue.shift();
|
|
764
|
-
if (visited.has(file))
|
|
765
|
-
continue;
|
|
766
|
-
visited.add(file);
|
|
767
|
-
if (currentDepth < depth) {
|
|
768
|
-
const edges = direction === "callers" ? this.getImporters(file) : this.getImports(file);
|
|
769
|
-
for (const next of edges) {
|
|
770
|
-
if (!visited.has(next)) {
|
|
771
|
-
queue.push({ file: next, currentDepth: currentDepth + 1 });
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
visited.delete(startFile);
|
|
777
|
-
return Array.from(visited);
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Add a symbol to the index (primarily used for testing and incremental updates).
|
|
781
|
-
*/
|
|
782
|
-
addSymbol(filePath, symbol) {
|
|
783
|
-
const existing = this.symbolIndex.get(symbol.name) ?? [];
|
|
784
|
-
existing.push({
|
|
785
|
-
filePath,
|
|
786
|
-
type: symbol.type,
|
|
787
|
-
signature: symbol.signature,
|
|
788
|
-
startLine: symbol.startLine,
|
|
789
|
-
endLine: symbol.endLine
|
|
790
|
-
});
|
|
791
|
-
this.symbolIndex.set(symbol.name, existing);
|
|
792
|
-
if (!this.forwardEdges.has(filePath)) {
|
|
793
|
-
this.forwardEdges.set(filePath, /* @__PURE__ */ new Set());
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
/**
|
|
797
|
-
* Add a directed edge: fromFile imports toFile.
|
|
798
|
-
*/
|
|
799
|
-
addEdge(fromFile, toFile) {
|
|
800
|
-
if (!this.forwardEdges.has(fromFile)) {
|
|
801
|
-
this.forwardEdges.set(fromFile, /* @__PURE__ */ new Set());
|
|
802
|
-
}
|
|
803
|
-
this.forwardEdges.get(fromFile).add(toFile);
|
|
804
|
-
if (!this.reverseEdges.has(toFile)) {
|
|
805
|
-
this.reverseEdges.set(toFile, /* @__PURE__ */ new Set());
|
|
806
|
-
}
|
|
807
|
-
this.reverseEdges.get(toFile).add(fromFile);
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Remove all edges involving a file (used when a file is deleted or re-indexed).
|
|
811
|
-
*/
|
|
812
|
-
removeFile(fileRel) {
|
|
813
|
-
const imports = this.forwardEdges.get(fileRel);
|
|
814
|
-
if (imports) {
|
|
815
|
-
for (const imp of imports) {
|
|
816
|
-
this.reverseEdges.get(imp)?.delete(fileRel);
|
|
817
|
-
}
|
|
818
|
-
this.forwardEdges.delete(fileRel);
|
|
819
|
-
}
|
|
820
|
-
const importers = this.reverseEdges.get(fileRel);
|
|
821
|
-
if (importers) {
|
|
822
|
-
for (const imp of importers) {
|
|
823
|
-
this.forwardEdges.get(imp)?.delete(fileRel);
|
|
824
|
-
}
|
|
825
|
-
this.reverseEdges.delete(fileRel);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
/**
|
|
829
|
-
* Incrementally update the graph for a single changed file.
|
|
830
|
-
* Removes stale edges, re-parses imports, and rebuilds the symbol index
|
|
831
|
-
* entries for this file. Saves a new snapshot after the update.
|
|
832
|
-
*/
|
|
833
|
-
async updateFile(absPath, rootDir) {
|
|
834
|
-
if (!this.parser) {
|
|
835
|
-
logger.warn("DependencyGraph.updateFile: no parser set, skipping graph update");
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const relPath = path3.relative(rootDir, absPath);
|
|
839
|
-
this.removeFile(relPath);
|
|
840
|
-
if (!this.forwardEdges.has(relPath)) {
|
|
841
|
-
this.forwardEdges.set(relPath, /* @__PURE__ */ new Set());
|
|
842
|
-
}
|
|
843
|
-
this.callGraphIndex.removeEdgesForFile(relPath);
|
|
844
|
-
for (const [symbol, entries] of this.symbolIndex.entries()) {
|
|
845
|
-
const filtered = entries.filter((e) => e.filePath !== relPath);
|
|
846
|
-
if (filtered.length === 0) {
|
|
847
|
-
this.symbolIndex.delete(symbol);
|
|
848
|
-
} else {
|
|
849
|
-
this.symbolIndex.set(symbol, filtered);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
const ext = path3.extname(absPath).toLowerCase();
|
|
853
|
-
try {
|
|
854
|
-
if (AST_EXTENSIONS.has(ext)) {
|
|
855
|
-
const nodes = await this.parser.parse(absPath);
|
|
856
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
857
|
-
const importNodes = nodes.filter((n) => n.type === "import");
|
|
858
|
-
for (const importNode of importNodes) {
|
|
859
|
-
const src = importNode.source ?? "";
|
|
860
|
-
if (!src.startsWith("."))
|
|
861
|
-
continue;
|
|
862
|
-
const resolved = this.resolveImport(absPath, src, rootDir);
|
|
863
|
-
if (resolved)
|
|
864
|
-
this.addEdge(relPath, resolved);
|
|
865
|
-
}
|
|
866
|
-
} else if (ext === ".go") {
|
|
867
|
-
const goResolver = new GoModuleResolver(rootDir);
|
|
868
|
-
const importNodes = nodes.filter((n) => n.type === "import");
|
|
869
|
-
for (const imp of importNodes) {
|
|
870
|
-
const spec = imp.source ?? imp.name;
|
|
871
|
-
const isRelative = spec.startsWith(".");
|
|
872
|
-
const resolved = isRelative ? goResolver.resolveRelative(absPath, spec) : goResolver.resolve(spec);
|
|
873
|
-
if (resolved)
|
|
874
|
-
this.addEdge(relPath, resolved);
|
|
875
|
-
}
|
|
876
|
-
} else {
|
|
877
|
-
const importNodes = nodes.filter((n) => n.type === "import");
|
|
878
|
-
if (importNodes.length > 0) {
|
|
879
|
-
for (const imp of importNodes) {
|
|
880
|
-
const specifier = imp.source ?? imp.name;
|
|
881
|
-
const isRelative = specifier.startsWith(".");
|
|
882
|
-
const resolved = resolveImport(absPath, { specifier, isRelative }, rootDir);
|
|
883
|
-
if (resolved)
|
|
884
|
-
this.addEdge(relPath, resolved);
|
|
885
|
-
}
|
|
886
|
-
} else {
|
|
887
|
-
const content = fs3.readFileSync(absPath, "utf-8");
|
|
888
|
-
const rawImports = extractImports(absPath, content);
|
|
889
|
-
for (const raw of rawImports) {
|
|
890
|
-
const resolved = resolveImport(absPath, raw, rootDir);
|
|
891
|
-
if (resolved)
|
|
892
|
-
this.addEdge(relPath, resolved);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
for (const node of nodes) {
|
|
897
|
-
if (node.type === "function" || node.type === "class" || node.type === "interface") {
|
|
898
|
-
const existing = this.symbolIndex.get(node.name) ?? [];
|
|
899
|
-
existing.push({
|
|
900
|
-
filePath: relPath,
|
|
901
|
-
type: node.type,
|
|
902
|
-
signature: node.signature ?? `${node.type} ${node.name}`,
|
|
903
|
-
startLine: node.startLine,
|
|
904
|
-
endLine: node.endLine
|
|
905
|
-
});
|
|
906
|
-
this.symbolIndex.set(node.name, existing);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
if (TS_EXTENSIONS.has(ext)) {
|
|
910
|
-
const callEdges = await this.parser.parseAllCallEdges(absPath);
|
|
911
|
-
for (const edge of callEdges) {
|
|
912
|
-
this.callGraphIndex.addEdge({ callerFile: relPath, ...edge });
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
} else {
|
|
916
|
-
const content = fs3.readFileSync(absPath, "utf-8");
|
|
917
|
-
const rawImports = extractImports(absPath, content);
|
|
918
|
-
for (const raw of rawImports) {
|
|
919
|
-
const resolved = resolveImport(absPath, raw, rootDir);
|
|
920
|
-
if (resolved)
|
|
921
|
-
this.addEdge(relPath, resolved);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
logger.info("Graph updated", { file: relPath, edges: this.edgeCount() });
|
|
925
|
-
} catch (err) {
|
|
926
|
-
logger.error("Failed to update graph", { file: relPath, detail: err instanceof Error ? err.message : String(err) });
|
|
927
|
-
}
|
|
928
|
-
this.saveSnapshot();
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Get total number of edges in the graph.
|
|
932
|
-
*/
|
|
933
|
-
edgeCount() {
|
|
934
|
-
let count = 0;
|
|
935
|
-
for (const edges of this.forwardEdges.values()) {
|
|
936
|
-
count += edges.size;
|
|
937
|
-
}
|
|
938
|
-
return count;
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Get all files in the graph.
|
|
942
|
-
*/
|
|
943
|
-
allFiles() {
|
|
944
|
-
const files = /* @__PURE__ */ new Set();
|
|
945
|
-
for (const [file] of this.forwardEdges)
|
|
946
|
-
files.add(file);
|
|
947
|
-
for (const [file] of this.reverseEdges)
|
|
948
|
-
files.add(file);
|
|
949
|
-
return Array.from(files);
|
|
950
|
-
}
|
|
951
|
-
// ─── Snapshot persistence ──────────────────────────────────────────────
|
|
952
|
-
getSnapshotPath() {
|
|
953
|
-
return path3.join(this.snapshotDir, "graph-snapshot.json");
|
|
954
|
-
}
|
|
955
|
-
async saveSnapshot() {
|
|
956
|
-
if (!this.snapshotDir)
|
|
957
|
-
return;
|
|
958
|
-
if (!fs3.existsSync(this.snapshotDir)) {
|
|
959
|
-
fs3.mkdirSync(this.snapshotDir, { recursive: true });
|
|
960
|
-
}
|
|
961
|
-
const data = {
|
|
962
|
-
version: 1,
|
|
963
|
-
builtAt: Date.now(),
|
|
964
|
-
fileCount: this.forwardEdges.size,
|
|
965
|
-
forwardEdges: Object.fromEntries(Array.from(this.forwardEdges.entries()).map(([k, v]) => [k, Array.from(v)])),
|
|
966
|
-
reverseEdges: Object.fromEntries(Array.from(this.reverseEdges.entries()).map(([k, v]) => [k, Array.from(v)])),
|
|
967
|
-
symbolIndex: Object.fromEntries(this.symbolIndex.entries())
|
|
968
|
-
};
|
|
969
|
-
const snapshotPath = this.getSnapshotPath();
|
|
970
|
-
const tmpPath = snapshotPath + ".tmp";
|
|
971
|
-
fs3.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
972
|
-
fs3.renameSync(tmpPath, snapshotPath);
|
|
973
|
-
const callData = this.callGraphIndex.toJSON();
|
|
974
|
-
const callPath = path3.join(this.snapshotDir, "call-graph-snapshot.json");
|
|
975
|
-
const callTmp = callPath + ".tmp";
|
|
976
|
-
fs3.writeFileSync(callTmp, JSON.stringify(callData));
|
|
977
|
-
fs3.renameSync(callTmp, callPath);
|
|
978
|
-
}
|
|
979
|
-
/** M-2: Validate snapshot shape before hydrating to prevent prototype pollution. */
|
|
980
|
-
isValidSnapshot(data) {
|
|
981
|
-
if (!data || typeof data !== "object")
|
|
982
|
-
return false;
|
|
983
|
-
const d = data;
|
|
984
|
-
if (typeof d["version"] !== "number")
|
|
985
|
-
return false;
|
|
986
|
-
if (!d["forwardEdges"] || typeof d["forwardEdges"] !== "object")
|
|
987
|
-
return false;
|
|
988
|
-
if (!d["reverseEdges"] || typeof d["reverseEdges"] !== "object")
|
|
989
|
-
return false;
|
|
990
|
-
for (const v of Object.values(d["forwardEdges"])) {
|
|
991
|
-
if (!Array.isArray(v) || !v.every((s) => typeof s === "string"))
|
|
992
|
-
return false;
|
|
993
|
-
}
|
|
994
|
-
for (const v of Object.values(d["reverseEdges"])) {
|
|
995
|
-
if (!Array.isArray(v) || !v.every((s) => typeof s === "string"))
|
|
996
|
-
return false;
|
|
997
|
-
}
|
|
998
|
-
return true;
|
|
999
|
-
}
|
|
1000
|
-
async loadSnapshot(currentFileCount) {
|
|
1001
|
-
const snapshotPath = this.getSnapshotPath();
|
|
1002
|
-
if (!fs3.existsSync(snapshotPath))
|
|
1003
|
-
return false;
|
|
1004
|
-
try {
|
|
1005
|
-
const raw = JSON.parse(fs3.readFileSync(snapshotPath, "utf-8"));
|
|
1006
|
-
if (!this.isValidSnapshot(raw)) {
|
|
1007
|
-
logger.warn("Graph snapshot failed schema validation, will rebuild");
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
const data = raw;
|
|
1011
|
-
if (currentFileCount !== void 0 && data.fileCount !== void 0) {
|
|
1012
|
-
if (data.fileCount !== currentFileCount) {
|
|
1013
|
-
logger.info("Graph snapshot stale, rebuilding", { prev: data.fileCount, curr: currentFileCount });
|
|
1014
|
-
return false;
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
this.forwardEdges = new Map(Object.entries(data.forwardEdges).map(([k, v]) => [k, new Set(v)]));
|
|
1018
|
-
this.reverseEdges = new Map(Object.entries(data.reverseEdges).map(([k, v]) => [k, new Set(v)]));
|
|
1019
|
-
if (data.symbolIndex) {
|
|
1020
|
-
this.symbolIndex = new Map(Object.entries(data.symbolIndex));
|
|
1021
|
-
}
|
|
1022
|
-
const callPath = path3.join(this.snapshotDir, "call-graph-snapshot.json");
|
|
1023
|
-
if (fs3.existsSync(callPath)) {
|
|
1024
|
-
try {
|
|
1025
|
-
const callRaw = JSON.parse(fs3.readFileSync(callPath, "utf-8"));
|
|
1026
|
-
this.callGraphIndex = CallGraphIndex.fromJSON(callRaw);
|
|
1027
|
-
} catch {
|
|
1028
|
-
this.callGraphIndex = new CallGraphIndex();
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
return true;
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
logger.error("Failed to load graph snapshot, will rebuild", { detail: err instanceof Error ? err.message : String(err) });
|
|
1034
|
-
return false;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
resolveImport(fromAbs, specifier, rootDir) {
|
|
1038
|
-
const dir = path3.dirname(fromAbs);
|
|
1039
|
-
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.js"]) {
|
|
1040
|
-
const candidate = path3.resolve(dir, specifier.replace(/\.js$/, "") + ext);
|
|
1041
|
-
if (fs3.existsSync(candidate)) {
|
|
1042
|
-
return path3.relative(rootDir, candidate);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
return null;
|
|
1046
|
-
}
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
export {
|
|
1050
|
-
DependencyGraph
|
|
1051
|
-
};
|
|
1052
|
-
//# sourceMappingURL=chunk-RE2V3FRF.js.map
|