weavatrix 0.2.5 → 0.2.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/README.md +77 -14
- package/SECURITY.md +14 -0
- package/docs/agent-task-benchmark.md +58 -0
- package/docs/releases/v0.2.7.md +93 -0
- package/package.json +6 -1
- package/scripts/run-agent-task-benchmark.mjs +119 -0
- package/skill/SKILL.md +31 -10
- package/src/analysis/allowed-test-runner.js +59 -0
- package/src/analysis/data-flow-evidence.js +114 -0
- package/src/analysis/dead-code-review.js +51 -0
- package/src/analysis/dep-check.js +42 -3
- package/src/analysis/duplicate-groups.js +84 -0
- 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 +20 -2
- package/src/analysis/internal-audit.run.js +6 -1
- package/src/analysis/task-retrieval.js +116 -0
- package/src/bounds.js +4 -0
- package/src/graph/builder/lang-js.js +52 -17
- package/src/graph/freshness-probe.js +4 -2
- package/src/graph/incremental-refresh.js +4 -2
- package/src/graph/internal-builder.barrels.js +39 -2
- package/src/graph/internal-builder.build.js +58 -12
- package/src/mcp/catalog.mjs +28 -6
- package/src/mcp/graph-context.mjs +5 -1
- package/src/mcp/graph-diff.mjs +1 -1
- package/src/mcp/sync-payload.mjs +1 -1
- package/src/mcp/tools-actions.mjs +3 -1
- package/src/mcp/tools-context.mjs +164 -0
- package/src/mcp/tools-graph.mjs +48 -3
- package/src/mcp/tools-health.mjs +15 -28
- package/src/mcp/tools-impact-change.mjs +1 -0
- package/src/mcp/tools-source.mjs +7 -6
- package/src/mcp/tools-verified-change.mjs +169 -0
- package/src/precision/lsp-client.js +1 -1
- package/src/precision/lsp-overlay.js +1 -5
- package/src/precision/symbol-query.js +1 -5
- package/src/security/malware-heuristics.exclusions.js +3 -3
- package/src/security/malware-heuristics.roots.js +4 -1
- package/src/security/malware-heuristics.scan.js +4 -2
- package/src/security/malware-scoring.js +22 -5
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
2
|
+
|
|
3
|
+
const words = (value) => new Set(String(value || '').toLowerCase().match(/[\p{L}_$][\p{L}\p{N}_$-]{2,}/gu) || [])
|
|
4
|
+
const fileOf = (node) => String(node?.source_file || (String(node?.id || '').includes('#') ? String(node.id).split('#')[0] : node?.id || '')).replace(/\\/g, '/')
|
|
5
|
+
const isSymbol = (id) => String(id || '').includes('#')
|
|
6
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
7
|
+
const CLASS_TERMS = Object.freeze({
|
|
8
|
+
test: ['test', 'tests', 'testing', 'spec', 'coverage', 'verify'],
|
|
9
|
+
e2e: ['e2e', 'playwright', 'cypress'],
|
|
10
|
+
generated: ['generated', 'autogenerated', 'dist'],
|
|
11
|
+
mock: ['mock', 'mocks', 'fixture', 'fixtures', 'fake'],
|
|
12
|
+
story: ['story', 'stories', 'storybook'],
|
|
13
|
+
docs: ['doc', 'docs', 'documentation', 'readme', 'guide'],
|
|
14
|
+
benchmark: ['benchmark', 'benchmarks', 'bench'],
|
|
15
|
+
temp: ['temp', 'temporary', 'tmp'],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const INTENT_TRANSLATIONS = [
|
|
19
|
+
[/авториз|аутентиф|логин|сесси|токен/iu, 'auth authentication login session token'],
|
|
20
|
+
[/маршрут|роут|эндпоинт|апи|http/iu, 'route router endpoint api http'],
|
|
21
|
+
[/тест|покрыти/iu, 'test spec coverage verify'],
|
|
22
|
+
[/к[эе]ш|хранилищ/iu, 'cache store storage'],
|
|
23
|
+
[/баз[аы]|запрос|sql/iu, 'database query sql'],
|
|
24
|
+
[/конфиг|настройк/iu, 'config settings configuration'],
|
|
25
|
+
[/безопас|вредонос|секрет/iu, 'security malware secret'],
|
|
26
|
+
[/зависим|импорт/iu, 'dependency import module'],
|
|
27
|
+
[/дублик|клон/iu, 'duplicate clone'],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
export function expandTaskQuery(task) {
|
|
31
|
+
const text = String(task || '')
|
|
32
|
+
const expansions = INTENT_TRANSLATIONS.filter(([pattern]) => pattern.test(text)).map(([, value]) => value)
|
|
33
|
+
return [...new Set([text, ...expansions])].join(' ')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function overlapScore(taskWords, node) {
|
|
37
|
+
const haystack = words(`${node?.label || ''} ${node?.id || ''} ${node?.symbol_kind || ''}`)
|
|
38
|
+
let score = 0
|
|
39
|
+
for (const word of taskWords) if (haystack.has(word)) score += 8
|
|
40
|
+
return score
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function addCandidate(map, node, score, reason) {
|
|
44
|
+
if (!node?.id) return
|
|
45
|
+
const id = String(node.id)
|
|
46
|
+
const current = map.get(id) || {node, score: 0, reasons: new Set()}
|
|
47
|
+
current.score += score
|
|
48
|
+
current.reasons.add(reason)
|
|
49
|
+
map.set(id, current)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function containedSymbols(g, node) {
|
|
53
|
+
if (isSymbol(node?.id)) return [node]
|
|
54
|
+
return (g.out.get(String(node?.id)) || [])
|
|
55
|
+
.filter((edge) => edge.relation === 'contains')
|
|
56
|
+
.map((edge) => g.byId.get(String(edge.id)))
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Combines intent-expanded search seeds with exact changed symbols. The result deliberately stays
|
|
61
|
+
// deterministic and source-free; exact LSP/source evidence is collected by context_bundle later.
|
|
62
|
+
export function retrieveTaskContext(g, {
|
|
63
|
+
task, semanticSeeds = [], changedSeedIds = [], maxSymbols = 3, repoRoot = null, includeClassified = false,
|
|
64
|
+
} = {}) {
|
|
65
|
+
const expandedTask = expandTaskQuery(task)
|
|
66
|
+
const taskWords = words(expandedTask)
|
|
67
|
+
const requestedClasses = new Set(Object.entries(CLASS_TERMS)
|
|
68
|
+
.filter(([, terms]) => terms.some((term) => taskWords.has(term)))
|
|
69
|
+
.map(([name]) => name))
|
|
70
|
+
if (requestedClasses.has('test')) requestedClasses.add('e2e')
|
|
71
|
+
if (requestedClasses.has('e2e')) requestedClasses.add('test')
|
|
72
|
+
const classifier = createPathClassifier(repoRoot)
|
|
73
|
+
const classificationCache = new Map()
|
|
74
|
+
const changedFiles = new Set((changedSeedIds || []).map((id) => fileOf(g.byId.get(String(id)) || {id})))
|
|
75
|
+
const pathAllowed = (node) => {
|
|
76
|
+
const file = fileOf(node)
|
|
77
|
+
if (!file || changedFiles.has(file) || includeClassified === true) return true
|
|
78
|
+
if (!classificationCache.has(file)) classificationCache.set(file, classifier.explain(file, {content: ''}))
|
|
79
|
+
const info = classificationCache.get(file)
|
|
80
|
+
const classes = NON_PRODUCT_CLASSES.filter((name) => hasPathClass(info, name))
|
|
81
|
+
if (!info?.excluded && !classes.length) return true
|
|
82
|
+
return classes.some((name) => requestedClasses.has(name))
|
|
83
|
+
}
|
|
84
|
+
const candidates = new Map()
|
|
85
|
+
for (const id of changedSeedIds || []) {
|
|
86
|
+
const node = g.byId.get(String(id))
|
|
87
|
+
if (node) addCandidate(candidates, node, 100 + overlapScore(taskWords, node), 'changed-symbol')
|
|
88
|
+
}
|
|
89
|
+
for (const seed of semanticSeeds || []) addCandidate(candidates, seed, 45 + overlapScore(taskWords, seed), 'task-intent')
|
|
90
|
+
|
|
91
|
+
for (const candidate of [...candidates.values()]) {
|
|
92
|
+
for (const symbol of containedSymbols(g, candidate.node)) {
|
|
93
|
+
const degree = (g.out.get(String(symbol.id)) || []).length + (g.inn.get(String(symbol.id)) || []).length
|
|
94
|
+
addCandidate(candidates, symbol, 22 + overlapScore(taskWords, symbol) + Math.min(12, degree), `symbol-in:${fileOf(candidate.node)}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const symbolCandidates = [...candidates.values()].filter((item) => isSymbol(item.node.id))
|
|
99
|
+
const ranked = symbolCandidates
|
|
100
|
+
.filter((item) => pathAllowed(item.node))
|
|
101
|
+
.sort((left, right) => right.score - left.score || String(left.node.id).localeCompare(String(right.node.id)))
|
|
102
|
+
const limit = Math.max(1, Math.min(5, Number(maxSymbols) || 3))
|
|
103
|
+
return {
|
|
104
|
+
method: 'intent-expanded graph retrieval + exact changed-symbol seeds',
|
|
105
|
+
status: ranked.length ? 'COMPLETE' : 'NO_SYMBOLS',
|
|
106
|
+
selected: ranked.slice(0, limit).map((item) => ({
|
|
107
|
+
id: String(item.node.id), label: item.node.label || String(item.node.id), file: fileOf(item.node),
|
|
108
|
+
kind: item.node.symbol_kind || null, score: item.score, reasons: [...item.reasons].sort(),
|
|
109
|
+
})),
|
|
110
|
+
candidateCount: ranked.length,
|
|
111
|
+
suppressedClassified: symbolCandidates.length - ranked.length,
|
|
112
|
+
pathPolicy: includeClassified === true
|
|
113
|
+
? {mode: 'ALL_CLASSIFIED'}
|
|
114
|
+
: {mode: 'PRODUCTION_FIRST', requestedClasses: [...requestedClasses].sort(), changedFilesPinned: changedFiles.size},
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/bounds.js
ADDED
|
@@ -14,6 +14,10 @@ const TOPVARS = `
|
|
|
14
14
|
(program (variable_declaration (variable_declarator) @decl))
|
|
15
15
|
(program (export_statement (lexical_declaration (variable_declarator) @decl)))
|
|
16
16
|
(program (export_statement (variable_declaration (variable_declarator) @decl)))`;
|
|
17
|
+
const TYPES = `
|
|
18
|
+
(interface_declaration name: (_) @interface)
|
|
19
|
+
(type_alias_declaration name: (_) @type)
|
|
20
|
+
(enum_declaration name: (_) @enum)`;
|
|
17
21
|
const REQUIRE = `(variable_declarator name: (_) @lhs value: (call_expression function: (identifier) @req arguments: (arguments (string (string_fragment) @src))))`;
|
|
18
22
|
|
|
19
23
|
function parseExportSpecifiers(raw) {
|
|
@@ -95,7 +99,13 @@ export default {
|
|
|
95
99
|
sourceNode: cap.node.parent,
|
|
96
100
|
selectionNode: cap.node,
|
|
97
101
|
...(exportStatement ? { exported: true } : {}),
|
|
98
|
-
...(isMethod
|
|
102
|
+
...(isMethod
|
|
103
|
+
? {...methodMetadata(cap.node), symbolSpace: "value"}
|
|
104
|
+
: {
|
|
105
|
+
symbolKind: cap.name === "class" ? "class" : "function",
|
|
106
|
+
symbolSpace: cap.name === "class" ? "both" : "value",
|
|
107
|
+
moduleDeclaration: isModuleDeclaration(cap.node),
|
|
108
|
+
})
|
|
99
109
|
});
|
|
100
110
|
if (exportStatement) {
|
|
101
111
|
recordJsExport({
|
|
@@ -103,6 +113,7 @@ export default {
|
|
|
103
113
|
exported: /^\s*export\s+default\b/.test(exportStatement.text) ? "default" : cap.node.text,
|
|
104
114
|
local: cap.node.text,
|
|
105
115
|
typeOnly: false,
|
|
116
|
+
line: exportStatement.startPosition.row + 1,
|
|
106
117
|
});
|
|
107
118
|
}
|
|
108
119
|
}
|
|
@@ -115,9 +126,40 @@ export default {
|
|
|
115
126
|
selectionNode: nameNode,
|
|
116
127
|
...(exported ? { exported: true } : {}),
|
|
117
128
|
symbolKind: "variable",
|
|
129
|
+
symbolSpace: "value",
|
|
118
130
|
moduleDeclaration: true
|
|
119
131
|
});
|
|
120
|
-
if (exported) recordJsExport({
|
|
132
|
+
if (exported) recordJsExport({
|
|
133
|
+
kind: "local", exported: nameNode.text, local: nameNode.text, typeOnly: false,
|
|
134
|
+
line: exportStatementOf(cap.node)?.startPosition.row + 1 || nameNode.startPosition.row + 1,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// TypeScript has separate type and value namespaces. Keep interface/type declarations as
|
|
139
|
+
// first-class type-space nodes instead of folding them into a same-named runtime declaration.
|
|
140
|
+
// Enums and classes intentionally occupy both spaces because TypeScript emits runtime values.
|
|
141
|
+
if (grammar !== "javascript") for (const cap of caps(grammar, TYPES, tree.rootNode)) {
|
|
142
|
+
const declaration = cap.node.parent;
|
|
143
|
+
const exportStatement = exportStatementOf(cap.node);
|
|
144
|
+
const symbolKind = cap.name === "interface" ? "interface" : cap.name === "type" ? "type" : "enum";
|
|
145
|
+
const symbolSpace = cap.name === "enum" ? "both" : "type";
|
|
146
|
+
addSym(cap.node.text, cap.node.startPosition.row + 1, false, {
|
|
147
|
+
sourceNode: declaration,
|
|
148
|
+
selectionNode: cap.node,
|
|
149
|
+
idSuffix: symbolSpace === "type" ? ":type" : "",
|
|
150
|
+
...(exportStatement ? {exported: true} : {}),
|
|
151
|
+
symbolKind,
|
|
152
|
+
symbolSpace,
|
|
153
|
+
moduleDeclaration: isModuleDeclaration(cap.node),
|
|
154
|
+
});
|
|
155
|
+
if (exportStatement) recordJsExport({
|
|
156
|
+
kind: "local",
|
|
157
|
+
exported: cap.node.text,
|
|
158
|
+
local: cap.node.text,
|
|
159
|
+
typeOnly: symbolSpace === "type",
|
|
160
|
+
line: exportStatement.startPosition.row + 1,
|
|
161
|
+
symbolSpace,
|
|
162
|
+
});
|
|
121
163
|
}
|
|
122
164
|
|
|
123
165
|
const importTypeOnly = (node) => {
|
|
@@ -218,12 +260,12 @@ export default {
|
|
|
218
260
|
const namespace = text.match(/^export\s+(type\s+)?\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\b/);
|
|
219
261
|
const clause = text.match(/^export\s+(type\s+)?\{([\s\S]*?)\}\s+from\b/);
|
|
220
262
|
if (namespace) {
|
|
221
|
-
recordJsExport({ kind: "namespace", exported: namespace[2], targetFile: tgt, typeOnly: !!namespace[1] });
|
|
263
|
+
recordJsExport({ kind: "namespace", exported: namespace[2], imported: "*", targetFile: tgt, typeOnly: !!namespace[1], line, specifier: rawSpec });
|
|
222
264
|
} else if (star) {
|
|
223
|
-
recordJsExport({ kind: "star", targetFile: tgt, typeOnly: !!star[1] });
|
|
265
|
+
recordJsExport({ kind: "star", exported: "*", imported: "*", targetFile: tgt, typeOnly: !!star[1], line, specifier: rawSpec });
|
|
224
266
|
} else if (clause) {
|
|
225
267
|
for (const spec of parseExportSpecifiers(clause[2])) {
|
|
226
|
-
recordJsExport({ kind: "named", exported: spec.exported, imported: spec.imported, targetFile: tgt, typeOnly: !!clause[1] || spec.typeOnly });
|
|
268
|
+
recordJsExport({ kind: "named", exported: spec.exported, imported: spec.imported, targetFile: tgt, typeOnly: !!clause[1] || spec.typeOnly, line, specifier: rawSpec });
|
|
227
269
|
}
|
|
228
270
|
}
|
|
229
271
|
}
|
|
@@ -239,26 +281,19 @@ export default {
|
|
|
239
281
|
const text = node.text.trim();
|
|
240
282
|
const clause = text.match(/^export\s+(type\s+)?\{([\s\S]*?)\}/);
|
|
241
283
|
if (clause) for (const spec of parseExportSpecifiers(clause[2])) {
|
|
242
|
-
recordJsExport({ kind: "local", exported: spec.exported, local: spec.imported, typeOnly: !!clause[1] || spec.typeOnly });
|
|
284
|
+
recordJsExport({ kind: "local", exported: spec.exported, local: spec.imported, typeOnly: !!clause[1] || spec.typeOnly, line: node.startPosition.row + 1 });
|
|
243
285
|
}
|
|
244
286
|
const identifierDefault = text.match(/^export\s+default\s+([A-Za-z_$][\w$]*)\s*;?$/);
|
|
245
|
-
if (identifierDefault) recordJsExport({ kind: "local", exported: "default", local: identifierDefault[1], typeOnly: false });
|
|
246
|
-
const typedDeclaration = text.match(/^export\s+(?:declare\s+)?(type|interface|enum)\s+([A-Za-z_$][\w$]*)\b/);
|
|
247
|
-
if (typedDeclaration) recordJsExport({
|
|
248
|
-
kind: "local",
|
|
249
|
-
exported: typedDeclaration[2],
|
|
250
|
-
local: typedDeclaration[2],
|
|
251
|
-
typeOnly: typedDeclaration[1] !== "enum",
|
|
252
|
-
});
|
|
287
|
+
if (identifierDefault) recordJsExport({ kind: "local", exported: "default", local: identifierDefault[1], typeOnly: false, line: node.startPosition.row + 1 });
|
|
253
288
|
const defaultValue = field(node, "value");
|
|
254
289
|
if (defaultValue?.type === "object") {
|
|
255
290
|
// Service facades commonly expose local helpers through `export default { getSchema,
|
|
256
291
|
// save: persist }`. Record the public member -> local binding instead of treating the
|
|
257
292
|
// object as an opaque default export.
|
|
258
|
-
recordJsExport({kind: "facade-root", exported: "default", local: "default", typeOnly: false});
|
|
293
|
+
recordJsExport({kind: "facade-root", exported: "default", local: "default", typeOnly: false, line: node.startPosition.row + 1});
|
|
259
294
|
for (const property of defaultValue.namedChildren || []) {
|
|
260
295
|
if (["shorthand_property_identifier", "shorthand_property_identifier_pattern"].includes(property.type)) {
|
|
261
|
-
recordJsExport({kind: "facade-member", member: property.text, local: property.text, typeOnly: false});
|
|
296
|
+
recordJsExport({kind: "facade-member", member: property.text, local: property.text, typeOnly: false, line: node.startPosition.row + 1});
|
|
262
297
|
markExported(property.text);
|
|
263
298
|
continue;
|
|
264
299
|
}
|
|
@@ -268,7 +303,7 @@ export default {
|
|
|
268
303
|
if (!key || value?.type !== "identifier") continue;
|
|
269
304
|
const member = key.text.replace(/^['"`]|['"`]$/g, "");
|
|
270
305
|
if (!/^[A-Za-z_$][\w$]*$/.test(member)) continue;
|
|
271
|
-
recordJsExport({kind: "facade-member", member, local: value.text, typeOnly: false});
|
|
306
|
+
recordJsExport({kind: "facade-member", member, local: value.text, typeOnly: false, line: node.startPosition.row + 1});
|
|
272
307
|
markExported(value.text);
|
|
273
308
|
}
|
|
274
309
|
}
|
|
@@ -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' }
|
|
@@ -25,7 +25,9 @@ const CURRENT_GRAPH_SCHEMA = Object.freeze({
|
|
|
25
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,7 +28,7 @@ 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
34
|
const table = { explicit: new Map(), stars: [], facadeMembers: new Map() };
|
|
@@ -111,6 +111,43 @@ export function resolveJsBarrels({ jsExports, importedLocals, links }) {
|
|
|
111
111
|
}));
|
|
112
112
|
};
|
|
113
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
|
+
|
|
114
151
|
// Every internal re-export is a physical facade hop. It remains in the graph for runtime cycle truth,
|
|
115
152
|
// but semantic consumers ignore it in favor of importer -> declaration edges below.
|
|
116
153
|
for (const link of links) {
|
|
@@ -194,5 +231,5 @@ export function resolveJsBarrels({ jsExports, importedLocals, links }) {
|
|
|
194
231
|
return result;
|
|
195
232
|
};
|
|
196
233
|
|
|
197
|
-
return { resolveExport, resolveNamespaceMember, resolveFacadeMember };
|
|
234
|
+
return { resolveExport, resolveNamespaceMember, resolveFacadeMember, reExportOccurrences };
|
|
198
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,6 +141,7 @@ 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
|
});
|
|
@@ -146,8 +153,12 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
146
153
|
end: endLine >= line ? endLine : 0,
|
|
147
154
|
...(extra?.memberOf ? {memberOf: extra.memberOf} : {}),
|
|
148
155
|
...(extra?.symbolKind ? {symbolKind: extra.symbolKind} : {}),
|
|
156
|
+
...(extra?.symbolSpace ? {symbolSpace: extra.symbolSpace} : {}),
|
|
149
157
|
});
|
|
150
158
|
if (!nameToId.has(name)) nameToId.set(name, id);
|
|
159
|
+
const ids = nameToIds.get(name) || [];
|
|
160
|
+
ids.push(id);
|
|
161
|
+
nameToIds.set(name, ids);
|
|
151
162
|
if (extra?.moduleDeclaration && !moduleNameToId.has(name)) moduleNameToId.set(name, id);
|
|
152
163
|
return id;
|
|
153
164
|
};
|
|
@@ -196,7 +207,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
196
207
|
for (let i = 0; i < syms.length; i++) {
|
|
197
208
|
if (!syms[i].end || syms[i].end < syms[i].start) syms[i].end = i + 1 < syms.length ? syms[i + 1].start - 1 : eof;
|
|
198
209
|
}
|
|
199
|
-
perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId);
|
|
210
|
+
perFileSymbols.set(fileRel, syms); symByFileName.set(fileRel, nameToId); symIdsByFileName.set(fileRel, nameToIds);
|
|
200
211
|
tree.delete();
|
|
201
212
|
}
|
|
202
213
|
|
|
@@ -211,7 +222,27 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
211
222
|
let dm = goDirSymbols.get(d); if (!dm) goDirSymbols.set(d, (dm = new Map()));
|
|
212
223
|
for (const [n, id] of m) if (!dm.has(n)) dm.set(n, id);
|
|
213
224
|
}
|
|
214
|
-
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
|
+
});
|
|
215
246
|
// Exact source ranges can overlap (a named function nested inside another function). Attribute a call
|
|
216
247
|
// to the innermost matching symbol, not whichever outer declaration happened to be added first.
|
|
217
248
|
const enclosing = (fileRel, line) => {
|
|
@@ -223,13 +254,13 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
223
254
|
return best;
|
|
224
255
|
};
|
|
225
256
|
const resolveCall = (name, fileRel) => {
|
|
226
|
-
const local =
|
|
257
|
+
const local = resolveNamedSymbol(fileRel, name, "value"); if (local) return local;
|
|
227
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); }
|
|
228
259
|
const imp = importedLocals.get(fileRel) && importedLocals.get(fileRel).get(name);
|
|
229
260
|
if (imp && imp.targetFile) {
|
|
230
261
|
const targetFile = imp.originFile || imp.targetFile;
|
|
231
262
|
const importedName = imp.originName || imp.imported;
|
|
232
|
-
|
|
263
|
+
return resolveNamedSymbol(targetFile, importedName, "value");
|
|
233
264
|
}
|
|
234
265
|
return null;
|
|
235
266
|
};
|
|
@@ -295,7 +326,7 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
295
326
|
if (!imp || !["*", "default"].includes(imp.imported) || imp.typeOnly) continue;
|
|
296
327
|
const origin = resolveNamespaceMember(fileRel, imp, property.text, "call");
|
|
297
328
|
if (origin.status !== "resolved") continue;
|
|
298
|
-
const target =
|
|
329
|
+
const target = resolveNamedSymbol(origin.origin.file, origin.origin.name, "value");
|
|
299
330
|
const caller = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
300
331
|
if (target && caller && target !== caller.id) links.push({ source: caller.id, target, relation: "calls", confidence: "INFERRED", line: cap.node.startPosition.row + 1 });
|
|
301
332
|
}
|
|
@@ -315,13 +346,25 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
315
346
|
const origin = resolveNamespaceMember(fileRel, imp, parts[parts.length - 1], "jsx");
|
|
316
347
|
if (origin.status === "resolved") { targetFile = origin.origin.file; importedName = origin.origin.name; }
|
|
317
348
|
}
|
|
318
|
-
const
|
|
319
|
-
if (!targetSymbols) continue;
|
|
320
|
-
const target = targetSymbols.get(importedName) || targetSymbols.get(localName);
|
|
349
|
+
const target = resolveNamedSymbol(targetFile, importedName, "value") || resolveNamedSymbol(targetFile, localName, "value");
|
|
321
350
|
if (!target) continue;
|
|
322
351
|
const owner = enclosing(fileRel, cap.node.startPosition.row + 1);
|
|
323
352
|
links.push({ source: owner?.id || fileRel, target, relation: "references", confidence: "INFERRED", usage: "jsx", line: cap.node.startPosition.row + 1 });
|
|
324
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
|
+
}
|
|
325
368
|
}
|
|
326
369
|
// Go value references: a top-level const/var/type/func used BY NAME (bare `X`, or cross-package `pkg.X`) in
|
|
327
370
|
// another file/scope → a `references` edge, so used-but-never-called symbols (message-type consts, etc.) are
|
|
@@ -379,7 +422,10 @@ export async function buildInternalGraph(repoDir, opts = {}) {
|
|
|
379
422
|
complexityV: 2,
|
|
380
423
|
repoBoundaryV: 1,
|
|
381
424
|
barrelResolutionV: 1,
|
|
382
|
-
|
|
425
|
+
reExportOccurrencesV: 1,
|
|
426
|
+
symbolSpacesV: 1,
|
|
427
|
+
extractorSchemaV: 5,
|
|
428
|
+
reExportOccurrences,
|
|
383
429
|
jsExportRecords: Object.fromEntries([...jsExports.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
384
430
|
fileHashes: snapshot.fileHashes,
|
|
385
431
|
fileExportSignatures: snapshot.fileExportSignatures,
|