@monoes/monograph 1.5.1 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.monodesign/hook.cache.json +1 -0
  2. package/dist/src/analysis/cycles.d.ts.map +1 -1
  3. package/dist/src/analysis/cycles.js +5 -3
  4. package/dist/src/analysis/cycles.js.map +1 -1
  5. package/dist/src/analysis/trace.d.ts.map +1 -1
  6. package/dist/src/analysis/trace.js +1 -19
  7. package/dist/src/analysis/trace.js.map +1 -1
  8. package/dist/src/analysis/worker-pool.d.ts.map +1 -1
  9. package/dist/src/analysis/worker-pool.js +9 -7
  10. package/dist/src/analysis/worker-pool.js.map +1 -1
  11. package/dist/src/cli/skill-gen.d.ts.map +1 -1
  12. package/dist/src/cli/skill-gen.js +16 -25
  13. package/dist/src/cli/skill-gen.js.map +1 -1
  14. package/dist/src/export/html.d.ts.map +1 -1
  15. package/dist/src/export/html.js +3 -2
  16. package/dist/src/export/html.js.map +1 -1
  17. package/dist/src/graph/regex-search.d.ts.map +1 -1
  18. package/dist/src/graph/regex-search.js +9 -3
  19. package/dist/src/graph/regex-search.js.map +1 -1
  20. package/dist/src/pipeline/phases/call-site-extractors.d.ts +17 -0
  21. package/dist/src/pipeline/phases/call-site-extractors.d.ts.map +1 -0
  22. package/dist/src/pipeline/phases/call-site-extractors.js +132 -0
  23. package/dist/src/pipeline/phases/call-site-extractors.js.map +1 -0
  24. package/dist/src/pipeline/phases/module-resolution.d.ts +10 -0
  25. package/dist/src/pipeline/phases/module-resolution.d.ts.map +1 -0
  26. package/dist/src/pipeline/phases/module-resolution.js +301 -0
  27. package/dist/src/pipeline/phases/module-resolution.js.map +1 -0
  28. package/dist/src/pipeline/phases/orm.js +1 -2
  29. package/dist/src/pipeline/phases/orm.js.map +1 -1
  30. package/dist/src/pipeline/phases/scope-resolution.d.ts +2 -25
  31. package/dist/src/pipeline/phases/scope-resolution.d.ts.map +1 -1
  32. package/dist/src/pipeline/phases/scope-resolution.js +18 -580
  33. package/dist/src/pipeline/phases/scope-resolution.js.map +1 -1
  34. package/dist/src/pipeline/phases/wildcard-phase.d.ts.map +1 -1
  35. package/dist/src/pipeline/phases/wildcard-phase.js +6 -1
  36. package/dist/src/pipeline/phases/wildcard-phase.js.map +1 -1
  37. package/dist/src/search/hybrid-query.js +1 -1
  38. package/dist/src/search/hybrid-query.js.map +1 -1
  39. package/dist/src/web/api.d.ts.map +1 -1
  40. package/dist/src/web/api.js +17 -14
  41. package/dist/src/web/api.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +1 -1
  44. package/src/__tests__/pipeline/phases/wildcard-phase.test.ts +39 -4
  45. package/src/analysis/cycles.ts +7 -3
  46. package/src/analysis/trace.ts +1 -21
  47. package/src/analysis/worker-pool.ts +8 -7
  48. package/src/cli/skill-gen.ts +6 -14
  49. package/src/export/html.ts +3 -2
  50. package/src/graph/regex-search.ts +9 -5
  51. package/src/pipeline/phases/call-site-extractors.ts +169 -0
  52. package/src/pipeline/phases/module-resolution.ts +294 -0
  53. package/src/pipeline/phases/orm.ts +1 -2
  54. package/src/pipeline/phases/scope-resolution.ts +25 -618
  55. package/src/pipeline/phases/wildcard-phase.ts +5 -1
  56. package/src/search/hybrid-query.ts +1 -1
  57. package/src/web/api.ts +17 -13
@@ -1,551 +1,12 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { join, extname, dirname, resolve as resolvePath } from 'path';
1
+ import { readFileSync } from 'fs';
2
+ import { join, extname } from 'path';
3
3
  import { makeId } from '../../types.js';
4
- // ── JS/TS keywords to skip for direct calls ───────────────────────────────────
5
- const JS_KEYWORDS = new Set([
6
- 'if', 'for', 'while', 'switch', 'return', 'function', 'class', 'new', 'typeof',
7
- 'await', 'catch', 'throw', 'delete', 'void', 'instanceof', 'in', 'of',
8
- 'import', 'export', 'yield', 'async', 'super', 'this', 'const', 'let', 'var',
9
- 'try', 'finally', 'break', 'continue', 'debugger', 'with', 'do',
10
- ]);
11
- const PY_KEYWORDS = new Set([
12
- 'if', 'for', 'while', 'with', 'return', 'def', 'class', 'import', 'from',
13
- 'raise', 'yield', 'lambda', 'await', 'async', 'del', 'pass', 'assert',
14
- 'except', 'elif', 'else', 'not', 'and', 'or', 'in', 'is', 'print',
15
- ]);
16
- const GO_KEYWORDS = new Set([
17
- 'if', 'for', 'range', 'return', 'func', 'type', 'var', 'const', 'import', 'package',
18
- 'go', 'defer', 'select', 'case', 'default', 'break', 'continue', 'goto', 'fallthrough',
19
- 'chan', 'map', 'struct', 'interface', 'make', 'new', 'len', 'cap', 'append', 'delete',
20
- 'panic', 'recover', 'close', 'switch', 'else',
21
- ]);
22
- const JAVA_KEYWORDS = new Set([
23
- 'if', 'for', 'while', 'do', 'switch', 'case', 'return', 'class', 'interface', 'enum',
24
- 'new', 'extends', 'implements', 'import', 'package', 'throw', 'throws', 'catch', 'try',
25
- 'finally', 'static', 'final', 'abstract', 'public', 'private', 'protected', 'void',
26
- 'break', 'continue', 'default', 'else', 'instanceof', 'this', 'super',
27
- ]);
28
- const RUST_KEYWORDS = new Set([
29
- 'if', 'let', 'for', 'while', 'loop', 'match', 'return', 'fn', 'struct', 'enum', 'trait',
30
- 'impl', 'use', 'mod', 'pub', 'super', 'self', 'type', 'where', 'in', 'as', 'mut',
31
- 'ref', 'move', 'async', 'await', 'dyn', 'extern', 'crate', 'static', 'const', 'unsafe',
32
- 'break', 'continue', 'else',
33
- ]);
34
- // ── Supported extensions ──────────────────────────────────────────────────────
35
- const TS_JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx']);
36
- const PY_EXTS = new Set(['.py']);
37
- const GO_EXTS = new Set(['.go']);
38
- const JAVA_EXTS = new Set(['.java']);
39
- const RUST_EXTS = new Set(['.rs']);
40
- const CJS_MJS_EXTS = new Set(['.cjs', '.mjs']);
41
- function isSupportedExt(ext) {
42
- return TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext) || PY_EXTS.has(ext) || GO_EXTS.has(ext) || JAVA_EXTS.has(ext) || RUST_EXTS.has(ext);
43
- }
44
- // ── Language-specific extractors ──────────────────────────────────────────────
45
- export function extractGoCallSites(source, filePath, fileNodeId) {
46
- const sites = [];
47
- const methodPattern = /(\w+)\.(\w+)\s*\(/g;
48
- let m;
49
- while ((m = methodPattern.exec(source)) !== null) {
50
- sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
51
- }
52
- const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
53
- while ((m = directPattern.exec(source)) !== null) {
54
- const name = m[1];
55
- if (GO_KEYWORDS.has(name))
56
- continue;
57
- sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
58
- }
59
- return sites;
60
- }
61
- export function extractJavaCallSites(source, filePath, fileNodeId) {
62
- const sites = [];
63
- const methodPattern = /(\w+)\.(\w+)\s*\(/g;
64
- let m;
65
- while ((m = methodPattern.exec(source)) !== null) {
66
- sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
67
- }
68
- const directPattern = /(?<![.[\w])([A-Za-z_$][\w$]*)\s*\(/g;
69
- while ((m = directPattern.exec(source)) !== null) {
70
- const name = m[1];
71
- if (JAVA_KEYWORDS.has(name))
72
- continue;
73
- sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
74
- }
75
- return sites;
76
- }
77
- export function extractRustCallSites(source, filePath, fileNodeId) {
78
- const sites = [];
79
- const methodPattern = /(\w+)\.(\w+)\s*\(/g;
80
- let m;
81
- while ((m = methodPattern.exec(source)) !== null) {
82
- sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: `${m[1]}.${m[2]}`, form: 'method', receiverName: m[1], methodName: m[2] });
83
- }
84
- const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
85
- while ((m = directPattern.exec(source)) !== null) {
86
- const name = m[1];
87
- if (RUST_KEYWORDS.has(name))
88
- continue;
89
- sites.push({ callerFileNodeId: fileNodeId, callerFilePath: filePath, calleeRaw: name, form: 'direct' });
90
- }
91
- return sites;
92
- }
93
- // ── Stage 0.5: Track constructor assignments for type-aware method resolution ─
94
- /**
95
- * Scan source for `const/let/var x = new ClassName(...)` patterns.
96
- * Returns a map: localVarName → ClassName (e.g. "svc" → "MyService").
97
- */
98
- function extractConstructorAssignments(source) {
99
- const result = new Map();
100
- const pattern = /(?:const|let|var)\s+(\w+)\s*=\s*new\s+([A-Z][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
101
- let m;
102
- while ((m = pattern.exec(source)) !== null) {
103
- result.set(m[1], m[2]);
104
- }
105
- return result;
106
- }
107
- // ── Stage 1 + 2: Extract and classify call sites ──────────────────────────────
108
- function extractCallSites(source, filePath, fileNodeId, ext) {
109
- const sites = [];
110
- if (TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext)) {
111
- // Method calls: word.word( or word.word<T>(
112
- const methodPattern = /(\w+)\.(\w+)\s*(?:<[^>]*>\s*)?\(/g;
113
- let m;
114
- while ((m = methodPattern.exec(source)) !== null) {
115
- sites.push({
116
- callerFileNodeId: fileNodeId,
117
- callerFilePath: filePath,
118
- calleeRaw: `${m[1]}.${m[2]}`,
119
- form: 'method',
120
- receiverName: m[1],
121
- methodName: m[2],
122
- });
123
- }
124
- // Chained method calls: ).method( or ].method( — receiver unknown, resolve by method name
125
- const chainedPattern = /[)\]]\s*\.(\w+)\s*(?:<[^>]*>\s*)?\(/g;
126
- while ((m = chainedPattern.exec(source)) !== null) {
127
- const name = m[1];
128
- if (JS_KEYWORDS.has(name))
129
- continue;
130
- sites.push({
131
- callerFileNodeId: fileNodeId,
132
- callerFilePath: filePath,
133
- calleeRaw: `?.${name}`,
134
- form: 'method',
135
- methodName: name,
136
- });
137
- }
138
- // Direct calls: word( or word<T>( — not preceded by . or word char
139
- const directPattern = /(?<![.[\w])([A-Za-z_$][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
140
- while ((m = directPattern.exec(source)) !== null) {
141
- const name = m[1];
142
- if (JS_KEYWORDS.has(name))
143
- continue;
144
- sites.push({
145
- callerFileNodeId: fileNodeId,
146
- callerFilePath: filePath,
147
- calleeRaw: name,
148
- form: 'direct',
149
- methodName: name,
150
- });
151
- }
152
- // Constructor calls: new ClassName( or new ClassName<T>(
153
- const newPattern = /\bnew\s+([A-Z][\w$]*)\s*(?:<[^>]*>\s*)?\(/g;
154
- while ((m = newPattern.exec(source)) !== null) {
155
- sites.push({
156
- callerFileNodeId: fileNodeId,
157
- callerFilePath: filePath,
158
- calleeRaw: `new ${m[1]}`,
159
- form: 'direct',
160
- methodName: m[1],
161
- });
162
- }
163
- // Dynamic calls: obj[key]( → mark as dynamic
164
- const dynamicPattern = /\w+\s*\[[\w'"` ]+\]\s*\(/g;
165
- while ((m = dynamicPattern.exec(source)) !== null) {
166
- sites.push({
167
- callerFileNodeId: fileNodeId,
168
- callerFilePath: filePath,
169
- calleeRaw: m[0],
170
- form: 'dynamic',
171
- });
172
- }
173
- }
174
- else if (PY_EXTS.has(ext)) {
175
- // Method calls: self.method( or obj.method(
176
- const methodPattern = /(\w+)\.(\w+)\s*\(/g;
177
- let m;
178
- while ((m = methodPattern.exec(source)) !== null) {
179
- sites.push({
180
- callerFileNodeId: fileNodeId,
181
- callerFilePath: filePath,
182
- calleeRaw: `${m[1]}.${m[2]}`,
183
- form: 'method',
184
- receiverName: m[1],
185
- methodName: m[2],
186
- });
187
- }
188
- // Direct calls: word( not preceded by . or word char
189
- const directPattern = /(?<![.[\w])([A-Za-z_][\w]*)\s*\(/g;
190
- while ((m = directPattern.exec(source)) !== null) {
191
- const name = m[1];
192
- if (PY_KEYWORDS.has(name))
193
- continue;
194
- sites.push({
195
- callerFileNodeId: fileNodeId,
196
- callerFilePath: filePath,
197
- calleeRaw: name,
198
- form: 'direct',
199
- methodName: name,
200
- });
201
- }
202
- }
203
- else if (GO_EXTS.has(ext)) {
204
- return extractGoCallSites(source, filePath, fileNodeId);
205
- }
206
- else if (JAVA_EXTS.has(ext)) {
207
- return extractJavaCallSites(source, filePath, fileNodeId);
208
- }
209
- else if (RUST_EXTS.has(ext)) {
210
- return extractRustCallSites(source, filePath, fileNodeId);
211
- }
212
- return sites;
213
- }
214
- // ── Stage 3: Build import maps by parsing source imports ─────────────────────
215
- const IMPORT_RE = /import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+))(?:\s*,\s*(?:\{([^}]+)\}|(\w+)|\*\s+as\s+(\w+)))?\s+from\s+['"]([^'"]+)['"]/g;
216
- // CJS require: const x = require('y') or const { a, b } = require('y')
217
- const REQUIRE_RE = /(?:const|let|var)\s+(?:\{([^}]+)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
218
- // Python: from x import y, z OR import x
219
- const PY_FROM_IMPORT_RE = /from\s+([\w.]+)\s+import\s+(.+)/g;
220
- const PY_IMPORT_RE = /^import\s+([\w.]+)(?:\s+as\s+(\w+))?/gm;
221
- // Go: import "path" or import ( "path" )
222
- const GO_IMPORT_RE = /import\s+(?:"([^"]+)"|(?:\w+\s+)?"([^"]+)"|\(\s*([\s\S]*?)\s*\))/g;
223
- // Java: import com.foo.Bar;
224
- const JAVA_IMPORT_RE = /import\s+(?:static\s+)?([\w.]+)\s*;/g;
225
- // Rust: use crate::module::Item; or use super::foo;
226
- const RUST_USE_RE = /use\s+((?:crate|super|self)(?:::\w+)+)(?:::\{([^}]+)\})?;/g;
227
- // TS/JS re-exports: export { foo, bar } from './module'
228
- const REEXPORT_RE = /export\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
229
- const RESOLVE_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
230
- const PY_RESOLVE_EXTS = ['.py'];
231
- const GO_RESOLVE_EXTS = ['.go'];
232
- const JAVA_RESOLVE_EXTS = ['.java'];
233
- const RUST_RESOLVE_EXTS = ['.rs'];
234
- const workspacePackageMapCache = new Map();
235
- /** Invalidate the cached workspace package map for a repo — call at the start
236
- * of each build so long-lived processes (watch mode) pick up package.json
237
- * additions/removals instead of serving a stale map from a prior build. */
238
- export function clearWorkspacePackageMapCache(repoPath) {
239
- workspacePackageMapCache.delete(repoPath);
240
- }
241
- /**
242
- * Build package-name → directory map from workspace package.json files.
243
- * Scans packages/ for package.json and maps npm name to its relative src path.
244
- * Cached per repoPath — multiple pipeline phases (cross-file, scope-resolution,
245
- * the latter's own re-export loop) call this per-file/per-edge within the same
246
- * build, and the workspace's package.json set doesn't change mid-build.
247
- */
248
- export function buildWorkspacePackageMap(repoPath) {
249
- const cached = workspacePackageMapCache.get(repoPath);
250
- if (cached)
251
- return cached;
252
- const result = new Map();
253
- const packagesDir = join(repoPath, 'packages');
254
- try {
255
- const scanDirs = (base, depth) => {
256
- if (depth > 2)
257
- return;
258
- let entries;
259
- try {
260
- entries = require('fs').readdirSync(base);
261
- }
262
- catch {
263
- return;
264
- }
265
- for (const e of entries) {
266
- const full = join(base, e);
267
- const pkgJson = join(full, 'package.json');
268
- try {
269
- const pkg = JSON.parse(readFileSync(pkgJson, 'utf-8'));
270
- if (pkg.name) {
271
- const relDir = full.slice(repoPath.length + 1); // e.g. packages/@monomind/cli
272
- result.set(pkg.name, relDir);
273
- }
274
- }
275
- catch {
276
- // No package.json — recurse for @scoped dirs
277
- if (e.startsWith('@'))
278
- scanDirs(full, depth + 1);
279
- }
280
- }
281
- };
282
- scanDirs(packagesDir, 0);
283
- }
284
- catch { /* no packages dir */ }
285
- workspacePackageMapCache.set(repoPath, result);
286
- return result;
287
- }
288
- export function resolveModuleSpecifier(importerPath, specifier, repoPath, knownFiles, workspaceMap) {
289
- if (specifier.startsWith('.')) {
290
- const dir = dirname(importerPath);
291
- const raw = resolvePath('/', dir, specifier).slice(1);
292
- // Strip .js/.jsx — TS source uses .js extensions but files are .ts
293
- const base = raw.replace(/\.(js|jsx)$/, '');
294
- for (const candidate of [
295
- raw,
296
- base,
297
- ...RESOLVE_EXTS.map(e => base + e),
298
- ...RESOLVE_EXTS.map(e => base + '/index' + e),
299
- ]) {
300
- if (knownFiles.has(candidate))
301
- return candidate;
302
- }
303
- for (const ext of RESOLVE_EXTS) {
304
- if (existsSync(join(repoPath, base + ext)))
305
- return base + ext;
306
- }
307
- return null;
308
- }
309
- // Workspace package specifier: @monoes/hooks → packages/@monomind/hooks/src/index.ts
310
- // Also handles subpath: @monoes/hooks/src/foo → packages/@monomind/hooks/src/foo.ts
311
- for (const [pkgName, pkgDir] of workspaceMap) {
312
- if (specifier === pkgName) {
313
- // Bare import: resolve to package entry point (src/index.ts)
314
- for (const entry of RESOLVE_EXTS.map(e => pkgDir + '/src/index' + e)) {
315
- if (knownFiles.has(entry))
316
- return entry;
317
- }
318
- return null;
319
- }
320
- if (specifier.startsWith(pkgName + '/')) {
321
- const subpath = specifier.slice(pkgName.length + 1);
322
- const base = pkgDir + '/' + subpath;
323
- for (const candidate of [
324
- base,
325
- ...RESOLVE_EXTS.map(e => base + e),
326
- ...RESOLVE_EXTS.map(e => base + '/index' + e),
327
- ]) {
328
- if (knownFiles.has(candidate))
329
- return candidate;
330
- }
331
- return null;
332
- }
333
- }
334
- return null;
335
- }
336
- function resolvePythonModule(importerPath, modulePath, knownFiles) {
337
- // Try relative to importer's directory, then absolute from repo root
338
- const dir = dirname(importerPath);
339
- for (const base of [dir + '/' + modulePath, modulePath]) {
340
- for (const candidate of [base + '.py', base + '/__init__.py']) {
341
- if (knownFiles.has(candidate))
342
- return candidate;
343
- }
344
- }
345
- return null;
346
- }
347
- function resolveGoPackage(importerPath, goPath, knownFiles) {
348
- for (const f of knownFiles) {
349
- if (f.startsWith(goPath + '/') && f.endsWith('.go'))
350
- return f;
351
- }
352
- return null;
353
- }
354
- function resolveJavaImport(qualifiedName, knownFiles) {
355
- // com.foo.Bar → com/foo/Bar.java or src/main/java/com/foo/Bar.java
356
- const pathPart = qualifiedName.replace(/\./g, '/');
357
- for (const candidate of [pathPart + '.java', 'src/main/java/' + pathPart + '.java', 'src/' + pathPart + '.java']) {
358
- if (knownFiles.has(candidate))
359
- return candidate;
360
- }
361
- // Wildcard: com.foo.* → find any .java file in com/foo/
362
- if (pathPart.endsWith('/*')) {
363
- const dir = pathPart.slice(0, -2);
364
- for (const f of knownFiles) {
365
- if (f.endsWith('.java') && (f.startsWith(dir + '/') || f.includes('/' + dir + '/')))
366
- return f;
367
- }
368
- }
369
- return null;
370
- }
371
- function resolveRustUse(usePath, importerPath, knownFiles) {
372
- // crate::module::item → src/module.rs or src/module/mod.rs
373
- const parts = usePath.split('::');
374
- if (parts[0] === 'crate')
375
- parts[0] = 'src';
376
- else if (parts[0] === 'super') {
377
- const dir = dirname(importerPath);
378
- parts[0] = dirname(dir);
379
- }
380
- else if (parts[0] === 'self') {
381
- parts[0] = dirname(importerPath);
382
- }
383
- // Last segment is the item name — try with and without it
384
- for (let len = parts.length; len >= 2; len--) {
385
- const base = parts.slice(0, len).join('/');
386
- for (const candidate of [base + '.rs', base + '/mod.rs']) {
387
- if (knownFiles.has(candidate))
388
- return candidate;
389
- }
390
- }
391
- return null;
392
- }
393
- function extractImportNames(clause) {
394
- return clause
395
- .split(',')
396
- .map(s => s.trim().split(/\s+as\s+/).pop().trim())
397
- .filter(Boolean);
398
- }
399
- /**
400
- * Parse each file's import statements to build importedName → resolvedFilePath maps.
401
- * Replaces the broken DB-edge approach (IMPORTS edges target Variable nodes, not Files).
402
- */
403
- function buildAllImportMapsFromSource(repoPath, fileNodesByPath, fileContents) {
404
- const result = new Map();
405
- const knownFiles = new Set(fileNodesByPath.keys());
406
- const workspaceMap = buildWorkspacePackageMap(repoPath);
407
- for (const [filePath, fileNode] of fileNodesByPath) {
408
- const ext = extname(filePath).toLowerCase();
409
- const importMap = new Map();
410
- let source = fileContents?.get(filePath);
411
- if (!source) {
412
- try {
413
- source = readFileSync(join(repoPath, filePath), 'utf-8');
414
- }
415
- catch {
416
- continue;
417
- }
418
- }
419
- if (TS_JS_EXTS.has(ext) || ext === '.cjs' || ext === '.mjs') {
420
- // ESM imports
421
- IMPORT_RE.lastIndex = 0;
422
- let m;
423
- while ((m = IMPORT_RE.exec(source)) !== null) {
424
- const specifier = m[7];
425
- const resolved = resolveModuleSpecifier(filePath, specifier, repoPath, knownFiles, workspaceMap);
426
- if (!resolved)
427
- continue;
428
- if (m[1])
429
- for (const n of extractImportNames(m[1]))
430
- importMap.set(n, resolved);
431
- if (m[4])
432
- for (const n of extractImportNames(m[4]))
433
- importMap.set(n, resolved);
434
- if (m[2])
435
- importMap.set(m[2], resolved);
436
- if (m[5])
437
- importMap.set(m[5], resolved);
438
- if (m[3])
439
- importMap.set(m[3], resolved);
440
- if (m[6])
441
- importMap.set(m[6], resolved);
442
- const baseName = resolved.split('/').pop().replace(/\.\w+$/, '');
443
- importMap.set(baseName, resolved);
444
- }
445
- // CJS require()
446
- REQUIRE_RE.lastIndex = 0;
447
- while ((m = REQUIRE_RE.exec(source)) !== null) {
448
- const specifier = m[3];
449
- const resolved = resolveModuleSpecifier(filePath, specifier, repoPath, knownFiles, workspaceMap);
450
- if (!resolved)
451
- continue;
452
- if (m[1])
453
- for (const n of extractImportNames(m[1]))
454
- importMap.set(n, resolved);
455
- if (m[2])
456
- importMap.set(m[2], resolved);
457
- const baseName = resolved.split('/').pop().replace(/\.\w+$/, '');
458
- importMap.set(baseName, resolved);
459
- }
460
- }
461
- else if (PY_EXTS.has(ext)) {
462
- // Python: from module import name
463
- PY_FROM_IMPORT_RE.lastIndex = 0;
464
- let m;
465
- while ((m = PY_FROM_IMPORT_RE.exec(source)) !== null) {
466
- const modulePath = m[1].replace(/\./g, '/');
467
- const resolved = resolvePythonModule(filePath, modulePath, knownFiles);
468
- if (!resolved)
469
- continue;
470
- for (const name of m[2].split(',').map(s => s.trim().split(/\s+as\s+/).pop().trim()).filter(Boolean)) {
471
- importMap.set(name, resolved);
472
- }
473
- const baseName = resolved.split('/').pop().replace(/\.\w+$/, '');
474
- importMap.set(baseName, resolved);
475
- }
476
- // Python: import module
477
- PY_IMPORT_RE.lastIndex = 0;
478
- while ((m = PY_IMPORT_RE.exec(source)) !== null) {
479
- const modulePath = m[1].replace(/\./g, '/');
480
- const resolved = resolvePythonModule(filePath, modulePath, knownFiles);
481
- if (!resolved)
482
- continue;
483
- const alias = m[2] ?? m[1].split('.').pop();
484
- importMap.set(alias, resolved);
485
- }
486
- }
487
- else if (GO_EXTS.has(ext)) {
488
- GO_IMPORT_RE.lastIndex = 0;
489
- let m;
490
- while ((m = GO_IMPORT_RE.exec(source)) !== null) {
491
- const paths = m[3]
492
- ? m[3].match(/"([^"]+)"/g)?.map(s => s.slice(1, -1)) ?? []
493
- : [m[1] ?? m[2]].filter(Boolean);
494
- for (const goPath of paths) {
495
- const resolved = resolveGoPackage(filePath, goPath, knownFiles);
496
- if (!resolved)
497
- continue;
498
- const pkgName = goPath.split('/').pop();
499
- importMap.set(pkgName, resolved);
500
- }
501
- }
502
- }
503
- else if (JAVA_EXTS.has(ext)) {
504
- JAVA_IMPORT_RE.lastIndex = 0;
505
- let m;
506
- while ((m = JAVA_IMPORT_RE.exec(source)) !== null) {
507
- const resolved = resolveJavaImport(m[1], knownFiles);
508
- if (!resolved)
509
- continue;
510
- const className = m[1].split('.').pop();
511
- importMap.set(className, resolved);
512
- }
513
- }
514
- else if (RUST_EXTS.has(ext)) {
515
- RUST_USE_RE.lastIndex = 0;
516
- let m;
517
- while ((m = RUST_USE_RE.exec(source)) !== null) {
518
- if (m[2]) {
519
- // use crate::module::{Item1, Item2}
520
- for (const name of m[2].split(',').map(s => s.trim()).filter(Boolean)) {
521
- const resolved = resolveRustUse(m[1] + '::' + name, filePath, knownFiles);
522
- if (resolved)
523
- importMap.set(name.split('::').pop(), resolved);
524
- }
525
- }
526
- else {
527
- const resolved = resolveRustUse(m[1], filePath, knownFiles);
528
- if (!resolved)
529
- continue;
530
- const itemName = m[1].split('::').pop();
531
- importMap.set(itemName, resolved);
532
- }
533
- }
534
- }
535
- if (importMap.size > 0) {
536
- result.set(fileNode.id, importMap);
537
- }
538
- }
539
- return result;
540
- }
541
- function getImportMap(allImportMaps, fileNodeId) {
542
- return allImportMaps.get(fileNodeId) ?? new Map();
543
- }
544
- // ── Stage 4 helpers: preloaded function/method index ──────────────────────────
545
- /**
546
- * Load all callable nodes once (Function, Method, Constructor, Class).
547
- * Class is included so `new ClassName()` can resolve to Class nodes.
548
- */
4
+ import { TS_JS_EXTS, CJS_MJS_EXTS, isSupportedExt, extractCallSites, extractConstructorAssignments } from './call-site-extractors.js';
5
+ import { buildWorkspacePackageMap, resolveModuleSpecifier, extractImportNames, buildAllImportMapsFromSource, REEXPORT_RE } from './module-resolution.js';
6
+ // Re-export for external consumers
7
+ export { clearWorkspacePackageMapCache, buildWorkspacePackageMap, resolveModuleSpecifier } from './module-resolution.js';
8
+ export { extractGoCallSites, extractJavaCallSites, extractRustCallSites } from './call-site-extractors.js';
9
+ // ── Function index ───────────────────────────────────────────────────────────
549
10
  function buildFunctionIndex(ctx) {
550
11
  const byFilePath = new Map();
551
12
  const nameCounts = new Map();
@@ -555,7 +16,6 @@ function buildFunctionIndex(ctx) {
555
16
  .prepare(`SELECT id, name, file_path FROM nodes WHERE label IN ('Function', 'Method', 'Constructor', 'Class') AND file_path IS NOT NULL`)
556
17
  .all();
557
18
  for (const row of rows) {
558
- // byFilePath index
559
19
  let fileMap = byFilePath.get(row.file_path);
560
20
  if (!fileMap) {
561
21
  fileMap = new Map();
@@ -567,21 +27,18 @@ function buildFunctionIndex(ctx) {
567
27
  fileMap.set(row.name, ids);
568
28
  }
569
29
  ids.push(row.id);
570
- // global count for ambiguity detection
571
30
  nameCounts.set(row.name, (nameCounts.get(row.name) ?? 0) + 1);
572
31
  }
573
32
  return { byFilePath, nameCounts };
574
33
  }
575
- // ── Stage 4 + 5: Resolve target node (index-based, no per-call DB query) ──────
34
+ // ── Target resolution ────────────────────────────────────────────────────────
576
35
  function pickBestId(ids, site) {
577
36
  if (ids.length === 1)
578
37
  return ids[0];
579
38
  if (ids.length === 0)
580
39
  return null;
581
- // Disambiguate: prefer Function for direct calls, Method for method calls
582
40
  const suffix = site.form === 'method' ? '_method' : '_function';
583
41
  const match = ids.find(id => id.endsWith(suffix));
584
- // For `new ClassName()` prefer _class
585
42
  if (!match && site.calleeRaw.startsWith('new ')) {
586
43
  const classMatch = ids.find(id => id.endsWith('_class'));
587
44
  if (classMatch)
@@ -670,6 +127,9 @@ function emitEdge(stmts, sourceId, targetId) {
670
127
  }
671
128
  }
672
129
  // ── Phase definition ──────────────────────────────────────────────────────────
130
+ function getImportMap(allImportMaps, fileNodeId) {
131
+ return allImportMaps.get(fileNodeId) ?? new Map();
132
+ }
673
133
  export const scopeResolutionPhase = {
674
134
  name: 'scope-resolution',
675
135
  deps: ['parse', 'cross-file'],
@@ -677,19 +137,16 @@ export const scopeResolutionPhase = {
677
137
  if (ctx.allFilesCached) {
678
138
  return { resolvedEdges: 0, skippedDynamic: 0, ambiguous: 0, reexportEdges: 0, orphanImportsRemoved: 0, importsReconstructed: 0 };
679
139
  }
680
- const { symbolNodes } = deps.get('parse');
140
+ const { symbolNodes, fileContents } = deps.get('parse');
681
141
  let resolvedEdges = 0;
682
142
  let skippedDynamic = 0;
683
143
  let ambiguous = 0;
684
- // ── Batch preload: one query per table instead of one per file/call-site ──
685
- // Build a map from file_path → file node id using the parse output
686
144
  const fileNodesByPath = new Map();
687
145
  for (const node of symbolNodes) {
688
146
  if (node.label === 'File' && node.filePath) {
689
147
  fileNodesByPath.set(node.filePath, node);
690
148
  }
691
149
  }
692
- // Also look up File nodes from the DB (includes nodes created by structure phase)
693
150
  if (ctx.db) {
694
151
  const dbFileNodes = ctx.db
695
152
  .prepare(`SELECT id, file_path FROM nodes WHERE label = 'File'`)
@@ -707,10 +164,7 @@ export const scopeResolutionPhase = {
707
164
  }
708
165
  }
709
166
  }
710
- // Parse source imports → per-file import maps (replaces broken DB-edge approach)
711
- const { fileContents } = deps.get('parse');
712
167
  const allImportMaps = buildAllImportMapsFromSource(ctx.repoPath, fileNodesByPath, fileContents);
713
- // Preload all Function/Method/Constructor nodes into indices (one DB query total)
714
168
  const { byFilePath: fnIndex, nameCounts } = buildFunctionIndex(ctx);
715
169
  const edgeStmts = ctx.db ? prepareEdgeStmts(ctx.db) : null;
716
170
  const resolveAllCalls = ctx.db?.transaction(() => {
@@ -718,7 +172,6 @@ export const scopeResolutionPhase = {
718
172
  const ext = extname(filePath).toLowerCase();
719
173
  if (!isSupportedExt(ext))
720
174
  continue;
721
- // Read file source (prefer cached from parse phase)
722
175
  let source = fileContents.get(filePath);
723
176
  if (!source) {
724
177
  try {
@@ -728,29 +181,21 @@ export const scopeResolutionPhase = {
728
181
  continue;
729
182
  }
730
183
  }
731
- // Stage 1+2: Extract call sites
732
184
  const callSites = extractCallSites(source, filePath, fileNode.id, ext);
733
- // Stage 3: Get pre-built import map for this file (O(1))
734
185
  const importMap = getImportMap(allImportMaps, fileNode.id);
735
- // Track constructor assignments for type-aware method resolution
736
186
  const ctorMap = (TS_JS_EXTS.has(ext) || CJS_MJS_EXTS.has(ext)) ? extractConstructorAssignments(source) : undefined;
737
- // Precompute once per file instead of per call site
738
187
  const importedFiles = [...new Set(importMap.values())];
739
188
  for (const site of callSites) {
740
189
  if (site.form === 'dynamic') {
741
190
  skippedDynamic++;
742
191
  continue;
743
192
  }
744
- // Stage 4+5: Resolve target using preloaded indices (no DB query)
745
193
  const resolved = resolveTarget(site, filePath, importMap, fnIndex, ctorMap, importedFiles);
746
- if (!resolved) {
194
+ if (!resolved)
747
195
  continue;
748
- }
749
- // Ambiguity check using preloaded name counts (no DB query)
750
196
  if (site.methodName && (nameCounts.get(site.methodName) ?? 0) > 1) {
751
197
  ambiguous++;
752
198
  }
753
- // Stage 6: Emit edge (source is the file node of the caller)
754
199
  const result = edgeStmts ? emitEdge(edgeStmts, site.callerFileNodeId, resolved.targetId) : 'skipped';
755
200
  if (result === 'inserted' || result === 'upgraded') {
756
201
  resolvedEdges++;
@@ -759,14 +204,14 @@ export const scopeResolutionPhase = {
759
204
  }
760
205
  });
761
206
  resolveAllCalls?.();
762
- // Scan for re-exports: export { foo, bar } from './module'
763
- // Creates REFERENCES edges so re-exported symbols don't appear as dead code.
764
207
  let reexportEdges = 0;
765
208
  if (ctx.db) {
766
209
  const insertRef = ctx.db.prepare(`
767
210
  INSERT OR IGNORE INTO edges (id, source_id, target_id, relation, confidence, confidence_score)
768
211
  VALUES (?, ?, ?, 'REFERENCES', 'EXTRACTED', 0.85)
769
212
  `);
213
+ const reexportKnownFiles = new Set(fileNodesByPath.keys());
214
+ const reexportWorkspaceMap = buildWorkspacePackageMap(ctx.repoPath);
770
215
  const insertReexports = ctx.db.transaction(() => {
771
216
  for (const [filePath, fileNode] of fileNodesByPath) {
772
217
  const ext = extname(filePath).toLowerCase();
@@ -781,20 +226,15 @@ export const scopeResolutionPhase = {
781
226
  continue;
782
227
  }
783
228
  }
784
- const importMap = getImportMap(allImportMaps, fileNode.id);
785
229
  REEXPORT_RE.lastIndex = 0;
786
230
  let m;
787
231
  while ((m = REEXPORT_RE.exec(source)) !== null) {
788
232
  const specifier = m[2];
789
233
  const names = extractImportNames(m[1]);
790
- // Resolve the source module
791
- const knownFiles = new Set(fileNodesByPath.keys());
792
- const workspaceMap = buildWorkspacePackageMap(ctx.repoPath);
793
- const resolvedFile = resolveModuleSpecifier(filePath, specifier, ctx.repoPath, knownFiles, workspaceMap);
234
+ const resolvedFile = resolveModuleSpecifier(filePath, specifier, ctx.repoPath, reexportKnownFiles, reexportWorkspaceMap);
794
235
  if (!resolvedFile)
795
236
  continue;
796
237
  for (const name of names) {
797
- // Find the function/class node in the source file
798
238
  const ids = fnIndex.get(resolvedFile)?.get(name);
799
239
  if (!ids || ids.length === 0)
800
240
  continue;
@@ -811,7 +251,6 @@ export const scopeResolutionPhase = {
811
251
  });
812
252
  insertReexports();
813
253
  }
814
- // Clean up orphan IMPORTS edges that target Variable nodes instead of Files.
815
254
  let orphanImportsRemoved = 0;
816
255
  if (ctx.db) {
817
256
  const result = ctx.db
@@ -825,7 +264,6 @@ export const scopeResolutionPhase = {
825
264
  .run();
826
265
  orphanImportsRemoved = result.changes;
827
266
  }
828
- // Reconstruct proper File→File IMPORTS edges from source-parsed import maps.
829
267
  let importsReconstructed = 0;
830
268
  if (ctx.db) {
831
269
  const insertImport = ctx.db.prepare(`
@@ -847,7 +285,7 @@ export const scopeResolutionPhase = {
847
285
  insertImport.run(edgeId, fileNodeId, targetFileId);
848
286
  importsReconstructed++;
849
287
  }
850
- catch { /* duplicate — already exists */ }
288
+ catch { /* duplicate */ }
851
289
  }
852
290
  }
853
291
  });