gitnexus 1.4.7 → 1.4.8

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 (92) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +2 -1
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +48 -1
  24. package/dist/core/ingestion/call-processor.js +368 -7
  25. package/dist/core/ingestion/call-routing.d.ts +6 -0
  26. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  27. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  28. package/dist/core/ingestion/framework-detection.js +49 -12
  29. package/dist/core/ingestion/heritage-processor.js +47 -49
  30. package/dist/core/ingestion/import-processor.d.ts +1 -1
  31. package/dist/core/ingestion/import-processor.js +103 -194
  32. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  33. package/dist/core/ingestion/import-resolution.js +251 -0
  34. package/dist/core/ingestion/language-config.d.ts +3 -0
  35. package/dist/core/ingestion/language-config.js +13 -0
  36. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  37. package/dist/core/ingestion/markdown-processor.js +124 -0
  38. package/dist/core/ingestion/mro-processor.js +8 -3
  39. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  40. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  41. package/dist/core/ingestion/parsing-processor.d.ts +2 -2
  42. package/dist/core/ingestion/parsing-processor.js +14 -73
  43. package/dist/core/ingestion/pipeline.d.ts +10 -0
  44. package/dist/core/ingestion/pipeline.js +421 -4
  45. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  46. package/dist/core/ingestion/resolution-context.js +7 -4
  47. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  48. package/dist/core/ingestion/resolvers/index.js +1 -1
  49. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  50. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  51. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  52. package/dist/core/ingestion/resolvers/php.js +43 -3
  53. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  54. package/dist/core/ingestion/resolvers/utils.js +16 -0
  55. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  56. package/dist/core/ingestion/symbol-table.js +20 -6
  57. package/dist/core/ingestion/tree-sitter-queries.d.ts +4 -4
  58. package/dist/core/ingestion/tree-sitter-queries.js +43 -2
  59. package/dist/core/ingestion/type-env.d.ts +28 -1
  60. package/dist/core/ingestion/type-env.js +419 -96
  61. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  62. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  63. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  64. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  65. package/dist/core/ingestion/type-extractors/index.js +1 -1
  66. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  67. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  68. package/dist/core/ingestion/type-extractors/shared.d.ts +0 -2
  69. package/dist/core/ingestion/type-extractors/shared.js +5 -10
  70. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  71. package/dist/core/ingestion/type-extractors/types.d.ts +37 -7
  72. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  73. package/dist/core/ingestion/utils.d.ts +2 -120
  74. package/dist/core/ingestion/utils.js +3 -1051
  75. package/dist/core/ingestion/workers/parse-worker.d.ts +13 -4
  76. package/dist/core/ingestion/workers/parse-worker.js +66 -87
  77. package/dist/core/lbug/csv-generator.js +18 -1
  78. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  79. package/dist/core/lbug/lbug-adapter.js +69 -4
  80. package/dist/core/lbug/schema.d.ts +5 -3
  81. package/dist/core/lbug/schema.js +26 -2
  82. package/dist/mcp/core/embedder.js +11 -3
  83. package/dist/mcp/core/lbug-adapter.js +12 -1
  84. package/dist/mcp/local/local-backend.d.ts +22 -0
  85. package/dist/mcp/local/local-backend.js +133 -29
  86. package/dist/mcp/resources.js +2 -0
  87. package/dist/mcp/tools.js +2 -2
  88. package/dist/server/api.d.ts +19 -1
  89. package/dist/server/api.js +66 -6
  90. package/dist/storage/git.d.ts +12 -0
  91. package/dist/storage/git.js +21 -0
  92. package/package.json +10 -2
@@ -1,8 +1,10 @@
1
1
  import { createKnowledgeGraph } from '../graph/graph.js';
2
2
  import { processStructure } from './structure-processor.js';
3
+ import { processMarkdown } from './markdown-processor.js';
3
4
  import { processParsing } from './parsing-processor.js';
4
5
  import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
5
- import { processCalls, processCallsFromExtracted, processAssignmentsFromExtracted, processRoutesFromExtracted } from './call-processor.js';
6
+ import { EMPTY_INDEX } from './resolvers/index.js';
7
+ import { processCalls, processCallsFromExtracted, processAssignmentsFromExtracted, processRoutesFromExtracted, seedCrossFileReceiverTypes, buildImportedReturnTypes, buildImportedRawReturnTypes, buildExportedTypeMapFromGraph } from './call-processor.js';
6
8
  import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
7
9
  import { computeMRO } from './mro-processor.js';
8
10
  import { processCommunities } from './community-processor.js';
@@ -12,11 +14,63 @@ import { createASTCache } from './ast-cache.js';
12
14
  import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
13
15
  import { getLanguageFromFilename } from './utils.js';
14
16
  import { isLanguageAvailable } from '../tree-sitter/parser-loader.js';
17
+ import { SupportedLanguages } from '../../config/supported-languages.js';
15
18
  import { createWorkerPool } from './workers/worker-pool.js';
16
19
  import fs from 'node:fs';
17
20
  import path from 'node:path';
18
21
  import { fileURLToPath, pathToFileURL } from 'node:url';
19
22
  const isDev = process.env.NODE_ENV === 'development';
23
+ /** Kahn's algorithm: returns files grouped by topological level.
24
+ * Files in the same level have no mutual dependencies — safe to process in parallel.
25
+ * Files in cycles are returned as a final group (no cross-cycle propagation). */
26
+ export function topologicalLevelSort(importMap) {
27
+ // Build in-degree map and reverse dependency map
28
+ const inDegree = new Map();
29
+ const reverseDeps = new Map();
30
+ for (const [file, deps] of importMap) {
31
+ if (!inDegree.has(file))
32
+ inDegree.set(file, 0);
33
+ for (const dep of deps) {
34
+ if (!inDegree.has(dep))
35
+ inDegree.set(dep, 0);
36
+ // file imports dep, so dep must be processed before file
37
+ // In Kahn's terms: dep → file (dep is a prerequisite of file)
38
+ inDegree.set(file, (inDegree.get(file) ?? 0) + 1);
39
+ let rev = reverseDeps.get(dep);
40
+ if (!rev) {
41
+ rev = [];
42
+ reverseDeps.set(dep, rev);
43
+ }
44
+ rev.push(file);
45
+ }
46
+ }
47
+ // BFS from zero-in-degree nodes, grouping by level
48
+ const levels = [];
49
+ let currentLevel = [...inDegree.entries()]
50
+ .filter(([, d]) => d === 0)
51
+ .map(([f]) => f);
52
+ while (currentLevel.length > 0) {
53
+ levels.push(currentLevel);
54
+ const nextLevel = [];
55
+ for (const file of currentLevel) {
56
+ for (const dependent of reverseDeps.get(file) ?? []) {
57
+ const newDeg = (inDegree.get(dependent) ?? 1) - 1;
58
+ inDegree.set(dependent, newDeg);
59
+ if (newDeg === 0)
60
+ nextLevel.push(dependent);
61
+ }
62
+ }
63
+ currentLevel = nextLevel;
64
+ }
65
+ // Files still with positive in-degree are in cycles — add as final group
66
+ const cycleFiles = [...inDegree.entries()]
67
+ .filter(([, d]) => d > 0)
68
+ .map(([f]) => f);
69
+ if (cycleFiles.length > 0) {
70
+ levels.push(cycleFiles);
71
+ }
72
+ return { levels, cycleCount: cycleFiles.length };
73
+ }
20
74
  /** Max bytes of source content to load per parse chunk. Each chunk's source +
21
75
  * parsed ASTs + extracted records + worker serialization overhead all live in
22
76
  * memory simultaneously, so this must be conservative. 20MB source ≈ 200-400MB
@@ -24,11 +78,286 @@ const isDev = process.env.NODE_ENV === 'development';
24
78
  const CHUNK_BYTE_BUDGET = 20 * 1024 * 1024; // 20MB
25
79
  /** Max AST trees to keep in LRU cache */
26
80
  const AST_CACHE_CAP = 50;
81
+ /** Minimum percentage of files that must benefit from cross-file seeding to justify the re-resolution pass. */
82
+ const CROSS_FILE_SKIP_THRESHOLD = 0.03;
83
+ /** Hard cap on files re-processed during cross-file propagation. */
84
+ const MAX_CROSS_FILE_REPROCESS = 2000;
85
+ /** Node labels that represent top-level importable symbols.
86
+ * Excludes Method, Property, Constructor (accessed via receiver, not directly imported),
87
+ * and structural labels (File, Folder, Package, Module, Project, etc.). */
88
+ const IMPORTABLE_SYMBOL_LABELS = new Set([
89
+ 'Function', 'Class', 'Interface', 'Struct', 'Enum', 'Trait',
90
+ 'TypeAlias', 'Const', 'Static', 'Record', 'Union', 'Typedef', 'Macro',
91
+ ]);
92
+ /** Max synthetic bindings per importing file — prevents memory bloat for
93
+ * C/C++ files that include many large headers. */
94
+ const MAX_SYNTHETIC_BINDINGS_PER_FILE = 1000;
95
+ /** Languages with whole-module import semantics (no per-symbol named imports).
96
+ * For these languages, namedImportMap entries are synthesized from graph-exported
97
+ * symbols after parsing, enabling Phase 14 cross-file binding propagation.
98
+ *
99
+ * Note: Python is intentionally excluded here. `import models` is a namespace import
100
+ * (not wildcard symbol expansion) — expanding all exported symbols produces ambiguous
101
+ * bindings when multiple modules export the same name (e.g. models.User vs auth.User).
102
+ * Python module aliases are built in synthesizeWildcardImportBindings via moduleAliasMap. */
103
+ const WILDCARD_IMPORT_LANGUAGES = new Set([
104
+ SupportedLanguages.Go,
105
+ SupportedLanguages.Ruby,
106
+ SupportedLanguages.C,
107
+ SupportedLanguages.CPlusPlus,
108
+ SupportedLanguages.Swift,
109
+ ]);
110
+ /** Languages that require synthesizeWildcardImportBindings to run before call resolution.
111
+ * Superset of WILDCARD_IMPORT_LANGUAGES — includes Python for moduleAliasMap building. */
112
+ const SYNTHESIS_LANGUAGES = new Set([...WILDCARD_IMPORT_LANGUAGES, SupportedLanguages.Python]);
113
+ /** Synthesize namedImportMap entries for languages with whole-module imports.
114
+ * These languages (Go, Ruby, C/C++, Swift, Python) import all exported symbols from a
115
+ * file, not specific named symbols. After parsing, we know which symbols each file
116
+ * exports (via graph isExported), so we can expand ImportMap edges into per-symbol
117
+ * bindings that Phase 14 can use for cross-file type propagation. */
118
+ function synthesizeWildcardImportBindings(graph, ctx) {
119
+ // Pre-compute exported symbols per file from graph (single pass)
120
+ const exportedSymbolsByFile = new Map();
121
+ graph.forEachNode(node => {
122
+ if (!node.properties?.isExported)
123
+ return;
124
+ if (!IMPORTABLE_SYMBOL_LABELS.has(node.label))
125
+ return;
126
+ const fp = node.properties.filePath;
127
+ const name = node.properties.name;
128
+ if (!fp || !name)
129
+ return;
130
+ let symbols = exportedSymbolsByFile.get(fp);
131
+ if (!symbols) {
132
+ symbols = [];
133
+ exportedSymbolsByFile.set(fp, symbols);
134
+ }
135
+ symbols.push({ name, filePath: fp });
136
+ });
137
+ if (exportedSymbolsByFile.size === 0)
138
+ return 0;
139
+ // Build a merged import map: ctx.importMap has file-based imports (Ruby, C/C++),
140
+ // but Go/C# package imports use graph IMPORTS edges + PackageMap instead.
141
+ // Collect graph-level IMPORTS edges for wildcard languages missing from ctx.importMap.
142
+ const FILE_PREFIX = 'File:';
143
+ const graphImports = new Map();
144
+ graph.forEachRelationship(rel => {
145
+ if (rel.type !== 'IMPORTS')
146
+ return;
147
+ if (!rel.sourceId.startsWith(FILE_PREFIX) || !rel.targetId.startsWith(FILE_PREFIX))
148
+ return;
149
+ const srcFile = rel.sourceId.slice(FILE_PREFIX.length);
150
+ const tgtFile = rel.targetId.slice(FILE_PREFIX.length);
151
+ const lang = getLanguageFromFilename(srcFile);
152
+ if (!lang || !WILDCARD_IMPORT_LANGUAGES.has(lang))
153
+ return;
154
+ // Only add if not already in ctx.importMap (avoid duplicates)
155
+ if (ctx.importMap.get(srcFile)?.has(tgtFile))
156
+ return;
157
+ let set = graphImports.get(srcFile);
158
+ if (!set) {
159
+ set = new Set();
160
+ graphImports.set(srcFile, set);
161
+ }
162
+ set.add(tgtFile);
163
+ });
164
+ let totalSynthesized = 0;
165
+ // Helper: synthesize bindings for a file given its imported files
166
+ const synthesizeForFile = (filePath, importedFiles) => {
167
+ let fileBindings = ctx.namedImportMap.get(filePath);
168
+ let fileCount = fileBindings?.size ?? 0;
169
+ for (const importedFile of importedFiles) {
170
+ const exportedSymbols = exportedSymbolsByFile.get(importedFile);
171
+ if (!exportedSymbols)
172
+ continue;
173
+ for (const sym of exportedSymbols) {
174
+ if (fileCount >= MAX_SYNTHETIC_BINDINGS_PER_FILE)
175
+ return;
176
+ if (fileBindings?.has(sym.name))
177
+ continue;
178
+ if (!fileBindings) {
179
+ fileBindings = new Map();
180
+ ctx.namedImportMap.set(filePath, fileBindings);
181
+ }
182
+ fileBindings.set(sym.name, {
183
+ sourcePath: importedFile,
184
+ exportedName: sym.name,
185
+ });
186
+ fileCount++;
187
+ totalSynthesized++;
188
+ }
189
+ }
190
+ };
191
+ // Process files from ctx.importMap (Ruby, C/C++, Swift file-based imports)
192
+ for (const [filePath, importedFiles] of ctx.importMap) {
193
+ const lang = getLanguageFromFilename(filePath);
194
+ if (!lang || !WILDCARD_IMPORT_LANGUAGES.has(lang))
195
+ continue;
196
+ synthesizeForFile(filePath, importedFiles);
197
+ }
198
+ // Process files from graph IMPORTS edges (Go and other wildcard-import languages)
199
+ for (const [filePath, importedFiles] of graphImports) {
200
+ synthesizeForFile(filePath, importedFiles);
201
+ }
202
+ // Build module alias map for Python namespace imports.
203
+ // `import models` in app.py → ctx.moduleAliasMap['app.py']['models'] = 'models.py'
204
+ // Enables `models.User()` to resolve to models.py:User without ambiguous symbol expansion.
205
+ const buildPythonModuleAliasForFile = (callerFile, importedFiles) => {
206
+ let aliasMap = ctx.moduleAliasMap.get(callerFile);
207
+ for (const importedFile of importedFiles) {
208
+ // Derive the module alias from the imported filename stem (e.g. "models.py" → "models")
209
+ const lastSlash = importedFile.lastIndexOf('/');
210
+ const base = lastSlash >= 0 ? importedFile.slice(lastSlash + 1) : importedFile;
211
+ const dot = base.lastIndexOf('.');
212
+ const stem = dot >= 0 ? base.slice(0, dot) : base;
213
+ if (!stem)
214
+ continue;
215
+ if (!aliasMap) {
216
+ aliasMap = new Map();
217
+ ctx.moduleAliasMap.set(callerFile, aliasMap);
218
+ }
219
+ aliasMap.set(stem, importedFile);
220
+ }
221
+ };
222
+ for (const [filePath, importedFiles] of ctx.importMap) {
223
+ if (getLanguageFromFilename(filePath) !== SupportedLanguages.Python)
224
+ continue;
225
+ buildPythonModuleAliasForFile(filePath, importedFiles);
226
+ }
227
+ return totalSynthesized;
228
+ }
229
+ /** Phase 14: Cross-file binding propagation.
230
+ * Seeds downstream files with resolved type bindings from upstream exports.
231
+ * Files are processed in topological import order so upstream bindings are
232
+ * available when downstream files are re-resolved. */
233
+ async function runCrossFileBindingPropagation(graph, ctx, exportedTypeMap, allPaths, totalFiles, repoPath, pipelineStart, onProgress) {
234
+ // For the worker path, buildTypeEnv runs inside workers without SymbolTable,
235
+ // so exported bindings must be collected from graph + SymbolTable in main thread.
236
+ if (exportedTypeMap.size === 0 && graph.nodeCount > 0) {
237
+ const graphExports = buildExportedTypeMapFromGraph(graph, ctx.symbols);
238
+ for (const [fp, exports] of graphExports)
239
+ exportedTypeMap.set(fp, exports);
240
+ }
241
+ if (exportedTypeMap.size === 0 || ctx.namedImportMap.size === 0)
242
+ return;
243
+ const allPathSet = new Set(allPaths);
244
+ const { levels, cycleCount } = topologicalLevelSort(ctx.importMap);
245
+ // Cycle diagnostic: only log when actual cycles detected (cycleCount from Kahn's BFS)
246
+ if (isDev && cycleCount > 0) {
247
+ console.log(`🔄 ${cycleCount} files in import cycles (skipped for cross-file propagation)`);
248
+ }
249
+ // Quick count of files with cross-file binding gaps (early exit once threshold exceeded)
250
+ let filesWithGaps = 0;
251
+ const gapThreshold = Math.max(1, Math.ceil(totalFiles * CROSS_FILE_SKIP_THRESHOLD));
252
+ outer: for (const level of levels) {
253
+ for (const filePath of level) {
254
+ const imports = ctx.namedImportMap.get(filePath);
255
+ if (!imports)
256
+ continue;
257
+ for (const [, binding] of imports) {
258
+ const upstream = exportedTypeMap.get(binding.sourcePath);
259
+ if (upstream?.has(binding.exportedName)) {
260
+ filesWithGaps++;
261
+ break;
262
+ }
263
+ const def = ctx.symbols.lookupExactFull(binding.sourcePath, binding.exportedName);
264
+ if (def?.returnType) {
265
+ filesWithGaps++;
266
+ break;
267
+ }
268
+ }
269
+ if (filesWithGaps >= gapThreshold)
270
+ break outer;
271
+ }
272
+ }
273
+ const gapRatio = totalFiles > 0 ? filesWithGaps / totalFiles : 0;
274
+ if (gapRatio < CROSS_FILE_SKIP_THRESHOLD && filesWithGaps < gapThreshold) {
275
+ if (isDev) {
276
+ console.log(`⏭️ Cross-file re-resolution skipped (${filesWithGaps}/${totalFiles} files, ${(gapRatio * 100).toFixed(1)}% < ${CROSS_FILE_SKIP_THRESHOLD * 100}% threshold)`);
277
+ }
278
+ return;
279
+ }
280
+ onProgress({
281
+ phase: 'parsing',
282
+ percent: 82,
283
+ message: `Cross-file type propagation (${filesWithGaps}+ files)...`,
284
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
285
+ });
286
+ let crossFileResolved = 0;
287
+ const crossFileStart = Date.now();
288
+ let astCache = createASTCache(AST_CACHE_CAP);
289
+ for (const level of levels) {
290
+ const levelCandidates = [];
291
+ for (const filePath of level) {
292
+ if (crossFileResolved + levelCandidates.length >= MAX_CROSS_FILE_REPROCESS)
293
+ break;
294
+ const imports = ctx.namedImportMap.get(filePath);
295
+ if (!imports)
296
+ continue;
297
+ const seeded = new Map();
298
+ for (const [localName, binding] of imports) {
299
+ const upstream = exportedTypeMap.get(binding.sourcePath);
300
+ if (upstream) {
301
+ const type = upstream.get(binding.exportedName);
302
+ if (type)
303
+ seeded.set(localName, type);
304
+ }
305
+ }
306
+ const importedReturns = buildImportedReturnTypes(filePath, ctx.namedImportMap, ctx.symbols);
307
+ const importedRawReturns = buildImportedRawReturnTypes(filePath, ctx.namedImportMap, ctx.symbols);
308
+ if (seeded.size === 0 && importedReturns.size === 0)
309
+ continue;
310
+ if (!allPathSet.has(filePath))
311
+ continue;
312
+ const lang = getLanguageFromFilename(filePath);
313
+ if (!lang || !isLanguageAvailable(lang))
314
+ continue;
315
+ levelCandidates.push({ filePath, seeded, importedReturns, importedRawReturns });
316
+ }
317
+ if (levelCandidates.length === 0)
318
+ continue;
319
+ const levelPaths = levelCandidates.map(c => c.filePath);
320
+ const contentMap = await readFileContents(repoPath, levelPaths);
321
+ for (const { filePath, seeded, importedReturns, importedRawReturns } of levelCandidates) {
322
+ const content = contentMap.get(filePath);
323
+ if (!content)
324
+ continue;
325
+ const reFile = [{ path: filePath, content }];
326
+ const bindings = new Map();
327
+ if (seeded.size > 0)
328
+ bindings.set(filePath, seeded);
329
+ const importedReturnTypesMap = new Map();
330
+ if (importedReturns.size > 0) {
331
+ importedReturnTypesMap.set(filePath, importedReturns);
332
+ }
333
+ const importedRawReturnTypesMap = new Map();
334
+ if (importedRawReturns.size > 0) {
335
+ importedRawReturnTypesMap.set(filePath, importedRawReturns);
336
+ }
337
+ await processCalls(graph, reFile, astCache, ctx, undefined, exportedTypeMap, bindings.size > 0 ? bindings : undefined, importedReturnTypesMap.size > 0 ? importedReturnTypesMap : undefined, importedRawReturnTypesMap.size > 0 ? importedRawReturnTypesMap : undefined);
338
+ crossFileResolved++;
339
+ }
340
+ if (crossFileResolved >= MAX_CROSS_FILE_REPROCESS) {
341
+ if (isDev)
342
+ console.log(`⚠️ Cross-file re-resolution capped at ${MAX_CROSS_FILE_REPROCESS} files`);
343
+ break;
344
+ }
345
+ }
346
+ astCache.clear();
347
+ if (isDev) {
348
+ const elapsed = Date.now() - crossFileStart;
349
+ const totalElapsed = Date.now() - pipelineStart;
350
+ const reResolutionPct = totalElapsed > 0 ? ((elapsed / totalElapsed) * 100).toFixed(1) : '0';
351
+ console.log(`🔗 Cross-file re-resolution: ${crossFileResolved} candidates re-processed` +
352
+ ` in ${elapsed}ms (${reResolutionPct}% of total ingestion time so far)`);
353
+ }
354
+ }
27
355
  export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
28
356
  const graph = createKnowledgeGraph();
29
357
  const ctx = createResolutionContext();
30
358
  const symbolTable = ctx.symbols;
31
359
  let astCache = createASTCache(AST_CACHE_CAP);
360
+ const pipelineStart = Date.now();
32
361
  const cleanup = () => {
33
362
  astCache.clear();
34
363
  ctx.clear();
@@ -72,6 +401,19 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
72
401
  message: 'Project structure analyzed',
73
402
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
74
403
  });
404
+ // ── Phase 2.5: Markdown processing (headings + cross-links) ────────
405
+ const mdScanned = scannedFiles.filter(f => f.path.endsWith('.md') || f.path.endsWith('.mdx'));
406
+ if (mdScanned.length > 0) {
407
+ const mdContents = await readFileContents(repoPath, mdScanned.map(f => f.path));
408
+ const mdFiles = mdScanned
409
+ .filter(f => mdContents.has(f.path))
410
+ .map(f => ({ path: f.path, content: mdContents.get(f.path) }));
411
+ const allPathSet = new Set(allPaths);
412
+ const mdResult = processMarkdown(graph, mdFiles, allPathSet);
413
+ if (isDev) {
414
+ console.log(` Markdown: ${mdResult.sections} sections, ${mdResult.links} cross-links from ${mdFiles.length} files`);
415
+ }
416
+ }
75
417
  // ── Phase 3+4: Chunked read + parse ────────────────────────────────
76
418
  // Group parseable files into byte-budget chunks so only ~20MB of source
77
419
  // is in memory at a time. Each chunk is: read → parse → extract → free.
@@ -163,6 +505,15 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
163
505
  // are already registered). This trades ~5% cross-chunk resolution accuracy for
164
506
  // 200-400MB less memory — critical for Linux-kernel-scale repos.
165
507
  const sequentialChunkPaths = [];
508
+ // Pre-compute which chunks need synthesis — O(1) lookup per chunk.
509
+ const chunkNeedsSynthesis = chunks.map(paths => paths.some(p => {
510
+ const lang = getLanguageFromFilename(p);
511
+ return lang != null && SYNTHESIS_LANGUAGES.has(lang);
512
+ }));
513
+ // Phase 14: Collect exported type bindings for cross-file propagation
514
+ const exportedTypeMap = new Map();
515
+ // Accumulate file-scope TypeEnv bindings from workers (closes worker/sequential quality gap)
516
+ const workerTypeEnvBindings = [];
166
517
  try {
167
518
  for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
168
519
  const chunkPaths = chunks[chunkIdx];
@@ -195,6 +546,23 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
195
546
  stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
196
547
  });
197
548
  }, repoPath, importCtx);
549
+ // ── Wildcard-import synthesis (Ruby / C/C++ / Swift / Go) + Python module aliases ─
550
+ // Synthesize namedImportMap entries for wildcard-import languages and build
551
+ // moduleAliasMap for Python namespace imports. Must run after imports are resolved
552
+ // (importMap is populated) but BEFORE call resolution.
553
+ if (chunkNeedsSynthesis[chunkIdx])
554
+ synthesizeWildcardImportBindings(graph, ctx);
555
+ // Phase 14 E1: Seed cross-file receiver types from ExportedTypeMap
556
+ // before call resolution — eliminates re-parse for single-hop imported receivers.
557
+ // NOTE: In the worker path, exportedTypeMap is empty during chunk processing
558
+ // (populated later in runCrossFileBindingPropagation). This block is latent —
559
+ // it activates only if incremental export collection is added per-chunk.
560
+ if (exportedTypeMap.size > 0 && ctx.namedImportMap.size > 0) {
561
+ const { enrichedCount } = seedCrossFileReceiverTypes(chunkWorkerData.calls, ctx.namedImportMap, exportedTypeMap);
562
+ if (isDev && enrichedCount > 0) {
563
+ console.log(`🔗 E1: Seeded ${enrichedCount} cross-file receiver types (chunk ${chunkIdx + 1})`);
564
+ }
565
+ }
198
566
  // Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
199
567
  // This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
200
568
  // and the single-threaded event loop prevents races between synchronous addRelationship calls.
@@ -231,6 +599,10 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
231
599
  if (chunkWorkerData.assignments?.length) {
232
600
  processAssignmentsFromExtracted(graph, chunkWorkerData.assignments, ctx, chunkWorkerData.constructorBindings);
233
601
  }
602
+ // Collect TypeEnv file-scope bindings for exported type enrichment
603
+ if (chunkWorkerData.typeEnvBindings?.length) {
604
+ workerTypeEnvBindings.push(...chunkWorkerData.typeEnvBindings);
605
+ }
234
606
  }
235
607
  else {
236
608
  await processImports(graph, chunkFiles, astCache, ctx, undefined, repoPath, allPaths);
@@ -246,13 +618,17 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
246
618
  await workerPool?.terminate();
247
619
  }
248
620
  // Sequential fallback chunks: re-read source for call/heritage resolution
621
+ // Synthesize wildcard import bindings once after ALL imports are processed,
622
+ // before any call resolution — same rationale as the worker-path inline synthesis.
623
+ if (sequentialChunkPaths.length > 0)
624
+ synthesizeWildcardImportBindings(graph, ctx);
249
625
  for (const chunkPaths of sequentialChunkPaths) {
250
626
  const chunkContents = await readFileContents(repoPath, chunkPaths);
251
627
  const chunkFiles = chunkPaths
252
628
  .filter(p => chunkContents.has(p))
253
629
  .map(p => ({ path: p, content: chunkContents.get(p) }));
254
630
  astCache = createASTCache(chunkFiles.length);
255
- const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx);
631
+ const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx, undefined, exportedTypeMap);
256
632
  await processHeritage(graph, chunkFiles, astCache, ctx);
257
633
  if (rubyHeritage.length > 0) {
258
634
  await processHeritageFromExtracted(graph, rubyHeritage, ctx);
@@ -266,12 +642,53 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
266
642
  const hitRate = total > 0 ? ((rcStats.cacheHits / total) * 100).toFixed(1) : '0';
267
643
  console.log(`🔍 Resolution cache: ${rcStats.cacheHits} hits, ${rcStats.cacheMisses} misses (${hitRate}% hit rate)`);
268
644
  }
645
+ // ── Worker path quality enrichment: merge TypeEnv file-scope bindings into ExportedTypeMap ──
646
+ // Workers return file-scope bindings from their TypeEnv fixpoint (includes inferred types
647
+ // like `const config = getConfig()` → Config). Filter by graph isExported to match
648
+ // the sequential path's collectExportedBindings behavior.
649
+ if (workerTypeEnvBindings.length > 0) {
650
+ let enriched = 0;
651
+ for (const { filePath, bindings } of workerTypeEnvBindings) {
652
+ for (const [name, type] of bindings) {
653
+ // Verify the symbol is exported via graph node
654
+ const nodeId = `Function:${filePath}:${name}`;
655
+ const varNodeId = `Variable:${filePath}:${name}`;
656
+ const constNodeId = `Const:${filePath}:${name}`;
657
+ const node = graph.getNode(nodeId) ?? graph.getNode(varNodeId) ?? graph.getNode(constNodeId);
658
+ if (!node?.properties?.isExported)
659
+ continue;
660
+ let fileExports = exportedTypeMap.get(filePath);
661
+ if (!fileExports) {
662
+ fileExports = new Map();
663
+ exportedTypeMap.set(filePath, fileExports);
664
+ }
665
+ // Don't overwrite existing entries (Tier 0 from SymbolTable is authoritative)
666
+ if (!fileExports.has(name)) {
667
+ fileExports.set(name, type);
668
+ enriched++;
669
+ }
670
+ }
671
+ }
672
+ if (isDev && enriched > 0) {
673
+ console.log(`🔗 Worker TypeEnv enrichment: ${enriched} fixpoint-inferred exports added to ExportedTypeMap`);
674
+ }
675
+ }
676
+ // ── Phase 14 pre-pass: Final synthesis pass for whole-module-import languages ──
677
+ // Per-chunk synthesis (above) already ran incrementally. This final pass ensures
678
+ // any remaining files whose imports were not covered inline are also synthesized,
679
+ // and that Phase 14 type propagation has complete namedImportMap data.
680
+ const synthesized = synthesizeWildcardImportBindings(graph, ctx);
681
+ if (isDev && synthesized > 0) {
682
+ console.log(`🔗 Synthesized ${synthesized} additional wildcard import bindings (Go/Ruby/C++/Swift/Python)`);
683
+ }
684
+ // ── Phase 14: Cross-file binding propagation ──────────────────────
685
+ await runCrossFileBindingPropagation(graph, ctx, exportedTypeMap, allPaths, totalFiles, repoPath, pipelineStart, onProgress);
269
686
  // Free import resolution context — suffix index + resolve cache no longer needed
270
687
  // (allPathObjects and importCtx hold ~94MB+ for large repos)
271
688
  allPathObjects.length = 0;
272
689
  importCtx.resolveCache.clear();
273
- importCtx.suffixIndex = null;
274
- importCtx.normalizedFileList = null;
690
+ importCtx.index = EMPTY_INDEX; // Release suffix index memory (~30MB for large repos)
691
+ importCtx.normalizedFileList = [];
275
692
  let communityResult;
276
693
  let processResult;
277
694
  if (!options?.skipGraphPhases) {
@@ -26,6 +26,9 @@ export declare const TIER_CONFIDENCE: Record<ResolutionTier, number>;
26
26
  export type ImportMap = Map<string, Set<string>>;
27
27
  export type PackageMap = Map<string, Set<string>>;
28
28
  export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
29
+ /** Maps callerFile → (moduleAlias → sourceFilePath) for Python namespace imports.
30
+ * e.g. `import models` in app.py → moduleAliasMap.get('app.py')?.get('models') === 'models.py' */
31
+ export type ModuleAliasMap = Map<string, Map<string, string>>;
29
32
  export interface ResolutionContext {
30
33
  /**
31
34
  * The only resolution API. Returns all candidates at the winning tier.
@@ -40,6 +43,8 @@ export interface ResolutionContext {
40
43
  readonly importMap: ImportMap;
41
44
  readonly packageMap: PackageMap;
42
45
  readonly namedImportMap: NamedImportMap;
46
+ /** Module-alias map for Python namespace imports: callerFile → (alias → sourceFile). */
47
+ readonly moduleAliasMap: ModuleAliasMap;
43
48
  enableCache(filePath: string): void;
44
49
  clearCache(): void;
45
50
  getStats(): {
@@ -26,6 +26,7 @@ export const createResolutionContext = () => {
26
26
  const importMap = new Map();
27
27
  const packageMap = new Map();
28
28
  const namedImportMap = new Map();
29
+ const moduleAliasMap = new Map();
29
30
  // Per-file cache state
30
31
  let cacheFile = null;
31
32
  let cache = null;
@@ -33,10 +34,10 @@ export const createResolutionContext = () => {
33
34
  let cacheMisses = 0;
34
35
  // --- Core resolution (single implementation of tier logic) ---
35
36
  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' };
37
+ // Tier 1: Same file — authoritative match (returns all overloads)
38
+ const localDefs = symbols.lookupExactAll(fromFile, name);
39
+ if (localDefs.length > 0) {
40
+ return { candidates: localDefs, tier: 'same-file' };
40
41
  }
41
42
  // Get all global definitions for subsequent tiers
42
43
  const allDefs = symbols.lookupFuzzy(name);
@@ -114,6 +115,7 @@ export const createResolutionContext = () => {
114
115
  importMap.clear();
115
116
  packageMap.clear();
116
117
  namedImportMap.clear();
118
+ moduleAliasMap.clear();
117
119
  clearCache();
118
120
  cacheHits = 0;
119
121
  cacheMisses = 0;
@@ -124,6 +126,7 @@ export const createResolutionContext = () => {
124
126
  importMap,
125
127
  packageMap,
126
128
  namedImportMap,
129
+ moduleAliasMap,
127
130
  enableCache,
128
131
  clearCache,
129
132
  getStats,
@@ -2,7 +2,7 @@
2
2
  * Language-specific import resolvers.
3
3
  * Extracted from import-processor.ts for maintainability.
4
4
  */
5
- export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve } from './utils.js';
5
+ export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve, EMPTY_INDEX } from './utils.js';
6
6
  export type { SuffixIndex } from './utils.js';
7
7
  export { KOTLIN_EXTENSIONS, appendKotlinWildcard, resolveJvmWildcard, resolveJvmMemberImport } from './jvm.js';
8
8
  export { resolveGoPackageDir, resolveGoPackage } from './go.js';
@@ -2,7 +2,7 @@
2
2
  * Language-specific import resolvers.
3
3
  * Extracted from import-processor.ts for maintainability.
4
4
  */
5
- export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve } from './utils.js';
5
+ export { EXTENSIONS, tryResolveWithExtensions, buildSuffixIndex, suffixResolve, EMPTY_INDEX } from './utils.js';
6
6
  export { KOTLIN_EXTENSIONS, appendKotlinWildcard, resolveJvmWildcard, resolveJvmMemberImport } from './jvm.js';
7
7
  export { resolveGoPackageDir, resolveGoPackage } from './go.js';
8
8
  export { resolveCSharpImport, resolveCSharpNamespaceDir } from './csharp.js';
@@ -3,13 +3,14 @@
3
3
  * Handles wildcard imports, member/static imports, and Kotlin-specific patterns.
4
4
  */
5
5
  import type { SuffixIndex } from './utils.js';
6
+ import type { SyntaxNode } from '../utils.js';
6
7
  /** Kotlin file extensions for JVM resolver reuse */
7
8
  export declare const KOTLIN_EXTENSIONS: readonly string[];
8
9
  /**
9
10
  * Append .* to a Kotlin import path if the AST has a wildcard_import sibling node.
10
11
  * Pure function — returns a new string without mutating the input.
11
12
  */
12
- export declare const appendKotlinWildcard: (importPath: string, importNode: any) => string;
13
+ export declare const appendKotlinWildcard: (importPath: string, importNode: SyntaxNode) => string;
13
14
  /**
14
15
  * Resolve a JVM wildcard import (com.example.*) to all matching files.
15
16
  * Works for both Java (.java) and Kotlin (.kt, .kts).
@@ -27,26 +27,42 @@ export function resolveJvmWildcard(importPath, normalizedFileList, allFileList,
27
27
  const candidates = extensions.flatMap(ext => index.getFilesInDir(packagePath, ext));
28
28
  // Filter to only direct children (no subdirectories)
29
29
  const packageSuffix = '/' + packagePath + '/';
30
+ const packagePrefix = packagePath + '/';
30
31
  return candidates.filter(f => {
31
32
  const normalized = f.replace(/\\/g, '/');
32
- const idx = normalized.indexOf(packageSuffix);
33
- if (idx < 0)
33
+ // Match both nested (src/models/User.kt) and root-level (models/User.kt) packages
34
+ let afterPkg;
35
+ const idx = normalized.lastIndexOf(packageSuffix);
36
+ if (idx >= 0) {
37
+ afterPkg = normalized.substring(idx + packageSuffix.length);
38
+ }
39
+ else if (normalized.startsWith(packagePrefix)) {
40
+ afterPkg = normalized.substring(packagePrefix.length);
41
+ }
42
+ else {
34
43
  return false;
35
- const afterPkg = normalized.substring(idx + packageSuffix.length);
44
+ }
36
45
  return !afterPkg.includes('/');
37
46
  });
38
47
  }
39
48
  // Fallback: linear scan
40
49
  const packageSuffix = '/' + packagePath + '/';
50
+ const packagePrefix = packagePath + '/';
41
51
  const matches = [];
42
52
  for (let i = 0; i < normalizedFileList.length; i++) {
43
53
  const normalized = normalizedFileList[i];
44
- if (normalized.includes(packageSuffix) &&
45
- extensions.some(ext => normalized.endsWith(ext))) {
46
- const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
47
- if (!afterPackage.includes('/')) {
48
- matches.push(allFileList[i]);
49
- }
54
+ if (!extensions.some(ext => normalized.endsWith(ext)))
55
+ continue;
56
+ // Match both nested (src/models/User.kt) and root-level (models/User.kt) packages
57
+ let afterPackage = null;
58
+ if (normalized.includes(packageSuffix)) {
59
+ afterPackage = normalized.substring(normalized.lastIndexOf(packageSuffix) + packageSuffix.length);
60
+ }
61
+ else if (normalized.startsWith(packagePrefix)) {
62
+ afterPackage = normalized.substring(packagePrefix.length);
63
+ }
64
+ if (afterPackage !== null && !afterPackage.includes('/')) {
65
+ matches.push(allFileList[i]);
50
66
  }
51
67
  }
52
68
  return matches;