@zuvia-software-solutions/code-mapper 2.5.2 → 2.6.1

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.
@@ -6,12 +6,10 @@ import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/pa
6
6
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
7
7
  import { generateId } from '../../lib/utils.js';
8
8
  import { toNodeId, toEdgeId } from '../db/schema.js';
9
- import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
9
+ import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, findEnclosingClassName, SELF_RECEIVER_KEYWORDS, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
10
10
  import { buildTypeEnv } from './type-env.js';
11
11
  import { getTreeSitterBufferSize } from './constants.js';
12
12
  import { callRouters } from './call-routing.js';
13
- import { TsgoService } from '../semantic/tsgo-service.js';
14
- import path from 'node:path';
15
13
  /** Walk up the AST to find the enclosing function/method, or null for top-level code */
16
14
  const findEnclosingFunction = (node, filePath, ctx) => {
17
15
  let current = node.parent;
@@ -302,6 +300,12 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
302
300
  const callForm = inferCallForm(callNode, nameNode);
303
301
  const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
304
302
  let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
303
+ // Resolve this/self/base to enclosing class name
304
+ if (!receiverTypeName && receiverName && SELF_RECEIVER_KEYWORDS.has(receiverName)) {
305
+ const className = findEnclosingClassName(callNode);
306
+ if (className)
307
+ receiverTypeName = className;
308
+ }
305
309
  // Fall back to verified constructor bindings
306
310
  if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
307
311
  const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
@@ -728,223 +732,7 @@ const lookupReceiverType = (map, funcName, varName) => {
728
732
  // Fallback: file-level scope
729
733
  return map.get(fileLevelKey);
730
734
  };
731
- /** Check if a file is TypeScript or JavaScript (extensions tsgo can handle) */
732
- function isTypeScriptOrJavaScript(filePath) {
733
- return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
734
- }
735
- /**
736
- * Batch-resolve call sites via tsgo LSP before heuristic resolution.
737
- *
738
- * For each TS/JS call with line+column info, asks tsgo for go-to-definition.
739
- * Returns a Map from callKey -> ResolveResult for calls that tsgo resolved.
740
- *
741
- * Call key format: "sourceId\0calledName\0callLine" — unique per call site.
742
- */
743
- async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPath, onProgress) {
744
- const results = new Map();
745
- // Collect eligible calls (TS/JS files with line+column info)
746
- const eligible = [];
747
- for (const call of extractedCalls) {
748
- if (isTypeScriptOrJavaScript(call.filePath) &&
749
- call.callLine !== undefined &&
750
- call.callColumn !== undefined) {
751
- eligible.push(call);
752
- }
753
- }
754
- if (eligible.length === 0)
755
- return results;
756
- // Built-in receiver names that resolve to external types, not project code.
757
- const BUILTIN_RECEIVERS = new Set([
758
- 'console', 'Math', 'JSON', 'Object', 'Array', 'String', 'Number', 'Boolean',
759
- 'Date', 'RegExp', 'Error', 'Promise', 'Map', 'Set', 'WeakMap', 'WeakSet',
760
- 'Buffer', 'process', 'globalThis', 'window', 'document', 'navigator',
761
- 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
762
- 'require', 'module', 'exports', '__dirname', '__filename',
763
- 'fs', 'path', 'os', 'url', 'util', 'crypto', 'http', 'https', 'net',
764
- 'child_process', 'stream', 'events', 'assert', 'zlib',
765
- ]);
766
- // Pre-filter calls where tsgo won't add value:
767
- // A. Free-form calls with unambiguous name — heuristic resolves perfectly
768
- // B. Member calls on built-in receivers — tsgo always fails on these
769
- const tsgoEligible = [];
770
- let skippedUnambiguous = 0;
771
- let skippedBuiltin = 0;
772
- for (const call of eligible) {
773
- if (call.callForm === 'free' || call.callForm === undefined) {
774
- const resolved = ctx.resolve(call.calledName, call.filePath);
775
- if (resolved && resolved.candidates.length === 1) {
776
- skippedUnambiguous++;
777
- continue;
778
- }
779
- }
780
- if (call.callForm === 'member' && call.receiverName && BUILTIN_RECEIVERS.has(call.receiverName)) {
781
- skippedBuiltin++;
782
- continue;
783
- }
784
- tsgoEligible.push(call);
785
- }
786
- // Regroup filtered calls by file
787
- const tsgoByFile = new Map();
788
- for (const call of tsgoEligible) {
789
- let list = tsgoByFile.get(call.filePath);
790
- if (!list) {
791
- list = [];
792
- tsgoByFile.set(call.filePath, list);
793
- }
794
- list.push(call);
795
- }
796
- const t0 = Date.now();
797
- const skippedTotal = skippedUnambiguous + skippedBuiltin;
798
- // Adaptive parallelism — conservative to avoid freezing the machine.
799
- // Each tsgo LSP process loads the full project into memory (~1.5-3GB for
800
- // large codebases) and pins a CPU core at 100%, so we cap aggressively.
801
- const osModule = await import('os');
802
- const cpuCount = osModule.cpus().length;
803
- const freeMemGB = osModule.freemem() / (1024 * 1024 * 1024);
804
- const maxByCpu = Math.max(1, Math.floor(cpuCount * 0.5));
805
- const maxByMemory = Math.max(1, Math.floor(freeMemGB / 2)); // ~2GB per process
806
- const maxByWorkload = Math.max(1, Math.floor(tsgoByFile.size / 100));
807
- const HARD_CAP = 4; // never more than 4 tsgo processes regardless of hardware
808
- const actualWorkers = Math.min(maxByCpu, maxByMemory, maxByWorkload, HARD_CAP);
809
- if (process.env['CODE_MAPPER_VERBOSE']) {
810
- console.error(`Code Mapper: tsgo resolving ${tsgoEligible.length} calls across ${tsgoByFile.size} files with ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''} (skipped ${skippedTotal}: ${skippedUnambiguous} unambiguous, ${skippedBuiltin} builtin)...`);
811
- }
812
- // Dynamic dispatch: shared queue sorted by call count descending
813
- const fileEntries = [...tsgoByFile.entries()];
814
- fileEntries.sort((a, b) => b[1].length - a[1].length);
815
- let totalFilesProcessed = 0;
816
- let nextFileIdx = 0;
817
- const tsgoTotalFiles = tsgoByFile.size;
818
- const getNextFile = () => {
819
- if (nextFileIdx >= fileEntries.length)
820
- return null;
821
- return fileEntries[nextFileIdx++];
822
- };
823
- const resolveWorker = async (service) => {
824
- const sliceResults = new Map();
825
- let sliceResolved = 0;
826
- let sliceFailed = 0;
827
- let entry;
828
- while ((entry = getNextFile()) !== null) {
829
- // Bail out early if tsgo process died — no point sending more requests
830
- if (!service.isReady())
831
- break;
832
- const [filePath, calls] = entry;
833
- totalFilesProcessed++;
834
- if (totalFilesProcessed % 25 === 0) {
835
- onProgress?.(totalFilesProcessed, tsgoTotalFiles, actualWorkers);
836
- }
837
- const absFilePath = path.resolve(repoPath, filePath);
838
- // Pipeline: fire all definition requests for this file concurrently.
839
- // The LSP server processes them serially, but we eliminate round-trip
840
- // latency between requests — major speedup on large files.
841
- const BATCH = 50;
842
- for (let i = 0; i < calls.length; i += BATCH) {
843
- const batch = calls.slice(i, i + BATCH);
844
- const defs = await Promise.all(batch.map(call => service.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn)
845
- .catch(() => null)));
846
- for (let j = 0; j < batch.length; j++) {
847
- const call = batch[j];
848
- const def = defs[j];
849
- if (!def) {
850
- sliceFailed++;
851
- continue;
852
- }
853
- const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
854
- if (targetSymbols.length === 0) {
855
- sliceFailed++;
856
- continue;
857
- }
858
- let bestMatch;
859
- for (const sym of targetSymbols) {
860
- const node = graph.getNode(toNodeId(sym.nodeId));
861
- if (node && node.properties.startLine === def.line) {
862
- bestMatch = sym;
863
- break;
864
- }
865
- }
866
- if (!bestMatch) {
867
- for (const sym of targetSymbols) {
868
- const node = graph.getNode(toNodeId(sym.nodeId));
869
- if (node) {
870
- const sl = node.properties.startLine;
871
- const el = node.properties.endLine;
872
- if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
873
- bestMatch = sym;
874
- break;
875
- }
876
- }
877
- }
878
- }
879
- if (bestMatch) {
880
- if (bestMatch.nodeId === call.sourceId) {
881
- sliceFailed++;
882
- continue;
883
- }
884
- const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
885
- sliceResults.set(callKey, {
886
- nodeId: bestMatch.nodeId,
887
- confidence: TIER_CONFIDENCE['tsgo-resolved'],
888
- reason: 'tsgo-lsp',
889
- });
890
- sliceResolved++;
891
- }
892
- else {
893
- sliceFailed++;
894
- }
895
- }
896
- }
897
- service.notifyFileDeleted(absFilePath);
898
- }
899
- return { resolved: sliceResolved, failed: sliceFailed, results: sliceResults };
900
- };
901
- let resolved = 0;
902
- let failed = 0;
903
- if (actualWorkers === 1) {
904
- const outcome = await resolveWorker(tsgoService);
905
- resolved = outcome.resolved;
906
- failed = outcome.failed;
907
- for (const [k, v] of outcome.results)
908
- results.set(k, v);
909
- }
910
- else {
911
- const extraServices = [];
912
- try {
913
- const startPromises = [];
914
- for (let i = 1; i < actualWorkers; i++) {
915
- startPromises.push((async () => {
916
- const svc = new TsgoService(repoPath);
917
- if (await svc.start())
918
- return svc;
919
- return null;
920
- })());
921
- }
922
- const started = await Promise.all(startPromises);
923
- for (const svc of started) {
924
- if (svc)
925
- extraServices.push(svc);
926
- }
927
- const services = [tsgoService, ...extraServices];
928
- if (process.env['CODE_MAPPER_VERBOSE'])
929
- console.error(`Code Mapper: ${services.length} tsgo processes ready, resolving with dynamic dispatch...`);
930
- const outcomes = await Promise.all(services.map(svc => resolveWorker(svc)));
931
- for (const outcome of outcomes) {
932
- resolved += outcome.resolved;
933
- failed += outcome.failed;
934
- for (const [k, v] of outcome.results)
935
- results.set(k, v);
936
- }
937
- }
938
- finally {
939
- for (const svc of extraServices)
940
- svc.stop();
941
- }
942
- }
943
- const elapsed = Date.now() - t0;
944
- if (process.env['CODE_MAPPER_VERBOSE'])
945
- console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable, ${actualWorkers} process${actualWorkers > 1 ? 'es' : ''})`);
946
- return results;
947
- }
735
+ /** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
948
736
  /** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
949
737
  const GENERIC_MEMBER_METHODS_WORKER = new Set([
950
738
  'has', 'get', 'set', 'add', 'remove', 'delete', 'close', 'stop', 'clear', 'reset',
@@ -958,7 +746,7 @@ const GENERIC_MEMBER_METHODS_WORKER = new Set([
958
746
  'json', 'text', 'blob', 'status', 'send', 'end', // HTTP response
959
747
  ]);
960
748
  /** Resolve pre-extracted call sites from workers (no AST parsing needed) */
961
- export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings, tsgoService, repoPath) => {
749
+ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
962
750
  // Scope-aware receiver types keyed by filePath -> scope\0varName -> typeName
963
751
  const fileReceiverTypes = new Map();
964
752
  if (constructorBindings) {
@@ -978,10 +766,41 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
978
766
  }
979
767
  list.push(call);
980
768
  }
981
- // Batch pre-resolve via tsgo LSP (highest confidence, TS/JS only)
982
- let tsgoResolved;
983
- if (tsgoService?.isReady() && repoPath) {
984
- tsgoResolved = await batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPath, onProgress);
769
+ // Pre-build indexes for O(1) lookups in the hot loop.
770
+ // Without these, interface dispatch does graph.relationships.filter() per call = O(calls × edges).
771
+ const implementsIndex = new Map(); // interfaceNodeId → [classNodeId, ...]
772
+ const hasMethodIndex = new Map(); // classNodeId methods
773
+ const reverseImportIndex = new Map(); // importedFile → [importingFile, ...]
774
+ for (const rel of graph.iterRelationships()) {
775
+ if (rel.type === 'IMPLEMENTS') {
776
+ let arr = implementsIndex.get(rel.targetId);
777
+ if (!arr) {
778
+ arr = [];
779
+ implementsIndex.set(rel.targetId, arr);
780
+ }
781
+ arr.push(rel.sourceId);
782
+ }
783
+ else if (rel.type === 'HAS_METHOD') {
784
+ const methodNode = graph.getNode(rel.targetId);
785
+ if (methodNode) {
786
+ let arr = hasMethodIndex.get(rel.sourceId);
787
+ if (!arr) {
788
+ arr = [];
789
+ hasMethodIndex.set(rel.sourceId, arr);
790
+ }
791
+ arr.push({ targetId: rel.targetId, name: methodNode.properties.name });
792
+ }
793
+ }
794
+ }
795
+ for (const [file, importedFiles] of ctx.importMap) {
796
+ for (const imported of importedFiles) {
797
+ let arr = reverseImportIndex.get(imported);
798
+ if (!arr) {
799
+ arr = [];
800
+ reverseImportIndex.set(imported, arr);
801
+ }
802
+ arr.push(file);
803
+ }
985
804
  }
986
805
  const totalFiles = byFile.size;
987
806
  let filesProcessed = 0;
@@ -1031,16 +850,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1031
850
  if (effectiveCall.callForm === 'member' && !effectiveCall.receiverTypeName && GENERIC_MEMBER_METHODS_WORKER.has(effectiveCall.calledName)) {
1032
851
  continue;
1033
852
  }
1034
- // Check tsgo pre-resolved map first (highest confidence)
1035
- let resolved;
1036
- if (tsgoResolved && effectiveCall.callLine !== undefined) {
1037
- const callKey = `${effectiveCall.sourceId}\0${effectiveCall.calledName}\0${effectiveCall.callLine}`;
1038
- resolved = tsgoResolved.get(callKey);
1039
- }
1040
- // Fall through to heuristic resolution if tsgo didn't resolve
1041
- if (!resolved) {
1042
- resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
1043
- }
853
+ let resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
1044
854
  // RC-I: Interface dispatch — when receiver type is an Interface, find implementations.
1045
855
  // Strategy 1: Classes with IMPLEMENTS edges to the interface.
1046
856
  // Strategy 2: Factory functions whose return type matches the interface name.
@@ -1051,38 +861,29 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1051
861
  const interfaceDefs = receiverResolved.candidates.filter(d => d.type === 'Interface' || d.type === 'Trait');
1052
862
  if (interfaceDefs.length > 0) {
1053
863
  for (const ifaceDef of interfaceDefs) {
1054
- // Strategy 1: Class-based — find IMPLEMENTS edges
1055
- const implRows = graph.relationships.filter(r => r.type === 'IMPLEMENTS' && r.targetId === toNodeId(ifaceDef.nodeId));
1056
- for (const implRel of implRows) {
1057
- const methodEdges = graph.relationships.filter(r => r.type === 'HAS_METHOD' && r.sourceId === implRel.sourceId);
1058
- for (const methodEdge of methodEdges) {
1059
- const methodNode = graph.getNode(methodEdge.targetId);
1060
- if (methodNode && methodNode.properties.name === effectiveCall.calledName) {
1061
- resolved = { nodeId: methodEdge.targetId, confidence: 0.85, reason: 'interface-dispatch' };
1062
- break;
864
+ // Strategy 1: Class-based — find IMPLEMENTS edges via pre-built index
865
+ const implClassIds = implementsIndex.get(toNodeId(ifaceDef.nodeId));
866
+ if (implClassIds) {
867
+ for (const classId of implClassIds) {
868
+ const methods = hasMethodIndex.get(classId);
869
+ if (methods) {
870
+ const match = methods.find(m => m.name === effectiveCall.calledName);
871
+ if (match) {
872
+ resolved = { nodeId: match.targetId, confidence: 0.85, reason: 'interface-dispatch' };
873
+ break;
874
+ }
1063
875
  }
1064
876
  }
1065
- if (resolved)
1066
- break;
1067
877
  }
1068
878
  if (resolved)
1069
879
  break;
1070
880
  // Strategy 2: Factory/closure pattern — find functions returning this interface
1071
- // e.g. createEventBus(): EventBus → the closure has emit/subscribe as inner functions
1072
- // Look for the method name in files that import the interface's file
1073
881
  if (!resolved) {
1074
882
  const methodName = effectiveCall.calledName;
1075
883
  const ifaceFile = ifaceDef.filePath;
1076
- // Check: which files import the interface's file?
1077
- const importingFiles = [];
1078
- for (const [file, importedFiles] of ctx.importMap) {
1079
- if (importedFiles.has(ifaceFile)) {
1080
- importingFiles.push(file);
1081
- }
1082
- }
884
+ const importingFiles = reverseImportIndex.get(ifaceFile) ?? [];
1083
885
  // Also check the interface's own file
1084
- importingFiles.push(ifaceFile);
1085
- for (const file of importingFiles) {
886
+ for (const file of [...importingFiles, ifaceFile]) {
1086
887
  const method = ctx.symbols.lookupExactFull(file, methodName);
1087
888
  if (method && (method.type === 'Function' || method.type === 'Method')) {
1088
889
  resolved = { nodeId: method.nodeId, confidence: 0.75, reason: 'interface-factory-dispatch' };
@@ -1371,7 +1172,7 @@ export const createProvidesEdges = async (graph, ctx) => {
1371
1172
  * Creates CALLS edges: InterfaceMethod → ImplementationFunction.
1372
1173
  *
1373
1174
  * This makes the call chain traversable:
1374
- * register() →[tsgo]→ EventBus.emit (Method) →[dispatch]→ emit (Function in event-bus.ts)
1175
+ * register() → EventBus.emit (Method) →[dispatch]→ emit (Function in event-bus.ts)
1375
1176
  */
1376
1177
  export const resolveInterfaceDispatches = async (graph, ctx) => {
1377
1178
  let edgesCreated = 0;
@@ -1,5 +1,3 @@
1
1
  /** @file pipeline.ts @description Main ingestion pipeline that orchestrates scanning, parsing, resolution, community detection, and process detection across chunked file batches */
2
2
  import type { PipelineProgress, PipelineResult } from '../../types/pipeline.js';
3
- export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void, opts?: {
4
- tsgo?: boolean;
5
- }) => Promise<PipelineResult>;
3
+ export declare const runPipelineFromRepo: (repoPath: string, onProgress: (progress: PipelineProgress) => void) => Promise<PipelineResult>;
@@ -20,7 +20,6 @@ import path from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
  import { memoryGuard } from '../../lib/memory-guard.js';
22
22
  import { toNodeId, toEdgeId } from '../db/schema.js';
23
- import { getTsgoService } from '../semantic/tsgo-service.js';
24
23
  const verbose = (...args) => {
25
24
  if (process.env['CODE_MAPPER_VERBOSE'])
26
25
  console.error(...args);
@@ -33,7 +32,7 @@ const DEFAULT_CHUNK_BYTE_BUDGET = 50 * 1024 * 1024;
33
32
  const WORKING_MEMORY_MULTIPLIER = 20;
34
33
  // Max AST trees to keep in LRU cache
35
34
  const AST_CACHE_CAP = 50;
36
- export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
35
+ export const runPipelineFromRepo = async (repoPath, onProgress) => {
37
36
  const graph = createKnowledgeGraph();
38
37
  const ctx = createResolutionContext();
39
38
  const symbolTable = ctx.symbols;
@@ -272,7 +271,6 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
272
271
  }
273
272
  astCache.clear();
274
273
  }
275
- let tsgoWasUsed = false;
276
274
  // Phase B: Resolve ALL deferred calls now that every symbol is registered
277
275
  // Progress range: 70-82% (advancing, not fixed)
278
276
  if (allExtractedCalls.length > 0) {
@@ -282,35 +280,15 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
282
280
  message: `Resolving ${allExtractedCalls.length} calls across all files...`,
283
281
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
284
282
  });
285
- // Start tsgo for high-confidence TS/JS resolution (optional — graceful fallback)
286
- let tsgoService = null;
287
- if (opts?.tsgo !== false) {
288
- try {
289
- const service = getTsgoService(repoPath);
290
- if (await service.start()) {
291
- tsgoService = service;
292
- tsgoWasUsed = true;
293
- }
294
- }
295
- catch {
296
- // tsgo is optional — if @typescript/native-preview isn't installed, skip silently
297
- }
298
- }
299
- try {
300
- await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total, workerCount) => {
301
- const callPercent = 70 + Math.round((current / Math.max(total, 1)) * 12);
302
- onProgress({
303
- phase: 'calls',
304
- percent: callPercent,
305
- message: `Resolving calls: ${current}/${total} files...`,
306
- stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount, ...(workerCount ? { workerCount } : {}) },
307
- });
308
- }, allConstructorBindings.length > 0 ? allConstructorBindings : undefined, tsgoService, repoPath);
309
- }
310
- finally {
311
- // Stop tsgo after call resolution completes
312
- tsgoService?.stop();
313
- }
283
+ await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total, workerCount) => {
284
+ const callPercent = 70 + Math.round((current / Math.max(total, 1)) * 12);
285
+ onProgress({
286
+ phase: 'calls',
287
+ percent: callPercent,
288
+ message: `Resolving calls: ${current}/${total} files...`,
289
+ stats: { filesProcessed: current, totalFiles: total, nodesCreated: graph.nodeCount, ...(workerCount ? { workerCount } : {}) },
290
+ });
291
+ }, allConstructorBindings.length > 0 ? allConstructorBindings : undefined);
314
292
  }
315
293
  {
316
294
  const rcStats = ctx.getStats();
@@ -446,7 +424,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
446
424
  },
447
425
  });
448
426
  astCache.clear();
449
- return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult, tsgoEnabled: tsgoWasUsed };
427
+ return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult };
450
428
  }
451
429
  catch (error) {
452
430
  cleanup();
@@ -11,7 +11,7 @@
11
11
  */
12
12
  import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
13
13
  import type { NamedImportBinding } from './import-processor.js';
14
- export type ResolutionTier = 'tsgo-resolved' | 'same-file' | 'import-scoped' | 'global';
14
+ export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
15
15
  export interface TieredCandidates {
16
16
  readonly candidates: readonly SymbolDefinition[];
17
17
  readonly tier: ResolutionTier;
@@ -15,7 +15,6 @@ import { isFileInPackageDir } from './import-processor.js';
15
15
  import { walkBindingChain } from './named-binding-extraction.js';
16
16
  // Confidence scores per resolution tier
17
17
  export const TIER_CONFIDENCE = {
18
- 'tsgo-resolved': 0.99,
19
18
  'same-file': 0.95,
20
19
  'import-scoped': 0.9,
21
20
  'global': 0.5,
@@ -46,6 +46,11 @@ export declare const CLASS_CONTAINER_TYPES: Set<string>;
46
46
  export declare const CONTAINER_TYPE_TO_LABEL: Record<string, string>;
47
47
  /** Walk up AST to find enclosing class/struct/impl and return its generateId (handles Go receivers) */
48
48
  export declare const findEnclosingClassId: (node: any, filePath: string) => string | null;
49
+ /** Self-receiver keywords that should resolve to the enclosing class */
50
+ export declare const SELF_RECEIVER_KEYWORDS: Set<string>;
51
+ /** Walk up AST to find enclosing class/struct/impl and return its NAME (not ID).
52
+ * Used to resolve `this.method()` / `self.method()` receiver type. */
53
+ export declare const findEnclosingClassName: (node: any) => string | null;
49
54
  /** Extract function name and label from a function/method AST node (handles C/C++ qualified_identifier) */
50
55
  export declare const extractFunctionName: (node: any) => {
51
56
  funcName: string | null;
@@ -369,6 +369,23 @@ export const findEnclosingClassId = (node, filePath) => {
369
369
  }
370
370
  return null;
371
371
  };
372
+ /** Self-receiver keywords that should resolve to the enclosing class */
373
+ export const SELF_RECEIVER_KEYWORDS = new Set(['this', 'self', 'base', 'parent']);
374
+ /** Walk up AST to find enclosing class/struct/impl and return its NAME (not ID).
375
+ * Used to resolve `this.method()` / `self.method()` receiver type. */
376
+ export const findEnclosingClassName = (node) => {
377
+ let current = node.parent;
378
+ while (current) {
379
+ if (CLASS_CONTAINER_TYPES.has(current.type)) {
380
+ const nameNode = current.childForFieldName?.('name')
381
+ ?? current.children?.find((c) => c.type === 'type_identifier' || c.type === 'identifier' || c.type === 'name' || c.type === 'constant');
382
+ if (nameNode)
383
+ return nameNode.text;
384
+ }
385
+ current = current.parent;
386
+ }
387
+ return null;
388
+ };
372
389
  /** Extract function name and label from a function/method AST node (handles C/C++ qualified_identifier) */
373
390
  export const extractFunctionName = (node) => {
374
391
  let funcName = null;
@@ -34,7 +34,7 @@ try {
34
34
  Kotlin = _require('tree-sitter-kotlin');
35
35
  }
36
36
  catch { }
37
- import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, CALL_EXPRESSION_TYPES, extractCallChain, } from '../utils.js';
37
+ import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, findEnclosingClassName, SELF_RECEIVER_KEYWORDS, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, CALL_EXPRESSION_TYPES, extractCallChain, } from '../utils.js';
38
38
  import { buildTypeEnv } from '../type-env.js';
39
39
  import { isNodeExported } from '../export-detection.js';
40
40
  import { detectFrameworkFromAST } from '../framework-detection.js';
@@ -851,6 +851,12 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
851
851
  const callForm = callForm_pre;
852
852
  let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
853
853
  let receiverTypeName = receiverName ? typeEnv.lookup(receiverName, callNode) : undefined;
854
+ // Resolve this/self/base to enclosing class name
855
+ if (!receiverTypeName && receiverName && SELF_RECEIVER_KEYWORDS.has(receiverName)) {
856
+ const className = findEnclosingClassName(callNode);
857
+ if (className)
858
+ receiverTypeName = className;
859
+ }
854
860
  let receiverCallChain;
855
861
  // When the receiver is complex (call chain or member chain),
856
862
  // extractReceiverName returns undefined. Walk the AST to extract
@@ -44,7 +44,7 @@ export declare class TsgoService {
44
44
  /** Whether the server is running and ready for queries */
45
45
  isReady(): boolean;
46
46
  /** Resolve what a symbol at a given position points to (go-to-definition) */
47
- resolveDefinition(absFilePath: string, line: number, character: number): Promise<TsgoDefinition | null>;
47
+ resolveDefinition(absFilePath: string, line: number, character: number, timeoutMs?: number): Promise<TsgoDefinition | null>;
48
48
  /** Find all references to the symbol at the given position */
49
49
  findReferences(absFilePath: string, line: number, character: number): Promise<TsgoReference[]>;
50
50
  /** Get the type signature at a position (hover) */
@@ -68,7 +68,7 @@ export class TsgoService {
68
68
  return this.ready;
69
69
  }
70
70
  /** Resolve what a symbol at a given position points to (go-to-definition) */
71
- async resolveDefinition(absFilePath, line, character) {
71
+ async resolveDefinition(absFilePath, line, character, timeoutMs = 3000) {
72
72
  if (!this.ready) {
73
73
  console.error('[tsgo-service] resolveDefinition called but not ready');
74
74
  return null;
@@ -77,7 +77,7 @@ export class TsgoService {
77
77
  const resp = await this.request('textDocument/definition', {
78
78
  textDocument: { uri: this.fileUri(absFilePath) },
79
79
  position: { line, character },
80
- }, 3000);
80
+ }, timeoutMs);
81
81
  if (!resp) {
82
82
  verbose('definition timeout', absFilePath, line, character);
83
83
  return null;
@@ -38,14 +38,10 @@ export declare class LocalBackend {
38
38
  /** Per-repo promise chain that serializes ensureFresh calls.
39
39
  * Prevents race: Call 2 skipping refresh while Call 1 is still writing. */
40
40
  private refreshLocks;
41
- /** Per-repo tsgo LSP service instances for live semantic enrichment */
42
- private tsgoServices;
43
41
  /** Per-repo in-memory embedding cache: nodeId → Float32Array (256-dim) */
44
42
  private embeddingCaches;
45
43
  /** Per-repo in-memory NL embedding cache: includes source text for match_reason */
46
44
  private nlEmbeddingCaches;
47
- /** Get (or lazily start) a tsgo LSP service for a repo. Returns null if unavailable. */
48
- private getTsgo;
49
45
  /** Get (or lazily open) the SQLite database for a repo. */
50
46
  private getDb;
51
47
  /** Load all embeddings into memory for fast vector search */
@@ -104,7 +100,7 @@ export declare class LocalBackend {
104
100
  lastCommit: string;
105
101
  stats?: any;
106
102
  }>>;
107
- /** Find the narrowest symbol node enclosing a given file position (for tsgo ref merging) */
103
+ /** Find the narrowest symbol node enclosing a given file position */
108
104
  private findNodeAtPosition;
109
105
  /** Extract signature from content. For interfaces/types, returns the full body (fields ARE the signature). */
110
106
  private extractSignature;