@zuvia-software-solutions/code-mapper 1.4.0 → 2.0.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.
Files changed (137) hide show
  1. package/dist/cli/ai-context.js +1 -1
  2. package/dist/cli/analyze.d.ts +1 -0
  3. package/dist/cli/analyze.js +73 -82
  4. package/dist/cli/augment.js +0 -2
  5. package/dist/cli/eval-server.d.ts +2 -2
  6. package/dist/cli/eval-server.js +6 -6
  7. package/dist/cli/index.js +6 -10
  8. package/dist/cli/mcp.d.ts +1 -3
  9. package/dist/cli/mcp.js +3 -3
  10. package/dist/cli/refresh.d.ts +2 -2
  11. package/dist/cli/refresh.js +24 -29
  12. package/dist/cli/status.js +4 -13
  13. package/dist/cli/tool.d.ts +5 -4
  14. package/dist/cli/tool.js +8 -10
  15. package/dist/config/ignore-service.js +14 -34
  16. package/dist/core/augmentation/engine.js +53 -83
  17. package/dist/core/db/adapter.d.ts +99 -0
  18. package/dist/core/db/adapter.js +402 -0
  19. package/dist/core/db/graph-loader.d.ts +27 -0
  20. package/dist/core/db/graph-loader.js +148 -0
  21. package/dist/core/db/queries.d.ts +160 -0
  22. package/dist/core/db/queries.js +441 -0
  23. package/dist/core/db/schema.d.ts +108 -0
  24. package/dist/core/db/schema.js +136 -0
  25. package/dist/core/embeddings/embedder.d.ts +21 -12
  26. package/dist/core/embeddings/embedder.js +104 -50
  27. package/dist/core/embeddings/embedding-pipeline.d.ts +48 -22
  28. package/dist/core/embeddings/embedding-pipeline.js +220 -262
  29. package/dist/core/embeddings/text-generator.js +4 -19
  30. package/dist/core/embeddings/types.d.ts +1 -1
  31. package/dist/core/graph/graph.d.ts +1 -1
  32. package/dist/core/graph/graph.js +1 -0
  33. package/dist/core/graph/types.d.ts +11 -9
  34. package/dist/core/graph/types.js +4 -1
  35. package/dist/core/incremental/refresh.d.ts +46 -0
  36. package/dist/core/incremental/refresh.js +503 -0
  37. package/dist/core/incremental/types.d.ts +2 -1
  38. package/dist/core/incremental/types.js +42 -44
  39. package/dist/core/ingestion/ast-cache.js +1 -0
  40. package/dist/core/ingestion/call-processor.d.ts +15 -3
  41. package/dist/core/ingestion/call-processor.js +448 -60
  42. package/dist/core/ingestion/cluster-enricher.d.ts +1 -1
  43. package/dist/core/ingestion/cluster-enricher.js +2 -0
  44. package/dist/core/ingestion/community-processor.d.ts +1 -1
  45. package/dist/core/ingestion/community-processor.js +8 -3
  46. package/dist/core/ingestion/export-detection.d.ts +1 -1
  47. package/dist/core/ingestion/export-detection.js +1 -1
  48. package/dist/core/ingestion/filesystem-walker.js +1 -1
  49. package/dist/core/ingestion/heritage-processor.d.ts +2 -2
  50. package/dist/core/ingestion/heritage-processor.js +22 -11
  51. package/dist/core/ingestion/import-processor.d.ts +2 -2
  52. package/dist/core/ingestion/import-processor.js +24 -9
  53. package/dist/core/ingestion/language-config.js +7 -4
  54. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  55. package/dist/core/ingestion/mro-processor.js +23 -11
  56. package/dist/core/ingestion/named-binding-extraction.js +5 -5
  57. package/dist/core/ingestion/parsing-processor.d.ts +4 -4
  58. package/dist/core/ingestion/parsing-processor.js +26 -18
  59. package/dist/core/ingestion/pipeline.d.ts +4 -2
  60. package/dist/core/ingestion/pipeline.js +50 -20
  61. package/dist/core/ingestion/process-processor.d.ts +2 -2
  62. package/dist/core/ingestion/process-processor.js +28 -14
  63. package/dist/core/ingestion/resolution-context.d.ts +1 -1
  64. package/dist/core/ingestion/resolution-context.js +14 -4
  65. package/dist/core/ingestion/resolvers/csharp.js +4 -3
  66. package/dist/core/ingestion/resolvers/go.js +3 -1
  67. package/dist/core/ingestion/resolvers/jvm.js +13 -4
  68. package/dist/core/ingestion/resolvers/standard.js +2 -2
  69. package/dist/core/ingestion/resolvers/utils.js +6 -2
  70. package/dist/core/ingestion/route-stitcher.d.ts +15 -0
  71. package/dist/core/ingestion/route-stitcher.js +92 -0
  72. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  73. package/dist/core/ingestion/structure-processor.js +3 -2
  74. package/dist/core/ingestion/symbol-table.d.ts +2 -0
  75. package/dist/core/ingestion/symbol-table.js +5 -1
  76. package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
  77. package/dist/core/ingestion/tree-sitter-queries.js +177 -0
  78. package/dist/core/ingestion/type-env.js +20 -0
  79. package/dist/core/ingestion/type-extractors/csharp.js +4 -3
  80. package/dist/core/ingestion/type-extractors/go.js +23 -12
  81. package/dist/core/ingestion/type-extractors/php.js +18 -10
  82. package/dist/core/ingestion/type-extractors/ruby.js +15 -3
  83. package/dist/core/ingestion/type-extractors/rust.js +3 -2
  84. package/dist/core/ingestion/type-extractors/shared.js +3 -2
  85. package/dist/core/ingestion/type-extractors/typescript.js +11 -5
  86. package/dist/core/ingestion/utils.d.ts +27 -4
  87. package/dist/core/ingestion/utils.js +145 -100
  88. package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
  89. package/dist/core/ingestion/workers/parse-worker.js +97 -29
  90. package/dist/core/ingestion/workers/worker-pool.js +3 -0
  91. package/dist/core/search/bm25-index.d.ts +15 -8
  92. package/dist/core/search/bm25-index.js +48 -98
  93. package/dist/core/search/hybrid-search.d.ts +9 -3
  94. package/dist/core/search/hybrid-search.js +30 -25
  95. package/dist/core/search/reranker.js +9 -7
  96. package/dist/core/search/types.d.ts +0 -4
  97. package/dist/core/semantic/tsgo-service.d.ts +7 -1
  98. package/dist/core/semantic/tsgo-service.js +165 -66
  99. package/dist/lib/tsgo-test.d.ts +2 -0
  100. package/dist/lib/tsgo-test.js +6 -0
  101. package/dist/lib/type-utils.d.ts +25 -0
  102. package/dist/lib/type-utils.js +22 -0
  103. package/dist/lib/utils.d.ts +3 -2
  104. package/dist/lib/utils.js +3 -2
  105. package/dist/mcp/compatible-stdio-transport.js +1 -1
  106. package/dist/mcp/local/local-backend.d.ts +29 -56
  107. package/dist/mcp/local/local-backend.js +808 -1118
  108. package/dist/mcp/resources.js +35 -25
  109. package/dist/mcp/server.d.ts +1 -1
  110. package/dist/mcp/server.js +5 -5
  111. package/dist/mcp/tools.js +24 -25
  112. package/dist/storage/repo-manager.d.ts +2 -12
  113. package/dist/storage/repo-manager.js +1 -47
  114. package/dist/types/pipeline.d.ts +8 -5
  115. package/dist/types/pipeline.js +5 -0
  116. package/package.json +18 -11
  117. package/dist/cli/serve.d.ts +0 -5
  118. package/dist/cli/serve.js +0 -8
  119. package/dist/core/incremental/child-process.d.ts +0 -8
  120. package/dist/core/incremental/child-process.js +0 -649
  121. package/dist/core/incremental/refresh-coordinator.d.ts +0 -32
  122. package/dist/core/incremental/refresh-coordinator.js +0 -147
  123. package/dist/core/lbug/csv-generator.d.ts +0 -28
  124. package/dist/core/lbug/csv-generator.js +0 -355
  125. package/dist/core/lbug/lbug-adapter.d.ts +0 -96
  126. package/dist/core/lbug/lbug-adapter.js +0 -753
  127. package/dist/core/lbug/schema.d.ts +0 -46
  128. package/dist/core/lbug/schema.js +0 -402
  129. package/dist/mcp/core/embedder.d.ts +0 -24
  130. package/dist/mcp/core/embedder.js +0 -168
  131. package/dist/mcp/core/lbug-adapter.d.ts +0 -29
  132. package/dist/mcp/core/lbug-adapter.js +0 -330
  133. package/dist/server/api.d.ts +0 -5
  134. package/dist/server/api.js +0 -340
  135. package/dist/server/mcp-http.d.ts +0 -7
  136. package/dist/server/mcp-http.js +0 -95
  137. package/models/mlx-embedder.py +0 -185
@@ -4,7 +4,7 @@ import { createKnowledgeGraph } from '../graph/graph.js';
4
4
  import { processStructure } from './structure-processor.js';
5
5
  import { processParsing } from './parsing-processor.js';
6
6
  import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
7
- import { processCalls, processCallsFromExtracted, processRoutesFromExtracted, createDependsOnEdges, createProvidesEdges } from './call-processor.js';
7
+ import { processCalls, processCallsFromExtracted, processRoutesFromExtracted, createDependsOnEdges, createProvidesEdges, resolveInterfaceDispatches } from './call-processor.js';
8
8
  import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
9
9
  import { computeMRO } from './mro-processor.js';
10
10
  import { processCommunities } from './community-processor.js';
@@ -19,7 +19,9 @@ import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import { fileURLToPath, pathToFileURL } from 'node:url';
21
21
  import { memoryGuard } from '../../lib/memory-guard.js';
22
- const isDev = process.env.NODE_ENV === 'development';
22
+ import { toNodeId, toEdgeId } from '../db/schema.js';
23
+ import { getTsgoService } from '../semantic/tsgo-service.js';
24
+ const isDev = process.env['NODE_ENV'] === 'development';
23
25
  // Default chunk budget — used when memory is plentiful.
24
26
  // Under memory pressure, adaptiveBatchSize() shrinks this automatically.
25
27
  const DEFAULT_CHUNK_BYTE_BUDGET = 50 * 1024 * 1024;
@@ -28,7 +30,7 @@ const DEFAULT_CHUNK_BYTE_BUDGET = 50 * 1024 * 1024;
28
30
  const WORKING_MEMORY_MULTIPLIER = 20;
29
31
  // Max AST trees to keep in LRU cache
30
32
  const AST_CACHE_CAP = 50;
31
- export const runPipelineFromRepo = async (repoPath, onProgress) => {
33
+ export const runPipelineFromRepo = async (repoPath, onProgress, opts) => {
32
34
  const graph = createKnowledgeGraph();
33
35
  const ctx = createResolutionContext();
34
36
  const symbolTable = ctx.symbols;
@@ -167,6 +169,8 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
167
169
  try {
168
170
  for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
169
171
  const chunkPaths = chunks[chunkIdx];
172
+ if (!chunkPaths)
173
+ continue;
170
174
  // Read content for this chunk
171
175
  const chunkContents = await readFileContents(repoPath, chunkPaths);
172
176
  const chunkFiles = chunkPaths
@@ -256,6 +260,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
256
260
  }
257
261
  astCache.clear();
258
262
  }
263
+ let tsgoWasUsed = false;
259
264
  // Phase B: Resolve ALL deferred calls now that every symbol is registered
260
265
  if (allExtractedCalls.length > 0) {
261
266
  onProgress({
@@ -264,14 +269,34 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
264
269
  message: `Resolving ${allExtractedCalls.length} calls across all files...`,
265
270
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
266
271
  });
267
- await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total) => {
268
- onProgress({
269
- phase: 'parsing',
270
- percent: 82,
271
- message: `Resolving calls: ${current}/${total} files...`,
272
- stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
273
- });
274
- }, allConstructorBindings.length > 0 ? allConstructorBindings : undefined);
272
+ // Start tsgo for high-confidence TS/JS resolution (optional graceful fallback)
273
+ let tsgoService = null;
274
+ if (opts?.tsgo !== false) {
275
+ try {
276
+ const service = getTsgoService(repoPath);
277
+ if (await service.start()) {
278
+ tsgoService = service;
279
+ tsgoWasUsed = true;
280
+ }
281
+ }
282
+ catch {
283
+ // tsgo is optional — if @typescript/native-preview isn't installed, skip silently
284
+ }
285
+ }
286
+ try {
287
+ await processCallsFromExtracted(graph, allExtractedCalls, ctx, (current, total) => {
288
+ onProgress({
289
+ phase: 'parsing',
290
+ percent: 82,
291
+ message: `Resolving calls: ${current}/${total} files...`,
292
+ stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
293
+ });
294
+ }, allConstructorBindings.length > 0 ? allConstructorBindings : undefined, tsgoService, repoPath);
295
+ }
296
+ finally {
297
+ // Stop tsgo after call resolution completes
298
+ tsgoService?.stop();
299
+ }
275
300
  }
276
301
  // Log resolution cache stats in dev mode
277
302
  if (isDev) {
@@ -293,6 +318,11 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
293
318
  if (isDev && (diEdgeCount > 0 || providesEdgeCount > 0)) {
294
319
  console.log(`💉 DI: ${diEdgeCount} DEPENDS_ON edges, ${providesEdgeCount} PROVIDES edges`);
295
320
  }
321
+ // Phase 4.5a2: Interface dispatch — connect callers of interfaces to implementations
322
+ const ifaceEdges = await resolveInterfaceDispatches(graph, ctx);
323
+ if (isDev && ifaceEdges > 0) {
324
+ console.log(`🔌 Interface dispatch: ${ifaceEdges} implementation edges`);
325
+ }
296
326
  // Phase 4.5b: Method Resolution Order
297
327
  onProgress({
298
328
  phase: 'parsing',
@@ -325,7 +355,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
325
355
  }
326
356
  communityResult.communities.forEach(comm => {
327
357
  graph.addNode({
328
- id: comm.id,
358
+ id: toNodeId(comm.id),
329
359
  label: 'Community',
330
360
  properties: {
331
361
  name: comm.label,
@@ -338,10 +368,10 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
338
368
  });
339
369
  communityResult.memberships.forEach(membership => {
340
370
  graph.addRelationship({
341
- id: `${membership.nodeId}_member_of_${membership.communityId}`,
371
+ id: toEdgeId(`${membership.nodeId}_member_of_${membership.communityId}`),
342
372
  type: 'MEMBER_OF',
343
- sourceId: membership.nodeId,
344
- targetId: membership.communityId,
373
+ sourceId: toNodeId(membership.nodeId),
374
+ targetId: toNodeId(membership.communityId),
345
375
  confidence: 1.0,
346
376
  reason: 'leiden-algorithm',
347
377
  });
@@ -371,7 +401,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
371
401
  }
372
402
  processResult.processes.forEach(proc => {
373
403
  graph.addNode({
374
- id: proc.id,
404
+ id: toNodeId(proc.id),
375
405
  label: 'Process',
376
406
  properties: {
377
407
  name: proc.label,
@@ -387,10 +417,10 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
387
417
  });
388
418
  processResult.steps.forEach(step => {
389
419
  graph.addRelationship({
390
- id: `${step.nodeId}_step_${step.step}_${step.processId}`,
420
+ id: toEdgeId(`${step.nodeId}_step_${step.step}_${step.processId}`),
391
421
  type: 'STEP_IN_PROCESS',
392
- sourceId: step.nodeId,
393
- targetId: step.processId,
422
+ sourceId: toNodeId(step.nodeId),
423
+ targetId: toNodeId(step.processId),
394
424
  confidence: 1.0,
395
425
  reason: 'trace-detection',
396
426
  step: step.step,
@@ -407,7 +437,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress) => {
407
437
  },
408
438
  });
409
439
  astCache.clear();
410
- return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult };
440
+ return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult, tsgoEnabled: tsgoWasUsed };
411
441
  }
412
442
  catch (error) {
413
443
  cleanup();
@@ -1,6 +1,6 @@
1
1
  /** @file process-processor.ts @description Detects execution flows (Processes) by finding entry points, tracing forward via confidence-gated beam search, deduplicating similar paths, and labeling with heuristic names */
2
- import { KnowledgeGraph } from '../graph/types.js';
3
- import { CommunityMembership } from './community-processor.js';
2
+ import type { KnowledgeGraph } from '../graph/types.js';
3
+ import type { CommunityMembership } from './community-processor.js';
4
4
  export interface ProcessDetectionConfig {
5
5
  maxTraceDepth: number;
6
6
  maxProcesses: number;
@@ -1,8 +1,8 @@
1
1
  // code-mapper/src/core/ingestion/process-processor.ts
2
2
  /** @file process-processor.ts @description Detects execution flows (Processes) by finding entry points, tracing forward via confidence-gated beam search, deduplicating similar paths, and labeling with heuristic names */
3
- import { calculateEntryPointScore, isTestFile } from './entry-point-scoring.js';
3
+ import { calculateEntryPointScore } from './entry-point-scoring.js';
4
4
  import { SupportedLanguages } from '../../config/supported-languages.js';
5
- const isDev = process.env.NODE_ENV === 'development';
5
+ const isDev = process.env['NODE_ENV'] === 'development';
6
6
  const DEFAULT_CONFIG = {
7
7
  maxTraceDepth: 20,
8
8
  maxProcesses: 120,
@@ -30,6 +30,8 @@ export const processProcesses = async (knowledgeGraph, memberships, onProgress,
30
30
  const allTraces = [];
31
31
  for (let i = 0; i < entryPoints.length && allTraces.length < cfg.maxProcesses * 2; i++) {
32
32
  const entryId = entryPoints[i];
33
+ if (entryId === undefined)
34
+ continue;
33
35
  const traces = traceFromEntryPoint(entryId, callsEdges, cfg);
34
36
  // Filter out traces that are too short
35
37
  traces.filter(t => t.length >= cfg.minSteps).forEach(t => allTraces.push(t));
@@ -64,6 +66,8 @@ export const processProcesses = async (knowledgeGraph, memberships, onProgress,
64
66
  limitedTraces.forEach((trace, idx) => {
65
67
  const entryPointId = trace[0];
66
68
  const terminalId = trace[trace.length - 1];
69
+ if (entryPointId === undefined || terminalId === undefined)
70
+ return;
67
71
  // Get communities touched
68
72
  const communitiesSet = new Set();
69
73
  trace.forEach(nodeId => {
@@ -126,10 +130,12 @@ const buildCallsGraph = (graph, minEdgeConfidence) => {
126
130
  const adj = new Map();
127
131
  for (const rel of graph.iterRelationships()) {
128
132
  if (rel.type === 'CALLS' && rel.confidence >= minEdgeConfidence) {
129
- if (!adj.has(rel.sourceId)) {
130
- adj.set(rel.sourceId, []);
133
+ let edges = adj.get(rel.sourceId);
134
+ if (edges === undefined) {
135
+ edges = [];
136
+ adj.set(rel.sourceId, edges);
131
137
  }
132
- adj.get(rel.sourceId).push({ targetId: rel.targetId, confidence: rel.confidence });
138
+ edges.push({ targetId: rel.targetId, confidence: rel.confidence });
133
139
  }
134
140
  }
135
141
  // Sort each caller's callees by confidence descending — beam search explores
@@ -143,10 +149,12 @@ const buildReverseCallsGraph = (graph, minEdgeConfidence) => {
143
149
  const adj = new Map();
144
150
  for (const rel of graph.iterRelationships()) {
145
151
  if (rel.type === 'CALLS' && rel.confidence >= minEdgeConfidence) {
146
- if (!adj.has(rel.targetId)) {
147
- adj.set(rel.targetId, []);
152
+ let callers = adj.get(rel.targetId);
153
+ if (callers === undefined) {
154
+ callers = [];
155
+ adj.set(rel.targetId, callers);
148
156
  }
149
- adj.get(rel.targetId).push(rel.sourceId);
157
+ callers.push(rel.sourceId);
150
158
  }
151
159
  }
152
160
  return adj;
@@ -163,9 +171,6 @@ const findEntryPoints = (graph, reverseCallsEdges, callsEdges) => {
163
171
  if (!symbolTypes.has(node.label))
164
172
  continue;
165
173
  const filePath = node.properties.filePath || '';
166
- // Skip test files entirely
167
- if (isTestFile(filePath))
168
- continue;
169
174
  const callers = reverseCallsEdges.get(node.id) || [];
170
175
  const calleeEdges = callsEdges.get(node.id) || [];
171
176
  // Must have at least 1 outgoing call to trace forward
@@ -221,6 +226,8 @@ const traceFromEntryPoint = (entryId, callsEdges, config) => {
221
226
  while (beam.length > 0) {
222
227
  // Pop highest cumulative confidence
223
228
  const entry = beam.shift();
229
+ if (entry === undefined)
230
+ break;
224
231
  const edges = callsEdges.get(entry.currentId) || [];
225
232
  // Filter edges by per-edge confidence gate
226
233
  const viable = edges.filter(e => e.confidence >= config.minEdgeConfidence);
@@ -254,7 +261,10 @@ const traceFromEntryPoint = (entryId, callsEdges, config) => {
254
261
  let hi = beam.length;
255
262
  while (lo < hi) {
256
263
  const mid = (lo + hi) >>> 1;
257
- if (beam[mid].cumulativeConf > childConf)
264
+ const midEntry = beam[mid];
265
+ if (midEntry === undefined)
266
+ break;
267
+ if (midEntry.cumulativeConf > childConf)
258
268
  lo = mid + 1;
259
269
  else
260
270
  hi = mid;
@@ -325,13 +335,17 @@ const mergeSimilarTraces = (traces) => {
325
335
  for (let i = 0; i < sorted.length; i++) {
326
336
  if (removed.has(i))
327
337
  continue;
328
- kept.push(sorted[i]);
338
+ const longer = sorted[i];
339
+ if (longer === undefined)
340
+ continue;
341
+ kept.push(longer);
329
342
  // Check remaining traces for high overlap with this one
330
343
  for (let j = i + 1; j < sorted.length; j++) {
331
344
  if (removed.has(j))
332
345
  continue;
333
346
  const shorter = sorted[j];
334
- const longer = sorted[i];
347
+ if (shorter === undefined)
348
+ continue;
335
349
  // Count shared steps
336
350
  const longerSet = new Set(longer);
337
351
  let shared = 0;
@@ -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 = 'same-file' | 'import-scoped' | 'global';
14
+ export type ResolutionTier = 'tsgo-resolved' | 'same-file' | 'import-scoped' | 'global';
15
15
  export interface TieredCandidates {
16
16
  readonly candidates: readonly SymbolDefinition[];
17
17
  readonly tier: ResolutionTier;
@@ -15,6 +15,7 @@ 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,
18
19
  'same-file': 0.95,
19
20
  'import-scoped': 0.9,
20
21
  'global': 0.5,
@@ -70,10 +71,19 @@ export const createResolutionContext = () => {
70
71
  return { candidates: importedDefs, tier: 'import-scoped' };
71
72
  }
72
73
  }
73
- // Last resort: if there's exactly one global definition and it's a Function, use it
74
- // This handles cross-file calls where the import map fails but the symbol is unique
75
- if (fuzzyDefs.length === 1 && (fuzzyDefs[0].type === 'Function' || fuzzyDefs[0].type === 'Method')) {
76
- return { candidates: fuzzyDefs, tier: 'global' };
74
+ // Last resort: if there are few global definitions that are callable, use them
75
+ // This handles cross-file calls where the import map fails but the symbol is resolvable
76
+ const callableDefs = fuzzyDefs.filter(d => d.type === 'Function' || d.type === 'Method' || d.type === 'Class' ||
77
+ d.type === 'Constructor' || d.type === 'Interface');
78
+ if (callableDefs.length === 1) {
79
+ return { candidates: callableDefs, tier: 'global' };
80
+ }
81
+ // If all callable defs point to the same nodeId (duplicates from re-exports), deduplicate
82
+ if (callableDefs.length > 1) {
83
+ const uniqueIds = new Set(callableDefs.map(d => d.nodeId));
84
+ if (uniqueIds.size === 1) {
85
+ return { candidates: [callableDefs[0]], tier: 'global' };
86
+ }
77
87
  }
78
88
  }
79
89
  return null;
@@ -56,14 +56,15 @@ export function resolveCSharpImport(importPath, csharpConfigs, normalizedFileLis
56
56
  const dirTrail = dirPrefix + '/';
57
57
  for (let i = 0; i < normalizedFileList.length; i++) {
58
58
  const normalized = normalizedFileList[i];
59
- if (!normalized.endsWith('.cs'))
59
+ if (!normalized || !normalized.endsWith('.cs'))
60
60
  continue;
61
61
  const prefixIdx = normalized.indexOf(dirTrail);
62
62
  if (prefixIdx < 0)
63
63
  continue;
64
64
  const afterDir = normalized.substring(prefixIdx + dirTrail.length);
65
- if (!afterDir.includes('/')) {
66
- results.push(allFileList[i]);
65
+ const original = allFileList[i];
66
+ if (!afterDir.includes('/') && original) {
67
+ results.push(original);
67
68
  }
68
69
  }
69
70
  if (results.length > 0)
@@ -25,7 +25,9 @@ export function resolveGoPackage(importPath, goModule, normalizedFileList, allFi
25
25
  if (normalized.includes(pkgSuffix) && normalized.endsWith('.go') && !normalized.endsWith('_test.go')) {
26
26
  const afterPkg = normalized.substring(normalized.indexOf(pkgSuffix) + pkgSuffix.length);
27
27
  if (!afterPkg.includes('/')) {
28
- matches.push(allFileList[i]);
28
+ const original = allFileList[i];
29
+ if (original !== undefined)
30
+ matches.push(original);
29
31
  }
30
32
  }
31
33
  }
@@ -33,11 +33,15 @@ export function resolveJvmWildcard(importPath, normalizedFileList, allFileList,
33
33
  const matches = [];
34
34
  for (let i = 0; i < normalizedFileList.length; i++) {
35
35
  const normalized = normalizedFileList[i];
36
+ if (normalized === undefined)
37
+ continue;
36
38
  if (normalized.includes(packageSuffix) &&
37
39
  extensions.some(ext => normalized.endsWith(ext))) {
38
40
  const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length);
39
41
  if (!afterPackage.includes('/')) {
40
- matches.push(allFileList[i]);
42
+ const original = allFileList[i];
43
+ if (original !== undefined)
44
+ matches.push(original);
41
45
  }
42
46
  }
43
47
  }
@@ -50,6 +54,8 @@ export function resolveJvmMemberImport(importPath, normalizedFileList, allFileLi
50
54
  if (segments.length < 3)
51
55
  return null;
52
56
  const lastSeg = segments[segments.length - 1];
57
+ if (lastSeg === undefined)
58
+ return null;
53
59
  if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) {
54
60
  const classPath = segments.slice(0, -1).join('/');
55
61
  for (const ext of extensions) {
@@ -62,9 +68,12 @@ export function resolveJvmMemberImport(importPath, normalizedFileList, allFileLi
62
68
  else {
63
69
  const fullSuffix = '/' + classSuffix;
64
70
  for (let i = 0; i < normalizedFileList.length; i++) {
65
- if (normalizedFileList[i].endsWith(fullSuffix) ||
66
- normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) {
67
- return allFileList[i];
71
+ const normalized = normalizedFileList[i];
72
+ if (normalized === undefined)
73
+ continue;
74
+ if (normalized.endsWith(fullSuffix) ||
75
+ normalized.toLowerCase().endsWith(fullSuffix.toLowerCase())) {
76
+ return allFileList[i] ?? null;
68
77
  }
69
78
  }
70
79
  }
@@ -82,8 +82,8 @@ export const resolveImportPath = (currentFile, importPath, allFiles, allFileList
82
82
  if (language === SupportedLanguages.Python && importPath.startsWith('.')) {
83
83
  const dotMatch = importPath.match(/^(\.+)(.*)/);
84
84
  if (dotMatch) {
85
- const dotCount = dotMatch[1].length;
86
- const modulePart = dotMatch[2]; // e.g., "models" from ".models"
85
+ const dotCount = dotMatch[1]?.length ?? 0;
86
+ const modulePart = dotMatch[2] ?? ''; // e.g., "models" from ".models"
87
87
  const dirParts = currentFile.split('/').slice(0, -1);
88
88
  // 1 dot = same package, each additional dot goes up one level
89
89
  for (let i = 1; i < dotCount; i++) {
@@ -43,6 +43,8 @@ export function buildSuffixIndex(normalizedFileList, allFileList) {
43
43
  for (let i = 0; i < normalizedFileList.length; i++) {
44
44
  const normalized = normalizedFileList[i];
45
45
  const original = allFileList[i];
46
+ if (normalized === undefined || original === undefined)
47
+ continue;
46
48
  const parts = normalized.split('/');
47
49
  // Index all suffixes: "a/b/c.java" -> "c.java", "b/c.java", "a/b/c.java"
48
50
  for (let j = parts.length - 1; j >= 0; j--) {
@@ -61,7 +63,9 @@ export function buildSuffixIndex(normalizedFileList, allFileList) {
61
63
  if (lastSlash >= 0) {
62
64
  // Build all directory suffix variants
63
65
  const dirParts = parts.slice(0, -1);
64
- const fileName = parts[parts.length - 1];
66
+ const fileName = parts.at(-1);
67
+ if (fileName === undefined)
68
+ continue;
65
69
  const ext = fileName.substring(fileName.lastIndexOf('.'));
66
70
  for (let j = dirParts.length - 1; j >= 0; j--) {
67
71
  const dirSuffix = dirParts.slice(j).join('/');
@@ -105,7 +109,7 @@ export function suffixResolve(pathParts, normalizedFileList, allFileList, index)
105
109
  const suffixPattern = '/' + suffixWithExt;
106
110
  const matchIdx = normalizedFileList.findIndex(filePath => filePath.endsWith(suffixPattern) || filePath.toLowerCase().endsWith(suffixPattern.toLowerCase()));
107
111
  if (matchIdx !== -1) {
108
- return allFileList[matchIdx];
112
+ return allFileList[matchIdx] ?? null;
109
113
  }
110
114
  }
111
115
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @file HTTP route stitching — connects client-side URL builders to server-side route handlers
3
+ *
4
+ * Runs AFTER DB loading (needs content field populated by graph-loader).
5
+ * Queries the DB directly for node content with route patterns.
6
+ *
7
+ * Strategy: "meet in the middle" via canonical URL patterns.
8
+ * Works for Express, Fastify, Koa, Hono, or any framework using router.method(path, handler)
9
+ */
10
+ import type Database from 'better-sqlite3';
11
+ /**
12
+ * Stitch client-side API path functions to server-side route handlers.
13
+ * Runs after loadGraphToDb — queries SQLite content field directly.
14
+ */
15
+ export declare function stitchRoutes(db: Database.Database): number;
@@ -0,0 +1,92 @@
1
+ // code-mapper/src/core/ingestion/route-stitcher.ts
2
+ /**
3
+ * @file HTTP route stitching — connects client-side URL builders to server-side route handlers
4
+ *
5
+ * Runs AFTER DB loading (needs content field populated by graph-loader).
6
+ * Queries the DB directly for node content with route patterns.
7
+ *
8
+ * Strategy: "meet in the middle" via canonical URL patterns.
9
+ * Works for Express, Fastify, Koa, Hono, or any framework using router.method(path, handler)
10
+ */
11
+ import { toEdgeId } from '../db/schema.js';
12
+ import { insertEdgesBatch } from '../db/adapter.js';
13
+ /** Normalize a URL pattern to canonical form for matching */
14
+ function canonicalizeRoute(pattern) {
15
+ return pattern
16
+ .replace(/['"``]/g, '')
17
+ .replace(/:[a-zA-Z_]\w*/g, ':p') // Express params :paramName → :p
18
+ .replace(/\$\{[^}]+\}/g, ':p') // Template literals ${var} → :p
19
+ .replace(/\/+$/, '') // Strip trailing slashes
20
+ .replace(/\/+/g, '/') // Collapse double slashes
21
+ .toLowerCase();
22
+ }
23
+ /**
24
+ * Stitch client-side API path functions to server-side route handlers.
25
+ * Runs after loadGraphToDb — queries SQLite content field directly.
26
+ */
27
+ export function stitchRoutes(db) {
28
+ const serverRoutes = [];
29
+ const clientRoutes = [];
30
+ // Regex for route registrations in server File nodes
31
+ const ROUTE_REG = /\.(get|post|put|patch|delete|options)\s*\(\s*['"`](\/[^'"`\n]+)['"`]/gi;
32
+ // Regex for template literal URLs in client Function nodes
33
+ const TEMPLATE_URL = /`(\/(?:api|v\d+)\/[^`\n]+)`/g;
34
+ // String literal URLs (fetch('/api/...'))
35
+ const STRING_URL = /['"](\/(api|v\d+)\/[^'"]+)['"]/g;
36
+ // Server: scan File nodes for route registrations
37
+ const serverNodes = db.prepare(`SELECT id, name, filePath, content FROM nodes WHERE label = 'File' AND content LIKE '%/api/%'`).all();
38
+ for (const node of serverNodes) {
39
+ let m;
40
+ ROUTE_REG.lastIndex = 0;
41
+ while ((m = ROUTE_REG.exec(node.content)) !== null) {
42
+ const routePattern = canonicalizeRoute(m[2]);
43
+ if (routePattern.includes('/api/') || routePattern.includes('/v1/') || routePattern.includes('/v2/')) {
44
+ serverRoutes.push({ pattern: routePattern, nodeId: node.id, filePath: node.filePath, name: node.name, side: 'server' });
45
+ }
46
+ }
47
+ }
48
+ // Client: scan Function nodes for template URLs
49
+ const clientNodes = db.prepare(`SELECT id, name, filePath, content FROM nodes WHERE label = 'Function' AND content LIKE '%/api/%'`).all();
50
+ for (const node of clientNodes) {
51
+ let m;
52
+ TEMPLATE_URL.lastIndex = 0;
53
+ while ((m = TEMPLATE_URL.exec(node.content)) !== null) {
54
+ const routePattern = canonicalizeRoute(m[1]);
55
+ if (routePattern.length > 3) {
56
+ clientRoutes.push({ pattern: routePattern, nodeId: node.id, filePath: node.filePath, name: node.name, side: 'client' });
57
+ }
58
+ }
59
+ STRING_URL.lastIndex = 0;
60
+ while ((m = STRING_URL.exec(node.content)) !== null) {
61
+ const routePattern = canonicalizeRoute(m[1]);
62
+ if (routePattern.length > 3) {
63
+ clientRoutes.push({ pattern: routePattern, nodeId: node.id, filePath: node.filePath, name: node.name, side: 'client' });
64
+ }
65
+ }
66
+ }
67
+ // Match client patterns to server patterns
68
+ const edges = [];
69
+ const seen = new Set();
70
+ for (const client of clientRoutes) {
71
+ for (const server of serverRoutes) {
72
+ if (client.pattern === server.pattern && client.filePath !== server.filePath) {
73
+ const edgeKey = `${client.nodeId}->${server.nodeId}`;
74
+ if (seen.has(edgeKey))
75
+ continue;
76
+ seen.add(edgeKey);
77
+ edges.push({
78
+ id: toEdgeId(`CALLS:route-stitch:${edgeKey}`),
79
+ sourceId: client.nodeId,
80
+ targetId: server.nodeId,
81
+ type: 'CALLS',
82
+ confidence: 0.85,
83
+ reason: 'http-route-match',
84
+ });
85
+ }
86
+ }
87
+ }
88
+ if (edges.length > 0) {
89
+ insertEdgesBatch(db, edges);
90
+ }
91
+ return edges.length;
92
+ }
@@ -1,4 +1,4 @@
1
1
  /** @file structure-processor.ts @description Builds the file/folder tree structure in the knowledge graph by creating File and Folder nodes with CONTAINS relationships */
2
- import { KnowledgeGraph } from "../graph/types.js";
2
+ import type { KnowledgeGraph } from "../graph/types.js";
3
3
  /** Build File/Folder nodes and CONTAINS edges from a list of file paths */
4
4
  export declare const processStructure: (graph: KnowledgeGraph, paths: string[]) => void;
@@ -1,12 +1,13 @@
1
1
  // code-mapper/src/core/ingestion/structure-processor.ts
2
2
  /** @file structure-processor.ts @description Builds the file/folder tree structure in the knowledge graph by creating File and Folder nodes with CONTAINS relationships */
3
3
  import { generateId } from "../../lib/utils.js";
4
+ import { toEdgeId } from "../db/schema.js";
4
5
  /** Build File/Folder nodes and CONTAINS edges from a list of file paths */
5
6
  export const processStructure = (graph, paths) => {
6
7
  paths.forEach(path => {
7
8
  const parts = path.split('/');
8
9
  let currentPath = '';
9
- let parentId = '';
10
+ let parentId = null;
10
11
  parts.forEach((part, index) => {
11
12
  const isFile = index === parts.length - 1;
12
13
  const label = isFile ? 'File' : 'Folder';
@@ -22,7 +23,7 @@ export const processStructure = (graph, paths) => {
22
23
  };
23
24
  graph.addNode(node);
24
25
  if (parentId) {
25
- const relId = generateId('CONTAINS', `${parentId}->${nodeId}`);
26
+ const relId = toEdgeId(`CONTAINS:${parentId}->${nodeId}`);
26
27
  const relationship = {
27
28
  id: relId,
28
29
  type: 'CONTAINS',
@@ -22,6 +22,8 @@ export interface SymbolTable {
22
22
  lookupExactFull: (filePath: string, name: string) => SymbolDefinition | undefined;
23
23
  /** Look up a symbol anywhere in the project (low confidence, used for missing imports) */
24
24
  lookupFuzzy: (name: string) => SymbolDefinition[];
25
+ /** Return all symbol definitions registered for a given file */
26
+ lookupAllInFile: (filePath: string) => SymbolDefinition[];
25
27
  /** Return file count and global symbol count for debugging */
26
28
  getStats: () => {
27
29
  fileCount: number;
@@ -36,6 +36,10 @@ export const createSymbolTable = () => {
36
36
  const lookupFuzzy = (name) => {
37
37
  return globalIndex.get(name) || [];
38
38
  };
39
+ const lookupAllInFile = (filePath) => {
40
+ const fileMap = fileIndex.get(filePath);
41
+ return fileMap ? Array.from(fileMap.values()) : [];
42
+ };
39
43
  const getStats = () => ({
40
44
  fileCount: fileIndex.size,
41
45
  globalSymbolCount: globalIndex.size
@@ -44,5 +48,5 @@ export const createSymbolTable = () => {
44
48
  fileIndex.clear();
45
49
  globalIndex.clear();
46
50
  };
47
- return { add, lookupExact, lookupExactFull, lookupFuzzy, getStats, clear };
51
+ return { add, lookupExact, lookupExactFull, lookupFuzzy, lookupAllInFile, getStats, clear };
48
52
  };