@veewo/gitnexus 1.3.11 → 1.4.6-rc

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 (181) hide show
  1. package/README.md +37 -80
  2. package/dist/benchmark/agent-context/tool-runner.js +2 -2
  3. package/dist/benchmark/neonspark-candidates.js +3 -3
  4. package/dist/benchmark/tool-runner.js +2 -2
  5. package/dist/cli/ai-context.d.ts +2 -1
  6. package/dist/cli/ai-context.js +16 -12
  7. package/dist/cli/analyze.d.ts +2 -0
  8. package/dist/cli/analyze.js +68 -48
  9. package/dist/cli/augment.js +1 -1
  10. package/dist/cli/eval-server.d.ts +8 -1
  11. package/dist/cli/eval-server.js +30 -13
  12. package/dist/cli/index.js +28 -82
  13. package/dist/cli/lazy-action.d.ts +6 -0
  14. package/dist/cli/lazy-action.js +18 -0
  15. package/dist/cli/mcp.js +3 -1
  16. package/dist/cli/setup.js +87 -48
  17. package/dist/cli/setup.test.js +18 -13
  18. package/dist/cli/skill-gen.d.ts +26 -0
  19. package/dist/cli/skill-gen.js +549 -0
  20. package/dist/cli/status.js +13 -4
  21. package/dist/cli/tool.d.ts +3 -2
  22. package/dist/cli/tool.js +50 -16
  23. package/dist/cli/wiki.js +8 -4
  24. package/dist/config/ignore-service.d.ts +25 -0
  25. package/dist/config/ignore-service.js +76 -0
  26. package/dist/config/supported-languages.d.ts +4 -1
  27. package/dist/config/supported-languages.js +3 -2
  28. package/dist/core/augmentation/engine.js +94 -67
  29. package/dist/core/embeddings/embedder.d.ts +1 -1
  30. package/dist/core/embeddings/embedder.js +1 -1
  31. package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
  32. package/dist/core/embeddings/embedding-pipeline.js +52 -25
  33. package/dist/core/embeddings/types.d.ts +1 -1
  34. package/dist/core/graph/types.d.ts +7 -2
  35. package/dist/core/ingestion/ast-cache.js +3 -2
  36. package/dist/core/ingestion/call-processor.d.ts +8 -6
  37. package/dist/core/ingestion/call-processor.js +468 -206
  38. package/dist/core/ingestion/call-routing.d.ts +53 -0
  39. package/dist/core/ingestion/call-routing.js +108 -0
  40. package/dist/core/ingestion/constants.d.ts +16 -0
  41. package/dist/core/ingestion/constants.js +16 -0
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +116 -23
  44. package/dist/core/ingestion/export-detection.d.ts +18 -0
  45. package/dist/core/ingestion/export-detection.js +231 -0
  46. package/dist/core/ingestion/filesystem-walker.js +4 -3
  47. package/dist/core/ingestion/framework-detection.d.ts +19 -4
  48. package/dist/core/ingestion/framework-detection.js +182 -6
  49. package/dist/core/ingestion/heritage-processor.d.ts +13 -5
  50. package/dist/core/ingestion/heritage-processor.js +109 -55
  51. package/dist/core/ingestion/import-processor.d.ts +16 -20
  52. package/dist/core/ingestion/import-processor.js +199 -579
  53. package/dist/core/ingestion/language-config.d.ts +46 -0
  54. package/dist/core/ingestion/language-config.js +167 -0
  55. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  56. package/dist/core/ingestion/mro-processor.js +369 -0
  57. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  58. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  59. package/dist/core/ingestion/parsing-processor.d.ts +4 -1
  60. package/dist/core/ingestion/parsing-processor.js +107 -109
  61. package/dist/core/ingestion/pipeline.d.ts +6 -3
  62. package/dist/core/ingestion/pipeline.js +208 -114
  63. package/dist/core/ingestion/process-processor.js +8 -2
  64. package/dist/core/ingestion/resolution-context.d.ts +53 -0
  65. package/dist/core/ingestion/resolution-context.js +132 -0
  66. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  67. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  68. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  69. package/dist/core/ingestion/resolvers/go.js +42 -0
  70. package/dist/core/ingestion/resolvers/index.d.ts +18 -0
  71. package/dist/core/ingestion/resolvers/index.js +13 -0
  72. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  73. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  74. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  75. package/dist/core/ingestion/resolvers/php.js +35 -0
  76. package/dist/core/ingestion/resolvers/python.d.ts +19 -0
  77. package/dist/core/ingestion/resolvers/python.js +52 -0
  78. package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
  79. package/dist/core/ingestion/resolvers/ruby.js +15 -0
  80. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  81. package/dist/core/ingestion/resolvers/rust.js +73 -0
  82. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  83. package/dist/core/ingestion/resolvers/standard.js +123 -0
  84. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  85. package/dist/core/ingestion/resolvers/utils.js +122 -0
  86. package/dist/core/ingestion/symbol-table.d.ts +21 -1
  87. package/dist/core/ingestion/symbol-table.js +40 -12
  88. package/dist/core/ingestion/tree-sitter-queries.d.ts +13 -10
  89. package/dist/core/ingestion/tree-sitter-queries.js +297 -7
  90. package/dist/core/ingestion/type-env.d.ts +49 -0
  91. package/dist/core/ingestion/type-env.js +611 -0
  92. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  93. package/dist/core/ingestion/type-extractors/c-cpp.js +385 -0
  94. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  95. package/dist/core/ingestion/type-extractors/csharp.js +383 -0
  96. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  97. package/dist/core/ingestion/type-extractors/go.js +467 -0
  98. package/dist/core/ingestion/type-extractors/index.d.ts +22 -0
  99. package/dist/core/ingestion/type-extractors/index.js +31 -0
  100. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  101. package/dist/core/ingestion/type-extractors/jvm.js +681 -0
  102. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  103. package/dist/core/ingestion/type-extractors/php.js +549 -0
  104. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  105. package/dist/core/ingestion/type-extractors/python.js +406 -0
  106. package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
  107. package/dist/core/ingestion/type-extractors/ruby.js +389 -0
  108. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  109. package/dist/core/ingestion/type-extractors/rust.js +449 -0
  110. package/dist/core/ingestion/type-extractors/shared.d.ts +133 -0
  111. package/dist/core/ingestion/type-extractors/shared.js +703 -0
  112. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  113. package/dist/core/ingestion/type-extractors/swift.js +137 -0
  114. package/dist/core/ingestion/type-extractors/types.d.ts +127 -0
  115. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  116. package/dist/core/ingestion/type-extractors/typescript.js +494 -0
  117. package/dist/core/ingestion/utils.d.ts +103 -0
  118. package/dist/core/ingestion/utils.js +1085 -4
  119. package/dist/core/ingestion/workers/parse-worker.d.ts +51 -4
  120. package/dist/core/ingestion/workers/parse-worker.js +634 -222
  121. package/dist/core/ingestion/workers/worker-pool.js +8 -0
  122. package/dist/core/{kuzu → lbug}/csv-generator.d.ts +12 -10
  123. package/dist/core/{kuzu → lbug}/csv-generator.js +82 -101
  124. package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +20 -25
  125. package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +150 -122
  126. package/dist/core/{kuzu → lbug}/schema.d.ts +4 -4
  127. package/dist/core/{kuzu → lbug}/schema.js +23 -22
  128. package/dist/core/lbug/schema.test.d.ts +1 -0
  129. package/dist/core/search/bm25-index.d.ts +4 -4
  130. package/dist/core/search/bm25-index.js +12 -11
  131. package/dist/core/search/hybrid-search.d.ts +2 -2
  132. package/dist/core/search/hybrid-search.js +6 -6
  133. package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
  134. package/dist/core/tree-sitter/parser-loader.js +19 -0
  135. package/dist/core/wiki/generator.d.ts +2 -2
  136. package/dist/core/wiki/generator.js +6 -6
  137. package/dist/core/wiki/graph-queries.d.ts +4 -4
  138. package/dist/core/wiki/graph-queries.js +7 -7
  139. package/dist/mcp/compatible-stdio-transport.d.ts +25 -0
  140. package/dist/mcp/compatible-stdio-transport.js +200 -0
  141. package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +11 -10
  142. package/dist/mcp/core/lbug-adapter.js +327 -0
  143. package/dist/mcp/local/local-backend.d.ts +21 -16
  144. package/dist/mcp/local/local-backend.js +306 -706
  145. package/dist/mcp/local/unity-parity-seed-loader.d.ts +6 -1
  146. package/dist/mcp/local/unity-parity-seed-loader.js +119 -9
  147. package/dist/mcp/local/unity-parity-seed-loader.test.js +95 -7
  148. package/dist/mcp/resources.js +2 -2
  149. package/dist/mcp/server.js +28 -13
  150. package/dist/mcp/staleness.js +2 -2
  151. package/dist/mcp/tools.js +12 -3
  152. package/dist/server/api.js +12 -12
  153. package/dist/server/mcp-http.d.ts +1 -1
  154. package/dist/server/mcp-http.js +1 -1
  155. package/dist/storage/git.js +4 -1
  156. package/dist/storage/repo-manager.d.ts +20 -2
  157. package/dist/storage/repo-manager.js +74 -4
  158. package/dist/types/pipeline.d.ts +1 -1
  159. package/hooks/claude/gitnexus-hook.cjs +149 -46
  160. package/hooks/claude/pre-tool-use.sh +2 -1
  161. package/hooks/claude/session-start.sh +0 -0
  162. package/package.json +20 -4
  163. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  164. package/skills/gitnexus-cli.md +8 -8
  165. package/skills/gitnexus-debugging.md +1 -1
  166. package/skills/gitnexus-exploring.md +1 -1
  167. package/skills/gitnexus-guide.md +1 -1
  168. package/skills/gitnexus-impact-analysis.md +1 -1
  169. package/skills/gitnexus-pr-review.md +163 -0
  170. package/skills/gitnexus-refactoring.md +1 -1
  171. package/dist/cli/claude-hooks.d.ts +0 -22
  172. package/dist/cli/claude-hooks.js +0 -97
  173. package/dist/mcp/core/kuzu-adapter.js +0 -231
  174. /package/dist/core/{kuzu/csv-generator.test.d.ts → ingestion/type-extractors/types.js} +0 -0
  175. /package/dist/core/{kuzu/relationship-pair-buckets.test.d.ts → lbug/csv-generator.test.d.ts} +0 -0
  176. /package/dist/core/{kuzu → lbug}/csv-generator.test.js +0 -0
  177. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.d.ts +0 -0
  178. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.js +0 -0
  179. /package/dist/core/{kuzu/schema.test.d.ts → lbug/relationship-pair-buckets.test.d.ts} +0 -0
  180. /package/dist/core/{kuzu → lbug}/relationship-pair-buckets.test.js +0 -0
  181. /package/dist/core/{kuzu → lbug}/schema.test.js +0 -0
@@ -1,3 +1,6 @@
1
- import { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
2
- import type { PipelineRunOptions } from '../../types/pipeline.js';
3
- export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void, options?: PipelineRunOptions) => Promise<PipelineResult>;
1
+ import { PipelineProgress, PipelineResult, type PipelineRunOptions } from '../../types/pipeline.js';
2
+ export interface PipelineOptions extends PipelineRunOptions {
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,19 +1,23 @@
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, buildImportResolutionContext } from './import-processor.js';
5
- import { processCalls, processCallsFromExtracted } from './call-processor.js';
4
+ import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
5
+ import { processCalls, processCallsFromExtracted, processRoutesFromExtracted } from './call-processor.js';
6
6
  import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
7
+ import { computeMRO } from './mro-processor.js';
7
8
  import { processCommunities } from './community-processor.js';
8
9
  import { processProcesses } from './process-processor.js';
9
10
  import { processUnityResources } from './unity-resource-processor.js';
10
- import { createSymbolTable } from './symbol-table.js';
11
+ import { createResolutionContext } from './resolution-context.js';
11
12
  import { createASTCache } from './ast-cache.js';
12
13
  import { walkRepositoryPaths, readFileContents, walkUnityResourcePaths } from './filesystem-walker.js';
13
14
  import { getLanguageFromFilename } from './utils.js';
15
+ import { isLanguageAvailable } from '../tree-sitter/parser-loader.js';
14
16
  import { createWorkerPool } from './workers/worker-pool.js';
15
17
  import { selectEntriesByScopeRules } from './scope-filter.js';
16
- import path from 'path';
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath, pathToFileURL } from 'node:url';
17
21
  const isDev = process.env.NODE_ENV === 'development';
18
22
  /** Max bytes of source content to load per parse chunk. Each chunk's source +
19
23
  * parsed ASTs + extracted records + worker serialization overhead all live in
@@ -24,12 +28,12 @@ const CHUNK_BYTE_BUDGET = 20 * 1024 * 1024; // 20MB
24
28
  const AST_CACHE_CAP = 50;
25
29
  export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
26
30
  const graph = createKnowledgeGraph();
27
- const symbolTable = createSymbolTable();
31
+ const ctx = createResolutionContext();
32
+ const symbolTable = ctx.symbols;
28
33
  let astCache = createASTCache(AST_CACHE_CAP);
29
- const importMap = createImportMap();
30
34
  const cleanup = () => {
31
35
  astCache.clear();
32
- symbolTable.clear();
36
+ ctx.clear();
33
37
  };
34
38
  try {
35
39
  // ── Phase 1: Scan paths only (no content read) ─────────────────────
@@ -89,8 +93,30 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
89
93
  // ── Phase 3+4: Chunked read + parse ────────────────────────────────
90
94
  // Group parseable files into byte-budget chunks so only ~20MB of source
91
95
  // is in memory at a time. Each chunk is: read → parse → extract → free.
92
- const parseableScanned = extensionFiltered.filter(f => getLanguageFromFilename(f.path));
96
+ const parseableScanned = extensionFiltered.filter(f => {
97
+ const lang = getLanguageFromFilename(f.path);
98
+ return lang && isLanguageAvailable(lang);
99
+ });
100
+ // Warn about files skipped due to unavailable parsers
101
+ const skippedByLang = new Map();
102
+ for (const f of extensionFiltered) {
103
+ const lang = getLanguageFromFilename(f.path);
104
+ if (lang && !isLanguageAvailable(lang)) {
105
+ skippedByLang.set(lang, (skippedByLang.get(lang) || 0) + 1);
106
+ }
107
+ }
108
+ for (const [lang, count] of skippedByLang) {
109
+ console.warn(`Skipping ${count} ${lang} file(s) — ${lang} parser not available (native binding may not have built). Try: npm rebuild tree-sitter-${lang}`);
110
+ }
93
111
  const totalParseable = parseableScanned.length;
112
+ if (totalParseable === 0) {
113
+ onProgress({
114
+ phase: 'parsing',
115
+ percent: 82,
116
+ message: 'No parseable files found — skipping parsing phase',
117
+ stats: { filesProcessed: 0, totalFiles: 0, nodesCreated: graph.nodeCount },
118
+ });
119
+ }
94
120
  // Build byte-budget chunks
95
121
  const chunks = [];
96
122
  let currentChunk = [];
@@ -117,14 +143,30 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
117
143
  message: `Parsing ${totalParseable} files in ${numChunks} chunk${numChunks !== 1 ? 's' : ''}...`,
118
144
  stats: { filesProcessed: 0, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
119
145
  });
146
+ // Don't spawn workers for tiny repos — overhead exceeds benefit
147
+ const MIN_FILES_FOR_WORKERS = 15;
148
+ const MIN_BYTES_FOR_WORKERS = 512 * 1024;
149
+ const totalBytes = parseableScanned.reduce((s, f) => s + f.size, 0);
120
150
  // Create worker pool once, reuse across chunks
121
151
  let workerPool;
122
- try {
123
- const workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
124
- workerPool = createWorkerPool(workerUrl);
125
- }
126
- catch (err) {
127
- // Worker pool creation failed — sequential fallback
152
+ if (totalParseable >= MIN_FILES_FOR_WORKERS || totalBytes >= MIN_BYTES_FOR_WORKERS) {
153
+ try {
154
+ let workerUrl = new URL('./workers/parse-worker.js', import.meta.url);
155
+ // When running under vitest, import.meta.url points to src/ where no .js exists.
156
+ // Fall back to the compiled dist/ worker so the pool can spawn real worker threads.
157
+ const thisDir = fileURLToPath(new URL('.', import.meta.url));
158
+ if (!fs.existsSync(fileURLToPath(workerUrl))) {
159
+ const distWorker = path.resolve(thisDir, '..', '..', '..', 'dist', 'core', 'ingestion', 'workers', 'parse-worker.js');
160
+ if (fs.existsSync(distWorker)) {
161
+ workerUrl = pathToFileURL(distWorker);
162
+ }
163
+ }
164
+ workerPool = createWorkerPool(workerUrl);
165
+ }
166
+ catch (err) {
167
+ if (isDev)
168
+ console.warn('Worker pool creation failed, using sequential fallback:', err.message);
169
+ }
128
170
  }
129
171
  let filesParsedSoFar = 0;
130
172
  // AST cache sized for one chunk (sequential fallback uses it for import/call/heritage)
@@ -159,20 +201,53 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
159
201
  stats: { filesProcessed: globalCurrent, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
160
202
  });
161
203
  }, workerPool);
204
+ const chunkBasePercent = 20 + ((filesParsedSoFar / totalParseable) * 62);
162
205
  if (chunkWorkerData) {
163
206
  // Imports
164
- await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, importMap, undefined, repoPath, importCtx);
165
- // Calls — resolve immediately, then free the array
166
- if (chunkWorkerData.calls.length > 0) {
167
- await processCallsFromExtracted(graph, chunkWorkerData.calls, symbolTable, importMap);
168
- }
169
- // Heritage — resolve immediately, then free
170
- if (chunkWorkerData.heritage.length > 0) {
171
- await processHeritageFromExtracted(graph, chunkWorkerData.heritage, symbolTable);
172
- }
207
+ await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, ctx, (current, total) => {
208
+ onProgress({
209
+ phase: 'parsing',
210
+ percent: Math.round(chunkBasePercent),
211
+ message: `Resolving imports (chunk ${chunkIdx + 1}/${numChunks})...`,
212
+ detail: `${current}/${total} files`,
213
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
214
+ });
215
+ }, repoPath, importCtx);
216
+ // Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
217
+ // This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
218
+ // and the single-threaded event loop prevents races between synchronous addRelationship calls.
219
+ await Promise.all([
220
+ processCallsFromExtracted(graph, chunkWorkerData.calls, ctx, (current, total) => {
221
+ onProgress({
222
+ phase: 'parsing',
223
+ percent: Math.round(chunkBasePercent),
224
+ message: `Resolving calls (chunk ${chunkIdx + 1}/${numChunks})...`,
225
+ detail: `${current}/${total} files`,
226
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
227
+ });
228
+ }, chunkWorkerData.constructorBindings),
229
+ processHeritageFromExtracted(graph, chunkWorkerData.heritage, ctx, (current, total) => {
230
+ onProgress({
231
+ phase: 'parsing',
232
+ percent: Math.round(chunkBasePercent),
233
+ message: `Resolving heritage (chunk ${chunkIdx + 1}/${numChunks})...`,
234
+ detail: `${current}/${total} records`,
235
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
236
+ });
237
+ }),
238
+ processRoutesFromExtracted(graph, chunkWorkerData.routes ?? [], ctx, (current, total) => {
239
+ onProgress({
240
+ phase: 'parsing',
241
+ percent: Math.round(chunkBasePercent),
242
+ message: `Resolving routes (chunk ${chunkIdx + 1}/${numChunks})...`,
243
+ detail: `${current}/${total} routes`,
244
+ stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
245
+ });
246
+ }),
247
+ ]);
173
248
  }
174
249
  else {
175
- await processImports(graph, chunkFiles, astCache, importMap, undefined, repoPath, allPaths);
250
+ await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
176
251
  sequentialChunkPaths.push(chunkPaths);
177
252
  }
178
253
  filesParsedSoFar += chunkFiles.length;
@@ -191,116 +266,133 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
191
266
  .filter(p => chunkContents.has(p))
192
267
  .map(p => ({ path: p, content: chunkContents.get(p) }));
193
268
  astCache = createASTCache(chunkFiles.length);
194
- await processCalls(graph, chunkFiles, astCache, symbolTable, importMap);
195
- await processHeritage(graph, chunkFiles, astCache, symbolTable);
269
+ const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx);
270
+ await processHeritage(graph, chunkFiles, astCache, ctx);
271
+ if (rubyHeritage.length > 0) {
272
+ await processHeritageFromExtracted(graph, rubyHeritage, ctx);
273
+ }
196
274
  astCache.clear();
197
275
  }
276
+ // Log resolution cache stats
277
+ if (isDev) {
278
+ const rcStats = ctx.getStats();
279
+ const total = rcStats.cacheHits + rcStats.cacheMisses;
280
+ const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0';
281
+ console.log(`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`);
282
+ }
198
283
  // Free import resolution context — suffix index + resolve cache no longer needed
199
284
  // (allPathObjects and importCtx hold ~94MB+ for large repos)
200
285
  allPathObjects.length = 0;
201
286
  importCtx.resolveCache.clear();
202
287
  importCtx.suffixIndex = null;
203
288
  importCtx.normalizedFileList = null;
204
- if (isDev) {
205
- let importsCount = 0;
206
- for (const r of graph.iterRelationships()) {
207
- if (r.type === 'IMPORTS')
208
- importsCount++;
289
+ let communityResult;
290
+ let processResult;
291
+ if (!options?.skipGraphPhases) {
292
+ // ── Phase 4.5: Method Resolution Order ──────────────────────────────
293
+ onProgress({
294
+ phase: 'parsing',
295
+ percent: 81,
296
+ message: 'Computing method resolution order...',
297
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
298
+ });
299
+ const mroResult = computeMRO(graph);
300
+ if (isDev && mroResult.entries.length > 0) {
301
+ console.log(`🔀 MRO: ${mroResult.entries.length} classes analyzed, ${mroResult.ambiguityCount} ambiguities found, ${mroResult.overrideEdges} OVERRIDES edges`);
209
302
  }
210
- console.log(`📊 Pipeline: graph has ${importsCount} IMPORTS, ${graph.relationshipCount} total relationships`);
211
- }
212
- // ── Phase 5: Communities ───────────────────────────────────────────
213
- onProgress({
214
- phase: 'communities',
215
- percent: 82,
216
- message: 'Detecting code communities...',
217
- stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
218
- });
219
- const communityResult = await processCommunities(graph, (message, progress) => {
220
- const communityProgress = 82 + (progress * 0.10);
303
+ // ── Phase 5: Communities ───────────────────────────────────────────
221
304
  onProgress({
222
305
  phase: 'communities',
223
- percent: Math.round(communityProgress),
224
- message,
306
+ percent: 82,
307
+ message: 'Detecting code communities...',
225
308
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
226
309
  });
227
- });
228
- if (isDev) {
229
- console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
230
- }
231
- communityResult.communities.forEach(comm => {
232
- graph.addNode({
233
- id: comm.id,
234
- label: 'Community',
235
- properties: {
236
- name: comm.label,
237
- filePath: '',
238
- heuristicLabel: comm.heuristicLabel,
239
- cohesion: comm.cohesion,
240
- symbolCount: comm.symbolCount,
241
- }
310
+ communityResult = await processCommunities(graph, (message, progress) => {
311
+ const communityProgress = 82 + (progress * 0.10);
312
+ onProgress({
313
+ phase: 'communities',
314
+ percent: Math.round(communityProgress),
315
+ message,
316
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
317
+ });
242
318
  });
243
- });
244
- communityResult.memberships.forEach(membership => {
245
- graph.addRelationship({
246
- id: `${membership.nodeId}_member_of_${membership.communityId}`,
247
- type: 'MEMBER_OF',
248
- sourceId: membership.nodeId,
249
- targetId: membership.communityId,
250
- confidence: 1.0,
251
- reason: 'leiden-algorithm',
319
+ if (isDev) {
320
+ console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
321
+ }
322
+ communityResult.communities.forEach(comm => {
323
+ graph.addNode({
324
+ id: comm.id,
325
+ label: 'Community',
326
+ properties: {
327
+ name: comm.label,
328
+ filePath: '',
329
+ heuristicLabel: comm.heuristicLabel,
330
+ cohesion: comm.cohesion,
331
+ symbolCount: comm.symbolCount,
332
+ }
333
+ });
252
334
  });
253
- });
254
- // ── Phase 6: Processes ─────────────────────────────────────────────
255
- onProgress({
256
- phase: 'processes',
257
- percent: 94,
258
- message: 'Detecting execution flows...',
259
- stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
260
- });
261
- let symbolCount = 0;
262
- graph.forEachNode(n => { if (n.label !== 'File')
263
- symbolCount++; });
264
- const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
265
- const processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
266
- const processProgress = 94 + (progress * 0.05);
335
+ communityResult.memberships.forEach(membership => {
336
+ graph.addRelationship({
337
+ id: `${membership.nodeId}_member_of_${membership.communityId}`,
338
+ type: 'MEMBER_OF',
339
+ sourceId: membership.nodeId,
340
+ targetId: membership.communityId,
341
+ confidence: 1.0,
342
+ reason: 'leiden-algorithm',
343
+ });
344
+ });
345
+ // ── Phase 6: Processes ─────────────────────────────────────────────
267
346
  onProgress({
268
347
  phase: 'processes',
269
- percent: Math.round(processProgress),
270
- message,
348
+ percent: 94,
349
+ message: 'Detecting execution flows...',
271
350
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
272
351
  });
273
- }, { maxProcesses: dynamicMaxProcesses, minSteps: 3 });
274
- if (isDev) {
275
- console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
276
- }
277
- processResult.processes.forEach(proc => {
278
- graph.addNode({
279
- id: proc.id,
280
- label: 'Process',
281
- properties: {
282
- name: proc.label,
283
- filePath: '',
284
- heuristicLabel: proc.heuristicLabel,
285
- processType: proc.processType,
286
- stepCount: proc.stepCount,
287
- communities: proc.communities,
288
- entryPointId: proc.entryPointId,
289
- terminalId: proc.terminalId,
290
- }
352
+ let symbolCount = 0;
353
+ graph.forEachNode(n => { if (n.label !== 'File')
354
+ symbolCount++; });
355
+ const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
356
+ processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
357
+ const processProgress = 94 + (progress * 0.05);
358
+ onProgress({
359
+ phase: 'processes',
360
+ percent: Math.round(processProgress),
361
+ message,
362
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
363
+ });
364
+ }, { maxProcesses: dynamicMaxProcesses, minSteps: 3 });
365
+ if (isDev) {
366
+ console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
367
+ }
368
+ processResult.processes.forEach(proc => {
369
+ graph.addNode({
370
+ id: proc.id,
371
+ label: 'Process',
372
+ properties: {
373
+ name: proc.label,
374
+ filePath: '',
375
+ heuristicLabel: proc.heuristicLabel,
376
+ processType: proc.processType,
377
+ stepCount: proc.stepCount,
378
+ communities: proc.communities,
379
+ entryPointId: proc.entryPointId,
380
+ terminalId: proc.terminalId,
381
+ }
382
+ });
291
383
  });
292
- });
293
- processResult.steps.forEach(step => {
294
- graph.addRelationship({
295
- id: `${step.nodeId}_step_${step.step}_${step.processId}`,
296
- type: 'STEP_IN_PROCESS',
297
- sourceId: step.nodeId,
298
- targetId: step.processId,
299
- confidence: 1.0,
300
- reason: 'trace-detection',
301
- step: step.step,
384
+ processResult.steps.forEach(step => {
385
+ graph.addRelationship({
386
+ id: `${step.nodeId}_step_${step.step}_${step.processId}`,
387
+ type: 'STEP_IN_PROCESS',
388
+ sourceId: step.nodeId,
389
+ targetId: step.processId,
390
+ confidence: 1.0,
391
+ reason: 'trace-detection',
392
+ step: step.step,
393
+ });
302
394
  });
303
- });
395
+ }
304
396
  onProgress({
305
397
  phase: 'enriching',
306
398
  percent: 99,
@@ -311,7 +403,9 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
311
403
  onProgress({
312
404
  phase: 'complete',
313
405
  percent: 100,
314
- message: `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`,
406
+ message: communityResult && processResult
407
+ ? `Graph complete! ${communityResult.stats.totalCommunities} communities, ${processResult.stats.totalProcesses} processes detected.`
408
+ : 'Graph complete! (graph phases skipped)',
315
409
  stats: {
316
410
  filesProcessed: totalFiles,
317
411
  totalFiles,
@@ -10,6 +10,7 @@
10
10
  * Processes help agents understand how features work through the codebase.
11
11
  */
12
12
  import { calculateEntryPointScore, isTestFile } from './entry-point-scoring.js';
13
+ import { SupportedLanguages } from '../../config/supported-languages.js';
13
14
  const isDev = process.env.NODE_ENV === 'development';
14
15
  const DEFAULT_CONFIG = {
15
16
  maxTraceDepth: 10,
@@ -178,8 +179,14 @@ const findEntryPoints = (graph, reverseCallsEdges, callsEdges) => {
178
179
  if (callees.length === 0)
179
180
  continue;
180
181
  // Calculate entry point score using new scoring system
181
- const { score, reasons } = calculateEntryPointScore(node.properties.name, node.properties.language || 'javascript', node.properties.isExported ?? false, callers.length, callees.length, filePath // Pass filePath for framework detection
182
+ const { score: baseScore, reasons } = calculateEntryPointScore(node.properties.name, node.properties.language ?? SupportedLanguages.JavaScript, node.properties.isExported ?? false, callers.length, callees.length, filePath // Pass filePath for framework detection
182
183
  );
184
+ let score = baseScore;
185
+ const astFrameworkMultiplier = node.properties.astFrameworkMultiplier ?? 1.0;
186
+ if (astFrameworkMultiplier > 1.0) {
187
+ score *= astFrameworkMultiplier;
188
+ reasons.push(`framework-ast:${node.properties.astFrameworkReason || 'decorator'}`);
189
+ }
183
190
  if (score > 0) {
184
191
  entryPointCandidates.push({ id: node.id, score, reasons });
185
192
  }
@@ -213,7 +220,6 @@ const traceFromEntryPoint = (entryId, callsEdges, config) => {
213
220
  // BFS with path tracking
214
221
  // Each queue item: [currentNodeId, pathSoFar]
215
222
  const queue = [[entryId, [entryId]]];
216
- const visited = new Set();
217
223
  while (queue.length > 0 && traces.length < config.maxBranching * 3) {
218
224
  const [currentId, path] = queue.shift();
219
225
  // Get outgoing calls
@@ -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;
@@ -0,0 +1,132 @@
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 { createSymbolTable } from './symbol-table.js';
16
+ import { isFileInPackageDir } from './import-processor.js';
17
+ import { walkBindingChain } from './named-binding-extraction.js';
18
+ /** Confidence scores per resolution tier. */
19
+ export const TIER_CONFIDENCE = {
20
+ 'same-file': 0.95,
21
+ 'import-scoped': 0.9,
22
+ 'global': 0.5,
23
+ };
24
+ export const createResolutionContext = () => {
25
+ const symbols = createSymbolTable();
26
+ const importMap = new Map();
27
+ const packageMap = new Map();
28
+ const namedImportMap = new Map();
29
+ // Per-file cache state
30
+ let cacheFile = null;
31
+ let cache = null;
32
+ let cacheHits = 0;
33
+ let cacheMisses = 0;
34
+ // --- Core resolution (single implementation of tier logic) ---
35
+ const resolveUncached = (name, fromFile) => {
36
+ // Tier 1: Same file — authoritative match
37
+ const localDef = symbols.lookupExactFull(fromFile, name);
38
+ if (localDef) {
39
+ return { candidates: [localDef], tier: 'same-file' };
40
+ }
41
+ // Get all global definitions for subsequent tiers
42
+ const allDefs = symbols.lookupFuzzy(name);
43
+ // Tier 2a-named: Check named bindings BEFORE empty-allDefs early return
44
+ // because aliased imports mean lookupFuzzy('U') returns empty but we
45
+ // can resolve via the exported name.
46
+ const chainResult = walkBindingChain(name, fromFile, symbols, namedImportMap, allDefs);
47
+ if (chainResult && chainResult.length > 0) {
48
+ return { candidates: chainResult, tier: 'import-scoped' };
49
+ }
50
+ if (allDefs.length === 0)
51
+ return null;
52
+ // Tier 2a: Import-scoped — definition in a file imported by fromFile
53
+ const importedFiles = importMap.get(fromFile);
54
+ if (importedFiles) {
55
+ const importedDefs = allDefs.filter(def => importedFiles.has(def.filePath));
56
+ if (importedDefs.length > 0) {
57
+ return { candidates: importedDefs, tier: 'import-scoped' };
58
+ }
59
+ }
60
+ // Tier 2b: Package-scoped — definition in a package dir imported by fromFile
61
+ const importedPackages = packageMap.get(fromFile);
62
+ if (importedPackages) {
63
+ const packageDefs = allDefs.filter(def => {
64
+ for (const dirSuffix of importedPackages) {
65
+ if (isFileInPackageDir(def.filePath, dirSuffix))
66
+ return true;
67
+ }
68
+ return false;
69
+ });
70
+ if (packageDefs.length > 0) {
71
+ return { candidates: packageDefs, tier: 'import-scoped' };
72
+ }
73
+ }
74
+ // Tier 3: Global — pass all candidates through.
75
+ // Consumers must check candidate count and refuse ambiguous matches.
76
+ return { candidates: allDefs, tier: 'global' };
77
+ };
78
+ const resolve = (name, fromFile) => {
79
+ // Check cache (only when enabled AND fromFile matches cached file)
80
+ if (cache && cacheFile === fromFile) {
81
+ if (cache.has(name)) {
82
+ cacheHits++;
83
+ return cache.get(name);
84
+ }
85
+ cacheMisses++;
86
+ }
87
+ const result = resolveUncached(name, fromFile);
88
+ // Store in cache if active and file matches
89
+ if (cache && cacheFile === fromFile) {
90
+ cache.set(name, result);
91
+ }
92
+ return result;
93
+ };
94
+ // --- Cache lifecycle ---
95
+ const enableCache = (filePath) => {
96
+ cacheFile = filePath;
97
+ if (!cache)
98
+ cache = new Map();
99
+ else
100
+ cache.clear();
101
+ };
102
+ const clearCache = () => {
103
+ cacheFile = null;
104
+ // Reuse the Map instance — just clear entries to reduce GC pressure at scale.
105
+ cache?.clear();
106
+ };
107
+ const getStats = () => ({
108
+ ...symbols.getStats(),
109
+ cacheHits,
110
+ cacheMisses,
111
+ });
112
+ const clear = () => {
113
+ symbols.clear();
114
+ importMap.clear();
115
+ packageMap.clear();
116
+ namedImportMap.clear();
117
+ clearCache();
118
+ cacheHits = 0;
119
+ cacheMisses = 0;
120
+ };
121
+ return {
122
+ resolve,
123
+ symbols,
124
+ importMap,
125
+ packageMap,
126
+ namedImportMap,
127
+ enableCache,
128
+ clearCache,
129
+ getStats,
130
+ clear,
131
+ };
132
+ };