weavatrix 0.1.3 → 0.2.0
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 +168 -63
- package/SECURITY.md +25 -8
- package/package.json +2 -2
- package/skill/SKILL.md +108 -39
- package/src/analysis/architecture-contract.js +343 -0
- package/src/analysis/audit-debt.js +94 -0
- package/src/analysis/change-classification.js +509 -0
- package/src/analysis/cycle-route.js +14 -0
- package/src/analysis/dead-check.js +13 -6
- package/src/analysis/dead-code-review.js +245 -0
- package/src/analysis/dep-check-ecosystems.js +163 -0
- package/src/analysis/dep-check.js +69 -147
- package/src/analysis/dep-rules.js +47 -31
- package/src/analysis/duplicates.compute.js +47 -7
- package/src/analysis/endpoints-java.js +186 -0
- package/src/analysis/endpoints-rust.js +124 -0
- package/src/analysis/endpoints.js +18 -5
- package/src/analysis/findings.js +8 -1
- package/src/analysis/git-history.js +549 -0
- package/src/analysis/git-ref-graph.js +74 -0
- package/src/analysis/graph-analysis.aggregate.js +29 -28
- package/src/analysis/graph-analysis.edges.js +24 -0
- package/src/analysis/graph-analysis.js +1 -1
- package/src/analysis/graph-analysis.summaries.js +4 -1
- package/src/analysis/http-contracts.js +581 -0
- package/src/analysis/internal-audit.collect.js +43 -2
- package/src/analysis/internal-audit.reach.js +52 -1
- package/src/analysis/internal-audit.run.js +66 -13
- package/src/analysis/java-source.js +36 -0
- package/src/analysis/package-reachability.js +39 -0
- package/src/analysis/static-test-reachability.js +133 -0
- package/src/build-graph.js +72 -13
- package/src/graph/build-worker.js +3 -4
- package/src/graph/builder/lang-java.js +177 -14
- package/src/graph/builder/lang-js.js +71 -12
- package/src/graph/builder/lang-rust.js +129 -6
- package/src/graph/community.js +27 -0
- package/src/graph/file-lock.js +69 -0
- package/src/graph/freshness-probe.js +141 -0
- package/src/graph/graph-filter.js +28 -11
- package/src/graph/incremental-refresh.js +232 -0
- package/src/graph/internal-builder.barrels.js +169 -0
- package/src/graph/internal-builder.build.js +111 -16
- package/src/graph/internal-builder.java.js +33 -0
- package/src/graph/internal-builder.langs.js +2 -1
- package/src/graph/internal-builder.resolvers.js +109 -2
- package/src/graph/layout.js +21 -5
- package/src/graph/relations.js +4 -0
- package/src/graph/repo-registry.js +124 -0
- package/src/mcp/catalog.mjs +64 -21
- package/src/mcp/evidence-snapshot.architecture.mjs +80 -0
- package/src/mcp/evidence-snapshot.common.mjs +160 -0
- package/src/mcp/evidence-snapshot.duplicates.mjs +217 -0
- package/src/mcp/evidence-snapshot.health.mjs +95 -0
- package/src/mcp/evidence-snapshot.inventory.mjs +148 -0
- package/src/mcp/evidence-snapshot.mjs +50 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +249 -0
- package/src/mcp/evidence-snapshot.structure.mjs +81 -0
- package/src/mcp/graph-context.mjs +106 -181
- package/src/mcp/graph-diff.mjs +217 -0
- package/src/mcp/staleness-notice.mjs +20 -0
- package/src/mcp/sync-evidence.mjs +418 -0
- package/src/mcp/sync-payload.mjs +194 -8
- package/src/mcp/tool-result.mjs +51 -0
- package/src/mcp/tools-actions.mjs +114 -33
- package/src/mcp/tools-architecture.mjs +144 -0
- package/src/mcp/tools-company.mjs +273 -0
- package/src/mcp/tools-graph-hubs.mjs +58 -0
- package/src/mcp/tools-graph.mjs +36 -68
- package/src/mcp/tools-health.mjs +354 -20
- package/src/mcp/tools-history.mjs +22 -0
- package/src/mcp/tools-impact-change.mjs +261 -0
- package/src/mcp/tools-impact.mjs +35 -11
- package/src/mcp-server.mjs +100 -17
- package/src/mcp-source-tools.mjs +11 -3
- package/src/path-classification.js +201 -0
- package/src/path-ignore.js +69 -0
- package/src/security/typosquat.js +1 -1
package/src/mcp/tools-graph.mjs
CHANGED
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
// staleness probe in graph-context. Hot-reloadable (re-imported by catalog.mjs on change).
|
|
4
4
|
import {
|
|
5
5
|
isSymbol, degreeOf, labelOf, connList,
|
|
6
|
-
resolveNodeInfo, resolveNode, ambiguityNote, findSeeds, undirectedNeighbors,
|
|
6
|
+
resolveNodeInfo, resolveNode, ambiguityNote, findSeeds, resolveSeedFiles, undirectedNeighbors,
|
|
7
7
|
graphStaleness, fileStalenessNote,
|
|
8
8
|
} from './graph-context.mjs'
|
|
9
9
|
|
|
10
|
+
const compileKind = (edge) => edge?.typeOnly === true ? 'type-only' : edge?.compileOnly === true ? 'compile-only' : null
|
|
11
|
+
|
|
10
12
|
export function tGraphStats(g, ctx) {
|
|
11
13
|
const files = g.nodes.filter((n) => !isSymbol(n.id)).length
|
|
12
14
|
const symbols = g.nodes.length - files
|
|
13
15
|
const relCount = {}
|
|
14
16
|
const confCount = {}
|
|
15
17
|
let typeOnlyEdges = 0
|
|
18
|
+
let compileOnlyEdges = 0
|
|
16
19
|
for (const e of g.links) {
|
|
17
20
|
relCount[e.relation ?? '?'] = (relCount[e.relation ?? '?'] || 0) + 1
|
|
18
21
|
if (e.confidence != null) confCount[e.confidence] = (confCount[e.confidence] || 0) + 1
|
|
19
22
|
if (e.typeOnly === true) typeOnlyEdges++
|
|
23
|
+
if (e.compileOnly === true) compileOnlyEdges++
|
|
20
24
|
}
|
|
21
25
|
const comm = new Map()
|
|
22
26
|
for (const n of g.nodes) {
|
|
@@ -35,7 +39,8 @@ export function tGraphStats(g, ctx) {
|
|
|
35
39
|
ctx?.repoRoot ? `- Repo: ${ctx.repoRoot}` : null,
|
|
36
40
|
`- Nodes: ${g.nodes.length} (${files} files, ${symbols} symbols)`,
|
|
37
41
|
`- Edges: ${g.links.length}`,
|
|
38
|
-
g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only edges)` : `- Typed-edge metadata: unavailable (rebuild_graph
|
|
42
|
+
g.edgeTypesV ? `- Typed-edge metadata: v${g.edgeTypesV} (${typeOnlyEdges} type-only, ${compileOnlyEdges} compile-only edges)` : `- Typed-edge metadata: unavailable (rebuild_graph required)`,
|
|
43
|
+
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)`,
|
|
39
44
|
`- Relations: ${fmt(relCount)}`,
|
|
40
45
|
Object.keys(confCount).length ? `- Confidence: ${fmt(confCount)}` : null,
|
|
41
46
|
`- Communities: ${comm.size} (top by size: ${topComm.map(([c, n]) => `#${c}=${n}`).join(', ')})`,
|
|
@@ -54,10 +59,12 @@ export function tGetNode(g, {label} = {}, ctx) {
|
|
|
54
59
|
const drift = ctx ? fileStalenessNote(ctx, n.source_file || (isSymbol(id) ? id.split('#')[0] : id)) : null
|
|
55
60
|
const outs = g.out.get(id) || []
|
|
56
61
|
const ins = g.inn.get(id) || []
|
|
62
|
+
const semanticOuts = connList(outs)
|
|
63
|
+
const semanticIns = connList(ins)
|
|
57
64
|
const sample = (list, dir) =>
|
|
58
65
|
list
|
|
59
66
|
.slice(0, 12)
|
|
60
|
-
.map((e) => ` ${dir === 'out' ? '→' : '←'} ${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]`)
|
|
67
|
+
.map((e) => ` ${dir === 'out' ? '→' : '←'} ${compileKind(e) ? `${compileKind(e)} ` : ''}${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]`)
|
|
61
68
|
.join('\n') || ' (none)'
|
|
62
69
|
return [
|
|
63
70
|
note,
|
|
@@ -66,7 +73,7 @@ export function tGetNode(g, {label} = {}, ctx) {
|
|
|
66
73
|
`- kind: ${isSymbol(id) ? 'symbol' : 'file'}${n.file_type ? ` (${n.file_type})` : ''}`,
|
|
67
74
|
n.source_file ? `- source: ${n.source_file}${n.source_location ? ` ${n.source_location}` : ''}` : null,
|
|
68
75
|
n.community != null ? `- community: ${n.community}` : null,
|
|
69
|
-
`- degree: ${
|
|
76
|
+
`- semantic degree: ${semanticOuts.length + semanticIns.length} (out ${semanticOuts.length}, in ${semanticIns.length})${outs.length + ins.length !== semanticOuts.length + semanticIns.length ? `; ${outs.length + ins.length} physical/structural edges retained` : ''}`,
|
|
70
77
|
`Outgoing:\n${sample(outs, 'out')}`,
|
|
71
78
|
`Incoming:\n${sample(ins, 'in')}`,
|
|
72
79
|
drift,
|
|
@@ -80,10 +87,10 @@ export function tGetNode(g, {label} = {}, ctx) {
|
|
|
80
87
|
function dedupeEdges(list) {
|
|
81
88
|
const grouped = new Map()
|
|
82
89
|
for (const e of list) {
|
|
83
|
-
const key = `${e.relation || 'rel'}|${e
|
|
90
|
+
const key = `${e.relation || 'rel'}|${compileKind(e) || 'runtime'}|${e.id}`
|
|
84
91
|
const cur = grouped.get(key)
|
|
85
92
|
if (cur) cur.count += 1
|
|
86
|
-
else grouped.set(key, {id: e.id, relation: e.relation, typeOnly: e.typeOnly === true, count: 1})
|
|
93
|
+
else grouped.set(key, {id: e.id, relation: e.relation, typeOnly: e.typeOnly === true, compileOnly: e.compileOnly === true, count: 1})
|
|
87
94
|
}
|
|
88
95
|
return [...grouped.values()]
|
|
89
96
|
}
|
|
@@ -102,7 +109,7 @@ export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
|
102
109
|
const outs = dedupeEdges(outsRaw)
|
|
103
110
|
const ins = dedupeEdges(insRaw)
|
|
104
111
|
const line = (e, dir) =>
|
|
105
|
-
` ${dir === 'out' ? '→' : '←'} ${e
|
|
112
|
+
` ${dir === 'out' ? '→' : '←'} ${compileKind(e) ? `${compileKind(e)} ` : ''}${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]${e.count > 1 ? ` (${e.count} sites)` : ''}`
|
|
106
113
|
return [
|
|
107
114
|
note,
|
|
108
115
|
`Neighbors of ${n.label ?? id}${rf ? ` (relation=${rf})` : ''}: ${outs.length + ins.length} unique (${outsRaw.length + insRaw.length} edges)`,
|
|
@@ -114,55 +121,8 @@ export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
|
114
121
|
].filter(Boolean).join('\n')
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
export
|
|
118
|
-
|
|
119
|
-
const scored = g.nodes
|
|
120
|
-
.map((node) => {
|
|
121
|
-
const outs = connList(g.out.get(String(node.id)))
|
|
122
|
-
const ins = connList(g.inn.get(String(node.id)))
|
|
123
|
-
const outIds = new Set(outs.map((e) => String(e.id)))
|
|
124
|
-
const inIds = new Set(ins.map((e) => String(e.id)))
|
|
125
|
-
const allIds = new Set([...outIds, ...inIds])
|
|
126
|
-
const runtimeIds = new Set([...outs, ...ins].filter((e) => e.typeOnly !== true).map((e) => String(e.id)))
|
|
127
|
-
const compileOnlyIds = new Set([...outs, ...ins].filter((e) => e.typeOnly === true).map((e) => String(e.id)))
|
|
128
|
-
for (const id of runtimeIds) compileOnlyIds.delete(id)
|
|
129
|
-
const occurrences = outs.length + ins.length
|
|
130
|
-
return {
|
|
131
|
-
node,
|
|
132
|
-
deg: allIds.size,
|
|
133
|
-
runtime: runtimeIds.size,
|
|
134
|
-
compileOnly: compileOnlyIds.size,
|
|
135
|
-
out: outIds.size,
|
|
136
|
-
in: inIds.size,
|
|
137
|
-
occurrences,
|
|
138
|
-
}
|
|
139
|
-
})
|
|
140
|
-
.filter((entry) => entry.deg > 0)
|
|
141
|
-
.sort((a, b) => b.runtime - a.runtime || b.deg - a.deg || b.occurrences - a.occurrences)
|
|
142
|
-
const ranked = scored.slice(0, n)
|
|
143
|
-
const rankedIds = new Set(ranked.map((entry) => String(entry.node.id)))
|
|
144
|
-
// Unique neighbors measure coupling, but a large component repeatedly calling the same helper (for
|
|
145
|
-
// example i18n) can still be a valuable complexity hotspot. Preserve that second lens explicitly
|
|
146
|
-
// instead of letting repeated sites inflate the coupling rank.
|
|
147
|
-
const occurrenceHotspots = scored
|
|
148
|
-
.filter((entry) => !rankedIds.has(String(entry.node.id)))
|
|
149
|
-
.filter((entry) => entry.occurrences >= 20 && entry.occurrences - entry.deg >= 10)
|
|
150
|
-
.sort((a, b) => (b.occurrences - b.deg) - (a.occurrences - a.deg) || b.occurrences - a.occurrences)
|
|
151
|
-
.slice(0, Math.min(5, n))
|
|
152
|
-
return [
|
|
153
|
-
`Top ${ranked.length} connectivity hubs (ranked by unique runtime call/import/reference neighbors; structural containment excluded):`,
|
|
154
|
-
...ranked.map(
|
|
155
|
-
(r, i) =>
|
|
156
|
-
`${String(i + 1).padStart(2)}. ${r.node.label ?? r.node.id} (${r.deg} unique: ${r.runtime} runtime, ${r.compileOnly} compile-only; out ${r.out}, in ${r.in}; ${r.occurrences} edge occurrence${r.occurrences === 1 ? '' : 's'}) [${r.node.id}]`
|
|
157
|
-
),
|
|
158
|
-
`Repeated call sites affect the occurrence count, not the connectivity rank; compile-only neighbors are secondary to runtime coupling.`,
|
|
159
|
-
occurrenceHotspots.length ? `` : null,
|
|
160
|
-
occurrenceHotspots.length ? `High occurrence hotspots outside the connectivity rank (repeated call/reference sites; complexity signal, not broader coupling):` : null,
|
|
161
|
-
...occurrenceHotspots.map((r) =>
|
|
162
|
-
` ${r.node.label ?? r.node.id} (${r.occurrences} occurrences across ${r.deg} unique neighbors; ${r.occurrences - r.deg} repeats) [${r.node.id}]`
|
|
163
|
-
),
|
|
164
|
-
].filter((line) => line != null).join('\n')
|
|
165
|
-
}
|
|
124
|
+
export {tGodNodes} from './tools-graph-hubs.mjs'
|
|
125
|
+
|
|
166
126
|
|
|
167
127
|
export function tGetCommunity(g, {community_id} = {}) {
|
|
168
128
|
const groups = new Map()
|
|
@@ -194,8 +154,14 @@ export function tGetCommunity(g, {community_id} = {}) {
|
|
|
194
154
|
// A plain BFS/DFS flood dumps every reached node (thousands on a real graph) at near-zero signal.
|
|
195
155
|
// Instead: traverse to record reach + distance-from-seed, then show only the closest, most-connected
|
|
196
156
|
// slice as a coherent subgraph (edges kept only among shown nodes). Honest about what was trimmed.
|
|
197
|
-
export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filter, token_budget = 2000} = {}) {
|
|
198
|
-
const
|
|
157
|
+
export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filter, seed_files, augment_seeds = false, token_budget = 2000} = {}) {
|
|
158
|
+
const pinned = resolveSeedFiles(g, seed_files)
|
|
159
|
+
// Exact seed files are a control surface, not a hint: by default they disable fuzzy keyword seeds.
|
|
160
|
+
// Callers can opt back into augmentation when they explicitly want both behaviors.
|
|
161
|
+
const automatic = pinned.seeds.length && augment_seeds !== true
|
|
162
|
+
? []
|
|
163
|
+
: findSeeds(g, question, Math.max(0, 8 - pinned.seeds.length))
|
|
164
|
+
const seeds = [...pinned.seeds, ...automatic.filter((node) => !pinned.seeds.some((seed) => String(seed.id) === String(node.id)))]
|
|
199
165
|
if (!seeds.length) return `No nodes matched "${question}".`
|
|
200
166
|
const maxDepth = Math.max(1, Math.min(6, Number(depth) || 3))
|
|
201
167
|
const ctx = Array.isArray(context_filter) && context_filter.length ? new Set(context_filter.map((c) => String(c).toLowerCase())) : null
|
|
@@ -204,7 +170,6 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
|
|
|
204
170
|
// node budget scales gently with the token budget; edges follow the surviving nodes.
|
|
205
171
|
const nodeBudget = Math.max(20, Math.min(120, Math.round((Number(token_budget) || 2000) / 40)))
|
|
206
172
|
const depthOf = new Map() // id -> shortest distance from any seed
|
|
207
|
-
const edges = []
|
|
208
173
|
const start = seeds.map((s) => String(s.id))
|
|
209
174
|
if (mode === 'dfs') {
|
|
210
175
|
const stack = start.map((id) => ({id, d: 0}))
|
|
@@ -217,7 +182,6 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
|
|
|
217
182
|
if (d >= maxDepth) continue
|
|
218
183
|
for (const [nid, rel] of undirectedNeighbors(g, id)) {
|
|
219
184
|
if (!relOk(rel)) continue
|
|
220
|
-
edges.push([id, rel, nid])
|
|
221
185
|
if (!seen.has(nid)) stack.push({id: nid, d: d + 1})
|
|
222
186
|
}
|
|
223
187
|
}
|
|
@@ -229,7 +193,6 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
|
|
|
229
193
|
for (const id of frontier)
|
|
230
194
|
for (const [nid, rel] of undirectedNeighbors(g, id)) {
|
|
231
195
|
if (!relOk(rel)) continue
|
|
232
|
-
edges.push([id, rel, nid])
|
|
233
196
|
if (!depthOf.has(nid)) {
|
|
234
197
|
depthOf.set(nid, d + 1)
|
|
235
198
|
next.push(nid)
|
|
@@ -246,24 +209,29 @@ export function tQueryGraph(g, {question, mode = 'bfs', depth = 3, context_filte
|
|
|
246
209
|
const shownIds = new Set(shown.map((n) => n.id))
|
|
247
210
|
const edgeSeen = new Set()
|
|
248
211
|
const shownEdges = []
|
|
249
|
-
for (const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
212
|
+
for (const source of shownIds) {
|
|
213
|
+
for (const edge of g.out.get(source) || []) {
|
|
214
|
+
const target = String(edge.id)
|
|
215
|
+
if (edge.barrelProxy === true || !shownIds.has(target) || !relOk(edge.relation)) continue
|
|
216
|
+
const key = `${source}|${edge.relation}|${target}`
|
|
217
|
+
if (edgeSeen.has(key)) continue
|
|
218
|
+
edgeSeen.add(key)
|
|
219
|
+
shownEdges.push([source, edge.relation, target])
|
|
220
|
+
if (shownEdges.length >= 160) break
|
|
221
|
+
}
|
|
255
222
|
if (shownEdges.length >= 160) break
|
|
256
223
|
}
|
|
257
224
|
const head = [
|
|
258
225
|
`Query: "${question}" (${mode}, depth ${maxDepth}${ctx ? `, context ${[...ctx].join('/')}` : ''})`,
|
|
259
226
|
`Seeds: ${seeds.map((s) => s.label ?? s.id).join(', ')}`,
|
|
227
|
+
pinned.missing.length ? `Unresolved pinned seed files: ${pinned.missing.join(', ')}` : null,
|
|
260
228
|
`Reached ${depthOf.size} nodes; showing ${shown.length} closest by proximity + connectivity, ${shownEdges.length} edges among them.`,
|
|
261
229
|
``,
|
|
262
230
|
`Nodes:`,
|
|
263
231
|
]
|
|
264
232
|
const nodeLines = shown.map((n) => ` [d${n.d}] ${labelOf(g, n.id)} (deg ${n.deg}) [${n.id}]`)
|
|
265
233
|
const edgeLines = ['', 'Edges:', ...shownEdges.map(([s, r, t]) => ` ${labelOf(g, s)} --${r || 'rel'}--> ${labelOf(g, t)}`)]
|
|
266
|
-
let text = [...head, ...nodeLines, ...edgeLines].join('\n')
|
|
234
|
+
let text = [...head.filter(Boolean), ...nodeLines, ...edgeLines].join('\n')
|
|
267
235
|
if (text.length > charBudget) text = text.slice(0, charBudget) + `\n... (truncated to ~${token_budget} tokens)`
|
|
268
236
|
return text
|
|
269
237
|
}
|