milens 0.6.5 → 0.6.7
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.
- package/.agents/skills/adapters/SKILL.md +14 -14
- package/.agents/skills/analyzer/SKILL.md +26 -24
- package/.agents/skills/apps/SKILL.md +10 -8
- package/.agents/skills/docs/SKILL.md +14 -8
- package/.agents/skills/milens/SKILL.md +164 -43
- package/.agents/skills/milens-security-review/SKILL.md +224 -224
- package/.agents/skills/orchestrator/SKILL.md +3 -1
- package/.agents/skills/parser/SKILL.md +16 -15
- package/.agents/skills/root/SKILL.md +20 -20
- package/.agents/skills/security/SKILL.md +1 -0
- package/.agents/skills/server/SKILL.md +13 -12
- package/.agents/skills/store/SKILL.md +12 -12
- package/.agents/skills/test/SKILL.md +57 -26
- package/README.md +170 -171
- package/adapters/claude-code/CLAUDE.md +36 -15
- package/adapters/codex/.codex/codex.md +38 -23
- package/adapters/copilot/.github/copilot-instructions.md +29 -22
- package/adapters/gemini/.gemini/context.md +33 -10
- package/adapters/opencode/AGENTS.md +36 -15
- package/dist/agents-md.d.ts.map +1 -1
- package/dist/agents-md.js +51 -2
- package/dist/agents-md.js.map +1 -1
- package/dist/analyzer/engine.d.ts +3 -0
- package/dist/analyzer/engine.d.ts.map +1 -1
- package/dist/analyzer/engine.js +342 -8
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/resolver.d.ts +2 -0
- package/dist/analyzer/resolver.d.ts.map +1 -1
- package/dist/analyzer/resolver.js +187 -9
- package/dist/analyzer/resolver.js.map +1 -1
- package/dist/analyzer/review.d.ts.map +1 -1
- package/dist/analyzer/review.js +254 -32
- package/dist/analyzer/review.js.map +1 -1
- package/dist/analyzer/scope-resolver.d.ts +42 -0
- package/dist/analyzer/scope-resolver.d.ts.map +1 -0
- package/dist/analyzer/scope-resolver.js +687 -0
- package/dist/analyzer/scope-resolver.js.map +1 -0
- package/dist/cli.js +294 -1
- package/dist/cli.js.map +1 -1
- package/dist/parser/extract.d.ts +6 -1
- package/dist/parser/extract.d.ts.map +1 -1
- package/dist/parser/extract.js +14 -2
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-css.d.ts.map +1 -1
- package/dist/parser/lang-css.js +7 -1
- package/dist/parser/lang-css.js.map +1 -1
- package/dist/parser/lang-go.d.ts.map +1 -1
- package/dist/parser/lang-go.js +16 -0
- package/dist/parser/lang-go.js.map +1 -1
- package/dist/parser/lang-html.d.ts +4 -0
- package/dist/parser/lang-html.d.ts.map +1 -1
- package/dist/parser/lang-html.js +40 -1
- package/dist/parser/lang-html.js.map +1 -1
- package/dist/parser/lang-java.d.ts.map +1 -1
- package/dist/parser/lang-java.js +12 -0
- package/dist/parser/lang-java.js.map +1 -1
- package/dist/parser/lang-js.d.ts.map +1 -1
- package/dist/parser/lang-js.js +3 -0
- package/dist/parser/lang-js.js.map +1 -1
- package/dist/parser/lang-php.d.ts.map +1 -1
- package/dist/parser/lang-php.js +11 -0
- package/dist/parser/lang-php.js.map +1 -1
- package/dist/parser/lang-py.d.ts.map +1 -1
- package/dist/parser/lang-py.js +14 -0
- package/dist/parser/lang-py.js.map +1 -1
- package/dist/parser/lang-ruby.d.ts.map +1 -1
- package/dist/parser/lang-ruby.js +20 -0
- package/dist/parser/lang-ruby.js.map +1 -1
- package/dist/parser/lang-rust.d.ts.map +1 -1
- package/dist/parser/lang-rust.js +27 -4
- package/dist/parser/lang-rust.js.map +1 -1
- package/dist/parser/lang-ts.d.ts.map +1 -1
- package/dist/parser/lang-ts.js +3 -0
- package/dist/parser/lang-ts.js.map +1 -1
- package/dist/parser/lang-vue.d.ts +17 -1
- package/dist/parser/lang-vue.d.ts.map +1 -1
- package/dist/parser/lang-vue.js +177 -0
- package/dist/parser/lang-vue.js.map +1 -1
- package/dist/parser/language-provider.d.ts +27 -0
- package/dist/parser/language-provider.d.ts.map +1 -0
- package/dist/parser/language-provider.js +2 -0
- package/dist/parser/language-provider.js.map +1 -0
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +224 -50
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/watcher.d.ts +8 -0
- package/dist/server/watcher.d.ts.map +1 -1
- package/dist/server/watcher.js +10 -8
- package/dist/server/watcher.js.map +1 -1
- package/dist/skills.js +163 -42
- package/dist/skills.js.map +1 -1
- package/dist/store/schema.sql +1 -1
- package/dist/ui/progress.d.ts +28 -0
- package/dist/ui/progress.d.ts.map +1 -0
- package/dist/ui/progress.js +100 -0
- package/dist/ui/progress.js.map +1 -0
- package/dist/uninstall.d.ts +54 -0
- package/dist/uninstall.d.ts.map +1 -0
- package/dist/uninstall.js +795 -0
- package/dist/uninstall.js.map +1 -0
- package/docs/README.md +4 -1
- 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
|