weavatrix 0.2.4 → 0.2.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.
- package/README.md +53 -11
- package/package.json +1 -1
- package/skill/SKILL.md +18 -7
- package/src/analysis/dead-check.js +48 -2
- package/src/analysis/dead-code-review.js +9 -5
- package/src/analysis/duplicates.compute.js +11 -6
- package/src/analysis/graph-analysis.aggregate.js +3 -1
- package/src/analysis/graph-analysis.refs.js +19 -11
- package/src/analysis/hot-path-review.js +228 -0
- package/src/analysis/internal-audit.run.js +36 -0
- package/src/analysis/source-complexity.report.js +5 -0
- package/src/analysis/source-complexity.walk.js +64 -10
- package/src/bounds.js +4 -0
- package/src/graph/builder/lang-js.js +71 -14
- package/src/graph/builder/lang-python.js +161 -4
- package/src/graph/freshness-probe.js +5 -3
- package/src/graph/incremental-refresh.js +4 -2
- package/src/graph/internal-builder.barrels.js +71 -5
- package/src/graph/internal-builder.build.js +77 -16
- package/src/mcp/catalog.mjs +12 -7
- package/src/mcp/graph-context.mjs +4 -0
- package/src/mcp/graph-diff.mjs +1 -1
- package/src/mcp/sync-payload.mjs +3 -2
- package/src/mcp/tools-actions.mjs +3 -1
- package/src/mcp/tools-context.mjs +164 -0
- package/src/mcp/tools-graph.mjs +2 -0
- package/src/mcp/tools-health.mjs +66 -5
- package/src/mcp/tools-impact.mjs +3 -3
- package/src/mcp/tools-source.mjs +238 -0
- package/src/precision/lsp-client.js +1 -1
- package/src/precision/lsp-overlay.js +29 -8
- package/src/precision/symbol-query.js +133 -0
|
@@ -11,11 +11,12 @@ export default {
|
|
|
11
11
|
grammars: ["python"],
|
|
12
12
|
exts: { ".py": "python", ".pyi": "python" },
|
|
13
13
|
isWeb: false,
|
|
14
|
+
customCalls: true,
|
|
14
15
|
calls: `(call function: (identifier) @callee)`,
|
|
15
16
|
heritage: [`(class_definition superclasses: (argument_list (identifier) @super))`],
|
|
16
17
|
|
|
17
18
|
pass1(ctx) {
|
|
18
|
-
const { grammar, tree, fileRel, caps, field, addSym, addImportEdge, addExternalImport, imports, resolvePyPath, pyBaseDir, pyTopDirs } = ctx;
|
|
19
|
+
const { grammar, tree, fileRel, code, caps, field, addSym, addImportEdge, addExternalImport, markExported, imports, links, nameToId, resolvePyPath, pyBaseDir, pyTopDirs } = ctx;
|
|
19
20
|
// the importing file's own dir is on sys.path when it runs as a script — try siblings for absolute
|
|
20
21
|
// imports, EXCEPT stdlib names (a sibling logging.py must not shadow `import logging`; py3 absolute
|
|
21
22
|
// imports always pick the stdlib, and a wrong internal edge distorts dead-code + reachability)
|
|
@@ -30,15 +31,37 @@ export default {
|
|
|
30
31
|
|
|
31
32
|
// ---- symbols (decorated defs are framework-entered — @app.route/@app.event/@pytest.fixture — the
|
|
32
33
|
// flag keeps dead-code checks from calling them dead; tree wraps them in decorated_definition) ----
|
|
34
|
+
const ownedMethods = [];
|
|
33
35
|
for (const cap of caps(grammar, `(function_definition name: (identifier) @fn) (class_definition name: (identifier) @class)`, tree.rootNode)) {
|
|
34
36
|
const decorated = cap.node.parent?.parent?.type === "decorated_definition";
|
|
35
|
-
|
|
37
|
+
let ownerName = null;
|
|
38
|
+
if (cap.name === "fn") {
|
|
39
|
+
let parent = cap.node.parent?.parent;
|
|
40
|
+
while (parent && !["function_definition", "class_definition", "module"].includes(parent.type)) parent = parent.parent;
|
|
41
|
+
if (parent?.type === "class_definition") ownerName = field(parent, "name")?.text || null;
|
|
42
|
+
}
|
|
43
|
+
const visibility = /^__[^_].*/.test(cap.node.text) ? "private"
|
|
44
|
+
: /^_[^_]/.test(cap.node.text) ? "protected" : "public";
|
|
45
|
+
const id = addSym(cap.node.text, cap.node.startPosition.row + 1, cap.name === "fn", {
|
|
36
46
|
sourceNode: cap.node.parent,
|
|
37
|
-
|
|
47
|
+
selectionNode: cap.node,
|
|
48
|
+
...(decorated ? { decorated: true } : {}),
|
|
49
|
+
symbolKind: cap.name === "class" ? "class" : ownerName ? "method" : "function",
|
|
50
|
+
...(ownerName ? {memberOf: ownerName, visibility} : {moduleDeclaration: true}),
|
|
38
51
|
});
|
|
52
|
+
if (ownerName) ownedMethods.push({ownerName, id});
|
|
53
|
+
}
|
|
54
|
+
for (const method of ownedMethods) {
|
|
55
|
+
const ownerId = nameToId.get(method.ownerName);
|
|
56
|
+
if (ownerId) links.push({source: ownerId, target: method.id, relation: "contains", confidence: "EXTRACTED"});
|
|
39
57
|
}
|
|
40
58
|
for (const cap of caps(grammar, `(module (expression_statement (assignment left: (identifier) @var)))`, tree.rootNode))
|
|
41
|
-
addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent });
|
|
59
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, false, { sourceNode: cap.node.parent, selectionNode: cap.node, symbolKind: "variable", moduleDeclaration: true });
|
|
60
|
+
|
|
61
|
+
// Static __all__ is authoritative for wildcard imports. The assignment itself remains indexed,
|
|
62
|
+
// while listed declarations are marked as the explicit public module surface.
|
|
63
|
+
const allMatch = String(code || "").match(/(?:^|\n)\s*__all__\s*=\s*[\[(]([\s\S]*?)[\])]/m);
|
|
64
|
+
if (allMatch) for (const match of allMatch[1].matchAll(/["']([A-Za-z_]\w*)["']/g)) markExported(match[1]);
|
|
42
65
|
|
|
43
66
|
// ---- import a.b [as c] ----
|
|
44
67
|
for (const cap of caps(grammar, `(import_statement) @imp`, tree.rootNode)) {
|
|
@@ -61,6 +84,15 @@ export default {
|
|
|
61
84
|
else if (modNode.type === "dotted_name") modParts = modNode.text.split(".");
|
|
62
85
|
}
|
|
63
86
|
const baseDir = pyBaseDir(fileRel, dots);
|
|
87
|
+
const wildcard = node.namedChildren.some((child) => child.type === "wildcard_import");
|
|
88
|
+
if (wildcard) {
|
|
89
|
+
const modFile = resolvePyPath(baseDir, modParts) || (dots === 0 ? resolveAbs(modParts) : null);
|
|
90
|
+
if (modFile) {
|
|
91
|
+
addImportEdge(modFile);
|
|
92
|
+
imports.set(`*:${node.startPosition.row + 1}`, {imported: "*", targetFile: modFile, wildcard: true, line: node.startPosition.row + 1});
|
|
93
|
+
} else if (dots === 0 && modParts.length) recordExternal(modParts.join("."), node.startPosition.row + 1);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
64
96
|
const names = node.namedChildren.filter((c) => c !== modNode && (c.type === "dotted_name" || c.type === "aliased_import"));
|
|
65
97
|
let externalDone = false; // one record per statement, not per imported name
|
|
66
98
|
for (const nm of names) {
|
|
@@ -75,4 +107,129 @@ export default {
|
|
|
75
107
|
}
|
|
76
108
|
}
|
|
77
109
|
},
|
|
110
|
+
|
|
111
|
+
pass2(ctx) {
|
|
112
|
+
const {grammar, tree, fileRel, caps, field, enclosing, links, nodeById, perFileSymbols, symByFileName, importedLocals, resolveCall} = ctx;
|
|
113
|
+
const imports = importedLocals.get(fileRel) || new Map();
|
|
114
|
+
const wildcardImports = [...imports.values()].filter((entry) => entry?.wildcard && entry.targetFile);
|
|
115
|
+
const emitted = new Set();
|
|
116
|
+
const addCall = (caller, target, line, evidence) => {
|
|
117
|
+
if (!caller || !target || target === caller.id) return;
|
|
118
|
+
const key = `${caller.id}\0${target}\0${line}`;
|
|
119
|
+
if (emitted.has(key) || links.some((link) => link.source === caller.id && link.target === target && link.relation === "calls" && link.line === line)) return;
|
|
120
|
+
emitted.add(key);
|
|
121
|
+
const provenance = ["receiver-type", "wildcard-import", "module-member"].includes(evidence) ? "RESOLVED" : "INFERRED";
|
|
122
|
+
links.push({source: caller.id, target, relation: "calls", confidence: "INFERRED", provenance, line, pythonResolution: evidence});
|
|
123
|
+
};
|
|
124
|
+
const moduleSymbol = (targetFile, name) => {
|
|
125
|
+
const id = symByFileName.get(targetFile)?.get(name);
|
|
126
|
+
if (!id) return null;
|
|
127
|
+
const node = nodeById.get(id);
|
|
128
|
+
return node?.member_of ? null : id;
|
|
129
|
+
};
|
|
130
|
+
const wildcardCandidates = (name) => {
|
|
131
|
+
const candidates = [];
|
|
132
|
+
for (const imp of wildcardImports) {
|
|
133
|
+
const symbols = perFileSymbols.get(imp.targetFile) || [];
|
|
134
|
+
const explicitAll = symbols.some((symbol) => symbol.name === "__all__");
|
|
135
|
+
const symbol = symbols.find((entry) => entry.name === name && !entry.memberOf);
|
|
136
|
+
if (!symbol || (explicitAll ? nodeById.get(symbol.id)?.exported !== true : name.startsWith("_"))) continue;
|
|
137
|
+
candidates.push(symbol.id);
|
|
138
|
+
}
|
|
139
|
+
return [...new Set(candidates)];
|
|
140
|
+
};
|
|
141
|
+
const classInfo = (name) => {
|
|
142
|
+
const local = symByFileName.get(fileRel)?.get(name);
|
|
143
|
+
if (local && nodeById.get(local)?.symbol_kind === "class") return {id: local, name, file: fileRel};
|
|
144
|
+
const imp = imports.get(name);
|
|
145
|
+
if (imp?.targetFile && !imp.wildcard) {
|
|
146
|
+
const targetFile = imp.originFile || imp.targetFile;
|
|
147
|
+
const targetName = imp.originName || imp.imported || name;
|
|
148
|
+
const id = symByFileName.get(targetFile)?.get(targetName);
|
|
149
|
+
if (id && nodeById.get(id)?.symbol_kind === "class") return {id, name: targetName, file: targetFile};
|
|
150
|
+
}
|
|
151
|
+
const wildcard = wildcardCandidates(name).filter((id) => nodeById.get(id)?.symbol_kind === "class");
|
|
152
|
+
if (wildcard.length === 1) {
|
|
153
|
+
const node = nodeById.get(wildcard[0]);
|
|
154
|
+
return {id: wildcard[0], name: node.label || name, file: node.source_file};
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
};
|
|
158
|
+
const methodOf = (klass, method) => (perFileSymbols.get(klass.file) || [])
|
|
159
|
+
.find((symbol) => symbol.memberOf === String(nodeById.get(klass.id)?.label || klass.name).replace(/\(.*$/, "") && symbol.name === method)?.id || null;
|
|
160
|
+
const bindings = new Map();
|
|
161
|
+
const shadows = new Set();
|
|
162
|
+
const scopeKey = (caller, name) => `${caller?.id || ""}\0${name}`;
|
|
163
|
+
const bind = (caller, name, klass) => { if (caller && name && klass) bindings.set(scopeKey(caller, name), klass); };
|
|
164
|
+
const typeName = (node) => {
|
|
165
|
+
const text = String(node?.text || "").replace(/^['"]|['"]$/g, "");
|
|
166
|
+
return (text.match(/[A-Za-z_]\w*/) || [])[0] || "";
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
for (const cap of caps(grammar, `(parameters (identifier) @param) (typed_parameter) @typed`, tree.rootNode)) {
|
|
170
|
+
const line = cap.node.startPosition.row + 1;
|
|
171
|
+
const caller = enclosing(fileRel, line);
|
|
172
|
+
if (!caller) continue;
|
|
173
|
+
if (cap.name === "param") {
|
|
174
|
+
shadows.add(scopeKey(caller, cap.node.text));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const name = cap.node.namedChildren.find((child) => child.type === "identifier")?.text;
|
|
178
|
+
if (!name) continue;
|
|
179
|
+
shadows.add(scopeKey(caller, name));
|
|
180
|
+
bind(caller, name, classInfo(typeName(field(cap.node, "type"))));
|
|
181
|
+
}
|
|
182
|
+
for (const cap of caps(grammar, `(assignment) @assign`, tree.rootNode)) {
|
|
183
|
+
const assignment = cap.node;
|
|
184
|
+
const caller = enclosing(fileRel, assignment.startPosition.row + 1);
|
|
185
|
+
if (!caller) continue;
|
|
186
|
+
const left = field(assignment, "left");
|
|
187
|
+
const right = field(assignment, "right");
|
|
188
|
+
let receiver = "";
|
|
189
|
+
if (left?.type === "identifier") receiver = left.text;
|
|
190
|
+
else if (left?.type === "attribute") receiver = left.text;
|
|
191
|
+
if (!receiver) continue;
|
|
192
|
+
shadows.add(scopeKey(caller, receiver));
|
|
193
|
+
const annotation = typeName(field(assignment, "type"));
|
|
194
|
+
const constructor = right?.type === "call" && field(right, "function")?.type === "identifier"
|
|
195
|
+
? field(right, "function").text : "";
|
|
196
|
+
bind(caller, receiver, classInfo(annotation || constructor));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const cap of caps(grammar, `(call function: (identifier) @callee)`, tree.rootNode)) {
|
|
200
|
+
const line = cap.node.startPosition.row + 1;
|
|
201
|
+
const caller = enclosing(fileRel, line);
|
|
202
|
+
if (!caller || shadows.has(scopeKey(caller, cap.node.text))) continue;
|
|
203
|
+
let target = resolveCall(cap.node.text, fileRel);
|
|
204
|
+
if (target && nodeById.get(target)?.member_of) target = null;
|
|
205
|
+
let evidence = "bare";
|
|
206
|
+
if (!target) {
|
|
207
|
+
const candidates = wildcardCandidates(cap.node.text);
|
|
208
|
+
if (candidates.length === 1) { target = candidates[0]; evidence = "wildcard-import"; }
|
|
209
|
+
}
|
|
210
|
+
addCall(caller, target, line, evidence);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
for (const cap of caps(grammar, `(call function: (attribute) @attributeCall)`, tree.rootNode)) {
|
|
214
|
+
const attribute = cap.node;
|
|
215
|
+
const object = field(attribute, "object");
|
|
216
|
+
const member = field(attribute, "attribute");
|
|
217
|
+
if (!object || !member) continue;
|
|
218
|
+
const line = attribute.startPosition.row + 1;
|
|
219
|
+
const caller = enclosing(fileRel, line);
|
|
220
|
+
if (!caller) continue;
|
|
221
|
+
if (object.type === "identifier") {
|
|
222
|
+
const moduleImport = imports.get(object.text);
|
|
223
|
+
if (moduleImport?.targetFile && moduleImport.imported === "*" && !moduleImport.wildcard) {
|
|
224
|
+
addCall(caller, moduleSymbol(moduleImport.targetFile, member.text), line, "module-member");
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
let klass = null;
|
|
229
|
+
if (["self", "cls"].includes(object.text) && caller.memberOf) klass = classInfo(caller.memberOf);
|
|
230
|
+
if (!klass) klass = bindings.get(scopeKey(caller, object.text));
|
|
231
|
+
if (!klass && object.type === "call" && field(object, "function")?.type === "identifier") klass = classInfo(field(object, "function").text);
|
|
232
|
+
addCall(caller, klass && methodOf(klass, member.text), line, "receiver-type");
|
|
233
|
+
}
|
|
234
|
+
},
|
|
78
235
|
};
|
|
@@ -9,7 +9,7 @@ import {childProcessEnv} from '../child-env.js'
|
|
|
9
9
|
import {createRepoBoundary} from '../repo-path.js'
|
|
10
10
|
|
|
11
11
|
export const REPOSITORY_FRESHNESS_PROBE_V = 1
|
|
12
|
-
export const GRAPH_BUILDER_SCHEMA_V =
|
|
12
|
+
export const GRAPH_BUILDER_SCHEMA_V = 5
|
|
13
13
|
export const GRAPH_BUILDER_VERSION = (() => {
|
|
14
14
|
try { return String(createRequire(import.meta.url)('../../package.json').version) }
|
|
15
15
|
catch { return '0.0.0' }
|
|
@@ -22,10 +22,12 @@ const CURRENT_GRAPH_SCHEMA = Object.freeze({
|
|
|
22
22
|
extImportsV: 2,
|
|
23
23
|
edgeTypesV: 2,
|
|
24
24
|
edgeProvenanceV: 1,
|
|
25
|
-
complexityV:
|
|
25
|
+
complexityV: 2,
|
|
26
26
|
repoBoundaryV: 1,
|
|
27
27
|
barrelResolutionV: 1,
|
|
28
|
-
|
|
28
|
+
reExportOccurrencesV: 1,
|
|
29
|
+
symbolSpacesV: 1,
|
|
30
|
+
extractorSchemaV: 5,
|
|
29
31
|
})
|
|
30
32
|
const CONTROL_FILES = ['.gitignore', '.weavatrixignore', '.weavatrix.json']
|
|
31
33
|
const MAX_CONTROL_BYTES = 1_000_000
|
|
@@ -157,12 +157,13 @@ function mergeScopedGraph(base, scoped, affected, snapshot) {
|
|
|
157
157
|
links,
|
|
158
158
|
externalImports,
|
|
159
159
|
jsExportRecords: scoped.jsExportRecords || base.jsExportRecords || {},
|
|
160
|
+
reExportOccurrences: scoped.reExportOccurrences || base.reExportOccurrences || [],
|
|
160
161
|
fileHashes: snapshot.fileHashes,
|
|
161
162
|
fileExportSignatures: snapshot.fileExportSignatures,
|
|
162
163
|
controlHashes: snapshot.controlHashes,
|
|
163
164
|
graphRevision: snapshot.revision,
|
|
164
165
|
};
|
|
165
|
-
for (const key of ["extImportsV", "edgeTypesV", "edgeProvenanceV", "complexityV", "repoBoundaryV", "barrelResolutionV", "extractorSchemaV"]) {
|
|
166
|
+
for (const key of ["extImportsV", "edgeTypesV", "edgeProvenanceV", "complexityV", "repoBoundaryV", "barrelResolutionV", "reExportOccurrencesV", "symbolSpacesV", "extractorSchemaV"]) {
|
|
166
167
|
merged[key] = Math.max(Number(base[key]) || 0, Number(scoped[key]) || 0);
|
|
167
168
|
}
|
|
168
169
|
return merged;
|
|
@@ -183,7 +184,8 @@ export async function refreshGraphIncrementally(repoDir, existingGraph, {
|
|
|
183
184
|
|
|
184
185
|
if (!existingGraph || !existingGraph.fileHashes || !existingGraph.fileExportSignatures
|
|
185
186
|
|| !existingGraph.controlHashes || !existingGraph.jsExportRecords || Number(existingGraph.barrelResolutionV) < 1
|
|
186
|
-
|| Number(existingGraph.extractorSchemaV) <
|
|
187
|
+
|| Number(existingGraph.extractorSchemaV) < 5 || Number(existingGraph.reExportOccurrencesV) < 1
|
|
188
|
+
|| Number(existingGraph.symbolSpacesV) < 1 || Number(existingGraph.edgeProvenanceV) < 1) {
|
|
187
189
|
return full("incremental-baseline-unavailable");
|
|
188
190
|
}
|
|
189
191
|
if (!sameRecord(existingGraph.controlHashes, snapshot.controlHashes)) return full("ignore-or-control-config-changed");
|
|
@@ -28,15 +28,22 @@ function withTypeOnly(result, typeOnly) {
|
|
|
28
28
|
return resolved(result.origin.file, result.origin.name, true);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function resolveJsBarrels({ jsExports, importedLocals, links }) {
|
|
31
|
+
export function resolveJsBarrels({ jsExports, importedLocals, links, resolveSymbol = () => null }) {
|
|
32
32
|
const tables = new Map();
|
|
33
33
|
for (const [file, records] of jsExports) {
|
|
34
|
-
const table = { explicit: new Map(), stars: [] };
|
|
34
|
+
const table = { explicit: new Map(), stars: [], facadeMembers: new Map() };
|
|
35
35
|
for (const record of records || []) {
|
|
36
36
|
if (record.kind === "star") {
|
|
37
37
|
table.stars.push(record);
|
|
38
38
|
continue;
|
|
39
39
|
}
|
|
40
|
+
if (record.kind === "facade-member") {
|
|
41
|
+
if (!record.member) continue;
|
|
42
|
+
const list = table.facadeMembers.get(record.member) || [];
|
|
43
|
+
list.push(record);
|
|
44
|
+
table.facadeMembers.set(record.member, list);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
40
47
|
if (!record.exported) continue;
|
|
41
48
|
const list = table.explicit.get(record.exported) || [];
|
|
42
49
|
list.push(record);
|
|
@@ -87,6 +94,60 @@ export function resolveJsBarrels({ jsExports, importedLocals, links }) {
|
|
|
87
94
|
return result;
|
|
88
95
|
};
|
|
89
96
|
|
|
97
|
+
const resolveFacadeMember = (file, member, trail = new Set()) => {
|
|
98
|
+
const key = `${file}\0default.${member}`;
|
|
99
|
+
if (trail.has(key)) return MISSING;
|
|
100
|
+
const records = tables.get(file)?.facadeMembers.get(member) || [];
|
|
101
|
+
if (!records.length) return MISSING;
|
|
102
|
+
const nextTrail = new Set(trail);
|
|
103
|
+
nextTrail.add(key);
|
|
104
|
+
return mergeCandidates(records.map((record) => {
|
|
105
|
+
const local = record.local || member;
|
|
106
|
+
const binding = importedLocals.get(file)?.get(local);
|
|
107
|
+
if (binding?.targetFile && binding.imported && binding.imported !== "*") {
|
|
108
|
+
return withTypeOnly(resolveExport(binding.targetFile, binding.imported, nextTrail), record.typeOnly || binding.typeOnly);
|
|
109
|
+
}
|
|
110
|
+
return resolved(file, local, record.typeOnly);
|
|
111
|
+
}));
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Materialize source-free, occurrence-level re-export evidence. Named/local alias records retain
|
|
115
|
+
// their exact public name and final declaration origin; wildcard/namespace records retain the
|
|
116
|
+
// concrete facade statement and target for on-demand propagation in context tools.
|
|
117
|
+
const reExportOccurrences = [];
|
|
118
|
+
for (const [file, records] of jsExports) for (const record of records || []) {
|
|
119
|
+
if (["facade-root", "facade-member"].includes(record.kind)) continue;
|
|
120
|
+
const resolution = record.kind === "star" || !record.exported
|
|
121
|
+
? MISSING
|
|
122
|
+
: resolveExport(file, record.exported);
|
|
123
|
+
if (resolution.status === "resolved") {
|
|
124
|
+
record.originFile = resolution.origin.file;
|
|
125
|
+
record.originName = resolution.origin.name;
|
|
126
|
+
record.originTypeOnly = resolution.origin.typeOnly === true;
|
|
127
|
+
record.originId = resolveSymbol(resolution.origin.file, resolution.origin.name, resolution.origin.typeOnly === true) || null;
|
|
128
|
+
}
|
|
129
|
+
const reExportedLocal = record.kind === "local" && resolution.status === "resolved" && resolution.origin.file !== file;
|
|
130
|
+
if (!record.targetFile && !reExportedLocal) continue;
|
|
131
|
+
reExportOccurrences.push({
|
|
132
|
+
file,
|
|
133
|
+
line: Number(record.line) || 1,
|
|
134
|
+
kind: record.kind,
|
|
135
|
+
exported: record.exported || record.member || "*",
|
|
136
|
+
imported: record.imported || record.local || "*",
|
|
137
|
+
targetFile: record.targetFile || resolution.origin?.file || file,
|
|
138
|
+
typeOnly: record.typeOnly === true || resolution.origin?.typeOnly === true,
|
|
139
|
+
...(record.specifier ? {specifier: record.specifier} : {}),
|
|
140
|
+
status: record.kind === "star" ? "wildcard" : resolution.status,
|
|
141
|
+
...(resolution.status === "resolved" ? {
|
|
142
|
+
originFile: resolution.origin.file,
|
|
143
|
+
originName: resolution.origin.name,
|
|
144
|
+
...(record.originId ? {originId: record.originId} : {}),
|
|
145
|
+
} : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
reExportOccurrences.sort((left, right) => left.file.localeCompare(right.file)
|
|
149
|
+
|| left.line - right.line || left.exported.localeCompare(right.exported));
|
|
150
|
+
|
|
90
151
|
// Every internal re-export is a physical facade hop. It remains in the graph for runtime cycle truth,
|
|
91
152
|
// but semantic consumers ignore it in favor of importer -> declaration edges below.
|
|
92
153
|
for (const link of links) {
|
|
@@ -149,8 +210,13 @@ export function resolveJsBarrels({ jsExports, importedLocals, links }) {
|
|
|
149
210
|
// Namespace member usage is only knowable in pass 2 (`ui.Button`, `ui.run`). Expose a bounded helper
|
|
150
211
|
// that uses the same cache and emits the corresponding semantic file edge exactly once.
|
|
151
212
|
const resolveNamespaceMember = (source, imp, member, usage = null) => {
|
|
152
|
-
if (!imp?.targetFile
|
|
153
|
-
const
|
|
213
|
+
if (!imp?.targetFile) return MISSING;
|
|
214
|
+
const defaultFacade = imp.imported === "default" || imp.originName === "default";
|
|
215
|
+
const result = imp.imported === "*"
|
|
216
|
+
? resolveExport(imp.targetFile, member)
|
|
217
|
+
: defaultFacade
|
|
218
|
+
? resolveFacadeMember(imp.originFile || imp.targetFile, member)
|
|
219
|
+
: MISSING;
|
|
154
220
|
if (result.status !== "resolved") return result;
|
|
155
221
|
if (result.origin.file !== imp.targetFile) {
|
|
156
222
|
for (const link of links) {
|
|
@@ -165,5 +231,5 @@ export function resolveJsBarrels({ jsExports, importedLocals, links }) {
|
|
|
165
231
|
return result;
|
|
166
232
|
};
|
|
167
233
|
|
|
168
|
-
return { resolveExport, resolveNamespaceMember };
|
|
234
|
+
return { resolveExport, resolveNamespaceMember, resolveFacadeMember, reExportOccurrences };
|
|
169
235
|
}
|
|
@@ -37,6 +37,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
37
37
|
const addNode = (n) => { if (!nodeIds.has(n.id)) { nodeIds.add(n.id); nodes.push(n); nodeById.set(n.id, n); } };
|
|
38
38
|
const perFileSymbols = new Map();
|
|
39
39
|
const symByFileName = new Map();
|
|
40
|
+
const symIdsByFileName = new Map();
|
|
40
41
|
const importedLocals = new Map();
|
|
41
42
|
const jsExports = new Map();
|
|
42
43
|
if (requestedFiles && opts.baseGraph?.jsExportRecords) {
|
|
@@ -54,6 +55,11 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
54
55
|
let names = symByFileName.get(file);
|
|
55
56
|
if (!names) symByFileName.set(file, (names = new Map()));
|
|
56
57
|
if (!names.has(match[1])) names.set(match[1], id);
|
|
58
|
+
let idsByName = symIdsByFileName.get(file);
|
|
59
|
+
if (!idsByName) symIdsByFileName.set(file, (idsByName = new Map()));
|
|
60
|
+
const ids = idsByName.get(match[1]) || [];
|
|
61
|
+
ids.push(id);
|
|
62
|
+
idsByName.set(match[1], ids);
|
|
57
63
|
nodeById.set(id, node);
|
|
58
64
|
}
|
|
59
65
|
}
|
|
@@ -73,17 +79,17 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
73
79
|
for (const abs of files) {
|
|
74
80
|
const fileRel = rel(abs); const ext = extname(abs);
|
|
75
81
|
addNode({ id: fileRel, label: fileRel.split("/").pop(), file_type: "code", source_file: fileRel, source_location: "L1" });
|
|
76
|
-
if (isDataFile(fileRel) || isDocFile(fileRel)) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); continue; } // config/infra/docs file-only node
|
|
82
|
+
if (isDataFile(fileRel) || isDocFile(fileRel)) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; } // config/infra/docs file-only node
|
|
77
83
|
const grammar = EXT_LANG[ext]; const lang = LANGS[FAMILY[grammar]]; if (!lang || !langs[grammar]) continue;
|
|
78
84
|
let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
|
|
79
85
|
// giant / generated / minified file → keep the file NODE but don't parse symbols (avoids a wedged parse)
|
|
80
|
-
if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); continue; }
|
|
86
|
+
if (code.length > MAX_PARSE_BYTES) { perFileSymbols.set(fileRel, []); symByFileName.set(fileRel, new Map()); symIdsByFileName.set(fileRel, new Map()); continue; }
|
|
81
87
|
if ((++_parsed % 24) === 0) await new Promise((r) => setImmediate(r)); // breathe: let the UI paint between chunks
|
|
82
88
|
if (typeof opts.onParseFile === "function") opts.onParseFile(fileRel);
|
|
83
89
|
const parser = new Parser(); parser.setLanguage(langs[grammar]);
|
|
84
90
|
let tree; try { tree = parser.parse(code); } catch { continue; }
|
|
85
91
|
|
|
86
|
-
const syms = []; const nameToId = new Map(); const moduleNameToId = new Map();
|
|
92
|
+
const syms = []; const nameToId = new Map(); const nameToIds = new Map(); const moduleNameToId = new Map();
|
|
87
93
|
const addSym = (name, line, callable, extra) => {
|
|
88
94
|
if (!name || !/^[A-Za-z_$][\w$]*$/.test(name)) return;
|
|
89
95
|
const suffix = /^:[A-Za-z0-9_-]+$/.test(extra?.idSuffix || "") ? extra.idSuffix : "";
|
|
@@ -135,12 +141,24 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
135
141
|
...(extra && extra.exported ? { exported: true } : {}),
|
|
136
142
|
...(extra && extra.decorated ? { decorated: true } : {}),
|
|
137
143
|
...(extra && extra.symbolKind ? { symbol_kind: extra.symbolKind } : {}),
|
|
144
|
+
...(extra && extra.symbolSpace ? { symbol_space: extra.symbolSpace } : {}),
|
|
138
145
|
...(extra && extra.memberOf ? { member_of: extra.memberOf } : {}),
|
|
139
146
|
...(extra && extra.visibility ? { visibility: extra.visibility } : {})
|
|
140
147
|
});
|
|
141
148
|
links.push({ source: fileRel, target: id, relation: "contains", confidence: "EXTRACTED" });
|
|
142
|
-
syms.push({
|
|
149
|
+
syms.push({
|
|
150
|
+
id,
|
|
151
|
+
name,
|
|
152
|
+
start: line,
|
|
153
|
+
end: endLine >= line ? endLine : 0,
|
|
154
|
+
...(extra?.memberOf ? {memberOf: extra.memberOf} : {}),
|
|
155
|
+
...(extra?.symbolKind ? {symbolKind: extra.symbolKind} : {}),
|
|
156
|
+
...(extra?.symbolSpace ? {symbolSpace: extra.symbolSpace} : {}),
|
|
157
|
+
});
|
|
143
158
|
if (!nameToId.has(name)) nameToId.set(name, id);
|
|
159
|
+
const ids = nameToIds.get(name) || [];
|
|
160
|
+
ids.push(id);
|
|
161
|
+
nameToIds.set(name, ids);
|
|
144
162
|
if (extra?.moduleDeclaration && !moduleNameToId.has(name)) moduleNameToId.set(name, id);
|
|
145
163
|
return id;
|
|
146
164
|
};
|
|
@@ -189,7 +207,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
189
207
|
for (let i = 0; i < syms.length; i++) {
|
|
190
208
|
if (!syms[i].end || syms[i].end < syms[i].start) syms[i].end = i + 1 < syms.length ? syms[i + 1].start - 1 : eof;
|
|
191
209
|
}
|
|
192
|
-
perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId);
|
|
210
|
+
perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId); symIdsByFileName.set(fileRel, nameToIds);
|
|
193
211
|
tree.delete();
|
|
194
212
|
}
|
|
195
213
|
|
|
@@ -204,7 +222,27 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
204
222
|
let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
|
|
205
223
|
for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
|
|
206
224
|
}
|
|
207
|
-
const
|
|
225
|
+
const inferredSymbolSpace = (node) => {
|
|
226
|
+
const explicit = String(node?.symbol_space || "");
|
|
227
|
+
if (["value", "type", "both"].includes(explicit)) return explicit;
|
|
228
|
+
const kind = String(node?.symbol_kind || "").toLowerCase();
|
|
229
|
+
if (["interface", "type"].includes(kind)) return "type";
|
|
230
|
+
if (["class", "enum"].includes(kind)) return "both";
|
|
231
|
+
return "value";
|
|
232
|
+
};
|
|
233
|
+
const resolveNamedSymbol = (file, name, space = "value") => {
|
|
234
|
+
const ids = symIdsByFileName.get(file)?.get(name) || [];
|
|
235
|
+
const accepts = (id) => {
|
|
236
|
+
const symbolSpace = inferredSymbolSpace(nodeById.get(id));
|
|
237
|
+
return symbolSpace === "both" || symbolSpace === space;
|
|
238
|
+
};
|
|
239
|
+
const exact = ids.find((id) => inferredSymbolSpace(nodeById.get(id)) === space);
|
|
240
|
+
return exact || ids.find(accepts) || null;
|
|
241
|
+
};
|
|
242
|
+
const { resolveNamespaceMember, reExportOccurrences } = resolveJsBarrels({
|
|
243
|
+
jsExports, importedLocals, links,
|
|
244
|
+
resolveSymbol: (file, name, typeOnly) => resolveNamedSymbol(file, name, typeOnly ? "type" : "value"),
|
|
245
|
+
});
|
|
208
246
|
// Exact source ranges can overlap (a named function nested inside another function). Attribute a call
|
|
209
247
|
// to the innermost matching symbol, not whichever outer declaration happened to be added first.
|
|
210
248
|
const enclosing = (fileRel, line) => {
|
|
@@ -216,13 +254,13 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
216
254
|
return best;
|
|
217
255
|
};
|
|
218
256
|
const resolveCall = (name, fileRel) => {
|
|
219
|
-
const local =
|
|
257
|
+
const local = resolveNamedSymbol(fileRel, name, "value"); if (local) return local;
|
|
220
258
|
if (sharesDirScope(fileRel)) { const d = fileRel.includes("/") ? fileRel.slice(0, fileRel.lastIndexOf("/")) : ""; const dm = goDirSymbols.get(d); if (dm && dm.has(name)) return dm.get(name); }
|
|
221
259
|
const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
|
|
222
260
|
if (imp && imp.targetFile) {
|
|
223
261
|
const targetFile = imp.originFile || imp.targetFile;
|
|
224
262
|
const importedName = imp.originName || imp.imported;
|
|
225
|
-
|
|
263
|
+
return resolveNamedSymbol(targetFile, importedName, "value");
|
|
226
264
|
}
|
|
227
265
|
return null;
|
|
228
266
|
};
|
|
@@ -243,11 +281,19 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
243
281
|
let code; try { code = readFileSync(abs, "utf8"); } catch { continue; }
|
|
244
282
|
const parser = new Parser(); parser.setLanguage(langs[grammar]);
|
|
245
283
|
let tree; try { tree = parser.parse(code); } catch { continue; }
|
|
246
|
-
for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
|
|
284
|
+
if (!lang.customCalls) for (const cap of caps(grammar, lang.calls, tree.rootNode)) {
|
|
247
285
|
const caller = enclosing(fileRel, cap.node.startPosition.row + 1); if (!caller) continue;
|
|
248
286
|
const target = resolveCall(cap.node.text, fileRel); if (!target || target === caller.id) continue;
|
|
249
287
|
links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
|
|
250
288
|
}
|
|
289
|
+
if (typeof lang.pass2 === "function") {
|
|
290
|
+
try {
|
|
291
|
+
lang.pass2({
|
|
292
|
+
grammar, tree, fileRel, code, caps, field, enclosing, links, nodeById,
|
|
293
|
+
perFileSymbols, symByFileName, importedLocals, resolveCall,
|
|
294
|
+
});
|
|
295
|
+
} catch (e) { /* one language-specific resolver never sinks the graph */ void e; }
|
|
296
|
+
}
|
|
251
297
|
// qualified/selector calls (Go): `pkg.Func()` → the imported package's dir; else `receiver.Method()` → the
|
|
252
298
|
// SAME package (heuristic by method name — connects lifecycle methods like peer.Enable() that need type info).
|
|
253
299
|
if (lang.selectorCall) for (const cap of caps(grammar, lang.selectorCall, tree.rootNode)) {
|
|
@@ -277,10 +323,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
277
323
|
const object = field(cap.node, "object"), property = field(cap.node, "property");
|
|
278
324
|
if (!object || object.type !== "identifier" || !property) continue;
|
|
279
325
|
const imp = importedLocals.get(fileRel)?.get(object.text);
|
|
280
|
-
if (!imp || imp.imported
|
|
326
|
+
if (!imp || !["*", "default"].includes(imp.imported) || imp.typeOnly) continue;
|
|
281
327
|
const origin = resolveNamespaceMember(fileRel, imp, property.text, "call");
|
|
282
328
|
if (origin.status !== "resolved") continue;
|
|
283
|
-
const target =
|
|
329
|
+
const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, "value");
|
|
284
330
|
const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
285
331
|
if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
|
|
286
332
|
}
|
|
@@ -300,13 +346,25 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
300
346
|
const origin = resolveNamespaceMember(fileRel, imp, parts[parts.length - 1], "jsx");
|
|
301
347
|
if (origin.status === "resolved") { targetFile = origin.origin.file; importedName = origin.origin.name; }
|
|
302
348
|
}
|
|
303
|
-
const
|
|
304
|
-
if (!targetSymbols) continue;
|
|
305
|
-
const target = targetSymbols.get(importedName) || targetSymbols.get(localName);
|
|
349
|
+
const target = resolveNamedSymbol(targetFile, importedName, "value") || resolveNamedSymbol(targetFile, localName, "value");
|
|
306
350
|
if (!target) continue;
|
|
307
351
|
const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
308
352
|
links.push({ source: owner?.id || fileRel, target, relation: "references", confidence: "INFERRED", usage: "jsx", line: cap.node.startPosition.row + 1 });
|
|
309
353
|
}
|
|
354
|
+
// Type identifiers are a separate namespace in TypeScript. Resolve them to type/both-space
|
|
355
|
+
// declarations without creating runtime calls or keeping a same-named value symbol alive.
|
|
356
|
+
if (grammar !== "javascript") for (const cap of caps(grammar, `(type_identifier) @typeRef`, tree.rootNode)) {
|
|
357
|
+
const name = cap.node.text;
|
|
358
|
+
const imp = importedLocals.get(fileRel)?.get(name);
|
|
359
|
+
const targetFile = imp?.originFile || imp?.targetFile || fileRel;
|
|
360
|
+
const targetName = imp?.originName || imp?.imported || name;
|
|
361
|
+
const target = resolveNamedSymbol(targetFile, targetName, "type");
|
|
362
|
+
if (!target || String(target).startsWith(`${fileRel}#${name}@${cap.node.startPosition.row + 1}`)) continue;
|
|
363
|
+
const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
364
|
+
const source = owner?.id || fileRel;
|
|
365
|
+
if (source === target) continue;
|
|
366
|
+
links.push({source, target, relation: "references", confidence: "EXTRACTED", provenance: "RESOLVED", typeOnly: true, line: cap.node.startPosition.row + 1, usage: "type"});
|
|
367
|
+
}
|
|
310
368
|
}
|
|
311
369
|
// Go value references: a top-level const/var/type/func used BY NAME (bare `X`, or cross-package `pkg.X`) in
|
|
312
370
|
// another file/scope → a `references` edge, so used-but-never-called symbols (message-type consts, etc.) are
|
|
@@ -361,10 +419,13 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
361
419
|
extImportsV: 2,
|
|
362
420
|
edgeTypesV: 2,
|
|
363
421
|
edgeProvenanceV: EDGE_PROVENANCE_V,
|
|
364
|
-
complexityV:
|
|
422
|
+
complexityV: 2,
|
|
365
423
|
repoBoundaryV: 1,
|
|
366
424
|
barrelResolutionV: 1,
|
|
367
|
-
|
|
425
|
+
reExportOccurrencesV: 1,
|
|
426
|
+
symbolSpacesV: 1,
|
|
427
|
+
extractorSchemaV: 5,
|
|
428
|
+
reExportOccurrences,
|
|
368
429
|
jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
369
430
|
fileHashes: snapshot.fileHashes,
|
|
370
431
|
fileExportSignatures: snapshot.fileExportSignatures,
|