weavatrix 0.2.5 → 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 +22 -6
- package/package.json +1 -1
- package/skill/SKILL.md +5 -4
- package/src/analysis/graph-analysis.aggregate.js +3 -1
- package/src/analysis/graph-analysis.refs.js +19 -11
- 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 +6 -4
- package/src/mcp/graph-context.mjs +4 -0
- 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 +2 -0
- package/src/mcp/tools-source.mjs +7 -6
- 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/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
Grep sees text. Weavatrix sees structure. It builds a dependency graph of any local repository —
|
|
6
6
|
files, symbols, and the imports/calls/inheritance connecting them — and serves it to Claude Code,
|
|
7
7
|
Codex, or any MCP client: change impact, transitive dependents, health audit, clone detection,
|
|
8
|
-
coverage mapping. **
|
|
8
|
+
coverage mapping. **35 tools available; 32 enabled by the default offline profile. Local-first: with
|
|
9
9
|
the defaults, no repository data leaves your machine.**
|
|
10
10
|
|
|
11
11
|
- Website: [weavatrix.com](https://weavatrix.com)
|
|
@@ -22,7 +22,8 @@ answers grep can't produce:
|
|
|
22
22
|
depends on them — with test coverage attached, so the **untested part of the blast radius** stands
|
|
23
23
|
out before you ship.
|
|
24
24
|
- *"Who calls this function?"* → `inspect_symbol` returns exact bounded TS/JS occurrences plus
|
|
25
|
-
source context; `
|
|
25
|
+
source context; `context_bundle` adds grouped graph relations, exact re-export sites, and a
|
|
26
|
+
smaller source workset. `get_dependents` walks the symbol-level reverse graph transitively. Opt into
|
|
26
27
|
`include_container_importers:true` only when a broader module-import radius is intended.
|
|
27
28
|
- *"Did my refactor actually decouple anything?"* → `graph_diff base_ref=HEAD~1` builds an immutable
|
|
28
29
|
baseline graph without checking it out, then reports the structural delta: new module
|
|
@@ -110,7 +111,8 @@ An agent skill with recipes ships in [skill/SKILL.md](skill/SKILL.md) — instal
|
|
|
110
111
|
`change_impact`, `git_history`, `graph_diff`, `get_architecture_contract`,
|
|
111
112
|
`prepare_change`. Runtime dependencies, TypeScript type-only coupling and language
|
|
112
113
|
compile-only edges (Rust module/use, Java imports) are reported separately where that distinction
|
|
113
|
-
changes the result.
|
|
114
|
+
changes the result. TypeScript type-space and value-space declarations keep distinct identities;
|
|
115
|
+
classes and enums that inhabit both spaces are labelled `both`.
|
|
114
116
|
|
|
115
117
|
Every current edge carries versioned provenance. The parser emits `EXTRACTED`, `RESOLVED`, and
|
|
116
118
|
`INFERRED`; the built-in bounded TypeScript/JavaScript precision overlay upgrades only references
|
|
@@ -118,7 +120,7 @@ confirmed by its bundled `typescript-language-server` + TypeScript runtime to `E
|
|
|
118
120
|
`CONFLICT` means evidence disagrees. `graph_stats` reports the provenance breakdown and the semantic
|
|
119
121
|
provider's `COMPLETE`, `PARTIAL`, `UNAVAILABLE`, or `OFF` state; `OFF` means precision was explicitly
|
|
120
122
|
disabled and only static evidence is active. Java and Rust language-server providers are not bundled
|
|
121
|
-
in 0.2.
|
|
123
|
+
in 0.2.6: their edges never become `EXACT_LSP`, even when a mixed repository reports a complete
|
|
122
124
|
TypeScript/JavaScript overlay.
|
|
123
125
|
|
|
124
126
|
The bounded JS/TS provider is enabled by default for new graphs. Set `WEAVATRIX_PRECISION=off`
|
|
@@ -127,8 +129,9 @@ before starting the MCP server for parser-only operation from the first build, o
|
|
|
127
129
|
choice as **TypeScript/JavaScript semantic precision**.
|
|
128
130
|
|
|
129
131
|
**search / source** — `search_code` (ripgrep-backed, pure-Node fallback), `read_source` (a
|
|
130
|
-
symbol's actual code in one hop), `
|
|
131
|
-
|
|
132
|
+
symbol's actual code in one hop), `context_bundle` (definition plus grouped inbound/outbound
|
|
133
|
+
containers, exact re-export sites and a few bounded source excerpts), `inspect_symbol` (one exact
|
|
134
|
+
bounded TS/JS reference query, logical containers, impact and source excerpts), `list_endpoints` (HTTP route inventory:
|
|
132
135
|
Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web …)
|
|
133
136
|
|
|
134
137
|
**health** — `find_dead_code` (bounded review queue for statically unreferenced and test-only files,
|
|
@@ -193,6 +196,19 @@ disclosed instead of silently guessed; and the server **hot-reloads its watched
|
|
|
193
196
|
modules and catalog** when those files change — other MCP helpers and analysis engines require a
|
|
194
197
|
reconnect.
|
|
195
198
|
|
|
199
|
+
### 0.2.6 compact-context and TypeScript identity patch
|
|
200
|
+
|
|
201
|
+
- New `context_bundle` turns a symbol into a bounded workset: definition, grouped inbound/outbound
|
|
202
|
+
containers, reference evidence, exact re-export locations and a small number of source excerpts.
|
|
203
|
+
- Re-export records retain their concrete file, line, alias, specifier, type-only flag and resolved
|
|
204
|
+
origin. Barrel propagation through `export *` stays exact and private declarations are not exposed.
|
|
205
|
+
- TypeScript interfaces and type aliases are first-class type-space nodes. Same-named runtime values
|
|
206
|
+
keep separate identities, while classes and enums are explicitly marked as inhabiting both spaces.
|
|
207
|
+
- Graph schema v5 and freshness gates rebuild older caches before these precision-sensitive results
|
|
208
|
+
are used. The changes remain local-only and add no runtime dependency.
|
|
209
|
+
|
|
210
|
+
Full patch notes: [docs/releases/v0.2.6.md](docs/releases/v0.2.6.md).
|
|
211
|
+
|
|
196
212
|
### 0.2.5 exact-symbol and graph-fidelity patch
|
|
197
213
|
|
|
198
214
|
- New `inspect_symbol` spends a bounded LSP query on one requested TS/JS declaration, groups exact
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Weavatrix — code graph & blast-radius MCP server for AI coding agents: change impact, transitive dependents, health audit, duplicate detection, and coverage mapping over any local repository.",
|
|
6
6
|
"author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
|
package/skill/SKILL.md
CHANGED
|
@@ -40,13 +40,14 @@ Start from the task, not from the complete tool list:
|
|
|
40
40
|
`get_dependents` and `read_source`.
|
|
41
41
|
- **Trace an API across repositories**: `list_known_repos` -> `trace_api_contract` with an explicit
|
|
42
42
|
backend and client list; inspect each `graphReconciliation.buildMode` before using the verdict.
|
|
43
|
-
- **Inspect exact symbol references**: start with `graph_stats`, then call `
|
|
44
|
-
exact node ID (or an unambiguous label)
|
|
45
|
-
|
|
43
|
+
- **Inspect exact symbol references**: start with `graph_stats`, then call `context_bundle` with an
|
|
44
|
+
exact node ID (or an unambiguous label) for a compact definition, grouped relations, exact
|
|
45
|
+
re-export sites and source workset. Use `inspect_symbol` when the raw bounded point query is needed;
|
|
46
|
+
it spends evidence beyond the broad overlay cap. Use `get_dependents` for transitive
|
|
46
47
|
graph impact and `find_dead_code` for bounded zero-reference
|
|
47
48
|
candidates. Treat `PARTIAL`, `UNAVAILABLE`, `OFF`, or zero exact edges as incomplete evidence, not
|
|
48
49
|
a compiler-exact result; `OFF` means the caller explicitly selected static-only mode. Java and Rust
|
|
49
|
-
providers are not bundled in 0.2.
|
|
50
|
+
providers are not bundled in 0.2.6, so their edges never become `EXACT_LSP` even when a mixed
|
|
50
51
|
repository has a complete TypeScript/JavaScript overlay.
|
|
51
52
|
- **Explore an architectural question**: use broad `query_graph` when entry points are unknown; its
|
|
52
53
|
bootstrap/tool-execution ranking prefers production executables over docs, sites and fixtures.
|
|
@@ -63,7 +63,9 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
63
63
|
line: String(node.source_location || "").replace(/^L/, ""),
|
|
64
64
|
endLine: String(node.source_end || "").replace(/^L/, ""),
|
|
65
65
|
...(node.complexity ? { complexity: node.complexity } : {}),
|
|
66
|
-
...(node.decorated ? { decorated: true } : {})
|
|
66
|
+
...(node.decorated ? { decorated: true } : {}),
|
|
67
|
+
...(node.symbol_kind ? {symbolKind: node.symbol_kind} : {}),
|
|
68
|
+
...(node.symbol_space ? {symbolSpace: node.symbol_space} : {})
|
|
67
69
|
});
|
|
68
70
|
fileSymbols.set(fid, list);
|
|
69
71
|
}
|
|
@@ -63,7 +63,7 @@ function importCandidates(fromFile, spec) {
|
|
|
63
63
|
function stripModuleStatements(text) {
|
|
64
64
|
return String(text || "")
|
|
65
65
|
.replace(/\bimport\s+[\s\S]*?\s+from\s*['"][^'"]+['"]\s*;?/g, "")
|
|
66
|
-
.replace(/\bexport\s
|
|
66
|
+
.replace(/\bexport\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s*['"][^'"]+['"]\s*;?/g, "")
|
|
67
67
|
.replace(/\b(?:const|let|var)\s+\{[\s\S]*?\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)\s*;?/g, "");
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -73,8 +73,10 @@ function parseNamedSpecifiers(raw) {
|
|
|
73
73
|
.map((part) => part.trim())
|
|
74
74
|
.filter(Boolean)
|
|
75
75
|
.map((part) => {
|
|
76
|
-
const
|
|
77
|
-
|
|
76
|
+
const typeOnly = /^type\s+/.test(part);
|
|
77
|
+
const clean = part.replace(/^type\s+/, "").trim();
|
|
78
|
+
const m = clean.match(/^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
|
|
79
|
+
return m ? { imported: m[1], local: m[2] || m[1], typeOnly } : null;
|
|
78
80
|
})
|
|
79
81
|
.filter(Boolean);
|
|
80
82
|
}
|
|
@@ -93,17 +95,20 @@ export function computeSymbolExternalRefs(filePath, fileSymbols, fileText) {
|
|
|
93
95
|
const name = bareSymbolName(sym.label);
|
|
94
96
|
if (!isIdentifierName(name)) continue;
|
|
95
97
|
const ids = byName.get(name) || [];
|
|
96
|
-
ids.push(sym.id);
|
|
98
|
+
ids.push({id: sym.id, space: sym.symbolSpace || "value"});
|
|
97
99
|
byName.set(name, ids);
|
|
98
100
|
}
|
|
99
101
|
symbolIdsByFileAndName.set(fid, byName);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
const symbolExternalRefs = new Map();
|
|
103
|
-
const addExternalRefs = (targetFid, importedName, refs) => {
|
|
105
|
+
const addExternalRefs = (targetFid, importedName, refs, typeOnly = false) => {
|
|
104
106
|
if (!targetFid || refs <= 0 || !isIdentifierName(importedName)) return;
|
|
105
107
|
const ids = symbolIdsByFileAndName.get(targetFid)?.get(importedName) || [];
|
|
106
|
-
for (const
|
|
108
|
+
for (const entry of ids) {
|
|
109
|
+
const matches = entry.space === "both" || (typeOnly ? entry.space === "type" : entry.space !== "type");
|
|
110
|
+
if (matches) symbolExternalRefs.set(entry.id, (symbolExternalRefs.get(entry.id) || 0) + refs);
|
|
111
|
+
}
|
|
107
112
|
};
|
|
108
113
|
const resolveImportedFid = (fromPath, spec) => {
|
|
109
114
|
for (const candidate of importCandidates(fromPath, spec)) {
|
|
@@ -123,18 +128,21 @@ export function computeSymbolExternalRefs(filePath, fileSymbols, fileText) {
|
|
|
123
128
|
if (!targetFid) continue;
|
|
124
129
|
const named = String(m[1] || "").match(/\{([\s\S]*?)\}/);
|
|
125
130
|
if (named) {
|
|
126
|
-
|
|
131
|
+
const statementTypeOnly = /^\s*type\b/.test(String(m[1] || ""));
|
|
132
|
+
for (const spec of parseNamedSpecifiers(named[1])) addExternalRefs(targetFid, spec.imported, countIdentifierInText(scrubbed, spec.local), statementTypeOnly || spec.typeOnly);
|
|
127
133
|
}
|
|
128
134
|
const ns = String(m[1] || "").match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
|
|
129
135
|
if (ns) {
|
|
130
136
|
const byName = symbolIdsByFileAndName.get(targetFid) || new Map();
|
|
131
|
-
|
|
137
|
+
const statementTypeOnly = /^\s*type\b/.test(String(m[1] || ""));
|
|
138
|
+
for (const name of byName.keys()) addExternalRefs(targetFid, name, countMemberAccess(scrubbed, ns[1], name), statementTypeOnly);
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
|
-
for (const m of String(txt).matchAll(/\bexport\s
|
|
135
|
-
const targetFid = resolveImportedFid(importerPath, m[
|
|
141
|
+
for (const m of String(txt).matchAll(/\bexport\s+(type\s+)?\{([\s\S]*?)\}\s+from\s*['"]([^'"]+)['"]\s*;?/g)) {
|
|
142
|
+
const targetFid = resolveImportedFid(importerPath, m[3]);
|
|
136
143
|
if (!targetFid) continue;
|
|
137
|
-
|
|
144
|
+
const statementTypeOnly = Boolean(m[1]);
|
|
145
|
+
for (const spec of parseNamedSpecifiers(m[2])) addExternalRefs(targetFid, spec.imported, 1, statementTypeOnly || spec.typeOnly);
|
|
138
146
|
}
|
|
139
147
|
for (const m of String(txt).matchAll(/\b(?:const|let|var)\s+\{([\s\S]*?)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)\s*;?/g)) {
|
|
140
148
|
const targetFid = resolveImportedFid(importerPath, m[2]);
|
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,
|
package/src/mcp/catalog.mjs
CHANGED
|
@@ -26,9 +26,9 @@ const PROFILE_CAPS = Object.freeze({
|
|
|
26
26
|
|
|
27
27
|
// The files whose mtime the stdio shell watches for hot reload — keep in sync with the imports in
|
|
28
28
|
// loadHotApi below (catalog.mjs itself is last: a change here re-derives the whole table).
|
|
29
|
-
export const HOT_FILES = ['tools-graph.mjs', 'tools-impact.mjs', 'tools-health.mjs', 'tools-source.mjs', 'tools-actions.mjs', 'tools-architecture.mjs', 'tools-history.mjs', 'tools-company.mjs', 'catalog.mjs']
|
|
29
|
+
export const HOT_FILES = ['tools-graph.mjs', 'tools-impact.mjs', 'tools-health.mjs', 'tools-source.mjs', 'tools-context.mjs', 'tools-actions.mjs', 'tools-architecture.mjs', 'tools-history.mjs', 'tools-company.mjs', 'catalog.mjs']
|
|
30
30
|
|
|
31
|
-
function buildTools({tg, ti, th, ts, ta, tar, thi, tc}) {
|
|
31
|
+
function buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc}) {
|
|
32
32
|
const tools = [
|
|
33
33
|
{cap: 'graph', name: 'graph_stats', description: 'Return summary statistics: node count, edge count, communities, versioned edge-provenance/legacy-confidence breakdowns, and graph build time vs repo HEAD (staleness).', inputSchema: {type: 'object', properties: {}}, run: (g, a, ctx) => tg.tGraphStats(g, ctx)},
|
|
34
34
|
{cap: 'graph', name: 'get_node', description: 'Get full details for a specific node by label or ID.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Node label or ID to look up'}}, required: ['label']}, run: (g, a, ctx) => tg.tGetNode(g, a, ctx)},
|
|
@@ -86,6 +86,7 @@ function buildTools({tg, ti, th, ts, ta, tar, thi, tc}) {
|
|
|
86
86
|
{cap: 'search', name: 'search_code', description: 'Full-text or regex search across the repo source (ripgrep-backed, Node fallback). The graph only stores structure — use this to find literal text/patterns, then get_node/get_neighbors for structure.', inputSchema: {type: 'object', properties: {query: {type: 'string', description: 'text or regex to search for'}, is_regex: {type: 'boolean', default: false}, glob: {type: 'string', description: 'optional path glob, e.g. "*.js" or "src/**"'}, max_results: {type: 'integer', default: 40}}, required: ['query']}, run: (g, a, ctx) => searchCode({repoRoot: ctx.repoRoot, resolveRg}, a)},
|
|
87
87
|
{cap: 'source', name: 'read_source', description: "Read the actual source of a node (by label/ID) or a repo-relative file path — the symbol's lines with context. The graph stores only locations, not source text. For a path read, pass start_line to anchor the window anywhere in the file (otherwise it shows the head).", inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'node label or ID'}, path: {type: 'string', description: 'or a repo-relative file path'}, start_line: {type: 'integer', description: 'anchor line: window = start_line-before .. start_line+after'}, before: {type: 'integer', default: 3}, after: {type: 'integer', default: 40}}}, run: (g, a, ctx) => readSource({repoRoot: ctx.repoRoot, resolveNode, isSymbol}, g, a)},
|
|
88
88
|
{cap: 'source', refreshGraph: true, name: 'inspect_symbol', description: 'Inspect one exact symbol with an on-demand TypeScript/JavaScript LSP reference query, grouped occurrence containers, graph blast radius, complexity facts and bounded local source context. Ambiguous labels fail closed; point queries never replace the broad precision overlay.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_containers: {type: 'integer', minimum: 1, maximum: 50, default: 15}, context_lines: {type: 'integer', minimum: 0, maximum: 40, default: 8}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => ts.tInspectSymbol(g, a, ctx)},
|
|
89
|
+
{cap: 'source', refreshGraph: true, name: 'context_bundle', description: 'Return one compact, bounded source bundle for an exact symbol: definition, aggregated inbound/outbound containers, exact re-export sites, on-demand TS/JS reference evidence and a few focused source excerpts. Use before an edit when query_graph would be too broad.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_related: {type: 'integer', minimum: 1, maximum: 30, default: 10}, max_reexports: {type: 'integer', minimum: 1, maximum: 100, default: 20}, max_source_files: {type: 'integer', minimum: 1, maximum: 8, default: 4}, context_lines: {type: 'integer', minimum: 0, maximum: 12, default: 4}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => tb.tContextBundle(g, a, ctx, ts.tInspectSymbol)},
|
|
89
90
|
{cap: 'health', name: 'find_duplicates', description: "Content-based clone detection over production code (MOSS winnowing over method bodies). Supports high-confidence small clones down to 12 tokens when min_tokens is lowered. Tests, e2e, generated code, mocks, stories, docs, benchmarks and temporary roots are classified separately and excluded by default; opt them in explicitly.", inputSchema: {type: 'object', properties: {min_similarity: {type: 'integer', description: '50-100, default 80 (ignored in semantic mode)'}, min_tokens: {type: 'integer', minimum: 12, maximum: 400, description: 'min fragment size, 12-400; default 50'}, mode: {type: 'string', enum: ['renamed', 'strict', 'semantic'], default: 'renamed'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, include_strings: {type: 'boolean', description: 'Also clone-check large multi-line string literals', default: false}, top_n: {type: 'integer', default: 15}}}, run: (g, a, ctx) => th.tFindDuplicates(g, a, ctx)},
|
|
90
91
|
{cap: 'health', name: 'find_dead_code', description: 'Conservative review queue for statically unreferenced files, functions, methods and symbols. Returns confidence, reason, bounded evidence and explicit framework/dynamic/reflection/public-API caveats; never an auto-delete verdict. Tests, generated code, mocks, stories, docs, benchmarks and temporary roots are excluded by default.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repo-relative path prefix'}, kinds: {type: 'array', items: {type: 'string', enum: ['file', 'function', 'method', 'symbol']}, maxItems: 4, uniqueItems: true, description: 'Optional candidate kinds; defaults to all'}, min_confidence: {type: 'string', enum: ['high', 'medium', 'low'], default: 'medium', description: 'Minimum confidence to include. low explicitly includes public/framework/dynamic review candidates'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 30}}}, run: (g, a, ctx) => th.tFindDeadCode(g, a, ctx)},
|
|
91
92
|
{cap: 'health', name: 'run_audit', description: 'Full internal health audit. With base_ref, builds and audits an immutable Git checkout, derives changed files, and compares stable deterministic finding IDs; debt defaults to genuinely new findings. Supply-chain checks remain explicitly uncomparable across the source-only baseline. changed_files without base_ref is only changed-scope, never a new-debt claim.', inputSchema: {type: 'object', properties: {category: {type: 'string', enum: ['unused', 'structure', 'vulnerability', 'malware'], description: 'Only findings of this category'}, min_severity: {type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Minimum severity to include'}, max_findings: {type: 'integer', description: 'Max findings to list, default 30'}, include_malware_scan: {type: 'boolean', description: 'Also grep installed packages for malware heuristics (slow)', default: false}, base_ref: {type: 'string', maxLength: 200, description: 'Optional immutable Git baseline (for example HEAD~1 or origin/main). Enables honest new/existing/fixed debt comparison'}, changed_files: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 500, description: 'Optional explicit repo-relative scope. Without base_ref this is changed-scope only; when omitted with base_ref, files are derived from the Git diff'}, debt: {type: 'string', enum: ['new', 'existing', 'all'], default: 'new', description: 'Baseline comparison view. Defaults to genuinely new deterministic findings when base_ref is present'}}}, run: (g, a, ctx) => th.tRunAudit(g, a, ctx)},
|
|
@@ -133,17 +134,18 @@ function buildTools({tg, ti, th, ts, ta, tar, thi, tc}) {
|
|
|
133
134
|
// present string (even '') is an explicit selection, so "select nothing" really exposes nothing.
|
|
134
135
|
export async function loadHotApi(version, capsArg) {
|
|
135
136
|
const v = version ? `?v=${version}` : ''
|
|
136
|
-
const [tg, ti, th, ts, ta, tar, thi, tc] = await Promise.all([
|
|
137
|
+
const [tg, ti, th, ts, tb, ta, tar, thi, tc] = await Promise.all([
|
|
137
138
|
import(new URL(`./tools-graph.mjs${v}`, import.meta.url).href),
|
|
138
139
|
import(new URL(`./tools-impact.mjs${v}`, import.meta.url).href),
|
|
139
140
|
import(new URL(`./tools-health.mjs${v}`, import.meta.url).href),
|
|
140
141
|
import(new URL(`./tools-source.mjs${v}`, import.meta.url).href),
|
|
142
|
+
import(new URL(`./tools-context.mjs${v}`, import.meta.url).href),
|
|
141
143
|
import(new URL(`./tools-actions.mjs${v}`, import.meta.url).href),
|
|
142
144
|
import(new URL(`./tools-architecture.mjs${v}`, import.meta.url).href),
|
|
143
145
|
import(new URL(`./tools-history.mjs${v}`, import.meta.url).href),
|
|
144
146
|
import(new URL(`./tools-company.mjs${v}`, import.meta.url).href),
|
|
145
147
|
])
|
|
146
|
-
const all = buildTools({tg, ti, th, ts, ta, tar, thi, tc})
|
|
148
|
+
const all = buildTools({tg, ti, th, ts, tb, ta, tar, thi, tc})
|
|
147
149
|
const raw = capsArg == null ? 'offline' : String(capsArg).trim()
|
|
148
150
|
const selected = PROFILE_CAPS[raw] || raw.split(',').map((s) => s.trim()).filter(Boolean)
|
|
149
151
|
.flatMap((cap) => cap === 'online' ? ['advisories', 'hosted'] : [cap])
|
|
@@ -62,6 +62,10 @@ export function loadGraph(path, {repoRoot = null} = {}) {
|
|
|
62
62
|
edgeTypesV: Number(raw.edgeTypesV) || 0,
|
|
63
63
|
edgeProvenanceV: Number(raw.edgeProvenanceV) || 0,
|
|
64
64
|
barrelResolutionV: Number(raw.barrelResolutionV) || 0,
|
|
65
|
+
reExportOccurrencesV: Number(raw.reExportOccurrencesV) || 0,
|
|
66
|
+
symbolSpacesV: Number(raw.symbolSpacesV) || 0,
|
|
67
|
+
reExportOccurrences: Array.isArray(raw.reExportOccurrences) ? raw.reExportOccurrences : [],
|
|
68
|
+
jsExportRecords: raw.jsExportRecords && typeof raw.jsExportRecords === 'object' ? raw.jsExportRecords : {},
|
|
65
69
|
extractorSchemaV: Number(raw.extractorSchemaV) || 0,
|
|
66
70
|
extImportsV: Number(raw.extImportsV) || 0,
|
|
67
71
|
complexityV: Number(raw.complexityV) || 0,
|
package/src/mcp/graph-diff.mjs
CHANGED
|
@@ -35,7 +35,7 @@ function stableNodeIndex(graph) {
|
|
|
35
35
|
if (!raw) continue
|
|
36
36
|
const stem = terminalLineSuffix(raw)
|
|
37
37
|
const params = Number.isInteger(node?.complexity?.params) ? node.complexity.params : ''
|
|
38
|
-
const signature = [stem, node?.symbol_kind || '', node?.exported === true ? 'exported' : '', params].join('\u0000')
|
|
38
|
+
const signature = [stem, node?.symbol_kind || '', node?.symbol_space || '', node?.exported === true ? 'exported' : '', params].join('\u0000')
|
|
39
39
|
if (!groups.has(signature)) groups.set(signature, [])
|
|
40
40
|
groups.get(signature).push(node)
|
|
41
41
|
}
|
package/src/mcp/sync-payload.mjs
CHANGED
|
@@ -180,7 +180,7 @@ function sanitizeNodeV3(value) {
|
|
|
180
180
|
else delete out.file_type;
|
|
181
181
|
if (sourceFile) out.source_file = sourceFile;
|
|
182
182
|
else delete out.source_file;
|
|
183
|
-
for (const key of ['symbol_kind', 'member_of', 'visibility']) setIf(out, key, safeToken(value[key], 128));
|
|
183
|
+
for (const key of ['symbol_kind', 'symbol_space', 'member_of', 'visibility']) setIf(out, key, safeToken(value[key], 128));
|
|
184
184
|
if (out.complexity) {
|
|
185
185
|
for (const key of ['family', 'scope', 'complexityScope', 'confidence']) {
|
|
186
186
|
const safe = safeToken(value.complexity?.[key], 32);
|
|
@@ -91,7 +91,9 @@ export async function tOpenRepo(g, args, ctx) {
|
|
|
91
91
|
savedPrecision = ['lsp', 'off'].includes(saved.graphPrecisionMode) ? saved.graphPrecisionMode : defaultPrecisionMode()
|
|
92
92
|
schemaUpgrade = !Number.isInteger(saved.edgeTypesV) || saved.edgeTypesV < 2
|
|
93
93
|
|| !Number.isInteger(saved.edgeProvenanceV) || saved.edgeProvenanceV < 1
|
|
94
|
-
|| !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV <
|
|
94
|
+
|| !Number.isInteger(saved.extractorSchemaV) || saved.extractorSchemaV < 5
|
|
95
|
+
|| !Number.isInteger(saved.reExportOccurrencesV) || saved.reExportOccurrencesV < 1
|
|
96
|
+
|| !Number.isInteger(saved.symbolSpacesV) || saved.symbolSpacesV < 1
|
|
95
97
|
if (savedPrecision === 'lsp') {
|
|
96
98
|
const overlay = readPrecisionOverlay(graphPath, saved)
|
|
97
99
|
precisionUpgrade = !overlay || (typeof overlay.semanticInputFingerprint === 'string'
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {isStructuralRelation} from '../graph/relations.js'
|
|
2
|
+
import {boundedInteger} from '../bounds.js'
|
|
3
|
+
import {isSymbol, labelOf} from './graph-context.mjs'
|
|
4
|
+
import {toolResult} from './tool-result.mjs'
|
|
5
|
+
|
|
6
|
+
const MAX_LINE_SAMPLES = 5
|
|
7
|
+
|
|
8
|
+
const fileOf = (g, id) => {
|
|
9
|
+
const node = g.byId.get(String(id))
|
|
10
|
+
return String(node?.source_file || (isSymbol(id) ? String(id).split('#')[0] : id))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function aggregateEdges(g, edges, cap) {
|
|
14
|
+
const groups = new Map()
|
|
15
|
+
for (const edge of edges || []) {
|
|
16
|
+
if (isStructuralRelation(edge.relation) || edge.barrelProxy === true) continue
|
|
17
|
+
const id = String(edge.id)
|
|
18
|
+
const relation = String(edge.relation || 'references')
|
|
19
|
+
const key = `${id}\0${relation}`
|
|
20
|
+
let group = groups.get(key)
|
|
21
|
+
if (!group) {
|
|
22
|
+
group = {id, label: labelOf(g, id), file: fileOf(g, id), relation, count: 0, lines: []}
|
|
23
|
+
groups.set(key, group)
|
|
24
|
+
}
|
|
25
|
+
group.count++
|
|
26
|
+
if (Number.isInteger(edge.line) && group.lines.length < MAX_LINE_SAMPLES && !group.lines.includes(edge.line)) group.lines.push(edge.line)
|
|
27
|
+
if (edge.typeOnly === true) group.typeOnly = true
|
|
28
|
+
if (edge.compileOnly === true) group.compileOnly = true
|
|
29
|
+
}
|
|
30
|
+
const all = [...groups.values()].sort((left, right) => right.count - left.count
|
|
31
|
+
|| left.file.localeCompare(right.file) || left.id.localeCompare(right.id))
|
|
32
|
+
return {total: all.length, shown: all.slice(0, cap), capped: all.length > cap}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sameOrigin = (occurrence, definition) => occurrence.originId === definition.id
|
|
36
|
+
|| (occurrence.originFile === definition.file && occurrence.originName === definition.name)
|
|
37
|
+
|
|
38
|
+
function exactReExportSites(g, definition, cap) {
|
|
39
|
+
const occurrences = Array.isArray(g.reExportOccurrences) ? g.reExportOccurrences : []
|
|
40
|
+
const exposures = new Map()
|
|
41
|
+
if (definition.exported) exposures.set(definition.file, new Set([definition.name]))
|
|
42
|
+
const shown = new Map()
|
|
43
|
+
const add = (occurrence, exported = occurrence.exported) => {
|
|
44
|
+
const key = `${occurrence.file}\0${occurrence.line}\0${exported}\0${occurrence.kind}`
|
|
45
|
+
if (shown.has(key)) return false
|
|
46
|
+
shown.set(key, {
|
|
47
|
+
file: occurrence.file,
|
|
48
|
+
line: occurrence.line,
|
|
49
|
+
kind: occurrence.kind,
|
|
50
|
+
exported,
|
|
51
|
+
imported: occurrence.imported,
|
|
52
|
+
targetFile: occurrence.targetFile,
|
|
53
|
+
typeOnly: occurrence.typeOnly === true,
|
|
54
|
+
...(occurrence.specifier ? {specifier: occurrence.specifier} : {}),
|
|
55
|
+
...(occurrence.originFile ? {originFile: occurrence.originFile, originName: occurrence.originName} : {}),
|
|
56
|
+
})
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
for (let pass = 0; pass <= occurrences.length; pass++) {
|
|
60
|
+
let changed = false
|
|
61
|
+
for (const occurrence of occurrences) {
|
|
62
|
+
if (occurrence.kind === 'star') {
|
|
63
|
+
const names = exposures.get(occurrence.targetFile)
|
|
64
|
+
if (!names) continue
|
|
65
|
+
for (const name of names) {
|
|
66
|
+
if (name === 'default') continue
|
|
67
|
+
changed = add(occurrence, name) || changed
|
|
68
|
+
let exported = exposures.get(occurrence.file)
|
|
69
|
+
if (!exported) exposures.set(occurrence.file, (exported = new Set()))
|
|
70
|
+
const before = exported.size
|
|
71
|
+
exported.add(name)
|
|
72
|
+
changed = exported.size !== before || changed
|
|
73
|
+
}
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (!sameOrigin(occurrence, definition)) continue
|
|
77
|
+
changed = add(occurrence) || changed
|
|
78
|
+
if (occurrence.kind !== 'namespace') {
|
|
79
|
+
let exported = exposures.get(occurrence.file)
|
|
80
|
+
if (!exported) exposures.set(occurrence.file, (exported = new Set()))
|
|
81
|
+
const before = exported.size
|
|
82
|
+
exported.add(occurrence.exported)
|
|
83
|
+
changed = exported.size !== before || changed
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!changed) break
|
|
87
|
+
}
|
|
88
|
+
const all = [...shown.values()].sort((left, right) => left.file.localeCompare(right.file)
|
|
89
|
+
|| left.line - right.line || left.exported.localeCompare(right.exported))
|
|
90
|
+
return {total: all.length, shown: all.slice(0, cap), capped: all.length > cap}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function linesForGroups(title, groups) {
|
|
94
|
+
if (!groups.shown.length) return [`${title}: none`]
|
|
95
|
+
const lines = [`${title}: ${groups.total} container(s)${groups.capped ? ` (${groups.shown.length} shown)` : ''}`]
|
|
96
|
+
for (const group of groups.shown) {
|
|
97
|
+
const sites = group.lines.length ? `:${group.lines.join(',')}` : ''
|
|
98
|
+
lines.push(` ${group.count}× ${group.relation} ${group.label} [${group.file}${sites}]`)
|
|
99
|
+
}
|
|
100
|
+
return lines
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function textFor(result) {
|
|
104
|
+
if (result.status !== 'OK') return result.text || `Context bundle: ${result.status}`
|
|
105
|
+
const definition = result.definition
|
|
106
|
+
const lines = [
|
|
107
|
+
`Context bundle: ${definition.label} [${definition.id}]`,
|
|
108
|
+
`Definition: ${definition.file}:${definition.line} (${definition.kind}, ${definition.space} space)`,
|
|
109
|
+
`Evidence: ${result.evidence.state}; ${result.references.occurrences} exact reference occurrence(s) in ${result.references.files} file(s).`,
|
|
110
|
+
...linesForGroups('Inbound', result.inbound),
|
|
111
|
+
...linesForGroups('Outbound', result.outbound),
|
|
112
|
+
`Re-export sites: ${result.reExports.total}${result.reExports.capped ? ` (${result.reExports.shown.length} shown)` : ''}`,
|
|
113
|
+
]
|
|
114
|
+
for (const site of result.reExports.shown) lines.push(` ${site.file}:${site.line} ${site.kind} ${site.imported} → ${site.exported}${site.typeOnly ? ' [type]' : ''}`)
|
|
115
|
+
for (const source of result.source) lines.push('', `${source.role} source (${source.file}:${source.startLine}-${source.endLine}):`, source.text)
|
|
116
|
+
return lines.join('\n')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function tContextBundle(g, args = {}, ctx = {}, inspectSymbol) {
|
|
120
|
+
if (typeof inspectSymbol !== 'function') throw new Error('context_bundle requires inspect_symbol support')
|
|
121
|
+
const maxRelated = boundedInteger(args.max_related, 10, 1, 30)
|
|
122
|
+
const maxReExports = boundedInteger(args.max_reexports, 20, 1, 100)
|
|
123
|
+
const maxSourceFiles = boundedInteger(args.max_source_files, 4, 1, 8)
|
|
124
|
+
const inspected = await inspectSymbol(g, {
|
|
125
|
+
...args,
|
|
126
|
+
max_containers: Math.min(maxRelated, boundedInteger(args.max_containers, 10, 1, 30)),
|
|
127
|
+
context_lines: boundedInteger(args.context_lines, 4, 0, 12),
|
|
128
|
+
}, ctx)
|
|
129
|
+
const inspection = inspected?.result
|
|
130
|
+
if (!inspection || inspection.status !== 'OK') return inspected
|
|
131
|
+
const definition = {
|
|
132
|
+
...inspection.definition,
|
|
133
|
+
name: String(g.byId.get(inspection.definition.id)?.label || '').replace(/\(\)$/, ''),
|
|
134
|
+
}
|
|
135
|
+
const inbound = aggregateEdges(g, g.inn.get(definition.id), maxRelated)
|
|
136
|
+
const outbound = aggregateEdges(g, g.out.get(definition.id), maxRelated)
|
|
137
|
+
const reExports = exactReExportSites(g, definition, maxReExports)
|
|
138
|
+
const source = []
|
|
139
|
+
if (inspection.source.definition) source.push({role: 'Definition', ...inspection.source.definition})
|
|
140
|
+
for (const excerpt of inspection.source.callers || []) {
|
|
141
|
+
if (source.length >= maxSourceFiles) break
|
|
142
|
+
if (source.some((item) => item.file === excerpt.file && item.focusLine === excerpt.focusLine)) continue
|
|
143
|
+
source.push({role: 'Caller', ...excerpt})
|
|
144
|
+
}
|
|
145
|
+
const result = {
|
|
146
|
+
status: 'OK', definition, evidence: inspection.evidence,
|
|
147
|
+
references: {
|
|
148
|
+
occurrences: inspection.exact.occurrences,
|
|
149
|
+
files: inspection.exact.files,
|
|
150
|
+
containers: inspection.exact.containers,
|
|
151
|
+
capped: inspection.exact.capped,
|
|
152
|
+
},
|
|
153
|
+
inbound, outbound, reExports, source,
|
|
154
|
+
}
|
|
155
|
+
return toolResult(textFor(result), result, {
|
|
156
|
+
warnings: inspected.warnings,
|
|
157
|
+
completeness: {
|
|
158
|
+
status: inspection.evidence.state === 'EXACT' && !inspection.exact.capped && !inbound.capped && !outbound.capped && !reExports.capped ? 'complete' : 'bounded',
|
|
159
|
+
relatedLimit: maxRelated,
|
|
160
|
+
reExportLimit: maxReExports,
|
|
161
|
+
sourceFileLimit: maxSourceFiles,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
}
|
package/src/mcp/tools-graph.mjs
CHANGED
|
@@ -48,6 +48,8 @@ export function tGraphStats(g, ctx) {
|
|
|
48
48
|
g.edgeProvenanceV ? `- Edge provenance: v${g.edgeProvenanceV} (${fmt(provenance.counts)}; ${provenance.complete ? 'complete' : `${provenance.counts.UNKNOWN} unclassified`})` : `- Edge provenance: unavailable (rebuild_graph required)`,
|
|
49
49
|
`- Semantic precision: ${precision.state}${precision.provider ? ` via ${precision.provider}${precision.providerVersion ? ` ${precision.providerVersion}` : ''}${precision.typescriptVersion ? ` (TypeScript ${precision.typescriptVersion})` : ''}` : ''}; ${precision.verifiedEdges || 0} EXACT_LSP edge(s), ${precision.queried || 0}/${precision.candidates || 0} bounded target(s) queried${precision.truncated ? ' (partial/truncated)' : ''}${precision.reason ? `; ${precision.reason}` : ''}`,
|
|
50
50
|
g.barrelResolutionV ? `- Barrel resolution: v${g.barrelResolutionV} (semantic tools look through JS/TS re-export facades)` : `- Barrel resolution: unavailable (rebuild_graph required for JS/TS barrel transparency)`,
|
|
51
|
+
g.reExportOccurrencesV ? `- Re-export occurrences: v${g.reExportOccurrencesV} (${g.reExportOccurrences.length} exact site(s))` : `- Re-export occurrences: unavailable (rebuild_graph required)`,
|
|
52
|
+
g.symbolSpacesV ? `- TypeScript symbol spaces: v${g.symbolSpacesV} (type/value identities separated)` : `- TypeScript symbol spaces: unavailable (rebuild_graph required)`,
|
|
51
53
|
`- Relations: ${fmt(relCount)}`,
|
|
52
54
|
Object.keys(confCount).length ? `- Legacy confidence: ${fmt(confCount)}` : null,
|
|
53
55
|
`- Communities: ${comm.size} (top by size: ${topComm.map(([c, n]) => `#${c}=${n}`).join(', ')})`,
|
package/src/mcp/tools-source.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {readFileSync, statSync} from 'node:fs'
|
|
2
|
+
import {boundedInteger} from '../bounds.js'
|
|
2
3
|
import {resolveRepoPath} from '../repo-path.js'
|
|
3
4
|
import {isStructuralRelation} from '../graph/relations.js'
|
|
4
5
|
import {querySymbolPrecision} from '../precision/symbol-query.js'
|
|
@@ -9,11 +10,6 @@ const MAX_EXCERPT_CHARS = 4_000
|
|
|
9
10
|
const MAX_LOCATION_SAMPLES = 5
|
|
10
11
|
const MAX_GRAPH_OCCURRENCES = 100
|
|
11
12
|
|
|
12
|
-
const boundedInteger = (value, fallback, minimum, maximum) => {
|
|
13
|
-
const number = Number(value)
|
|
14
|
-
return Math.max(minimum, Math.min(maximum, Number.isFinite(number) ? Math.floor(number) : fallback))
|
|
15
|
-
}
|
|
16
|
-
|
|
17
13
|
const sourceLine = (node) => {
|
|
18
14
|
const match = /L(\d+)/.exec(String(node?.source_location || ''))
|
|
19
15
|
return match ? Number(match[1]) : 1
|
|
@@ -25,7 +21,10 @@ function candidateNodes(g, query, limit = 20) {
|
|
|
25
21
|
const exact = g.byLabel.get(text)
|
|
26
22
|
const nodes = exact?.length ? exact : g.nodes.filter((node) =>
|
|
27
23
|
String(node.id).toLowerCase().includes(text) || String(node.label || '').toLowerCase().includes(text))
|
|
28
|
-
return nodes.slice(0, limit).map((node) => ({
|
|
24
|
+
return nodes.slice(0, limit).map((node) => ({
|
|
25
|
+
id: String(node.id), label: String(node.label || node.id), file: node.source_file || null,
|
|
26
|
+
...(node.symbol_space ? {space: node.symbol_space} : {}),
|
|
27
|
+
}))
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function excerpt(repoRoot, file, focusLine, contextLines) {
|
|
@@ -192,6 +191,8 @@ export async function tInspectSymbol(g, args = {}, ctx = {}) {
|
|
|
192
191
|
id: targetId,
|
|
193
192
|
label: String(node.label || targetId),
|
|
194
193
|
kind: String(node.symbol_kind || 'symbol'),
|
|
194
|
+
space: String(node.symbol_space || 'value'),
|
|
195
|
+
exported: node.exported === true,
|
|
195
196
|
file: String(node.source_file || targetId.split('#')[0]),
|
|
196
197
|
line: sourceLine(node),
|
|
197
198
|
...(node.source_range ? {range: node.source_range} : {}),
|
|
@@ -425,7 +425,7 @@ export class StdioLspClient {
|
|
|
425
425
|
await this.writeMessage({jsonrpc: JSON_RPC_VERSION, method, params})
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
-
async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.
|
|
428
|
+
async initialize({capabilities = {}, initializationOptions, clientInfo = {name: 'weavatrix', version: '0.2.6'}} = {}) {
|
|
429
429
|
if (this.state !== 'running') throw new Error(`LSP initialize is invalid in state=${this.state}`)
|
|
430
430
|
const result = await this.request('initialize', {
|
|
431
431
|
processId: process.pid,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { boundedInteger } from "../bounds.js";
|
|
5
6
|
import { atomicWriteFileSync } from "../graph/file-lock.js";
|
|
6
7
|
import { edgeProvenance } from "../graph/edge-provenance.js";
|
|
7
8
|
import { isStructuralRelation } from "../graph/relations.js";
|
|
@@ -450,11 +451,6 @@ function publicSemanticSafetyReason(reason) {
|
|
|
450
451
|
: "TypeScript project configuration could not be verified safely";
|
|
451
452
|
}
|
|
452
453
|
|
|
453
|
-
function boundedInteger(value, fallback, minimum, maximum) {
|
|
454
|
-
const number = Number(value);
|
|
455
|
-
return Math.max(minimum, Math.min(maximum, Number.isFinite(number) ? Math.floor(number) : fallback));
|
|
456
|
-
}
|
|
457
|
-
|
|
458
454
|
export async function buildLspPrecisionOverlay({
|
|
459
455
|
repoRoot,
|
|
460
456
|
graph,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {existsSync, readFileSync, statSync} from 'node:fs'
|
|
2
2
|
import {dirname, resolve} from 'node:path'
|
|
3
|
+
import {boundedInteger} from '../bounds.js'
|
|
3
4
|
import {atomicWriteFileSync, withFileLock} from '../graph/file-lock.js'
|
|
4
5
|
import {
|
|
5
6
|
buildLspPrecisionOverlay,
|
|
@@ -15,11 +16,6 @@ const MAX_CACHE_BYTES = 8 * 1024 * 1024
|
|
|
15
16
|
const MAX_CACHE_READ_BYTES = 16 * 1024 * 1024
|
|
16
17
|
const inFlight = new Map()
|
|
17
18
|
|
|
18
|
-
const boundedInteger = (value, fallback, minimum, maximum) => {
|
|
19
|
-
const number = Number(value)
|
|
20
|
-
return Math.max(minimum, Math.min(maximum, Number.isFinite(number) ? Math.floor(number) : fallback))
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
export function symbolPrecisionCachePath(graphPath) {
|
|
24
20
|
return resolve(dirname(graphPath), SYMBOL_PRECISION_CACHE_FILE)
|
|
25
21
|
}
|