milens 0.6.5 → 0.6.6

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 (91) hide show
  1. package/.agents/skills/analyzer/SKILL.md +5 -5
  2. package/.agents/skills/docs/SKILL.md +4 -3
  3. package/.agents/skills/milens/SKILL.md +1 -1
  4. package/.agents/skills/root/SKILL.md +3 -3
  5. package/.agents/skills/server/SKILL.md +10 -9
  6. package/.agents/skills/store/SKILL.md +2 -2
  7. package/README.md +166 -166
  8. package/adapters/claude-code/CLAUDE.md +36 -15
  9. package/adapters/codex/.codex/codex.md +38 -23
  10. package/adapters/copilot/.github/copilot-instructions.md +29 -22
  11. package/adapters/gemini/.gemini/context.md +33 -10
  12. package/adapters/opencode/AGENTS.md +36 -15
  13. package/dist/agents-md.d.ts.map +1 -1
  14. package/dist/agents-md.js +51 -2
  15. package/dist/agents-md.js.map +1 -1
  16. package/dist/analyzer/engine.d.ts +3 -0
  17. package/dist/analyzer/engine.d.ts.map +1 -1
  18. package/dist/analyzer/engine.js +342 -8
  19. package/dist/analyzer/engine.js.map +1 -1
  20. package/dist/analyzer/resolver.d.ts +2 -0
  21. package/dist/analyzer/resolver.d.ts.map +1 -1
  22. package/dist/analyzer/resolver.js +187 -9
  23. package/dist/analyzer/resolver.js.map +1 -1
  24. package/dist/analyzer/review.d.ts.map +1 -1
  25. package/dist/analyzer/review.js +254 -32
  26. package/dist/analyzer/review.js.map +1 -1
  27. package/dist/analyzer/scope-resolver.d.ts +42 -0
  28. package/dist/analyzer/scope-resolver.d.ts.map +1 -0
  29. package/dist/analyzer/scope-resolver.js +687 -0
  30. package/dist/analyzer/scope-resolver.js.map +1 -0
  31. package/dist/cli.js +294 -1
  32. package/dist/cli.js.map +1 -1
  33. package/dist/parser/extract.d.ts +6 -1
  34. package/dist/parser/extract.d.ts.map +1 -1
  35. package/dist/parser/extract.js +14 -2
  36. package/dist/parser/extract.js.map +1 -1
  37. package/dist/parser/lang-css.d.ts.map +1 -1
  38. package/dist/parser/lang-css.js +7 -1
  39. package/dist/parser/lang-css.js.map +1 -1
  40. package/dist/parser/lang-go.d.ts.map +1 -1
  41. package/dist/parser/lang-go.js +16 -0
  42. package/dist/parser/lang-go.js.map +1 -1
  43. package/dist/parser/lang-html.d.ts +4 -0
  44. package/dist/parser/lang-html.d.ts.map +1 -1
  45. package/dist/parser/lang-html.js +40 -1
  46. package/dist/parser/lang-html.js.map +1 -1
  47. package/dist/parser/lang-java.d.ts.map +1 -1
  48. package/dist/parser/lang-java.js +12 -0
  49. package/dist/parser/lang-java.js.map +1 -1
  50. package/dist/parser/lang-js.d.ts.map +1 -1
  51. package/dist/parser/lang-js.js +3 -0
  52. package/dist/parser/lang-js.js.map +1 -1
  53. package/dist/parser/lang-php.d.ts.map +1 -1
  54. package/dist/parser/lang-php.js +11 -0
  55. package/dist/parser/lang-php.js.map +1 -1
  56. package/dist/parser/lang-py.d.ts.map +1 -1
  57. package/dist/parser/lang-py.js +14 -0
  58. package/dist/parser/lang-py.js.map +1 -1
  59. package/dist/parser/lang-ruby.d.ts.map +1 -1
  60. package/dist/parser/lang-ruby.js +20 -0
  61. package/dist/parser/lang-ruby.js.map +1 -1
  62. package/dist/parser/lang-rust.d.ts.map +1 -1
  63. package/dist/parser/lang-rust.js +27 -4
  64. package/dist/parser/lang-rust.js.map +1 -1
  65. package/dist/parser/lang-ts.d.ts.map +1 -1
  66. package/dist/parser/lang-ts.js +3 -0
  67. package/dist/parser/lang-ts.js.map +1 -1
  68. package/dist/parser/lang-vue.d.ts +17 -1
  69. package/dist/parser/lang-vue.d.ts.map +1 -1
  70. package/dist/parser/lang-vue.js +177 -0
  71. package/dist/parser/lang-vue.js.map +1 -1
  72. package/dist/parser/language-provider.d.ts +27 -0
  73. package/dist/parser/language-provider.d.ts.map +1 -0
  74. package/dist/parser/language-provider.js +2 -0
  75. package/dist/parser/language-provider.js.map +1 -0
  76. package/dist/server/mcp.d.ts.map +1 -1
  77. package/dist/server/mcp.js +224 -50
  78. package/dist/server/mcp.js.map +1 -1
  79. package/dist/server/watcher.d.ts +8 -0
  80. package/dist/server/watcher.d.ts.map +1 -1
  81. package/dist/server/watcher.js +10 -8
  82. package/dist/server/watcher.js.map +1 -1
  83. package/dist/skills.js +163 -42
  84. package/dist/skills.js.map +1 -1
  85. package/dist/store/schema.sql +1 -1
  86. package/dist/uninstall.d.ts +54 -0
  87. package/dist/uninstall.d.ts.map +1 -0
  88. package/dist/uninstall.js +795 -0
  89. package/dist/uninstall.js.map +1 -0
  90. package/docs/README.md +4 -1
  91. package/package.json +1 -1
@@ -0,0 +1,687 @@
1
+ import { dirname } from 'node:path';
2
+ const MIN_LINK_CONFIDENCE = 0.5;
3
+ const BUILTIN_GLOBALS = new Set([
4
+ 'console', 'Math', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Map', 'Set',
5
+ 'Promise', 'JSON', 'Date', 'Error', 'parseInt', 'parseFloat', 'isNaN',
6
+ 'Buffer', 'process', 'require', 'fetch', 'global', 'globalThis',
7
+ 'ref', 'reactive', 'computed', 'watch', 'onMounted', 'onUnmounted', 'h', 'createApp',
8
+ 'useState', 'useEffect', 'useRef', 'useMemo', 'useCallback', 'useReducer',
9
+ 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'tuple', 'set', 'type',
10
+ 'fmt', 'log', 'panic', 'make', 'append', 'new', 'delete', 'close',
11
+ 'println', 'vec', 'format', 'assert',
12
+ 'System', 'var_dump', 'echo', 'isset', 'empty', 'count', 'strlen', 'substr',
13
+ ]);
14
+ export function resolveWithScopes(input) {
15
+ const links = [];
16
+ let unresolvedImports = 0;
17
+ let unresolvedCalls = 0;
18
+ let externalImports = 0;
19
+ let externalCalls = 0;
20
+ const symbolById = buildIdIndex(input.allSymbols);
21
+ // Phase 1: Build scope graphs per file (AST-based preferred, symbol-based fallback)
22
+ const scopeForest = input.treeCache
23
+ ? buildScopeGraphFromAST(input.treeCache, input.symbolsByFile)
24
+ : buildScopeGraph(input.symbolsByFile);
25
+ // Phase 2: Resolve imports → add visible symbols to file scopes
26
+ const importedNamesPerFile = resolveImportsInScopes(input, scopeForest);
27
+ // Phase 3: Build name index for call resolution
28
+ const symbolByName = buildNameIndex(input.allSymbols);
29
+ // Phase 4: Collect visible symbols per scope (local + imports + ancestors)
30
+ collectVisibleSymbols(scopeForest, input.symbolsByFile, importedNamesPerFile, symbolById, input);
31
+ // F7: Track external imports per file to classify external vs unresolved calls
32
+ const externalNamesPerFile = new Map();
33
+ for (const imp of input.imports) {
34
+ const key = `${imp.filePath}::${imp.modulePath}`;
35
+ if (!input.resolvedImportPaths.has(key) && isExternalModule(imp.modulePath)) {
36
+ let extNames = externalNamesPerFile.get(imp.filePath);
37
+ if (!extNames) {
38
+ extNames = new Set();
39
+ externalNamesPerFile.set(imp.filePath, extNames);
40
+ }
41
+ for (const { name } of imp.names)
42
+ extNames.add(name);
43
+ }
44
+ }
45
+ // Build direct imports per file for proximity scoring
46
+ const directImportsPerFile = new Map();
47
+ for (const imp of input.imports) {
48
+ const targetFile = input.resolvedImportPaths.get(`${imp.filePath}::${imp.modulePath}`);
49
+ if (!targetFile)
50
+ continue;
51
+ let s = directImportsPerFile.get(imp.filePath);
52
+ if (!s) {
53
+ s = new Set();
54
+ directImportsPerFile.set(imp.filePath, s);
55
+ }
56
+ s.add(targetFile);
57
+ }
58
+ // Phase 5: Resolve calls via scope chain
59
+ for (const call of input.calls) {
60
+ const scope = findScopeForCall(call, scopeForest, input.symbolsByFile);
61
+ if (!scope) {
62
+ unresolvedCalls++;
63
+ continue;
64
+ }
65
+ // Walk scope chain to find callee
66
+ let resolved = resolveCallInScope(call.calleeName, scope, scopeForest, symbolById);
67
+ if (!resolved && call.receiver) {
68
+ resolved = resolveReceiverCall(call, scope, scopeForest, symbolById, input);
69
+ }
70
+ // F1: Proximity fallback when scope chain exhausted
71
+ if (!resolved) {
72
+ const candidates = symbolByName.get(call.calleeName);
73
+ if (candidates && candidates.length > 0) {
74
+ resolved = scoreCandidates(call, candidates, directImportsPerFile);
75
+ }
76
+ }
77
+ if (resolved && resolved.confidence >= MIN_LINK_CONFIDENCE) {
78
+ links.push(makeLink(call.enclosingSymbolId, resolved.symbol.id, 'calls', resolved.confidence, call.line));
79
+ }
80
+ else {
81
+ // F7: Classify as external or unresolved
82
+ const extNames = externalNamesPerFile.get(call.filePath);
83
+ if (BUILTIN_GLOBALS.has(call.calleeName) || extNames?.has(call.calleeName)) {
84
+ externalCalls++;
85
+ }
86
+ else {
87
+ unresolvedCalls++;
88
+ }
89
+ }
90
+ }
91
+ // Phase 6: Resolve imports (cross-file links)
92
+ for (const imp of input.imports) {
93
+ const targetFile = input.resolvedImportPaths.get(`${imp.filePath}::${imp.modulePath}`);
94
+ if (!targetFile) {
95
+ if (isExternalModule(imp.modulePath)) {
96
+ externalImports++;
97
+ }
98
+ else {
99
+ unresolvedImports++;
100
+ }
101
+ continue;
102
+ }
103
+ const targetSymbols = input.symbolsByFile.get(targetFile);
104
+ if (!targetSymbols) {
105
+ unresolvedImports++;
106
+ continue;
107
+ }
108
+ const fromId = `${imp.filePath}#module:_top:0`;
109
+ for (const { name } of imp.names) {
110
+ const target = targetSymbols.find(s => s.name === name && s.exported);
111
+ if (target) {
112
+ links.push(makeLink(fromId, target.id, 'imports', 0.95, imp.line));
113
+ }
114
+ else {
115
+ unresolvedImports++;
116
+ }
117
+ }
118
+ }
119
+ // Phase 7: Heritage + containment
120
+ for (const sym of input.allSymbols) {
121
+ if (sym.parentId) {
122
+ links.push(makeLink(sym.parentId, sym.id, 'contains', 1.0));
123
+ }
124
+ }
125
+ for (const h of input.heritage) {
126
+ const children = symbolByName.get(h.childName);
127
+ const parents = symbolByName.get(h.parentName);
128
+ if (!children || !parents)
129
+ continue;
130
+ const child = children.find(s => s.filePath === h.filePath) ?? children[0];
131
+ const parent = parents.find(s => s.filePath === h.filePath) ?? parents[0];
132
+ if (child && parent) {
133
+ links.push(makeLink(child.id, parent.id, h.type, 0.95, h.line));
134
+ }
135
+ }
136
+ return { links: deduplicateLinks(links), unresolvedImports, unresolvedCalls, externalImports, externalCalls };
137
+ }
138
+ // ── Scope Graph from AST (tree-sitter) ──
139
+ // Node types that introduce a new scope
140
+ const SCOPE_NODE_TYPES = new Set([
141
+ 'program', 'module',
142
+ 'function_declaration', 'method_definition', 'arrow_function',
143
+ 'class_declaration', 'class', 'struct_item', 'trait_item', 'interface_declaration',
144
+ 'function_definition', 'function_item', 'method_declaration',
145
+ 'impl_item', 'module',
146
+ 'statement_block', 'block',
147
+ 'constructor_declaration',
148
+ ]);
149
+ function buildScopeGraphFromAST(treeCache, symbolsByFile) {
150
+ const allScopes = new Map();
151
+ for (const [filePath, symbols] of symbolsByFile) {
152
+ const tree = treeCache.get(filePath);
153
+ if (!tree) {
154
+ // Fallback: no cached tree → build from symbols
155
+ const fallback = buildScopesFromSymbols(filePath, symbols, allScopes);
156
+ if (fallback)
157
+ allScopes.set(fallback.id, fallback);
158
+ continue;
159
+ }
160
+ // Walk tree-sitter AST and extract scopes
161
+ const symbolByLine = new Map();
162
+ for (const sym of symbols) {
163
+ const arr = symbolByLine.get(sym.startLine) ?? [];
164
+ arr.push(sym);
165
+ symbolByLine.set(sym.startLine, arr);
166
+ }
167
+ const rootNode = tree.rootNode;
168
+ const fileScopeId = `${filePath}#scope:file`;
169
+ const fileScope = {
170
+ id: fileScopeId,
171
+ filePath,
172
+ parentScopeId: null,
173
+ children: [],
174
+ symbols: [],
175
+ visibleNames: new Map(),
176
+ startLine: 0,
177
+ endLine: rootNode.endPosition.row + 1,
178
+ };
179
+ walkASTNode(rootNode, fileScope, filePath, symbols, allScopes, fileScopeId);
180
+ allScopes.set(fileScopeId, fileScope);
181
+ }
182
+ return allScopes;
183
+ }
184
+ function walkASTNode(node, parentScope, filePath, allSymbols, allScopes, _fileScopeId) {
185
+ const nodeType = node.type;
186
+ // Check if this node creates a scope
187
+ if (SCOPE_NODE_TYPES.has(nodeType) && nodeType !== 'program' && nodeType !== 'module') {
188
+ const startLine = node.startPosition.row + 1;
189
+ const endLine = node.endPosition.row + 1;
190
+ const scopeName = extractScopeName(node, nodeType);
191
+ const scopeId = `${filePath}#scope:${scopeName}:${startLine}`;
192
+ const scope = {
193
+ id: scopeId,
194
+ filePath,
195
+ parentScopeId: parentScope.id,
196
+ children: [],
197
+ symbols: [],
198
+ visibleNames: new Map(),
199
+ startLine,
200
+ endLine,
201
+ };
202
+ // Assign symbols that fall within this scope's line range
203
+ for (const sym of allSymbols) {
204
+ if (sym.startLine >= startLine && sym.endLine <= endLine && sym.kind !== 'module') {
205
+ // Only add if not already in a deeper scope
206
+ if (!parentScope.symbols.includes(sym) || sym.kind === 'method') {
207
+ scope.symbols.push(sym);
208
+ }
209
+ }
210
+ }
211
+ // Deduplicate: remove symbols from parent that belong to child scope
212
+ parentScope.symbols = parentScope.symbols.filter(s => !(s.startLine >= startLine && s.endLine <= endLine));
213
+ parentScope.children.push(scope);
214
+ allScopes.set(scopeId, scope);
215
+ // Walk children into the new scope
216
+ const namedChildren = node.namedChildren;
217
+ for (let i = 0; i < namedChildren.length; i++) {
218
+ walkASTNode(namedChildren[i], scope, filePath, allSymbols, allScopes, _fileScopeId);
219
+ }
220
+ }
221
+ else {
222
+ // Not a scope node — walk children into same parent scope
223
+ const namedChildren = node.namedChildren;
224
+ for (let i = 0; i < namedChildren.length; i++) {
225
+ walkASTNode(namedChildren[i], parentScope, filePath, allSymbols, allScopes, _fileScopeId);
226
+ }
227
+ }
228
+ }
229
+ function extractScopeName(node, nodeType) {
230
+ // Try to find a name child
231
+ for (const child of node.namedChildren) {
232
+ if (child.type === 'identifier' || child.type === 'property_identifier' || child.type === 'name' || child.type === 'type_identifier' || child.type === 'field_identifier') {
233
+ return child.text;
234
+ }
235
+ }
236
+ return nodeType.replace('_declaration', '').replace('_definition', '').replace('_item', '');
237
+ }
238
+ function buildScopesFromSymbols(filePath, symbols, allScopes) {
239
+ // Simplified fallback from original buildScopeGraph
240
+ const fileScopeId = `${filePath}#scope:file`;
241
+ const fileScope = {
242
+ id: fileScopeId,
243
+ filePath,
244
+ parentScopeId: null,
245
+ children: [],
246
+ symbols: [],
247
+ visibleNames: new Map(),
248
+ startLine: 0,
249
+ endLine: Infinity,
250
+ };
251
+ const containerSymbols = symbols.filter(s => s.kind === 'class' || s.kind === 'struct' || s.kind === 'trait' || s.kind === 'interface');
252
+ for (const container of containerSymbols) {
253
+ const scopeId = `${filePath}#scope:${container.name}:${container.startLine}`;
254
+ const scope = {
255
+ id: scopeId,
256
+ filePath,
257
+ parentScopeId: fileScopeId,
258
+ children: [],
259
+ symbols: [container],
260
+ visibleNames: new Map(),
261
+ startLine: container.startLine,
262
+ endLine: container.endLine,
263
+ };
264
+ fileScope.children.push(scope);
265
+ allScopes.set(scopeId, scope);
266
+ const methodSymbols = symbols.filter(s => s.kind === 'method' && s.parentId === container.id);
267
+ scope.symbols.push(...methodSymbols);
268
+ }
269
+ for (const sym of symbols) {
270
+ if (sym.kind === 'module' || sym.kind === 'method')
271
+ continue;
272
+ if (containerSymbols.includes(sym))
273
+ continue;
274
+ fileScope.symbols.push(sym);
275
+ }
276
+ return fileScope;
277
+ }
278
+ // ── Symbol-based Scope Graph (fallback) ──
279
+ function buildScopeGraph(symbolsByFile) {
280
+ const allScopes = new Map();
281
+ for (const [filePath, symbols] of symbolsByFile) {
282
+ // Create file-level scope
283
+ const fileScopeId = `${filePath}#scope:file`;
284
+ const fileScope = {
285
+ id: fileScopeId,
286
+ filePath,
287
+ parentScopeId: null,
288
+ children: [],
289
+ symbols: [],
290
+ visibleNames: new Map(),
291
+ startLine: 0,
292
+ endLine: Infinity,
293
+ };
294
+ allScopes.set(fileScopeId, fileScope);
295
+ // Nest scopes: top-level symbols in file scope, methods in class scopes
296
+ const containerSymbols = symbols.filter(s => s.kind === 'class' || s.kind === 'struct' || s.kind === 'trait' || s.kind === 'interface');
297
+ for (const container of containerSymbols) {
298
+ const scopeId = `${filePath}#scope:${container.name}:${container.startLine}`;
299
+ const scope = {
300
+ id: scopeId,
301
+ filePath,
302
+ parentScopeId: fileScopeId,
303
+ children: [],
304
+ symbols: [container],
305
+ visibleNames: new Map(),
306
+ startLine: container.startLine,
307
+ endLine: container.endLine,
308
+ };
309
+ fileScope.children.push(scope);
310
+ allScopes.set(scopeId, scope);
311
+ // Place methods inside their container scope
312
+ const methodSymbols = symbols.filter(s => s.kind === 'method' && s.parentId === container.id);
313
+ scope.symbols.push(...methodSymbols);
314
+ }
315
+ // Top-level non-method symbols go to file scope
316
+ for (const sym of symbols) {
317
+ if (sym.kind === 'module')
318
+ continue;
319
+ if (sym.kind === 'method')
320
+ continue; // already placed
321
+ if (containerSymbols.includes(sym))
322
+ continue;
323
+ fileScope.symbols.push(sym);
324
+ }
325
+ }
326
+ return allScopes;
327
+ }
328
+ function findScopeForFile(filePath, scopeForest) {
329
+ for (const [, scope] of scopeForest) {
330
+ if (scope.parentScopeId === null && scope.filePath === filePath)
331
+ return scope;
332
+ }
333
+ }
334
+ function findScopeForCall(call, scopeForest, symbolsByFile) {
335
+ // Find the deepest scope containing this call's line
336
+ const fileScope = findScopeForFile(call.filePath, scopeForest);
337
+ if (!fileScope)
338
+ return undefined;
339
+ // Walk down to deepest matching scope
340
+ function findDeepest(scope, line) {
341
+ for (const child of scope.children) {
342
+ if (child.startLine <= line && child.endLine >= line) {
343
+ return findDeepest(child, line);
344
+ }
345
+ }
346
+ return scope;
347
+ }
348
+ return findDeepest(fileScope, call.line);
349
+ }
350
+ // ── Import Resolution within Scopes ──
351
+ function resolveImportsInScopes(input, scopeForest) {
352
+ const importedNamesPerFile = new Map();
353
+ // F4: Build re-export map (port from resolver.ts buildReExportMap)
354
+ const reExportMap = new Map();
355
+ for (const re of input.reExports ?? []) {
356
+ const sourceFile = input.resolvedImportPaths.get(`${re.filePath}::${re.modulePath}`);
357
+ if (!sourceFile)
358
+ continue;
359
+ let fileMap = reExportMap.get(re.filePath);
360
+ if (!fileMap) {
361
+ fileMap = new Map();
362
+ reExportMap.set(re.filePath, fileMap);
363
+ }
364
+ if (re.names.length > 0) {
365
+ for (const name of re.names)
366
+ fileMap.set(name, sourceFile);
367
+ }
368
+ else {
369
+ fileMap.set('*', sourceFile);
370
+ }
371
+ }
372
+ for (const imp of input.imports) {
373
+ const targetFile = input.resolvedImportPaths.get(`${imp.filePath}::${imp.modulePath}`);
374
+ if (!targetFile)
375
+ continue;
376
+ let fileImports = importedNamesPerFile.get(imp.filePath);
377
+ if (!fileImports) {
378
+ fileImports = new Map();
379
+ importedNamesPerFile.set(imp.filePath, fileImports);
380
+ }
381
+ for (const { name } of imp.names) {
382
+ fileImports.set(name, targetFile);
383
+ }
384
+ // F8: Full wildcard import expansion
385
+ const semantics = input.perFileImportSemantics?.get(imp.filePath);
386
+ if (imp.isWildcard && (semantics === 'wildcard-leaf' || semantics === 'wildcard-transitive')) {
387
+ const targetSymbols = input.symbolsByFile.get(targetFile);
388
+ if (targetSymbols) {
389
+ for (const sym of targetSymbols.filter(s => s.exported)) {
390
+ if (!fileImports.has(sym.name)) {
391
+ fileImports.set(sym.name, targetFile);
392
+ }
393
+ }
394
+ }
395
+ }
396
+ }
397
+ return importedNamesPerFile;
398
+ }
399
+ // ── Visible Symbol Collection ──
400
+ function collectVisibleSymbols(scopeForest, symbolsByFile, importedNamesPerFile, symbolById, input) {
401
+ for (const [, scope] of scopeForest) {
402
+ // Inherit from parent first
403
+ if (scope.parentScopeId) {
404
+ const parentScope = scopeForest.get(scope.parentScopeId);
405
+ if (parentScope) {
406
+ for (const [name, syms] of parentScope.visibleNames) {
407
+ scope.visibleNames.set(name, syms);
408
+ }
409
+ }
410
+ }
411
+ // Add local declarations
412
+ for (const sym of scope.symbols) {
413
+ const arr = scope.visibleNames.get(sym.name) ?? [];
414
+ arr.push(sym);
415
+ scope.visibleNames.set(sym.name, arr);
416
+ }
417
+ // Add imported symbols (file scope only)
418
+ if (scope.parentScopeId === null) {
419
+ const fileImports = importedNamesPerFile.get(scope.filePath);
420
+ if (fileImports) {
421
+ for (const [importedName, targetFile] of fileImports) {
422
+ const targetSymbols = symbolsByFile.get(targetFile);
423
+ if (targetSymbols) {
424
+ const arr = scope.visibleNames.get(importedName) ?? [];
425
+ for (const ts of targetSymbols.filter(s => s.name === importedName && s.exported)) {
426
+ if (!arr.find(s => s.id === ts.id))
427
+ arr.push(ts);
428
+ }
429
+ scope.visibleNames.set(importedName, arr);
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+ // F5: Propagate assignment chains (port from resolver.ts)
436
+ // const b = a → b inherits type of a
437
+ if (input.assignmentBindings) {
438
+ for (const ab of input.assignmentBindings) {
439
+ const fileScope = findScopeForFile(ab.filePath, scopeForest);
440
+ if (!fileScope)
441
+ continue;
442
+ const sourceSyms = fileScope.visibleNames.get(ab.source);
443
+ if (!sourceSyms || sourceSyms.length === 0)
444
+ continue;
445
+ const targetSyms = fileScope.visibleNames.get(ab.target) ?? [];
446
+ if (targetSyms.length === 0) {
447
+ fileScope.visibleNames.set(ab.target, [...sourceSyms]);
448
+ }
449
+ }
450
+ }
451
+ // F6: Propagate call result bindings (port from resolver.ts)
452
+ // const x = getUser() → x gets return type of getUser
453
+ if (input.callResultBindings && input.returnTypes) {
454
+ const returnTypeMap = new Map();
455
+ for (const rt of input.returnTypes) {
456
+ returnTypeMap.set(rt.parentName ? `${rt.parentName}.${rt.functionName}` : rt.functionName, rt.returnType);
457
+ }
458
+ for (const crb of input.callResultBindings) {
459
+ const fileScope = findScopeForFile(crb.filePath, scopeForest);
460
+ if (!fileScope || fileScope.visibleNames.has(crb.target))
461
+ continue;
462
+ let returnType;
463
+ if (crb.receiver) {
464
+ const receiverSyms = fileScope.visibleNames.get(crb.receiver);
465
+ if (receiverSyms && receiverSyms.length > 0) {
466
+ returnType = returnTypeMap.get(`${receiverSyms[0].name}.${crb.calleeName}`);
467
+ }
468
+ }
469
+ if (!returnType) {
470
+ returnType = returnTypeMap.get(crb.calleeName);
471
+ }
472
+ if (returnType) {
473
+ const typeSymbols = input.allSymbols.filter(s => s.name === returnType);
474
+ if (typeSymbols.length > 0) {
475
+ fileScope.visibleNames.set(crb.target, typeSymbols);
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
481
+ // ── Call Resolution ──
482
+ function resolveCallInScope(calleeName, scope, allScopes, symbolById) {
483
+ let current = scope;
484
+ while (current) {
485
+ const visible = current.visibleNames.get(calleeName);
486
+ if (visible && visible.length > 0) {
487
+ // Prefer symbol in same file
488
+ const sameFile = visible.find(s => s.filePath === scope.filePath);
489
+ if (sameFile)
490
+ return { symbol: sameFile, confidence: 0.95 };
491
+ return { symbol: visible[0], confidence: 0.85 };
492
+ }
493
+ if (!current.parentScopeId)
494
+ break;
495
+ current = allScopes.get(current.parentScopeId);
496
+ }
497
+ return null;
498
+ }
499
+ // ── Receiver-aware call resolution ──
500
+ function resolveReceiverCall(call, scope, allScopes, symbolById, input) {
501
+ const receiver = call.receiver;
502
+ const candidateSymbols = input.allSymbols.filter(s => s.name === call.calleeName && s.kind === 'method');
503
+ // this/self → method of enclosing class
504
+ if (receiver === 'this' || receiver === 'self') {
505
+ let current = scope;
506
+ while (current) {
507
+ const cls = current.symbols.find(s => s.kind === 'class' || s.kind === 'struct' || s.kind === 'trait');
508
+ if (cls) {
509
+ const method = candidateSymbols.find(s => s.parentId === cls.id);
510
+ if (method)
511
+ return { symbol: method, confidence: 0.95 };
512
+ break;
513
+ }
514
+ if (!current.parentScopeId)
515
+ break;
516
+ current = allScopes.get(current.parentScopeId);
517
+ }
518
+ }
519
+ // Type binding lookup from scope chain
520
+ let lookupScope = scope;
521
+ while (lookupScope) {
522
+ const typeSymbols = lookupScope.visibleNames.get(receiver);
523
+ if (typeSymbols && typeSymbols.length > 0) {
524
+ const typeSym = typeSymbols[0];
525
+ const method = candidateSymbols.find(s => s.parentId === typeSym.id);
526
+ if (method)
527
+ return { symbol: method, confidence: 0.93 };
528
+ }
529
+ if (!lookupScope.parentScopeId)
530
+ break;
531
+ lookupScope = allScopes.get(lookupScope.parentScopeId);
532
+ }
533
+ // F2: PascalCase heuristic (port from narrowByReceiver Strategy 3)
534
+ const pascal = receiver.charAt(0).toUpperCase() + receiver.slice(1);
535
+ const byConvention = candidateSymbols.find(c => {
536
+ const parent = c.parentId ? symbolById.get(c.parentId) : null;
537
+ return parent?.name === pascal;
538
+ });
539
+ if (byConvention)
540
+ return { symbol: byConvention, confidence: 0.55 };
541
+ // F3: Local class match (port from narrowByReceiver Strategy 4)
542
+ const localSymbols = input.symbolsByFile.get(scope.filePath);
543
+ if (localSymbols) {
544
+ const localClass = localSymbols.find(s => (s.kind === 'class' || s.kind === 'struct' || s.kind === 'trait') &&
545
+ (s.name === receiver || s.name === pascal));
546
+ if (localClass) {
547
+ const method = candidateSymbols.find(c => c.parentId === localClass.id);
548
+ if (method)
549
+ return { symbol: method, confidence: 0.90 };
550
+ }
551
+ }
552
+ return null;
553
+ }
554
+ // ── F1: Proximity scoring (port from resolver.ts) ──
555
+ function scoreCandidates(call, candidates, importsPerFile) {
556
+ if (candidates.length === 1)
557
+ return { symbol: candidates[0], confidence: 0.9 };
558
+ const importFiles = importsPerFile.get(call.filePath);
559
+ let best = candidates[0];
560
+ let bestScore = 0;
561
+ for (const c of candidates) {
562
+ let score;
563
+ if (c.filePath === call.filePath)
564
+ score = 0.90;
565
+ else if (importFiles?.has(c.filePath))
566
+ score = 0.85;
567
+ else if (dirname(c.filePath) === dirname(call.filePath))
568
+ score = 0.60;
569
+ else
570
+ score = 0.35;
571
+ if (score > bestScore) {
572
+ bestScore = score;
573
+ best = c;
574
+ }
575
+ }
576
+ return { symbol: best, confidence: bestScore };
577
+ }
578
+ // ── Helpers ──
579
+ function buildNameIndex(symbols) {
580
+ const index = new Map();
581
+ for (const s of symbols) {
582
+ const arr = index.get(s.name) ?? [];
583
+ arr.push(s);
584
+ index.set(s.name, arr);
585
+ }
586
+ return index;
587
+ }
588
+ function buildIdIndex(symbols) {
589
+ const index = new Map();
590
+ for (const s of symbols)
591
+ index.set(s.id, s);
592
+ return index;
593
+ }
594
+ function makeLink(fromId, toId, type, confidence, line) {
595
+ return { id: `${fromId}->${type}->${toId}`, fromId, toId, type, confidence, line };
596
+ }
597
+ function deduplicateLinks(links) {
598
+ const seen = new Set();
599
+ return links.filter(l => { if (seen.has(l.id))
600
+ return false; seen.add(l.id); return true; });
601
+ }
602
+ function isExternalModule(modulePath) {
603
+ if (modulePath.startsWith('.') || modulePath.startsWith('/'))
604
+ return false;
605
+ return true;
606
+ }
607
+ function linkKey(link) {
608
+ return `${link.fromId}::${link.type}::${link.toId}`;
609
+ }
610
+ export function diffResolutions(legacy, scopeBased) {
611
+ // Build index by semantic key (fromId, type, toId) instead of link.id
612
+ const scopeIndex = new Map();
613
+ for (const link of scopeBased.links) {
614
+ const key = linkKey(link);
615
+ // If duplicate key, keep higher confidence
616
+ const existing = scopeIndex.get(key);
617
+ if (!existing || link.confidence > existing.confidence) {
618
+ scopeIndex.set(key, link);
619
+ }
620
+ }
621
+ const legacySeen = new Set();
622
+ const diffs = [];
623
+ for (const link of legacy.links) {
624
+ const key = linkKey(link);
625
+ legacySeen.add(key);
626
+ const scopeLink = scopeIndex.get(key);
627
+ if (!scopeLink) {
628
+ // Legacy link not found in scope-based
629
+ diffs.push({ linkId: key, legacy: link, scopeBased: null });
630
+ }
631
+ else if (Math.abs((scopeLink.confidence ?? 0) - (link.confidence ?? 0)) > 0.05) {
632
+ // Same semantic link but different confidence
633
+ diffs.push({ linkId: key, legacy: link, scopeBased: scopeLink });
634
+ }
635
+ }
636
+ // Find links unique to scope-based (added)
637
+ for (const [key, link] of scopeIndex) {
638
+ if (!legacySeen.has(key)) {
639
+ diffs.push({ linkId: key, legacy: link, scopeBased: null }); // legacy=null means added by scope
640
+ }
641
+ }
642
+ return diffs;
643
+ }
644
+ export function computeDiffStats(legacy, scopeBased, diffs) {
645
+ const scopeKeys = new Set(diffs.filter(d => !d.legacy).map(d => d.linkId));
646
+ const legacyKeys = new Set(diffs.filter(d => !d.scopeBased).map(d => d.linkId));
647
+ let addedInScope = 0;
648
+ let missingFromScope = 0;
649
+ let confidenceDiff = 0;
650
+ for (const d of diffs) {
651
+ if (!d.scopeBased)
652
+ missingFromScope++;
653
+ else if (scopeKeys.has(d.linkId))
654
+ addedInScope++;
655
+ else
656
+ confidenceDiff++;
657
+ }
658
+ const matchedKeys = new Set();
659
+ for (const link of legacy.links) {
660
+ const key = linkKey(link);
661
+ if (scopeIndex(diffs).has(key))
662
+ continue; // already counted as diff/missing
663
+ matchedKeys.add(key);
664
+ }
665
+ return {
666
+ addedInScope,
667
+ missingFromScope,
668
+ confidenceDiff,
669
+ totalMatched: legacy.links.length - missingFromScope - confidenceDiff + addedInScope,
670
+ totalLegacy: legacy.links.length,
671
+ totalScope: scopeBased.links.length,
672
+ };
673
+ }
674
+ function scopeIndex(diffs) {
675
+ const m = new Map();
676
+ for (const d of diffs)
677
+ m.set(d.linkId, d);
678
+ return m;
679
+ }
680
+ export function checkParity(languageId, legacy, scopeBased) {
681
+ const diffs = diffResolutions(legacy, scopeBased);
682
+ const stats = computeDiffStats(legacy, scopeBased, diffs);
683
+ const totalLinks = legacy.links.length;
684
+ const matchRate = totalLinks > 0 ? (totalLinks - diffs.length) / totalLinks : 1;
685
+ return { reached: matchRate >= 0.99, matchRate, stats };
686
+ }
687
+ //# sourceMappingURL=scope-resolver.js.map