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
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shared Ruby call routing logic.
3
+ *
4
+ * Ruby expresses imports, heritage (mixins), and property definitions as
5
+ * method calls rather than syntax-level constructs. This module provides a
6
+ * routing function used by the CLI call-processor, CLI parse-worker, and
7
+ * the web call-processor so that the classification logic lives in one place.
8
+ *
9
+ * NOTE: This file is intentionally duplicated in gitnexus-web/ because the
10
+ * two packages have separate build targets (Node native vs WASM/browser).
11
+ * Keep both copies in sync until a shared package is introduced.
12
+ */
13
+ import { SupportedLanguages } from '../../config/supported-languages.js';
14
+ /** null = this call was not routed; fall through to default call handling */
15
+ export type CallRoutingResult = RubyCallRouting | null;
16
+ export type CallRouter = (calledName: string, callNode: any) => CallRoutingResult;
17
+ /** Per-language call routing. noRouting = no special routing (normal call processing) */
18
+ export declare const callRouters: Record<SupportedLanguages, CallRouter>;
19
+ export type RubyCallRouting = {
20
+ kind: 'import';
21
+ importPath: string;
22
+ isRelative: boolean;
23
+ } | {
24
+ kind: 'heritage';
25
+ items: RubyHeritageItem[];
26
+ } | {
27
+ kind: 'properties';
28
+ items: RubyPropertyItem[];
29
+ } | {
30
+ kind: 'call';
31
+ } | {
32
+ kind: 'skip';
33
+ };
34
+ export interface RubyHeritageItem {
35
+ enclosingClass: string;
36
+ mixinName: string;
37
+ heritageKind: 'include' | 'extend' | 'prepend';
38
+ }
39
+ export type RubyAccessorType = 'attr_accessor' | 'attr_reader' | 'attr_writer';
40
+ export interface RubyPropertyItem {
41
+ propName: string;
42
+ accessorType: RubyAccessorType;
43
+ startLine: number;
44
+ endLine: number;
45
+ }
46
+ /**
47
+ * Classify a Ruby call node and extract its semantic payload.
48
+ *
49
+ * @param calledName - The method name (e.g. 'require', 'include', 'attr_accessor')
50
+ * @param callNode - The tree-sitter `call` AST node
51
+ * @returns A discriminated union describing the call's semantic role
52
+ */
53
+ export declare function routeRubyCall(calledName: string, callNode: any): RubyCallRouting;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Shared Ruby call routing logic.
3
+ *
4
+ * Ruby expresses imports, heritage (mixins), and property definitions as
5
+ * method calls rather than syntax-level constructs. This module provides a
6
+ * routing function used by the CLI call-processor, CLI parse-worker, and
7
+ * the web call-processor so that the classification logic lives in one place.
8
+ *
9
+ * NOTE: This file is intentionally duplicated in gitnexus-web/ because the
10
+ * two packages have separate build targets (Node native vs WASM/browser).
11
+ * Keep both copies in sync until a shared package is introduced.
12
+ */
13
+ import { SupportedLanguages } from '../../config/supported-languages.js';
14
+ /** No-op router: returns null for every call (passthrough to normal processing) */
15
+ const noRouting = () => null;
16
+ /** Per-language call routing. noRouting = no special routing (normal call processing) */
17
+ export const callRouters = {
18
+ [SupportedLanguages.JavaScript]: noRouting,
19
+ [SupportedLanguages.TypeScript]: noRouting,
20
+ [SupportedLanguages.Python]: noRouting,
21
+ [SupportedLanguages.Java]: noRouting,
22
+ [SupportedLanguages.Kotlin]: noRouting,
23
+ [SupportedLanguages.Go]: noRouting,
24
+ [SupportedLanguages.Rust]: noRouting,
25
+ [SupportedLanguages.CSharp]: noRouting,
26
+ [SupportedLanguages.PHP]: noRouting,
27
+ [SupportedLanguages.Swift]: noRouting,
28
+ [SupportedLanguages.CPlusPlus]: noRouting,
29
+ [SupportedLanguages.C]: noRouting,
30
+ [SupportedLanguages.Ruby]: routeRubyCall,
31
+ };
32
+ // ── Pre-allocated singletons for common return values ────────────────────────
33
+ const CALL_RESULT = { kind: 'call' };
34
+ const SKIP_RESULT = { kind: 'skip' };
35
+ /** Max depth for parent-walking loops to prevent pathological AST traversals */
36
+ const MAX_PARENT_DEPTH = 50;
37
+ // ── Routing function ────────────────────────────────────────────────────────
38
+ /**
39
+ * Classify a Ruby call node and extract its semantic payload.
40
+ *
41
+ * @param calledName - The method name (e.g. 'require', 'include', 'attr_accessor')
42
+ * @param callNode - The tree-sitter `call` AST node
43
+ * @returns A discriminated union describing the call's semantic role
44
+ */
45
+ export function routeRubyCall(calledName, callNode) {
46
+ // ── require / require_relative → import ─────────────────────────────────
47
+ if (calledName === 'require' || calledName === 'require_relative') {
48
+ const argList = callNode.childForFieldName?.('arguments');
49
+ const stringNode = argList?.children?.find((c) => c.type === 'string');
50
+ const contentNode = stringNode?.children?.find((c) => c.type === 'string_content');
51
+ if (!contentNode)
52
+ return SKIP_RESULT;
53
+ let importPath = contentNode.text;
54
+ // Validate: reject null bytes, control chars, excessively long paths
55
+ if (!importPath || importPath.length > 1024 || /[\x00-\x1f]/.test(importPath)) {
56
+ return SKIP_RESULT;
57
+ }
58
+ const isRelative = calledName === 'require_relative';
59
+ if (isRelative && !importPath.startsWith('.')) {
60
+ importPath = './' + importPath;
61
+ }
62
+ return { kind: 'import', importPath, isRelative };
63
+ }
64
+ // ── include / extend / prepend → heritage (mixin) ──────────────────────
65
+ if (calledName === 'include' || calledName === 'extend' || calledName === 'prepend') {
66
+ let enclosingClass = null;
67
+ let current = callNode.parent;
68
+ let depth = 0;
69
+ while (current && ++depth <= MAX_PARENT_DEPTH) {
70
+ if (current.type === 'class' || current.type === 'module') {
71
+ const nameNode = current.childForFieldName?.('name');
72
+ if (nameNode) {
73
+ enclosingClass = nameNode.text;
74
+ break;
75
+ }
76
+ }
77
+ current = current.parent;
78
+ }
79
+ if (!enclosingClass)
80
+ return SKIP_RESULT;
81
+ const items = [];
82
+ const argList = callNode.childForFieldName?.('arguments');
83
+ for (const arg of (argList?.children ?? [])) {
84
+ if (arg.type === 'constant' || arg.type === 'scope_resolution') {
85
+ items.push({ enclosingClass, mixinName: arg.text, heritageKind: calledName });
86
+ }
87
+ }
88
+ return items.length > 0 ? { kind: 'heritage', items } : SKIP_RESULT;
89
+ }
90
+ // ── attr_accessor / attr_reader / attr_writer → property definitions ───
91
+ if (calledName === 'attr_accessor' || calledName === 'attr_reader' || calledName === 'attr_writer') {
92
+ const items = [];
93
+ const argList = callNode.childForFieldName?.('arguments');
94
+ for (const arg of (argList?.children ?? [])) {
95
+ if (arg.type === 'simple_symbol') {
96
+ items.push({
97
+ propName: arg.text.startsWith(':') ? arg.text.slice(1) : arg.text,
98
+ accessorType: calledName,
99
+ startLine: arg.startPosition.row,
100
+ endLine: arg.endPosition.row,
101
+ });
102
+ }
103
+ }
104
+ return items.length > 0 ? { kind: 'properties', items } : SKIP_RESULT;
105
+ }
106
+ // ── Everything else → regular call ─────────────────────────────────────
107
+ return CALL_RESULT;
108
+ }
@@ -12,7 +12,7 @@
12
12
  import { detectFrameworkFromPath } from './framework-detection.js';
13
13
  import { SupportedLanguages } from '../../config/supported-languages.js';
14
14
  // ============================================================================
15
- // NAME PATTERNS - All 9 supported languages
15
+ // NAME PATTERNS - All 11 supported languages
16
16
  // ============================================================================
17
17
  /**
18
18
  * Common entry point naming patterns by language
@@ -178,6 +178,12 @@ const ENTRY_POINT_PATTERNS = {
178
178
  /^save$/, // Repository::save()
179
179
  /^delete$/, // Repository::delete()
180
180
  ],
181
+ // Ruby
182
+ [SupportedLanguages.Ruby]: [
183
+ /^call$/, // Service objects (MyService.call)
184
+ /^perform$/, // Background jobs (Sidekiq, ActiveJob)
185
+ /^execute$/, // Command pattern
186
+ ],
181
187
  };
182
188
  /** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */
183
189
  const MERGED_ENTRY_POINT_PATTERNS = {};
@@ -318,7 +324,12 @@ export function isTestFile(filePath) {
318
324
  p.endsWith('test.php') ||
319
325
  p.endsWith('spec.php') ||
320
326
  p.includes('/tests/feature/') ||
321
- p.includes('/tests/unit/'));
327
+ p.includes('/tests/unit/') ||
328
+ // Ruby test patterns
329
+ p.endsWith('_spec.rb') ||
330
+ p.endsWith('_test.rb') ||
331
+ p.includes('/spec/') ||
332
+ p.includes('/test/fixtures/'));
322
333
  }
323
334
  /**
324
335
  * Check if a file path is likely a utility/helper file
@@ -211,6 +211,7 @@ const exportCheckers = {
211
211
  [SupportedLanguages.CPlusPlus]: cCppExportChecker,
212
212
  [SupportedLanguages.PHP]: phpExportChecker,
213
213
  [SupportedLanguages.Swift]: swiftExportChecker,
214
+ [SupportedLanguages.Ruby]: (_node, _name) => true,
214
215
  };
215
216
  // ============================================================================
216
217
  // Public API
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import { glob } from 'glob';
4
- import { shouldIgnorePath } from '../../config/ignore-service.js';
4
+ import { createIgnoreFilter } from '../../config/ignore-service.js';
5
5
  const READ_CONCURRENCY = 32;
6
6
  /** Skip files larger than 512KB — they're usually generated/vendored and crash tree-sitter */
7
7
  const MAX_FILE_SIZE = 512 * 1024;
@@ -10,12 +10,13 @@ const MAX_FILE_SIZE = 512 * 1024;
10
10
  * Memory: ~10MB for 100K files vs ~1GB+ with content.
11
11
  */
12
12
  export const walkRepositoryPaths = async (repoPath, onProgress) => {
13
- const files = await glob('**/*', {
13
+ const ignoreFilter = await createIgnoreFilter(repoPath);
14
+ const filtered = await glob('**/*', {
14
15
  cwd: repoPath,
15
16
  nodir: true,
16
17
  dot: false,
18
+ ignore: ignoreFilter,
17
19
  });
18
- const filtered = files.filter(file => !shouldIgnorePath(file));
19
20
  const entries = [];
20
21
  let processed = 0;
21
22
  let skippedLarge = 0;
@@ -255,6 +255,15 @@ export function detectFrameworkFromPath(filePath) {
255
255
  if (p.includes('/repositories/') && p.endsWith('.php')) {
256
256
  return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' };
257
257
  }
258
+ // ========== RUBY ==========
259
+ // Ruby: bin/ or exe/ (CLI entry points)
260
+ if ((p.includes('/bin/') || p.includes('/exe/')) && p.endsWith('.rb')) {
261
+ return { framework: 'ruby', entryPointMultiplier: 2.5, reason: 'ruby-executable' };
262
+ }
263
+ // Ruby: Rakefile or *.rake (task definitions)
264
+ if (p.endsWith('/rakefile') || p.endsWith('.rake')) {
265
+ return { framework: 'ruby', entryPointMultiplier: 1.5, reason: 'ruby-rake' };
266
+ }
258
267
  // ========== SWIFT / iOS ==========
259
268
  // iOS App entry points (highest priority)
260
269
  if (p.endsWith('/appdelegate.swift') || p.endsWith('/scenedelegate.swift') || p.endsWith('/app.swift')) {
@@ -15,15 +15,14 @@
15
15
  */
16
16
  import { KnowledgeGraph } from '../graph/types.js';
17
17
  import { ASTCache } from './ast-cache.js';
18
- import { SymbolTable } from './symbol-table.js';
19
18
  import type { ExtractedHeritage } from './workers/parse-worker.js';
20
- import type { ImportMap, PackageMap } from './import-processor.js';
19
+ import type { ResolutionContext } from './resolution-context.js';
21
20
  export declare const processHeritage: (graph: KnowledgeGraph, files: {
22
21
  path: string;
23
22
  content: string;
24
- }[], astCache: ASTCache, symbolTable: SymbolTable, importMap: ImportMap, packageMap?: PackageMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
23
+ }[], astCache: ASTCache, ctx: ResolutionContext, onProgress?: (current: number, total: number) => void) => Promise<void>;
25
24
  /**
26
25
  * Fast path: resolve pre-extracted heritage from workers.
27
26
  * No AST parsing — workers already extracted className + parentName + kind.
28
27
  */
29
- export declare const processHeritageFromExtracted: (graph: KnowledgeGraph, extractedHeritage: ExtractedHeritage[], symbolTable: SymbolTable, importMap: ImportMap, packageMap?: PackageMap, onProgress?: (current: number, total: number) => void) => Promise<void>;
28
+ export declare const processHeritageFromExtracted: (graph: KnowledgeGraph, extractedHeritage: ExtractedHeritage[], ctx: ResolutionContext, onProgress?: (current: number, total: number) => void) => Promise<void>;
@@ -20,7 +20,6 @@ import { generateId } from '../../lib/utils.js';
20
20
  import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop } from './utils.js';
21
21
  import { SupportedLanguages } from '../../config/supported-languages.js';
22
22
  import { getTreeSitterBufferSize } from './constants.js';
23
- import { resolveSymbol } from './symbol-resolver.js';
24
23
  /** C#/Java convention: interfaces start with I followed by an uppercase letter */
25
24
  const INTERFACE_NAME_RE = /^I[A-Z]/;
26
25
  /**
@@ -31,10 +30,10 @@ const INTERFACE_NAME_RE = /^I[A-Z]/;
31
30
  * - Swift: default IMPLEMENTS (protocol conformance is the norm)
32
31
  * - All others: default EXTENDS
33
32
  */
34
- const resolveExtendsType = (parentName, currentFilePath, symbolTable, importMap, language, packageMap) => {
35
- const resolved = resolveSymbol(parentName, currentFilePath, symbolTable, importMap, packageMap);
36
- if (resolved) {
37
- const isInterface = resolved.type === 'Interface';
33
+ const resolveExtendsType = (parentName, currentFilePath, ctx, language) => {
34
+ const resolved = ctx.resolve(parentName, currentFilePath);
35
+ if (resolved && resolved.candidates.length > 0) {
36
+ const isInterface = resolved.candidates[0].type === 'Interface';
38
37
  return isInterface
39
38
  ? { type: 'IMPLEMENTS', idPrefix: 'Interface' }
40
39
  : { type: 'EXTENDS', idPrefix: 'Class' };
@@ -51,7 +50,22 @@ const resolveExtendsType = (parentName, currentFilePath, symbolTable, importMap,
51
50
  }
52
51
  return { type: 'EXTENDS', idPrefix: 'Class' };
53
52
  };
54
- export const processHeritage = async (graph, files, astCache, symbolTable, importMap, packageMap, onProgress) => {
53
+ /**
54
+ * Resolve a symbol ID for heritage, with fallback to generated ID.
55
+ * Uses ctx.resolve() → pick first candidate's nodeId → generate synthetic ID.
56
+ */
57
+ const resolveHeritageId = (name, filePath, ctx, fallbackLabel, fallbackKey) => {
58
+ const resolved = ctx.resolve(name, filePath);
59
+ if (resolved && resolved.candidates.length > 0) {
60
+ // For global with multiple candidates, refuse (a wrong edge is worse than no edge)
61
+ if (resolved.tier === 'global' && resolved.candidates.length > 1) {
62
+ return generateId(fallbackLabel, fallbackKey ?? name);
63
+ }
64
+ return resolved.candidates[0].nodeId;
65
+ }
66
+ return generateId(fallbackLabel, fallbackKey ?? name);
67
+ };
68
+ export const processHeritage = async (graph, files, astCache, ctx, onProgress) => {
55
69
  const parser = await loadParser();
56
70
  const logSkipped = isVerboseIngestionEnabled();
57
71
  const skippedByLang = logSkipped ? new Map() : null;
@@ -77,7 +91,6 @@ export const processHeritage = async (graph, files, astCache, symbolTable, impor
77
91
  await loadLanguage(language, file.path);
78
92
  // 3. Get AST
79
93
  let tree = astCache.get(file.path);
80
- let wasReparsed = false;
81
94
  if (!tree) {
82
95
  // Use larger bufferSize for files > 32KB
83
96
  try {
@@ -87,7 +100,6 @@ export const processHeritage = async (graph, files, astCache, symbolTable, impor
87
100
  // Skip files that can't be parsed
88
101
  continue;
89
102
  }
90
- wasReparsed = true;
91
103
  // Cache re-parsed tree for potential future use
92
104
  astCache.set(file.path, tree);
93
105
  }
@@ -119,12 +131,9 @@ export const processHeritage = async (graph, files, astCache, symbolTable, impor
119
131
  }
120
132
  const className = captureMap['heritage.class'].text;
121
133
  const parentClassName = captureMap['heritage.extends'].text;
122
- const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, symbolTable, importMap, language, packageMap);
123
- const childId = symbolTable.lookupExact(file.path, className) ||
124
- resolveSymbol(className, file.path, symbolTable, importMap, packageMap)?.nodeId ||
125
- generateId('Class', `${file.path}:${className}`);
126
- const parentId = resolveSymbol(parentClassName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
127
- generateId(idPrefix, `${parentClassName}`);
134
+ const { type: relType, idPrefix } = resolveExtendsType(parentClassName, file.path, ctx, language);
135
+ const childId = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
136
+ const parentId = resolveHeritageId(parentClassName, file.path, ctx, idPrefix);
128
137
  if (childId && parentId && childId !== parentId) {
129
138
  graph.addRelationship({
130
139
  id: generateId(relType, `${childId}->${parentId}`),
@@ -140,16 +149,11 @@ export const processHeritage = async (graph, files, astCache, symbolTable, impor
140
149
  if (captureMap['heritage.class'] && captureMap['heritage.implements']) {
141
150
  const className = captureMap['heritage.class'].text;
142
151
  const interfaceName = captureMap['heritage.implements'].text;
143
- // Resolve class and interface IDs
144
- const classId = symbolTable.lookupExact(file.path, className) ||
145
- resolveSymbol(className, file.path, symbolTable, importMap, packageMap)?.nodeId ||
146
- generateId('Class', `${file.path}:${className}`);
147
- const interfaceId = resolveSymbol(interfaceName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
148
- generateId('Interface', `${interfaceName}`);
152
+ const classId = resolveHeritageId(className, file.path, ctx, 'Class', `${file.path}:${className}`);
153
+ const interfaceId = resolveHeritageId(interfaceName, file.path, ctx, 'Interface');
149
154
  if (classId && interfaceId) {
150
- const relId = generateId('IMPLEMENTS', `${classId}->${interfaceId}`);
151
155
  graph.addRelationship({
152
- id: relId,
156
+ id: generateId('IMPLEMENTS', `${classId}->${interfaceId}`),
153
157
  sourceId: classId,
154
158
  targetId: interfaceId,
155
159
  type: 'IMPLEMENTS',
@@ -162,16 +166,11 @@ export const processHeritage = async (graph, files, astCache, symbolTable, impor
162
166
  if (captureMap['heritage.trait'] && captureMap['heritage.class']) {
163
167
  const structName = captureMap['heritage.class'].text;
164
168
  const traitName = captureMap['heritage.trait'].text;
165
- // Resolve struct and trait IDs
166
- const structId = symbolTable.lookupExact(file.path, structName) ||
167
- resolveSymbol(structName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
168
- generateId('Struct', `${file.path}:${structName}`);
169
- const traitId = resolveSymbol(traitName, file.path, symbolTable, importMap, packageMap)?.nodeId ||
170
- generateId('Trait', `${traitName}`);
169
+ const structId = resolveHeritageId(structName, file.path, ctx, 'Struct', `${file.path}:${structName}`);
170
+ const traitId = resolveHeritageId(traitName, file.path, ctx, 'Trait');
171
171
  if (structId && traitId) {
172
- const relId = generateId('IMPLEMENTS', `${structId}->${traitId}`);
173
172
  graph.addRelationship({
174
- id: relId,
173
+ id: generateId('IMPLEMENTS', `${structId}->${traitId}`),
175
174
  sourceId: structId,
176
175
  targetId: traitId,
177
176
  type: 'IMPLEMENTS',
@@ -193,7 +192,7 @@ export const processHeritage = async (graph, files, astCache, symbolTable, impor
193
192
  * Fast path: resolve pre-extracted heritage from workers.
194
193
  * No AST parsing — workers already extracted className + parentName + kind.
195
194
  */
196
- export const processHeritageFromExtracted = async (graph, extractedHeritage, symbolTable, importMap, packageMap, onProgress) => {
195
+ export const processHeritageFromExtracted = async (graph, extractedHeritage, ctx, onProgress) => {
197
196
  const total = extractedHeritage.length;
198
197
  for (let i = 0; i < extractedHeritage.length; i++) {
199
198
  if (i % 500 === 0) {
@@ -205,12 +204,9 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, sym
205
204
  const fileLanguage = getLanguageFromFilename(h.filePath);
206
205
  if (!fileLanguage)
207
206
  continue;
208
- const { type: relType, idPrefix } = resolveExtendsType(h.parentName, h.filePath, symbolTable, importMap, fileLanguage, packageMap);
209
- const childId = symbolTable.lookupExact(h.filePath, h.className) ||
210
- resolveSymbol(h.className, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
211
- generateId('Class', `${h.filePath}:${h.className}`);
212
- const parentId = resolveSymbol(h.parentName, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
213
- generateId(idPrefix, `${h.parentName}`);
207
+ const { type: relType, idPrefix } = resolveExtendsType(h.parentName, h.filePath, ctx, fileLanguage);
208
+ const childId = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
209
+ const parentId = resolveHeritageId(h.parentName, h.filePath, ctx, idPrefix);
214
210
  if (childId && parentId && childId !== parentId) {
215
211
  graph.addRelationship({
216
212
  id: generateId(relType, `${childId}->${parentId}`),
@@ -223,11 +219,8 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, sym
223
219
  }
224
220
  }
225
221
  else if (h.kind === 'implements') {
226
- const classId = symbolTable.lookupExact(h.filePath, h.className) ||
227
- resolveSymbol(h.className, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
228
- generateId('Class', `${h.filePath}:${h.className}`);
229
- const interfaceId = resolveSymbol(h.parentName, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
230
- generateId('Interface', `${h.parentName}`);
222
+ const classId = resolveHeritageId(h.className, h.filePath, ctx, 'Class', `${h.filePath}:${h.className}`);
223
+ const interfaceId = resolveHeritageId(h.parentName, h.filePath, ctx, 'Interface');
231
224
  if (classId && interfaceId) {
232
225
  graph.addRelationship({
233
226
  id: generateId('IMPLEMENTS', `${classId}->${interfaceId}`),
@@ -239,20 +232,17 @@ export const processHeritageFromExtracted = async (graph, extractedHeritage, sym
239
232
  });
240
233
  }
241
234
  }
242
- else if (h.kind === 'trait-impl') {
243
- const structId = symbolTable.lookupExact(h.filePath, h.className) ||
244
- resolveSymbol(h.className, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
245
- generateId('Struct', `${h.filePath}:${h.className}`);
246
- const traitId = resolveSymbol(h.parentName, h.filePath, symbolTable, importMap, packageMap)?.nodeId ||
247
- generateId('Trait', `${h.parentName}`);
235
+ else if (h.kind === 'trait-impl' || h.kind === 'include' || h.kind === 'extend' || h.kind === 'prepend') {
236
+ const structId = resolveHeritageId(h.className, h.filePath, ctx, 'Struct', `${h.filePath}:${h.className}`);
237
+ const traitId = resolveHeritageId(h.parentName, h.filePath, ctx, 'Trait');
248
238
  if (structId && traitId) {
249
239
  graph.addRelationship({
250
- id: generateId('IMPLEMENTS', `${structId}->${traitId}`),
240
+ id: generateId('IMPLEMENTS', `${structId}->${traitId}:${h.kind}`),
251
241
  sourceId: structId,
252
242
  targetId: traitId,
253
243
  type: 'IMPLEMENTS',
254
244
  confidence: 1.0,
255
- reason: 'trait-impl',
245
+ reason: h.kind,
256
246
  });
257
247
  }
258
248
  }
@@ -1,18 +1,16 @@
1
1
  import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { ASTCache } from './ast-cache.js';
3
3
  import type { ExtractedImport } from './workers/parse-worker.js';
4
+ import type { ResolutionContext } from './resolution-context.js';
4
5
  import type { SuffixIndex } from './resolvers/index.js';
5
6
  export type { SuffixIndex, TsconfigPaths, GoModuleConfig, CSharpProjectConfig, ComposerConfig } from './resolvers/index.js';
6
7
  export type ImportMap = Map<string, Set<string>>;
7
- export declare const createImportMap: () => ImportMap;
8
8
  export type PackageMap = Map<string, Set<string>>;
9
- export declare const createPackageMap: () => PackageMap;
10
9
  export interface NamedImportBinding {
11
10
  sourcePath: string;
12
11
  exportedName: string;
13
12
  }
14
13
  export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
15
- export declare const createNamedImportMap: () => NamedImportMap;
16
14
  /**
17
15
  * Check if a file path is directly inside a package directory identified by its suffix.
18
16
  * Used by the symbol resolver for Go and C# directory-level import matching.
@@ -30,7 +28,7 @@ export declare function buildImportResolutionContext(allPaths: string[]): Import
30
28
  export declare const processImports: (graph: KnowledgeGraph, files: {
31
29
  path: string;
32
30
  content: string;
33
- }[], astCache: ASTCache, importMap: ImportMap, onProgress?: (current: number, total: number) => void, repoRoot?: string, allPaths?: string[], packageMap?: PackageMap, namedImportMap?: NamedImportMap) => Promise<void>;
31
+ }[], astCache: ASTCache, ctx: ResolutionContext, onProgress?: (current: number, total: number) => void, repoRoot?: string, allPaths?: string[]) => Promise<void>;
34
32
  export declare const processImportsFromExtracted: (graph: KnowledgeGraph, files: {
35
33
  path: string;
36
- }[], extractedImports: ExtractedImport[], importMap: ImportMap, onProgress?: (current: number, total: number) => void, repoRoot?: string, prebuiltCtx?: ImportResolutionContext, packageMap?: PackageMap, namedImportMap?: NamedImportMap) => Promise<void>;
34
+ }[], extractedImports: ExtractedImport[], ctx: ResolutionContext, onProgress?: (current: number, total: number) => void, repoRoot?: string, prebuiltCtx?: ImportResolutionContext) => Promise<void>;
@@ -7,11 +7,9 @@ import { SupportedLanguages } from '../../config/supported-languages.js';
7
7
  import { extractNamedBindings } from './named-binding-extraction.js';
8
8
  import { getTreeSitterBufferSize } from './constants.js';
9
9
  import { loadTsconfigPaths, loadGoModulePath, loadComposerConfig, loadCSharpProjectConfig, loadSwiftPackageConfig, } from './language-config.js';
10
- import { buildSuffixIndex, resolveImportPath, appendKotlinWildcard, KOTLIN_EXTENSIONS, resolveJvmWildcard, resolveJvmMemberImport, resolveGoPackageDir, resolveGoPackage, resolveCSharpImport, resolveCSharpNamespaceDir, resolvePhpImport, resolveRustImport, } from './resolvers/index.js';
10
+ import { buildSuffixIndex, resolveImportPath, appendKotlinWildcard, KOTLIN_EXTENSIONS, resolveJvmWildcard, resolveJvmMemberImport, resolveGoPackageDir, resolveGoPackage, resolveCSharpImport, resolveCSharpNamespaceDir, resolvePhpImport, resolveRustImport, resolveRubyImport, resolvePythonImport, } from './resolvers/index.js';
11
+ import { callRouters } from './call-routing.js';
11
12
  const isDev = process.env.NODE_ENV === 'development';
12
- export const createImportMap = () => new Map();
13
- export const createPackageMap = () => new Map();
14
- export const createNamedImportMap = () => new Map();
15
13
  /**
16
14
  * Check if a file path is directly inside a package directory identified by its suffix.
17
15
  * Used by the symbol resolver for Go and C# directory-level import matching.
@@ -107,6 +105,20 @@ function resolveLanguageImport(filePath, rawImportPath, language, configs, ctx)
107
105
  }
108
106
  return null; // External framework (Foundation, UIKit, etc.)
109
107
  }
108
+ // Python: relative imports (PEP 328) + proximity-based bare imports
109
+ // Falls through to standard suffix resolution when proximity finds no match.
110
+ if (language === SupportedLanguages.Python) {
111
+ const resolved = resolvePythonImport(filePath, rawImportPath, allFilePaths);
112
+ if (resolved)
113
+ return { kind: 'files', files: [resolved] };
114
+ if (rawImportPath.startsWith('.'))
115
+ return null; // relative but unresolved — don't suffix-match
116
+ }
117
+ // Ruby: require / require_relative
118
+ if (language === SupportedLanguages.Ruby) {
119
+ const resolved = resolveRubyImport(rawImportPath, normalizedFileList, allFileList, index);
120
+ return resolved ? { kind: 'files', files: [resolved] } : null;
121
+ }
110
122
  // Rust: expand top-level grouped imports: use {crate::a, crate::b}
111
123
  if (language === SupportedLanguages.Rust && rawImportPath.startsWith('{') && rawImportPath.endsWith('}')) {
112
124
  const inner = rawImportPath.slice(1, -1);
@@ -161,7 +173,10 @@ function applyImportResult(result, filePath, importMap, packageMap, addImportEdg
161
173
  // ============================================================================
162
174
  // MAIN IMPORT PROCESSOR
163
175
  // ============================================================================
164
- export const processImports = async (graph, files, astCache, importMap, onProgress, repoRoot, allPaths, packageMap, namedImportMap) => {
176
+ export const processImports = async (graph, files, astCache, ctx, onProgress, repoRoot, allPaths) => {
177
+ const importMap = ctx.importMap;
178
+ const packageMap = ctx.packageMap;
179
+ const namedImportMap = ctx.namedImportMap;
165
180
  // Use allPaths (full repo) when available for cross-chunk resolution, else fall back to chunk files
166
181
  const allFileList = allPaths ?? files.map(f => f.path);
167
182
  const allFilePaths = new Set(allFileList);
@@ -185,7 +200,7 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
185
200
  swiftPackageConfig: await loadSwiftPackageConfig(effectiveRoot),
186
201
  csharpConfigs: await loadCSharpProjectConfig(effectiveRoot),
187
202
  };
188
- const ctx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
203
+ const resolveCtx = { allFilePaths, allFileList, normalizedFileList, index, resolveCache };
189
204
  // Helper: add an IMPORTS edge to the graph only (no ImportMap update)
190
205
  const addImportGraphEdge = (filePath, resolvedPath) => {
191
206
  const sourceId = generateId('File', filePath);
@@ -281,10 +296,23 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
281
296
  ? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
282
297
  : sourceNode.text.replace(/['"<>]/g, '');
283
298
  totalImportsFound++;
284
- const result = resolveLanguageImport(file.path, rawImportPath, language, configs, ctx);
299
+ const result = resolveLanguageImport(file.path, rawImportPath, language, configs, resolveCtx);
285
300
  const bindings = namedImportMap ? extractNamedBindings(captureMap['import'], language) : undefined;
286
301
  applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge, bindings, namedImportMap);
287
302
  }
303
+ // ---- Language-specific call-as-import routing (Ruby require, etc.) ----
304
+ if (captureMap['call']) {
305
+ const callNameNode = captureMap['call.name'];
306
+ if (callNameNode) {
307
+ const callRouter = callRouters[language];
308
+ const routed = callRouter(callNameNode.text, captureMap['call']);
309
+ if (routed && routed.kind === 'import') {
310
+ totalImportsFound++;
311
+ const result = resolveLanguageImport(file.path, routed.importPath, language, configs, resolveCtx);
312
+ applyImportResult(result, file.path, importMap, packageMap, addImportEdge, addImportGraphEdge);
313
+ }
314
+ }
315
+ }
288
316
  });
289
317
  // Tree is now owned by the LRU cache — no manual delete needed
290
318
  }
@@ -300,9 +328,12 @@ export const processImports = async (graph, files, astCache, importMap, onProgre
300
328
  // ============================================================================
301
329
  // FAST PATH: Resolve pre-extracted imports (no parsing needed)
302
330
  // ============================================================================
303
- export const processImportsFromExtracted = async (graph, files, extractedImports, importMap, onProgress, repoRoot, prebuiltCtx, packageMap, namedImportMap) => {
304
- const ctx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
305
- const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = ctx;
331
+ export const processImportsFromExtracted = async (graph, files, extractedImports, ctx, onProgress, repoRoot, prebuiltCtx) => {
332
+ const importMap = ctx.importMap;
333
+ const packageMap = ctx.packageMap;
334
+ const namedImportMap = ctx.namedImportMap;
335
+ const importCtx = prebuiltCtx ?? buildImportResolutionContext(files.map(f => f.path));
336
+ const { allFilePaths, allFileList, normalizedFileList, suffixIndex: index, resolveCache } = importCtx;
306
337
  let totalImportsFound = 0;
307
338
  let totalImportsResolved = 0;
308
339
  const effectiveRoot = repoRoot || '';
@@ -2,13 +2,14 @@ import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { SymbolTable } from './symbol-table.js';
3
3
  import { ASTCache } from './ast-cache.js';
4
4
  import { WorkerPool } from './workers/worker-pool.js';
5
- import type { ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute } from './workers/parse-worker.js';
5
+ import type { ExtractedImport, ExtractedCall, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
6
6
  export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
7
7
  export interface WorkerExtractedData {
8
8
  imports: ExtractedImport[];
9
9
  calls: ExtractedCall[];
10
10
  heritage: ExtractedHeritage[];
11
11
  routes: ExtractedRoute[];
12
+ constructorBindings: FileConstructorBindings[];
12
13
  }
13
14
  export { isNodeExported } from './export-detection.js';
14
15
  export declare const processParsing: (graph: KnowledgeGraph, files: {