gitnexus 1.4.0 → 1.4.5

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 (102) hide show
  1. package/README.md +19 -18
  2. package/dist/cli/analyze.js +37 -28
  3. package/dist/cli/augment.js +1 -1
  4. package/dist/cli/eval-server.d.ts +1 -1
  5. package/dist/cli/eval-server.js +1 -1
  6. package/dist/cli/index.js +1 -0
  7. package/dist/cli/mcp.js +1 -1
  8. package/dist/cli/setup.js +25 -13
  9. package/dist/cli/status.js +13 -4
  10. package/dist/cli/tool.d.ts +1 -1
  11. package/dist/cli/tool.js +2 -2
  12. package/dist/cli/wiki.js +2 -2
  13. package/dist/config/ignore-service.d.ts +25 -0
  14. package/dist/config/ignore-service.js +76 -0
  15. package/dist/config/supported-languages.d.ts +1 -0
  16. package/dist/config/supported-languages.js +1 -1
  17. package/dist/core/augmentation/engine.js +94 -67
  18. package/dist/core/embeddings/embedder.d.ts +1 -1
  19. package/dist/core/embeddings/embedder.js +1 -1
  20. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  21. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  22. package/dist/core/embeddings/types.d.ts +1 -1
  23. package/dist/core/ingestion/call-processor.d.ts +6 -7
  24. package/dist/core/ingestion/call-processor.js +490 -127
  25. package/dist/core/ingestion/call-routing.d.ts +53 -0
  26. package/dist/core/ingestion/call-routing.js +108 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +13 -2
  28. package/dist/core/ingestion/export-detection.js +1 -0
  29. package/dist/core/ingestion/filesystem-walker.js +4 -3
  30. package/dist/core/ingestion/framework-detection.js +9 -0
  31. package/dist/core/ingestion/heritage-processor.d.ts +3 -4
  32. package/dist/core/ingestion/heritage-processor.js +40 -50
  33. package/dist/core/ingestion/import-processor.d.ts +3 -5
  34. package/dist/core/ingestion/import-processor.js +41 -10
  35. package/dist/core/ingestion/parsing-processor.d.ts +2 -1
  36. package/dist/core/ingestion/parsing-processor.js +41 -4
  37. package/dist/core/ingestion/pipeline.d.ts +5 -1
  38. package/dist/core/ingestion/pipeline.js +174 -121
  39. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  40. package/dist/core/ingestion/resolution-context.js +132 -0
  41. package/dist/core/ingestion/resolvers/index.d.ts +2 -0
  42. package/dist/core/ingestion/resolvers/index.js +2 -0
  43. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  44. package/dist/core/ingestion/resolvers/python.js +52 -0
  45. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  46. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  47. package/dist/core/ingestion/resolvers/standard.js +0 -22
  48. package/dist/core/ingestion/resolvers/utils.js +2 -0
  49. package/dist/core/ingestion/symbol-table.d.ts +3 -0
  50. package/dist/core/ingestion/symbol-table.js +1 -0
  51. package/dist/core/ingestion/tree-sitter-queries.d.ts +3 -2
  52. package/dist/core/ingestion/tree-sitter-queries.js +53 -1
  53. package/dist/core/ingestion/type-env.d.ts +32 -10
  54. package/dist/core/ingestion/type-env.js +520 -47
  55. package/dist/core/ingestion/type-extractors/c-cpp.js +326 -1
  56. package/dist/core/ingestion/type-extractors/csharp.js +282 -2
  57. package/dist/core/ingestion/type-extractors/go.js +333 -2
  58. package/dist/core/ingestion/type-extractors/index.d.ts +3 -2
  59. package/dist/core/ingestion/type-extractors/index.js +3 -1
  60. package/dist/core/ingestion/type-extractors/jvm.js +537 -4
  61. package/dist/core/ingestion/type-extractors/php.js +387 -7
  62. package/dist/core/ingestion/type-extractors/python.js +356 -5
  63. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  64. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  65. package/dist/core/ingestion/type-extractors/rust.js +399 -2
  66. package/dist/core/ingestion/type-extractors/shared.d.ts +116 -1
  67. package/dist/core/ingestion/type-extractors/shared.js +488 -14
  68. package/dist/core/ingestion/type-extractors/swift.js +95 -1
  69. package/dist/core/ingestion/type-extractors/types.d.ts +81 -0
  70. package/dist/core/ingestion/type-extractors/typescript.js +436 -2
  71. package/dist/core/ingestion/utils.d.ts +33 -2
  72. package/dist/core/ingestion/utils.js +399 -27
  73. package/dist/core/ingestion/workers/parse-worker.d.ts +18 -1
  74. package/dist/core/ingestion/workers/parse-worker.js +169 -19
  75. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
  76. package/dist/core/{kuzu → lbug}/csv-generator.js +1 -1
  77. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
  78. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +70 -65
  79. package/dist/core/{kuzu → lbug}/schema.d.ts +1 -1
  80. package/dist/core/{kuzu → lbug}/schema.js +1 -1
  81. package/dist/core/search/bm25-index.d.ts +4 -4
  82. package/dist/core/search/bm25-index.js +10 -10
  83. package/dist/core/search/hybrid-search.d.ts +2 -2
  84. package/dist/core/search/hybrid-search.js +6 -6
  85. package/dist/core/tree-sitter/parser-loader.js +9 -2
  86. package/dist/core/wiki/generator.d.ts +2 -2
  87. package/dist/core/wiki/generator.js +4 -4
  88. package/dist/core/wiki/graph-queries.d.ts +4 -4
  89. package/dist/core/wiki/graph-queries.js +7 -7
  90. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -7
  91. package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +72 -43
  92. package/dist/mcp/local/local-backend.d.ts +6 -6
  93. package/dist/mcp/local/local-backend.js +25 -18
  94. package/dist/server/api.js +12 -12
  95. package/dist/server/mcp-http.d.ts +1 -1
  96. package/dist/server/mcp-http.js +1 -1
  97. package/dist/storage/repo-manager.d.ts +20 -2
  98. package/dist/storage/repo-manager.js +55 -1
  99. package/dist/types/pipeline.d.ts +1 -1
  100. package/package.json +5 -3
  101. package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
  102. package/dist/core/ingestion/symbol-resolver.js +0 -83
@@ -1,10 +1,11 @@
1
1
  import Parser from 'tree-sitter';
2
- import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
2
+ import { loadParser, loadLanguage, isLanguageAvailable } from '../tree-sitter/parser-loader.js';
3
3
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
4
  import { generateId } from '../../lib/utils.js';
5
5
  import { getLanguageFromFilename, yieldToEventLoop, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js';
6
6
  import { isNodeExported } from './export-detection.js';
7
7
  import { detectFrameworkFromAST } from './framework-detection.js';
8
+ import { typeConfigs } from './type-extractors/index.js';
8
9
  import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
9
10
  // isNodeExported imported from ./export-detection.js (shared module)
10
11
  // Re-export for backward compatibility with any external consumers
@@ -21,7 +22,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
21
22
  parseableFiles.push({ path: file.path, content: file.content });
22
23
  }
23
24
  if (parseableFiles.length === 0)
24
- return { imports: [], calls: [], heritage: [], routes: [] };
25
+ return { imports: [], calls: [], heritage: [], routes: [], constructorBindings: [] };
25
26
  const total = files.length;
26
27
  // Dispatch to worker pool — pool handles splitting into chunks and sub-batching
27
28
  const chunkResults = await workerPool.dispatch(parseableFiles, (filesProcessed) => {
@@ -32,6 +33,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
32
33
  const allCalls = [];
33
34
  const allHeritage = [];
34
35
  const allRoutes = [];
36
+ const allConstructorBindings = [];
35
37
  for (const result of chunkResults) {
36
38
  for (const node of result.nodes) {
37
39
  graph.addNode({
@@ -46,6 +48,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
46
48
  for (const sym of result.symbols) {
47
49
  symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
48
50
  parameterCount: sym.parameterCount,
51
+ returnType: sym.returnType,
49
52
  ownerId: sym.ownerId,
50
53
  });
51
54
  }
@@ -53,10 +56,24 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
53
56
  allCalls.push(...result.calls);
54
57
  allHeritage.push(...result.heritage);
55
58
  allRoutes.push(...result.routes);
59
+ allConstructorBindings.push(...result.constructorBindings);
60
+ }
61
+ // Merge and log skipped languages from workers
62
+ const skippedLanguages = new Map();
63
+ for (const result of chunkResults) {
64
+ for (const [lang, count] of Object.entries(result.skippedLanguages)) {
65
+ skippedLanguages.set(lang, (skippedLanguages.get(lang) || 0) + count);
66
+ }
67
+ }
68
+ if (skippedLanguages.size > 0) {
69
+ const summary = Array.from(skippedLanguages.entries())
70
+ .map(([lang, count]) => `${lang}: ${count}`)
71
+ .join(', ');
72
+ console.warn(` Skipped unsupported languages: ${summary}`);
56
73
  }
57
74
  // Final progress
58
75
  onFileProgress?.(total, total, 'done');
59
- return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes };
76
+ return { imports: allImports, calls: allCalls, heritage: allHeritage, routes: allRoutes, constructorBindings: allConstructorBindings };
60
77
  };
61
78
  // ============================================================================
62
79
  // Sequential fallback (original implementation)
@@ -64,6 +81,7 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
64
81
  const processParsingSequential = async (graph, files, symbolTable, astCache, onFileProgress) => {
65
82
  const parser = await loadParser();
66
83
  const total = files.length;
84
+ const skippedLanguages = new Map();
67
85
  for (let i = 0; i < files.length; i++) {
68
86
  const file = files[i];
69
87
  onFileProgress?.(i + 1, total, file.path);
@@ -72,6 +90,11 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
72
90
  const language = getLanguageFromFilename(file.path);
73
91
  if (!language)
74
92
  continue;
93
+ // Skip unsupported languages (e.g. Swift when tree-sitter-swift not installed)
94
+ if (!isLanguageAvailable(language)) {
95
+ skippedLanguages.set(language, (skippedLanguages.get(language) || 0) + 1);
96
+ continue;
97
+ }
75
98
  // Skip files larger than the max tree-sitter buffer (32 MB)
76
99
  if (file.content.length > TREE_SITTER_MAX_BUFFER)
77
100
  continue;
@@ -79,7 +102,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
79
102
  await loadLanguage(language, file.path);
80
103
  }
81
104
  catch {
82
- continue; // parser unavailable — already warned in pipeline
105
+ continue; // parser unavailable — safety net
83
106
  }
84
107
  let tree;
85
108
  try {
@@ -177,6 +200,13 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
177
200
  const methodSig = (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
178
201
  ? extractMethodSignature(definitionNode)
179
202
  : undefined;
203
+ // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
204
+ if (methodSig && !methodSig.returnType && definitionNode) {
205
+ const tc = typeConfigs[language];
206
+ if (tc?.extractReturnType) {
207
+ methodSig.returnType = tc.extractReturnType(definitionNode);
208
+ }
209
+ }
180
210
  const node = {
181
211
  id: nodeId,
182
212
  label: nodeLabel,
@@ -204,6 +234,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
204
234
  const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
205
235
  symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
206
236
  parameterCount: methodSig?.parameterCount,
237
+ returnType: methodSig?.returnType,
207
238
  ownerId: enclosingClassId ?? undefined,
208
239
  });
209
240
  const fileId = generateId('File', file.path);
@@ -230,6 +261,12 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
230
261
  }
231
262
  });
232
263
  }
264
+ if (skippedLanguages.size > 0) {
265
+ const summary = Array.from(skippedLanguages.entries())
266
+ .map(([lang, count]) => `${lang}: ${count}`)
267
+ .join(', ');
268
+ console.warn(` Skipped unsupported languages: ${summary}`);
269
+ }
233
270
  };
234
271
  // ============================================================================
235
272
  // Public API
@@ -1,2 +1,6 @@
1
1
  import { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
2
- export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void) => Promise<PipelineResult>;
2
+ export interface PipelineOptions {
3
+ /** Skip MRO, community detection, and process extraction for faster test runs. */
4
+ skipGraphPhases?: boolean;
5
+ }
6
+ export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void, options?: PipelineOptions) => Promise<PipelineResult>;
@@ -1,13 +1,13 @@
1
1
  import { createKnowledgeGraph } from '../graph/graph.js';
2
2
  import { processStructure } from './structure-processor.js';
3
3
  import { processParsing } from './parsing-processor.js';
4
- import { processImports, processImportsFromExtracted, createImportMap, createPackageMap, createNamedImportMap, buildImportResolutionContext } from './import-processor.js';
4
+ import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
5
5
  import { processCalls, processCallsFromExtracted, processRoutesFromExtracted } from './call-processor.js';
6
6
  import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
7
7
  import { computeMRO } from './mro-processor.js';
8
8
  import { processCommunities } from './community-processor.js';
9
9
  import { processProcesses } from './process-processor.js';
10
- import { createSymbolTable } from './symbol-table.js';
10
+ import { createResolutionContext } from './resolution-context.js';
11
11
  import { createASTCache } from './ast-cache.js';
12
12
  import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
13
13
  import { getLanguageFromFilename } from './utils.js';
@@ -24,16 +24,14 @@ const isDev = process.env.NODE_ENV === 'development';
24
24
  const CHUNK_BYTE_BUDGET = 20 * 1024 * 1024; // 20MB
25
25
  /** Max AST trees to keep in LRU cache */
26
26
  const AST_CACHE_CAP = 50;
27
- export const runPipelineFromRepo = async (repoPath, onProgress) => {
27
+ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
28
28
  const graph = createKnowledgeGraph();
29
- const symbolTable = createSymbolTable();
29
+ const ctx = createResolutionContext();
30
+ const symbolTable = ctx.symbols;
30
31
  let astCache = createASTCache(AST_CACHE_CAP);
31
- const importMap = createImportMap();
32
- const packageMap = createPackageMap();
33
- const namedImportMap = createNamedImportMap();
34
32
  const cleanup = () => {
35
33
  astCache.clear();
36
- symbolTable.clear();
34
+ ctx.clear();
37
35
  };
38
36
  try {
39
37
  // ── Phase 1: Scan paths only (no content read) ─────────────────────
@@ -127,24 +125,30 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
127
125
  message: `Parsing ${totalParseable} files in ${numChunks} chunk${numChunks !== 1 ? 's' : ''}...`,
128
126
  stats: { filesProcessed: 0, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
129
127
  });
128
+ // Don't spawn workers for tiny repos — overhead exceeds benefit
129
+ const MIN_FILES_FOR_WORKERS = 15;
130
+ const MIN_BYTES_FOR_WORKERS = 512 * 1024;
131
+ const totalBytes = parseableScanned.reduce((s, f) => s + f.size, 0);
130
132
  // Create worker pool once, reuse across chunks
131
133
  let workerPool;
132
- try {
133
- let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
134
- // When running under vitest, import.meta.url points to src/ where no .js exists.
135
- // Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
136
- const thisDir = fileURLToPath(new URL('.', import.meta.url));
137
- if (!fs.existsSync(fileURLToPath(workerUrl))) {
138
- const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
139
- if (fs.existsSync(distWorker)) {
140
- workerUrl = pathToFileURL(distWorker);
134
+ if (totalParseable >= MIN_FILES_FOR_WORKERS || totalBytes >= MIN_BYTES_FOR_WORKERS) {
135
+ try {
136
+ let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
137
+ // When running under vitest, import.meta.url points to src/ where no .js exists.
138
+ // Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
139
+ const thisDir = fileURLToPath(new URL('.', import.meta.url));
140
+ if (!fs.existsSync(fileURLToPath(workerUrl))) {
141
+ const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
142
+ if (fs.existsSync(distWorker)) {
143
+ workerUrl = pathToFileURL(distWorker);
144
+ }
141
145
  }
146
+ workerPool = createWorkerPool(workerUrl);
147
+ }
148
+ catch (err) {
149
+ if (isDev)
150
+ console.warn('Worker pool creation failed, using sequential fallback:', err.message);
142
151
  }
143
- workerPool = createWorkerPool(workerUrl);
144
- }
145
- catch (err) {
146
- if (isDev)
147
- console.warn('Worker pool creation failed, using sequential fallback:', err.message);
148
152
  }
149
153
  let filesParsedSoFar = 0;
150
154
  // AST cache sized for one chunk (sequential fallback uses it for import/call/heritage)
@@ -179,20 +183,53 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
179
183
  stats: { filesProcessed: globalCurrent, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
180
184
  });
181
185
  }, workerPool);
186
+ const chunkBasePercent = 20 + ((filesParsedSoFar / totalParseable) * 62);
182
187
  if (chunkWorkerData) {
183
188
  // Imports
184
- await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, importMap, undefined, repoPath, importCtx, packageMap, namedImportMap);
189
+ await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, ctx, (current, total) => {
190
+ onProgress({
191
+ phase: 'parsing',
192
+ percent: Math.round(chunkBasePercent),
193
+ message: `Resolving imports (chunk ${chunkIdx + 1}/${numChunks})...`,
194
+ detail: `${current}/${total} files`,
195
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
196
+ });
197
+ }, repoPath, importCtx);
185
198
  // Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
186
199
  // This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
187
200
  // and the single-threaded event loop prevents races between synchronous addRelationship calls.
188
201
  await Promise.all([
189
- processCallsFromExtracted(graph, chunkWorkerData.calls, symbolTable, importMap, packageMap, undefined, namedImportMap),
190
- processHeritageFromExtracted(graph, chunkWorkerData.heritage, symbolTable, importMap, packageMap),
191
- processRoutesFromExtracted(graph, chunkWorkerData.routes ?? [], symbolTable, importMap, packageMap),
202
+ processCallsFromExtracted(graph, chunkWorkerData.calls, ctx, (current, total) => {
203
+ onProgress({
204
+ phase: 'parsing',
205
+ percent: Math.round(chunkBasePercent),
206
+ message: `Resolving calls (chunk ${chunkIdx + 1}/${numChunks})...`,
207
+ detail: `${current}/${total} files`,
208
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
209
+ });
210
+ }, chunkWorkerData.constructorBindings),
211
+ processHeritageFromExtracted(graph, chunkWorkerData.heritage, ctx, (current, total) => {
212
+ onProgress({
213
+ phase: 'parsing',
214
+ percent: Math.round(chunkBasePercent),
215
+ message: `Resolving heritage (chunk ${chunkIdx + 1}/${numChunks})...`,
216
+ detail: `${current}/${total} records`,
217
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
218
+ });
219
+ }),
220
+ processRoutesFromExtracted(graph, chunkWorkerData.routes ?? [], ctx, (current, total) => {
221
+ onProgress({
222
+ phase: 'parsing',
223
+ percent: Math.round(chunkBasePercent),
224
+ message: `Resolving routes (chunk ${chunkIdx + 1}/${numChunks})...`,
225
+ detail: `${current}/${total} routes`,
226
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
227
+ });
228
+ }),
192
229
  ]);
193
230
  }
194
231
  else {
195
- await processImports(graph, chunkFiles, astCache, importMap, undefined, repoPath, allPaths, packageMap, namedImportMap);
232
+ await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
196
233
  sequentialChunkPaths.push(chunkPaths);
197
234
  }
198
235
  filesParsedSoFar += chunkFiles.length;
@@ -211,123 +248,139 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
211
248
  .filter(p => chunkContents.has(p))
212
249
  .map(p => ({ path: p, content: chunkContents.get(p) }));
213
250
  astCache = createASTCache(chunkFiles.length);
214
- await processCalls(graph, chunkFiles, astCache, symbolTable, importMap, packageMap, undefined, namedImportMap);
215
- await processHeritage(graph, chunkFiles, astCache, symbolTable, importMap, packageMap);
251
+ const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx);
252
+ await processHeritage(graph, chunkFiles, astCache, ctx);
253
+ if (rubyHeritage.length > 0) {
254
+ await processHeritageFromExtracted(graph, rubyHeritage, ctx);
255
+ }
216
256
  astCache.clear();
217
257
  }
258
+ // Log resolution cache stats
259
+ if (isDev) {
260
+ const rcStats = ctx.getStats();
261
+ const total = rcStats.cacheHits + rcStats.cacheMisses;
262
+ const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0';
263
+ console.log(`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`);
264
+ }
218
265
  // Free import resolution context — suffix index + resolve cache no longer needed
219
266
  // (allPathObjects and importCtx hold ~94MB+ for large repos)
220
267
  allPathObjects.length = 0;
221
268
  importCtx.resolveCache.clear();
222
269
  importCtx.suffixIndex = null;
223
270
  importCtx.normalizedFileList = null;
224
- // ── Phase 4.5: Method Resolution Order ──────────────────────────────
225
- onProgress({
226
- phase: 'parsing',
227
- percent: 81,
228
- message: 'Computing method resolution order...',
229
- stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
230
- });
231
- const mroResult = computeMRO(graph);
232
- if (isDev && mroResult.entries.length > 0) {
233
- console.log(`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`);
234
- }
235
- // ── Phase 5: Communities ───────────────────────────────────────────
236
- onProgress({
237
- phase: 'communities',
238
- percent: 82,
239
- message: 'Detecting code communities...',
240
- stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
241
- });
242
- const communityResult = await processCommunities(graph, (message, progress) => {
243
- const communityProgress = 82 + (progress * 0.10);
271
+ let communityResult;
272
+ let processResult;
273
+ if (!options?.skipGraphPhases) {
274
+ // ── Phase 4.5: Method Resolution Order ──────────────────────────────
275
+ onProgress({
276
+ phase: 'parsing',
277
+ percent: 81,
278
+ message: 'Computing method resolution order...',
279
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
280
+ });
281
+ const mroResult = computeMRO(graph);
282
+ if (isDev && mroResult.entries.length > 0) {
283
+ console.log(`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`);
284
+ }
285
+ // ── Phase 5: Communities ───────────────────────────────────────────
244
286
  onProgress({
245
287
  phase: 'communities',
246
- percent: Math.round(communityProgress),
247
- message,
288
+ percent: 82,
289
+ message: 'Detecting code communities...',
248
290
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
249
291
  });
250
- });
251
- if (isDev) {
252
- console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
253
- }
254
- communityResult.communities.forEach(comm => {
255
- graph.addNode({
256
- id: comm.id,
257
- label: 'Community',
258
- properties: {
259
- name: comm.label,
260
- filePath: '',
261
- heuristicLabel: comm.heuristicLabel,
262
- cohesion: comm.cohesion,
263
- symbolCount: comm.symbolCount,
264
- }
292
+ communityResult = await processCommunities(graph, (message, progress) => {
293
+ const communityProgress = 82 + (progress * 0.10);
294
+ onProgress({
295
+ phase: 'communities',
296
+ percent: Math.round(communityProgress),
297
+ message,
298
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
299
+ });
265
300
  });
266
- });
267
- communityResult.memberships.forEach(membership => {
268
- graph.addRelationship({
269
- id: `${membership.nodeId}_member_of_${membership.communityId}`,
270
- type: 'MEMBER_OF',
271
- sourceId: membership.nodeId,
272
- targetId: membership.communityId,
273
- confidence: 1.0,
274
- reason: 'leiden-algorithm',
301
+ if (isDev) {
302
+ console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
303
+ }
304
+ communityResult.communities.forEach(comm => {
305
+ graph.addNode({
306
+ id: comm.id,
307
+ label: 'Community',
308
+ properties: {
309
+ name: comm.label,
310
+ filePath: '',
311
+ heuristicLabel: comm.heuristicLabel,
312
+ cohesion: comm.cohesion,
313
+ symbolCount: comm.symbolCount,
314
+ }
315
+ });
275
316
  });
276
- });
277
- // ── Phase 6: Processes ─────────────────────────────────────────────
278
- onProgress({
279
- phase: 'processes',
280
- percent: 94,
281
- message: 'Detecting execution flows...',
282
- stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
283
- });
284
- let symbolCount = 0;
285
- graph.forEachNode(n => { if (n.label !== 'File')
286
- symbolCount++; });
287
- const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
288
- const processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
289
- const processProgress = 94 + (progress * 0.05);
317
+ communityResult.memberships.forEach(membership => {
318
+ graph.addRelationship({
319
+ id: `${membership.nodeId}_member_of_${membership.communityId}`,
320
+ type: 'MEMBER_OF',
321
+ sourceId: membership.nodeId,
322
+ targetId: membership.communityId,
323
+ confidence: 1.0,
324
+ reason: 'leiden-algorithm',
325
+ });
326
+ });
327
+ // ── Phase 6: Processes ─────────────────────────────────────────────
290
328
  onProgress({
291
329
  phase: 'processes',
292
- percent: Math.round(processProgress),
293
- message,
330
+ percent: 94,
331
+ message: 'Detecting execution flows...',
294
332
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
295
333
  });
296
- }, { maxProcesses: dynamicMaxProcesses, minSteps: 3 });
297
- if (isDev) {
298
- console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
299
- }
300
- processResult.processes.forEach(proc => {
301
- graph.addNode({
302
- id: proc.id,
303
- label: 'Process',
304
- properties: {
305
- name: proc.label,
306
- filePath: '',
307
- heuristicLabel: proc.heuristicLabel,
308
- processType: proc.processType,
309
- stepCount: proc.stepCount,
310
- communities: proc.communities,
311
- entryPointId: proc.entryPointId,
312
- terminalId: proc.terminalId,
313
- }
334
+ let symbolCount = 0;
335
+ graph.forEachNode(n => { if (n.label !== 'File')
336
+ symbolCount++; });
337
+ const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
338
+ processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
339
+ const processProgress = 94 + (progress * 0.05);
340
+ onProgress({
341
+ phase: 'processes',
342
+ percent: Math.round(processProgress),
343
+ message,
344
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
345
+ });
346
+ }, { maxProcesses: dynamicMaxProcesses, minSteps: 3 });
347
+ if (isDev) {
348
+ console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
349
+ }
350
+ processResult.processes.forEach(proc => {
351
+ graph.addNode({
352
+ id: proc.id,
353
+ label: 'Process',
354
+ properties: {
355
+ name: proc.label,
356
+ filePath: '',
357
+ heuristicLabel: proc.heuristicLabel,
358
+ processType: proc.processType,
359
+ stepCount: proc.stepCount,
360
+ communities: proc.communities,
361
+ entryPointId: proc.entryPointId,
362
+ terminalId: proc.terminalId,
363
+ }
364
+ });
314
365
  });
315
- });
316
- processResult.steps.forEach(step => {
317
- graph.addRelationship({
318
- id: `${step.nodeId}_step_${step.step}_${step.processId}`,
319
- type: 'STEP_IN_PROCESS',
320
- sourceId: step.nodeId,
321
- targetId: step.processId,
322
- confidence: 1.0,
323
- reason: 'trace-detection',
324
- step: step.step,
366
+ processResult.steps.forEach(step => {
367
+ graph.addRelationship({
368
+ id: `${step.nodeId}_step_${step.step}_${step.processId}`,
369
+ type: 'STEP_IN_PROCESS',
370
+ sourceId: step.nodeId,
371
+ targetId: step.processId,
372
+ confidence: 1.0,
373
+ reason: 'trace-detection',
374
+ step: step.step,
375
+ });
325
376
  });
326
- });
377
+ }
327
378
  onProgress({
328
379
  phase: 'complete',
329
380
  percent: 100,
330
- message: `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`,
381
+ message: communityResult && processResult
382
+ ? `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`
383
+ : 'Graph complete! (graph phases skipped)',
331
384
  stats: {
332
385
  filesProcessed: totalFiles,
333
386
  totalFiles,
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Resolution Context
3
+ *
4
+ * Single implementation of tiered name resolution. Replaces the duplicated
5
+ * tier-selection logic previously split between symbol-resolver.ts and
6
+ * call-processor.ts.
7
+ *
8
+ * Resolution tiers (highest confidence first):
9
+ * 1. Same file (lookupExactFull — authoritative)
10
+ * 2a-named. Named binding chain (walkBindingChain via NamedImportMap)
11
+ * 2a. Import-scoped (lookupFuzzy filtered by ImportMap)
12
+ * 2b. Package-scoped (lookupFuzzy filtered by PackageMap)
13
+ * 3. Global (all candidates — consumers must check candidate count)
14
+ */
15
+ import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
16
+ import type { NamedImportBinding } from './import-processor.js';
17
+ /** Resolution tier for tracking, logging, and test assertions. */
18
+ export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
19
+ /** Tier-selected candidates with metadata. */
20
+ export interface TieredCandidates {
21
+ readonly candidates: readonly SymbolDefinition[];
22
+ readonly tier: ResolutionTier;
23
+ }
24
+ /** Confidence scores per resolution tier. */
25
+ export declare const TIER_CONFIDENCE: Record<ResolutionTier, number>;
26
+ export type ImportMap = Map<string, Set<string>>;
27
+ export type PackageMap = Map<string, Set<string>>;
28
+ export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
29
+ export interface ResolutionContext {
30
+ /**
31
+ * The only resolution API. Returns all candidates at the winning tier.
32
+ *
33
+ * Tier 3 ('global') returns ALL candidates regardless of count —
34
+ * consumers must check candidates.length and refuse ambiguous matches.
35
+ */
36
+ resolve(name: string, fromFile: string): TieredCandidates | null;
37
+ /** Symbol table — used by parsing-processor to populate symbols. */
38
+ readonly symbols: SymbolTable;
39
+ /** Raw maps — used by import-processor to populate import data. */
40
+ readonly importMap: ImportMap;
41
+ readonly packageMap: PackageMap;
42
+ readonly namedImportMap: NamedImportMap;
43
+ enableCache(filePath: string): void;
44
+ clearCache(): void;
45
+ getStats(): {
46
+ fileCount: number;
47
+ globalSymbolCount: number;
48
+ cacheHits: number;
49
+ cacheMisses: number;
50
+ };
51
+ clear(): void;
52
+ }
53
+ export declare const createResolutionContext: () => ResolutionContext;