@zuvia-software-solutions/code-mapper 1.4.0

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 (213) hide show
  1. package/README.md +215 -0
  2. package/dist/cli/ai-context.d.ts +19 -0
  3. package/dist/cli/ai-context.js +168 -0
  4. package/dist/cli/analyze.d.ts +7 -0
  5. package/dist/cli/analyze.js +325 -0
  6. package/dist/cli/augment.d.ts +7 -0
  7. package/dist/cli/augment.js +27 -0
  8. package/dist/cli/clean.d.ts +5 -0
  9. package/dist/cli/clean.js +56 -0
  10. package/dist/cli/eval-server.d.ts +25 -0
  11. package/dist/cli/eval-server.js +365 -0
  12. package/dist/cli/index.d.ts +6 -0
  13. package/dist/cli/index.js +102 -0
  14. package/dist/cli/lazy-action.d.ts +6 -0
  15. package/dist/cli/lazy-action.js +19 -0
  16. package/dist/cli/list.d.ts +2 -0
  17. package/dist/cli/list.js +27 -0
  18. package/dist/cli/mcp.d.ts +8 -0
  19. package/dist/cli/mcp.js +35 -0
  20. package/dist/cli/refresh.d.ts +12 -0
  21. package/dist/cli/refresh.js +165 -0
  22. package/dist/cli/serve.d.ts +5 -0
  23. package/dist/cli/serve.js +8 -0
  24. package/dist/cli/setup.d.ts +6 -0
  25. package/dist/cli/setup.js +218 -0
  26. package/dist/cli/status.d.ts +2 -0
  27. package/dist/cli/status.js +33 -0
  28. package/dist/cli/tool.d.ts +28 -0
  29. package/dist/cli/tool.js +87 -0
  30. package/dist/config/ignore-service.d.ts +32 -0
  31. package/dist/config/ignore-service.js +282 -0
  32. package/dist/config/supported-languages.d.ts +23 -0
  33. package/dist/config/supported-languages.js +52 -0
  34. package/dist/core/augmentation/engine.d.ts +22 -0
  35. package/dist/core/augmentation/engine.js +232 -0
  36. package/dist/core/embeddings/embedder.d.ts +35 -0
  37. package/dist/core/embeddings/embedder.js +171 -0
  38. package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
  39. package/dist/core/embeddings/embedding-pipeline.js +402 -0
  40. package/dist/core/embeddings/index.d.ts +5 -0
  41. package/dist/core/embeddings/index.js +6 -0
  42. package/dist/core/embeddings/text-generator.d.ts +20 -0
  43. package/dist/core/embeddings/text-generator.js +159 -0
  44. package/dist/core/embeddings/types.d.ts +60 -0
  45. package/dist/core/embeddings/types.js +23 -0
  46. package/dist/core/graph/graph.d.ts +4 -0
  47. package/dist/core/graph/graph.js +65 -0
  48. package/dist/core/graph/types.d.ts +69 -0
  49. package/dist/core/graph/types.js +3 -0
  50. package/dist/core/incremental/child-process.d.ts +8 -0
  51. package/dist/core/incremental/child-process.js +649 -0
  52. package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
  53. package/dist/core/incremental/refresh-coordinator.js +147 -0
  54. package/dist/core/incremental/types.d.ts +78 -0
  55. package/dist/core/incremental/types.js +153 -0
  56. package/dist/core/incremental/watcher.d.ts +63 -0
  57. package/dist/core/incremental/watcher.js +338 -0
  58. package/dist/core/ingestion/ast-cache.d.ts +12 -0
  59. package/dist/core/ingestion/ast-cache.js +34 -0
  60. package/dist/core/ingestion/call-processor.d.ts +34 -0
  61. package/dist/core/ingestion/call-processor.js +937 -0
  62. package/dist/core/ingestion/call-routing.d.ts +40 -0
  63. package/dist/core/ingestion/call-routing.js +97 -0
  64. package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
  65. package/dist/core/ingestion/cluster-enricher.js +151 -0
  66. package/dist/core/ingestion/community-processor.d.ts +26 -0
  67. package/dist/core/ingestion/community-processor.js +272 -0
  68. package/dist/core/ingestion/constants.d.ts +5 -0
  69. package/dist/core/ingestion/constants.js +8 -0
  70. package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
  71. package/dist/core/ingestion/entry-point-scoring.js +317 -0
  72. package/dist/core/ingestion/export-detection.d.ts +11 -0
  73. package/dist/core/ingestion/export-detection.js +203 -0
  74. package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
  75. package/dist/core/ingestion/filesystem-walker.js +64 -0
  76. package/dist/core/ingestion/framework-detection.d.ts +42 -0
  77. package/dist/core/ingestion/framework-detection.js +405 -0
  78. package/dist/core/ingestion/heritage-processor.d.ts +15 -0
  79. package/dist/core/ingestion/heritage-processor.js +237 -0
  80. package/dist/core/ingestion/import-processor.d.ts +31 -0
  81. package/dist/core/ingestion/import-processor.js +416 -0
  82. package/dist/core/ingestion/language-config.d.ts +32 -0
  83. package/dist/core/ingestion/language-config.js +161 -0
  84. package/dist/core/ingestion/mro-processor.d.ts +32 -0
  85. package/dist/core/ingestion/mro-processor.js +343 -0
  86. package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
  87. package/dist/core/ingestion/named-binding-extraction.js +343 -0
  88. package/dist/core/ingestion/parsing-processor.d.ts +20 -0
  89. package/dist/core/ingestion/parsing-processor.js +282 -0
  90. package/dist/core/ingestion/pipeline.d.ts +3 -0
  91. package/dist/core/ingestion/pipeline.js +416 -0
  92. package/dist/core/ingestion/process-processor.d.ts +42 -0
  93. package/dist/core/ingestion/process-processor.js +357 -0
  94. package/dist/core/ingestion/resolution-context.d.ts +40 -0
  95. package/dist/core/ingestion/resolution-context.js +171 -0
  96. package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
  97. package/dist/core/ingestion/resolvers/csharp.js +101 -0
  98. package/dist/core/ingestion/resolvers/go.d.ts +8 -0
  99. package/dist/core/ingestion/resolvers/go.js +33 -0
  100. package/dist/core/ingestion/resolvers/index.d.ts +14 -0
  101. package/dist/core/ingestion/resolvers/index.js +10 -0
  102. package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
  103. package/dist/core/ingestion/resolvers/jvm.js +74 -0
  104. package/dist/core/ingestion/resolvers/php.d.ts +7 -0
  105. package/dist/core/ingestion/resolvers/php.js +30 -0
  106. package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
  107. package/dist/core/ingestion/resolvers/ruby.js +13 -0
  108. package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
  109. package/dist/core/ingestion/resolvers/rust.js +62 -0
  110. package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
  111. package/dist/core/ingestion/resolvers/standard.js +144 -0
  112. package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
  113. package/dist/core/ingestion/resolvers/utils.js +113 -0
  114. package/dist/core/ingestion/structure-processor.d.ts +4 -0
  115. package/dist/core/ingestion/structure-processor.js +39 -0
  116. package/dist/core/ingestion/symbol-table.d.ts +34 -0
  117. package/dist/core/ingestion/symbol-table.js +48 -0
  118. package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
  119. package/dist/core/ingestion/tree-sitter-queries.js +691 -0
  120. package/dist/core/ingestion/type-env.d.ts +52 -0
  121. package/dist/core/ingestion/type-env.js +349 -0
  122. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
  123. package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
  124. package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
  125. package/dist/core/ingestion/type-extractors/csharp.js +224 -0
  126. package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
  127. package/dist/core/ingestion/type-extractors/go.js +261 -0
  128. package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
  129. package/dist/core/ingestion/type-extractors/index.js +30 -0
  130. package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
  131. package/dist/core/ingestion/type-extractors/jvm.js +386 -0
  132. package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
  133. package/dist/core/ingestion/type-extractors/php.js +280 -0
  134. package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
  135. package/dist/core/ingestion/type-extractors/python.js +175 -0
  136. package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
  137. package/dist/core/ingestion/type-extractors/ruby.js +218 -0
  138. package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
  139. package/dist/core/ingestion/type-extractors/rust.js +290 -0
  140. package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
  141. package/dist/core/ingestion/type-extractors/shared.js +322 -0
  142. package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
  143. package/dist/core/ingestion/type-extractors/swift.js +140 -0
  144. package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
  145. package/dist/core/ingestion/type-extractors/types.js +4 -0
  146. package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
  147. package/dist/core/ingestion/type-extractors/typescript.js +227 -0
  148. package/dist/core/ingestion/utils.d.ts +73 -0
  149. package/dist/core/ingestion/utils.js +992 -0
  150. package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
  151. package/dist/core/ingestion/workers/parse-worker.js +1055 -0
  152. package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
  153. package/dist/core/ingestion/workers/worker-pool.js +123 -0
  154. package/dist/core/lbug/csv-generator.d.ts +28 -0
  155. package/dist/core/lbug/csv-generator.js +355 -0
  156. package/dist/core/lbug/lbug-adapter.d.ts +96 -0
  157. package/dist/core/lbug/lbug-adapter.js +753 -0
  158. package/dist/core/lbug/schema.d.ts +46 -0
  159. package/dist/core/lbug/schema.js +402 -0
  160. package/dist/core/search/bm25-index.d.ts +20 -0
  161. package/dist/core/search/bm25-index.js +123 -0
  162. package/dist/core/search/hybrid-search.d.ts +32 -0
  163. package/dist/core/search/hybrid-search.js +131 -0
  164. package/dist/core/search/query-cache.d.ts +18 -0
  165. package/dist/core/search/query-cache.js +47 -0
  166. package/dist/core/search/query-expansion.d.ts +19 -0
  167. package/dist/core/search/query-expansion.js +75 -0
  168. package/dist/core/search/reranker.d.ts +29 -0
  169. package/dist/core/search/reranker.js +122 -0
  170. package/dist/core/search/types.d.ts +154 -0
  171. package/dist/core/search/types.js +51 -0
  172. package/dist/core/semantic/tsgo-service.d.ts +67 -0
  173. package/dist/core/semantic/tsgo-service.js +355 -0
  174. package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
  175. package/dist/core/tree-sitter/parser-loader.js +71 -0
  176. package/dist/lib/memory-guard.d.ts +35 -0
  177. package/dist/lib/memory-guard.js +70 -0
  178. package/dist/lib/utils.d.ts +3 -0
  179. package/dist/lib/utils.js +6 -0
  180. package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
  181. package/dist/mcp/compatible-stdio-transport.js +209 -0
  182. package/dist/mcp/core/embedder.d.ts +24 -0
  183. package/dist/mcp/core/embedder.js +168 -0
  184. package/dist/mcp/core/lbug-adapter.d.ts +29 -0
  185. package/dist/mcp/core/lbug-adapter.js +330 -0
  186. package/dist/mcp/local/local-backend.d.ts +188 -0
  187. package/dist/mcp/local/local-backend.js +2759 -0
  188. package/dist/mcp/resources.d.ts +22 -0
  189. package/dist/mcp/resources.js +379 -0
  190. package/dist/mcp/server.d.ts +10 -0
  191. package/dist/mcp/server.js +217 -0
  192. package/dist/mcp/staleness.d.ts +10 -0
  193. package/dist/mcp/staleness.js +25 -0
  194. package/dist/mcp/tools.d.ts +21 -0
  195. package/dist/mcp/tools.js +202 -0
  196. package/dist/server/api.d.ts +5 -0
  197. package/dist/server/api.js +340 -0
  198. package/dist/server/mcp-http.d.ts +7 -0
  199. package/dist/server/mcp-http.js +95 -0
  200. package/dist/storage/git.d.ts +6 -0
  201. package/dist/storage/git.js +35 -0
  202. package/dist/storage/repo-manager.d.ts +87 -0
  203. package/dist/storage/repo-manager.js +249 -0
  204. package/dist/types/pipeline.d.ts +35 -0
  205. package/dist/types/pipeline.js +20 -0
  206. package/hooks/claude/code-mapper-hook.cjs +238 -0
  207. package/hooks/claude/pre-tool-use.sh +79 -0
  208. package/hooks/claude/session-start.sh +42 -0
  209. package/models/mlx-embedder.py +185 -0
  210. package/package.json +100 -0
  211. package/scripts/patch-tree-sitter-swift.cjs +74 -0
  212. package/vendor/leiden/index.cjs +355 -0
  213. package/vendor/leiden/utils.cjs +392 -0
@@ -0,0 +1,357 @@
1
+ // code-mapper/src/core/ingestion/process-processor.ts
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';
4
+ import { SupportedLanguages } from '../../config/supported-languages.js';
5
+ const isDev = process.env.NODE_ENV === 'development';
6
+ const DEFAULT_CONFIG = {
7
+ maxTraceDepth: 20,
8
+ maxProcesses: 120,
9
+ minSteps: 2,
10
+ minEdgeConfidence: 0.5,
11
+ minPathConfidence: 0.1,
12
+ maxBeamSize: 5000,
13
+ };
14
+ /** Detect processes (execution flows) in the knowledge graph, runs after community detection */
15
+ export const processProcesses = async (knowledgeGraph, memberships, onProgress, config = {}) => {
16
+ const cfg = { ...DEFAULT_CONFIG, ...config };
17
+ onProgress?.('Finding entry points...', 0);
18
+ // Build lookup maps
19
+ const membershipMap = new Map();
20
+ memberships.forEach(m => membershipMap.set(m.nodeId, m.communityId));
21
+ const callsEdges = buildCallsGraph(knowledgeGraph, cfg.minEdgeConfidence);
22
+ const reverseCallsEdges = buildReverseCallsGraph(knowledgeGraph, cfg.minEdgeConfidence);
23
+ const nodeMap = new Map();
24
+ for (const n of knowledgeGraph.iterNodes())
25
+ nodeMap.set(n.id, n);
26
+ // Find entry points (functions that call others but have few callers)
27
+ const entryPoints = findEntryPoints(knowledgeGraph, reverseCallsEdges, callsEdges);
28
+ onProgress?.(`Found ${entryPoints.length} entry points, tracing flows...`, 20);
29
+ // Trace processes from each entry point
30
+ const allTraces = [];
31
+ for (let i = 0; i < entryPoints.length && allTraces.length < cfg.maxProcesses * 2; i++) {
32
+ const entryId = entryPoints[i];
33
+ const traces = traceFromEntryPoint(entryId, callsEdges, cfg);
34
+ // Filter out traces that are too short
35
+ traces.filter(t => t.length >= cfg.minSteps).forEach(t => allTraces.push(t));
36
+ if (i % 10 === 0) {
37
+ onProgress?.(`Tracing entry point ${i + 1}/${entryPoints.length}...`, 20 + (i / entryPoints.length) * 40);
38
+ }
39
+ }
40
+ onProgress?.(`Found ${allTraces.length} traces, deduplicating...`, 60);
41
+ // Deduplicate similar traces (subset removal)
42
+ const uniqueTraces = deduplicateTraces(allTraces);
43
+ // Deduplicate by entry+terminal pair (keep longest path per pair)
44
+ const endpointDeduped = deduplicateByEndpoints(uniqueTraces);
45
+ // D4: Merge traces that share ≥ 80% of their steps (near-duplicate paths)
46
+ // Two flows like [A,B,C,D,E] and [A,B,C,D,F] share 4/5 = 80% — keep the longer one
47
+ const prefixMerged = mergeSimilarTraces(endpointDeduped);
48
+ onProgress?.(`Deduped ${uniqueTraces.length} → ${prefixMerged.length} unique flows`, 70);
49
+ // Limit to max processes (prioritize longer traces)
50
+ const limitedTraces = prefixMerged
51
+ .sort((a, b) => b.length - a.length)
52
+ .slice(0, cfg.maxProcesses);
53
+ onProgress?.(`Creating ${limitedTraces.length} process nodes...`, 80);
54
+ // Create process nodes
55
+ const processes = [];
56
+ const steps = [];
57
+ // Build communityId -> heuristicLabel map from Community nodes in the graph
58
+ const communityLabels = new Map();
59
+ for (const n of knowledgeGraph.iterNodes()) {
60
+ if (n.label === 'Community') {
61
+ communityLabels.set(n.id, n.properties.heuristicLabel || n.properties.name || 'Unknown');
62
+ }
63
+ }
64
+ limitedTraces.forEach((trace, idx) => {
65
+ const entryPointId = trace[0];
66
+ const terminalId = trace[trace.length - 1];
67
+ // Get communities touched
68
+ const communitiesSet = new Set();
69
+ trace.forEach(nodeId => {
70
+ const comm = membershipMap.get(nodeId);
71
+ if (comm)
72
+ communitiesSet.add(comm);
73
+ });
74
+ const communities = Array.from(communitiesSet);
75
+ // Determine process type
76
+ const processType = communities.length > 1 ? 'cross_community' : 'intra_community';
77
+ // Generate label with module context from entry point's community
78
+ const entryNode = nodeMap.get(entryPointId);
79
+ const terminalNode = nodeMap.get(terminalId);
80
+ const entryName = entryNode?.properties.name || 'Unknown';
81
+ const terminalName = terminalNode?.properties.name || 'Unknown';
82
+ // Find the community for the entry point
83
+ const entryMembership = memberships.find(m => m.nodeId === entryPointId);
84
+ const entryModule = entryMembership ? communityLabels.get(entryMembership.communityId) : undefined;
85
+ const modulePrefix = entryModule && entryModule !== 'Unknown' ? `${entryModule}: ` : '';
86
+ const heuristicLabel = `${modulePrefix}${capitalize(entryName)} → ${capitalize(terminalName)}`;
87
+ const processId = `proc_${idx}_${sanitizeId(entryName)}`;
88
+ processes.push({
89
+ id: processId,
90
+ label: heuristicLabel,
91
+ heuristicLabel,
92
+ processType,
93
+ stepCount: trace.length,
94
+ communities,
95
+ entryPointId,
96
+ terminalId,
97
+ trace,
98
+ });
99
+ // Create step relationships
100
+ trace.forEach((nodeId, stepIdx) => {
101
+ steps.push({
102
+ nodeId,
103
+ processId,
104
+ step: stepIdx + 1,
105
+ });
106
+ });
107
+ });
108
+ onProgress?.('Process detection complete!', 100);
109
+ // Calculate stats
110
+ const crossCommunityCount = processes.filter(p => p.processType === 'cross_community').length;
111
+ const avgStepCount = processes.length > 0
112
+ ? processes.reduce((sum, p) => sum + p.stepCount, 0) / processes.length
113
+ : 0;
114
+ return {
115
+ processes,
116
+ steps,
117
+ stats: {
118
+ totalProcesses: processes.length,
119
+ crossCommunityCount,
120
+ avgStepCount: Math.round(avgStepCount * 10) / 10,
121
+ entryPointsFound: entryPoints.length,
122
+ },
123
+ };
124
+ };
125
+ const buildCallsGraph = (graph, minEdgeConfidence) => {
126
+ const adj = new Map();
127
+ for (const rel of graph.iterRelationships()) {
128
+ if (rel.type === 'CALLS' && rel.confidence >= minEdgeConfidence) {
129
+ if (!adj.has(rel.sourceId)) {
130
+ adj.set(rel.sourceId, []);
131
+ }
132
+ adj.get(rel.sourceId).push({ targetId: rel.targetId, confidence: rel.confidence });
133
+ }
134
+ }
135
+ // Sort each caller's callees by confidence descending — beam search explores
136
+ // highest-confidence edges first within each expansion
137
+ for (const [, edges] of adj) {
138
+ edges.sort((a, b) => b.confidence - a.confidence);
139
+ }
140
+ return adj;
141
+ };
142
+ const buildReverseCallsGraph = (graph, minEdgeConfidence) => {
143
+ const adj = new Map();
144
+ for (const rel of graph.iterRelationships()) {
145
+ if (rel.type === 'CALLS' && rel.confidence >= minEdgeConfidence) {
146
+ if (!adj.has(rel.targetId)) {
147
+ adj.set(rel.targetId, []);
148
+ }
149
+ adj.get(rel.targetId).push(rel.sourceId);
150
+ }
151
+ }
152
+ return adj;
153
+ };
154
+ /**
155
+ * Find functions/methods that are good entry points for tracing
156
+ *
157
+ * Scored by call ratio, export status, and name patterns; test files excluded
158
+ */
159
+ const findEntryPoints = (graph, reverseCallsEdges, callsEdges) => {
160
+ const symbolTypes = new Set(['Function', 'Method']);
161
+ const entryPointCandidates = [];
162
+ for (const node of graph.iterNodes()) {
163
+ if (!symbolTypes.has(node.label))
164
+ continue;
165
+ const filePath = node.properties.filePath || '';
166
+ // Skip test files entirely
167
+ if (isTestFile(filePath))
168
+ continue;
169
+ const callers = reverseCallsEdges.get(node.id) || [];
170
+ const calleeEdges = callsEdges.get(node.id) || [];
171
+ // Must have at least 1 outgoing call to trace forward
172
+ if (calleeEdges.length === 0)
173
+ continue;
174
+ // Calculate entry point score
175
+ const { score: baseScore, reasons } = calculateEntryPointScore(node.properties.name, node.properties.language ?? SupportedLanguages.JavaScript, node.properties.isExported ?? false, callers.length, calleeEdges.length, filePath);
176
+ let score = baseScore;
177
+ const astFrameworkMultiplier = node.properties.astFrameworkMultiplier ?? 1.0;
178
+ if (astFrameworkMultiplier > 1.0) {
179
+ score *= astFrameworkMultiplier;
180
+ reasons.push(`framework-ast:${node.properties.astFrameworkReason || 'decorator'}`);
181
+ }
182
+ if (score > 0) {
183
+ entryPointCandidates.push({ id: node.id, score, reasons });
184
+ }
185
+ }
186
+ // Sort by score descending and return top candidates
187
+ const sorted = entryPointCandidates.sort((a, b) => b.score - a.score);
188
+ if (sorted.length > 0 && isDev) {
189
+ console.log(`[Process] Top 10 entry point candidates:`);
190
+ sorted.slice(0, 10).forEach((c, i) => {
191
+ const node = graph.getNode(c.id);
192
+ const exported = node?.properties.isExported ? '✓' : '✗';
193
+ const shortPath = node?.properties.filePath?.split('/').slice(-2).join('/') || '';
194
+ console.log(` ${i + 1}. ${node?.properties.name} [exported:${exported}] (${shortPath})`);
195
+ console.log(` score: ${c.score.toFixed(2)} = [${c.reasons.join(' × ')}]`);
196
+ });
197
+ }
198
+ return sorted.map(c => c.id);
199
+ };
200
+ /**
201
+ * Trace forward from an entry point using confidence-gated beam search.
202
+ *
203
+ * Instead of a static per-node branching limit, this follows ALL callees whose
204
+ * edge confidence meets the threshold, then prunes paths whose cumulative
205
+ * confidence (product of edge confidences along the path) drops below a floor.
206
+ *
207
+ * A global beam size caps the number of active paths in memory, ensuring bounded
208
+ * resource usage regardless of graph fan-out. Paths are explored in order of
209
+ * cumulative confidence (highest first), so well-resolved execution flows are
210
+ * discovered before speculative ones.
211
+ */
212
+ const traceFromEntryPoint = (entryId, callsEdges, config) => {
213
+ const traces = [];
214
+ // Priority queue sorted by cumulative confidence descending
215
+ const beam = [{
216
+ cumulativeConf: 1.0,
217
+ currentId: entryId,
218
+ path: [entryId],
219
+ pathSet: new Set([entryId]),
220
+ }];
221
+ while (beam.length > 0) {
222
+ // Pop highest cumulative confidence
223
+ const entry = beam.shift();
224
+ const edges = callsEdges.get(entry.currentId) || [];
225
+ // Filter edges by per-edge confidence gate
226
+ const viable = edges.filter(e => e.confidence >= config.minEdgeConfidence);
227
+ if (viable.length === 0 || entry.path.length >= config.maxTraceDepth) {
228
+ // Terminal — record if long enough
229
+ if (entry.path.length >= config.minSteps) {
230
+ traces.push(entry.path);
231
+ }
232
+ continue;
233
+ }
234
+ let addedBranch = false;
235
+ for (const edge of viable) {
236
+ // O(1) cycle detection
237
+ if (entry.pathSet.has(edge.targetId))
238
+ continue;
239
+ const childConf = entry.cumulativeConf * edge.confidence;
240
+ // Prune paths whose cumulative confidence is too low
241
+ if (childConf < config.minPathConfidence)
242
+ continue;
243
+ const newPath = [...entry.path, edge.targetId];
244
+ const newSet = new Set(entry.pathSet);
245
+ newSet.add(edge.targetId);
246
+ const child = {
247
+ cumulativeConf: childConf,
248
+ currentId: edge.targetId,
249
+ path: newPath,
250
+ pathSet: newSet,
251
+ };
252
+ // Binary search for sorted insert (descending by cumulativeConf)
253
+ let lo = 0;
254
+ let hi = beam.length;
255
+ while (lo < hi) {
256
+ const mid = (lo + hi) >>> 1;
257
+ if (beam[mid].cumulativeConf > childConf)
258
+ lo = mid + 1;
259
+ else
260
+ hi = mid;
261
+ }
262
+ beam.splice(lo, 0, child);
263
+ addedBranch = true;
264
+ }
265
+ // Trim beam if it exceeds max size (drop lowest-confidence tails)
266
+ if (beam.length > config.maxBeamSize) {
267
+ beam.length = config.maxBeamSize;
268
+ }
269
+ if (!addedBranch && entry.path.length >= config.minSteps) {
270
+ traces.push(entry.path);
271
+ }
272
+ }
273
+ return traces;
274
+ };
275
+ /** Merge traces that are subsets of other traces, keeping longer ones */
276
+ const deduplicateTraces = (traces) => {
277
+ if (traces.length === 0)
278
+ return [];
279
+ const sorted = [...traces].sort((a, b) => b.length - a.length);
280
+ const unique = [];
281
+ for (const trace of sorted) {
282
+ // Check if this trace is an ordered subsequence of any already-added trace.
283
+ // Fixed: previous string .includes() had false positives (e.g. "A->B" inside "A->B->D").
284
+ const isSubset = unique.some(existing => isOrderedSubsequence(trace, existing));
285
+ if (!isSubset) {
286
+ unique.push(trace);
287
+ }
288
+ }
289
+ return unique;
290
+ };
291
+ /** Check if every element of shorter appears in longer in the same order */
292
+ function isOrderedSubsequence(shorter, longer) {
293
+ if (shorter.length >= longer.length)
294
+ return false;
295
+ let j = 0;
296
+ for (let i = 0; i < longer.length && j < shorter.length; i++) {
297
+ if (longer[i] === shorter[j])
298
+ j++;
299
+ }
300
+ return j === shorter.length;
301
+ }
302
+ /** Keep only the longest trace per unique entry->terminal pair */
303
+ const deduplicateByEndpoints = (traces) => {
304
+ if (traces.length === 0)
305
+ return [];
306
+ const byEndpoints = new Map();
307
+ // Sort longest first so the first seen per key is the longest
308
+ const sorted = [...traces].sort((a, b) => b.length - a.length);
309
+ for (const trace of sorted) {
310
+ const key = `${trace[0]}::${trace[trace.length - 1]}`;
311
+ if (!byEndpoints.has(key)) {
312
+ byEndpoints.set(key, trace);
313
+ }
314
+ }
315
+ return Array.from(byEndpoints.values());
316
+ };
317
+ /** D4: Merge traces that share ≥ 80% of steps — keeps the longer one.
318
+ * Reduces near-duplicate flows like [A,B,C,D,E] and [A,B,C,D,F] to just the longer. */
319
+ const mergeSimilarTraces = (traces) => {
320
+ if (traces.length <= 1)
321
+ return traces;
322
+ const sorted = [...traces].sort((a, b) => b.length - a.length);
323
+ const kept = [];
324
+ const removed = new Set();
325
+ for (let i = 0; i < sorted.length; i++) {
326
+ if (removed.has(i))
327
+ continue;
328
+ kept.push(sorted[i]);
329
+ // Check remaining traces for high overlap with this one
330
+ for (let j = i + 1; j < sorted.length; j++) {
331
+ if (removed.has(j))
332
+ continue;
333
+ const shorter = sorted[j];
334
+ const longer = sorted[i];
335
+ // Count shared steps
336
+ const longerSet = new Set(longer);
337
+ let shared = 0;
338
+ for (const step of shorter) {
339
+ if (longerSet.has(step))
340
+ shared++;
341
+ }
342
+ // If ≥ 80% of the shorter trace's steps are in the longer trace, merge (remove shorter)
343
+ if (shared >= shorter.length * 0.8) {
344
+ removed.add(j);
345
+ }
346
+ }
347
+ }
348
+ return kept;
349
+ };
350
+ const capitalize = (s) => {
351
+ if (!s)
352
+ return s;
353
+ return s.charAt(0).toUpperCase() + s.slice(1);
354
+ };
355
+ const sanitizeId = (s) => {
356
+ return s.replace(/[^a-zA-Z0-9]/g, '_').substring(0, 20).toLowerCase();
357
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @file resolution-context.ts
3
+ * @description Single implementation of tiered name resolution
4
+ *
5
+ * Resolution tiers (highest confidence first):
6
+ * 1 Same file (lookupExactFull — authoritative)
7
+ * 2a Named binding chain (walkBindingChain via NamedImportMap)
8
+ * 2a Import-scoped (lookupFuzzy filtered by ImportMap)
9
+ * 2b Package-scoped (lookupFuzzy filtered by PackageMap)
10
+ * 3 Global (all candidates — consumers must check candidate count)
11
+ */
12
+ import type { SymbolTable, SymbolDefinition } from './symbol-table.js';
13
+ import type { NamedImportBinding } from './import-processor.js';
14
+ export type ResolutionTier = 'same-file' | 'import-scoped' | 'global';
15
+ export interface TieredCandidates {
16
+ readonly candidates: readonly SymbolDefinition[];
17
+ readonly tier: ResolutionTier;
18
+ }
19
+ export declare const TIER_CONFIDENCE: Record<ResolutionTier, number>;
20
+ export type ImportMap = Map<string, Set<string>>;
21
+ export type PackageMap = Map<string, Set<string>>;
22
+ export type NamedImportMap = Map<string, Map<string, NamedImportBinding>>;
23
+ export interface ResolutionContext {
24
+ /** Resolve a name from a file, returning candidates at the winning tier */
25
+ resolve(name: string, fromFile: string): TieredCandidates | null;
26
+ readonly symbols: SymbolTable;
27
+ readonly importMap: ImportMap;
28
+ readonly packageMap: PackageMap;
29
+ readonly namedImportMap: NamedImportMap;
30
+ enableCache(filePath: string): void;
31
+ clearCache(): void;
32
+ getStats(): {
33
+ fileCount: number;
34
+ globalSymbolCount: number;
35
+ cacheHits: number;
36
+ cacheMisses: number;
37
+ };
38
+ clear(): void;
39
+ }
40
+ export declare const createResolutionContext: () => ResolutionContext;
@@ -0,0 +1,171 @@
1
+ // code-mapper/src/core/ingestion/resolution-context.ts
2
+ /**
3
+ * @file resolution-context.ts
4
+ * @description Single implementation of tiered name resolution
5
+ *
6
+ * Resolution tiers (highest confidence first):
7
+ * 1 Same file (lookupExactFull — authoritative)
8
+ * 2a Named binding chain (walkBindingChain via NamedImportMap)
9
+ * 2a Import-scoped (lookupFuzzy filtered by ImportMap)
10
+ * 2b Package-scoped (lookupFuzzy filtered by PackageMap)
11
+ * 3 Global (all candidates — consumers must check candidate count)
12
+ */
13
+ import { createSymbolTable } from './symbol-table.js';
14
+ import { isFileInPackageDir } from './import-processor.js';
15
+ import { walkBindingChain } from './named-binding-extraction.js';
16
+ // Confidence scores per resolution tier
17
+ export const TIER_CONFIDENCE = {
18
+ 'same-file': 0.95,
19
+ 'import-scoped': 0.9,
20
+ 'global': 0.5,
21
+ };
22
+ export const createResolutionContext = () => {
23
+ const symbols = createSymbolTable();
24
+ const importMap = new Map();
25
+ const packageMap = new Map();
26
+ const namedImportMap = new Map();
27
+ // Per-file cache state
28
+ let cacheFile = null;
29
+ let cache = null;
30
+ let cacheHits = 0;
31
+ let cacheMisses = 0;
32
+ // Core resolution (single implementation of tier logic)
33
+ const resolveUncached = (name, fromFile) => {
34
+ // Tier 1: Same file — authoritative match
35
+ const localDef = symbols.lookupExactFull(fromFile, name);
36
+ if (localDef) {
37
+ return { candidates: [localDef], tier: 'same-file' };
38
+ }
39
+ // Get all global definitions for subsequent tiers
40
+ const allDefs = symbols.lookupFuzzy(name);
41
+ // Tier 2a-named: Check named bindings BEFORE empty-allDefs early return
42
+ // because aliased imports mean lookupFuzzy('U') returns empty
43
+ const chainResult = walkBindingChain(name, fromFile, symbols, namedImportMap, allDefs);
44
+ if (chainResult && chainResult.length > 0) {
45
+ return { candidates: chainResult, tier: 'import-scoped' };
46
+ }
47
+ if (allDefs.length === 0) {
48
+ // Don't exit early — still check import map. The symbol may be imported from another file.
49
+ // First try lookupExactFull with the exact imported file path
50
+ const importedFiles = importMap.get(fromFile);
51
+ if (importedFiles) {
52
+ for (const importedFile of importedFiles) {
53
+ const imported = symbols.lookupExactFull(importedFile, name);
54
+ if (imported)
55
+ return { candidates: [imported], tier: 'import-scoped' };
56
+ }
57
+ }
58
+ // Fallback: try fuzzy lookup — the symbol may exist globally but path normalization differs
59
+ const fuzzyDefs = symbols.lookupFuzzy(name);
60
+ if (fuzzyDefs.length > 0) {
61
+ if (importedFiles) {
62
+ const stripExt = (p) => p.replace(/\.[^.]+$/, '').replace(/\\/g, '/');
63
+ const importedArr = Array.from(importedFiles).map(stripExt);
64
+ const importedDefs = fuzzyDefs.filter(def => {
65
+ const defStripped = stripExt(def.filePath);
66
+ return importedArr.some(imp => defStripped.endsWith(imp) || imp.endsWith(defStripped)
67
+ || defStripped.includes(imp) || imp.includes(defStripped));
68
+ });
69
+ if (importedDefs.length > 0) {
70
+ return { candidates: importedDefs, tier: 'import-scoped' };
71
+ }
72
+ }
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' };
77
+ }
78
+ }
79
+ return null;
80
+ }
81
+ // Tier 2a: Import-scoped — definition in a file imported by fromFile
82
+ const importedFiles = importMap.get(fromFile);
83
+ if (importedFiles) {
84
+ // First try exact match
85
+ let importedDefs = allDefs.filter(def => importedFiles.has(def.filePath));
86
+ // Fallback: suffix matching for path normalization + extension differences (.ts vs .js)
87
+ if (importedDefs.length === 0) {
88
+ const stripExt = (p) => p.replace(/\.[^.]+$/, '').replace(/\\/g, '/');
89
+ const importedArr = Array.from(importedFiles).map(stripExt);
90
+ importedDefs = allDefs.filter(def => {
91
+ const defStripped = stripExt(def.filePath);
92
+ return importedArr.some(imp => defStripped.endsWith(imp) || imp.endsWith(defStripped)
93
+ || defStripped.includes(imp) || imp.includes(defStripped));
94
+ });
95
+ }
96
+ if (importedDefs.length > 0) {
97
+ return { candidates: importedDefs, tier: 'import-scoped' };
98
+ }
99
+ }
100
+ // Tier 2b: Package-scoped — definition in a package dir imported by fromFile
101
+ const importedPackages = packageMap.get(fromFile);
102
+ if (importedPackages) {
103
+ const packageDefs = allDefs.filter(def => {
104
+ for (const dirSuffix of importedPackages) {
105
+ if (isFileInPackageDir(def.filePath, dirSuffix))
106
+ return true;
107
+ }
108
+ return false;
109
+ });
110
+ if (packageDefs.length > 0) {
111
+ return { candidates: packageDefs, tier: 'import-scoped' };
112
+ }
113
+ }
114
+ // Tier 3: Global — consumers must check candidate count for ambiguity
115
+ return { candidates: allDefs, tier: 'global' };
116
+ };
117
+ const resolve = (name, fromFile) => {
118
+ // Check cache (only when enabled AND fromFile matches cached file)
119
+ if (cache && cacheFile === fromFile) {
120
+ if (cache.has(name)) {
121
+ cacheHits++;
122
+ return cache.get(name);
123
+ }
124
+ cacheMisses++;
125
+ }
126
+ const result = resolveUncached(name, fromFile);
127
+ // Store in cache if active and file matches
128
+ if (cache && cacheFile === fromFile) {
129
+ cache.set(name, result);
130
+ }
131
+ return result;
132
+ };
133
+ // Cache lifecycle
134
+ const enableCache = (filePath) => {
135
+ cacheFile = filePath;
136
+ if (!cache)
137
+ cache = new Map();
138
+ else
139
+ cache.clear();
140
+ };
141
+ const clearCache = () => {
142
+ cacheFile = null;
143
+ // Reuse the Map instance to reduce GC pressure
144
+ cache?.clear();
145
+ };
146
+ const getStats = () => ({
147
+ ...symbols.getStats(),
148
+ cacheHits,
149
+ cacheMisses,
150
+ });
151
+ const clear = () => {
152
+ symbols.clear();
153
+ importMap.clear();
154
+ packageMap.clear();
155
+ namedImportMap.clear();
156
+ clearCache();
157
+ cacheHits = 0;
158
+ cacheMisses = 0;
159
+ };
160
+ return {
161
+ resolve,
162
+ symbols,
163
+ importMap,
164
+ packageMap,
165
+ namedImportMap,
166
+ enableCache,
167
+ clearCache,
168
+ getStats,
169
+ clear,
170
+ };
171
+ };
@@ -0,0 +1,10 @@
1
+ /** @file C# namespace import resolution via .csproj root namespace stripping */
2
+ import type { SuffixIndex } from './utils.js';
3
+ export interface CSharpProjectConfig {
4
+ rootNamespace: string;
5
+ projectDir: string;
6
+ }
7
+ /** Resolve a C# using-directive to matching .cs files (single-file or directory match) */
8
+ export declare function resolveCSharpImport(importPath: string, csharpConfigs: CSharpProjectConfig[], normalizedFileList: string[], allFileList: string[], index?: SuffixIndex): string[];
9
+ /** Compute the directory suffix for a C# namespace import (e.g., "/ProjectDir/Models/") */
10
+ export declare function resolveCSharpNamespaceDir(importPath: string, csharpConfigs: CSharpProjectConfig[]): string | null;
@@ -0,0 +1,101 @@
1
+ // code-mapper/src/core/ingestion/resolvers/csharp.ts
2
+ /** @file C# namespace import resolution via .csproj root namespace stripping */
3
+ import { suffixResolve } from './utils.js';
4
+ /** Resolve a C# using-directive to matching .cs files (single-file or directory match) */
5
+ export function resolveCSharpImport(importPath, csharpConfigs, normalizedFileList, allFileList, index) {
6
+ const namespacePath = importPath.replace(/\./g, '/');
7
+ const results = [];
8
+ for (const config of csharpConfigs) {
9
+ const nsPath = config.rootNamespace.replace(/\./g, '/');
10
+ let relative;
11
+ if (namespacePath.startsWith(nsPath + '/')) {
12
+ relative = namespacePath.slice(nsPath.length + 1);
13
+ }
14
+ else if (namespacePath === nsPath) {
15
+ // Import IS the root namespace — resolve to all .cs files in project root
16
+ relative = '';
17
+ }
18
+ else {
19
+ continue;
20
+ }
21
+ const dirPrefix = config.projectDir
22
+ ? (relative ? config.projectDir + '/' + relative : config.projectDir)
23
+ : relative;
24
+ // 1. Try as single file (e.g., "Models/DlqMessage.cs")
25
+ if (relative) {
26
+ const candidate = dirPrefix + '.cs';
27
+ if (index) {
28
+ const result = index.get(candidate) || index.getInsensitive(candidate);
29
+ if (result)
30
+ return [result];
31
+ }
32
+ // Also try suffix match
33
+ const suffixResult = index?.get(relative + '.cs') || index?.getInsensitive(relative + '.cs');
34
+ if (suffixResult)
35
+ return [suffixResult];
36
+ }
37
+ // 2. Try as directory: all direct .cs children (namespace import)
38
+ if (index) {
39
+ const dirFiles = index.getFilesInDir(dirPrefix, '.cs');
40
+ for (const f of dirFiles) {
41
+ const normalized = f.replace(/\\/g, '/');
42
+ // Ensure file is a direct child (no deeper slashes)
43
+ const prefixIdx = normalized.indexOf(dirPrefix + '/');
44
+ if (prefixIdx < 0)
45
+ continue;
46
+ const afterDir = normalized.substring(prefixIdx + dirPrefix.length + 1);
47
+ if (!afterDir.includes('/')) {
48
+ results.push(f);
49
+ }
50
+ }
51
+ if (results.length > 0)
52
+ return results;
53
+ }
54
+ // 3. Linear scan fallback for directory matching
55
+ if (results.length === 0) {
56
+ const dirTrail = dirPrefix + '/';
57
+ for (let i = 0; i < normalizedFileList.length; i++) {
58
+ const normalized = normalizedFileList[i];
59
+ if (!normalized.endsWith('.cs'))
60
+ continue;
61
+ const prefixIdx = normalized.indexOf(dirTrail);
62
+ if (prefixIdx < 0)
63
+ continue;
64
+ const afterDir = normalized.substring(prefixIdx + dirTrail.length);
65
+ if (!afterDir.includes('/')) {
66
+ results.push(allFileList[i]);
67
+ }
68
+ }
69
+ if (results.length > 0)
70
+ return results;
71
+ }
72
+ }
73
+ // Fallback: suffix matching without namespace stripping
74
+ const pathParts = namespacePath.split('/').filter(Boolean);
75
+ const fallback = suffixResolve(pathParts, normalizedFileList, allFileList, index);
76
+ return fallback ? [fallback] : [];
77
+ }
78
+ /** Compute the directory suffix for a C# namespace import (e.g., "/ProjectDir/Models/") */
79
+ export function resolveCSharpNamespaceDir(importPath, csharpConfigs) {
80
+ const namespacePath = importPath.replace(/\./g, '/');
81
+ for (const config of csharpConfigs) {
82
+ const nsPath = config.rootNamespace.replace(/\./g, '/');
83
+ let relative;
84
+ if (namespacePath.startsWith(nsPath + '/')) {
85
+ relative = namespacePath.slice(nsPath.length + 1);
86
+ }
87
+ else if (namespacePath === nsPath) {
88
+ relative = '';
89
+ }
90
+ else {
91
+ continue;
92
+ }
93
+ const dirPrefix = config.projectDir
94
+ ? (relative ? config.projectDir + '/' + relative : config.projectDir)
95
+ : relative;
96
+ if (!dirPrefix)
97
+ continue;
98
+ return '/' + dirPrefix + '/';
99
+ }
100
+ return null;
101
+ }