weavatrix 0.1.3 → 0.1.4
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 +32 -12
- package/package.json +1 -1
- package/skill/SKILL.md +20 -13
- package/src/analysis/dead-check.js +8 -3
- package/src/analysis/dep-check-ecosystems.js +157 -0
- package/src/analysis/dep-check.js +32 -144
- package/src/analysis/dep-rules.js +36 -23
- package/src/analysis/endpoints-rust.js +124 -0
- package/src/analysis/endpoints.js +11 -4
- package/src/analysis/graph-analysis.aggregate.js +27 -26
- package/src/analysis/graph-analysis.edges.js +21 -0
- package/src/analysis/graph-analysis.js +1 -1
- package/src/analysis/graph-analysis.summaries.js +4 -1
- package/src/analysis/internal-audit.collect.js +40 -0
- package/src/analysis/internal-audit.run.js +9 -4
- package/src/graph/builder/lang-java.js +177 -14
- package/src/graph/builder/lang-rust.js +129 -6
- package/src/graph/community.js +17 -0
- package/src/graph/internal-builder.build.js +32 -8
- package/src/graph/internal-builder.java.js +33 -0
- package/src/graph/internal-builder.resolvers.js +109 -2
- package/src/graph/relations.js +4 -0
- package/src/mcp/catalog.mjs +4 -4
- package/src/mcp/graph-context.mjs +7 -166
- package/src/mcp/graph-diff.mjs +163 -0
- package/src/mcp/sync-payload.mjs +30 -8
- package/src/mcp/tools-actions.mjs +3 -3
- package/src/mcp/tools-graph-hubs.mjs +58 -0
- package/src/mcp/tools-graph.mjs +11 -54
- package/src/mcp/tools-health.mjs +18 -6
- package/src/mcp/tools-impact.mjs +11 -6
- package/src/mcp-server.mjs +2 -2
package/src/mcp/tools-graph.mjs
CHANGED
|
@@ -7,16 +7,20 @@ import {
|
|
|
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,7 @@ 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)`,
|
|
39
43
|
`- Relations: ${fmt(relCount)}`,
|
|
40
44
|
Object.keys(confCount).length ? `- Confidence: ${fmt(confCount)}` : null,
|
|
41
45
|
`- Communities: ${comm.size} (top by size: ${topComm.map(([c, n]) => `#${c}=${n}`).join(', ')})`,
|
|
@@ -57,7 +61,7 @@ export function tGetNode(g, {label} = {}, ctx) {
|
|
|
57
61
|
const sample = (list, dir) =>
|
|
58
62
|
list
|
|
59
63
|
.slice(0, 12)
|
|
60
|
-
.map((e) => ` ${dir === 'out' ? '→' : '←'} ${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]`)
|
|
64
|
+
.map((e) => ` ${dir === 'out' ? '→' : '←'} ${compileKind(e) ? `${compileKind(e)} ` : ''}${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]`)
|
|
61
65
|
.join('\n') || ' (none)'
|
|
62
66
|
return [
|
|
63
67
|
note,
|
|
@@ -80,10 +84,10 @@ export function tGetNode(g, {label} = {}, ctx) {
|
|
|
80
84
|
function dedupeEdges(list) {
|
|
81
85
|
const grouped = new Map()
|
|
82
86
|
for (const e of list) {
|
|
83
|
-
const key = `${e.relation || 'rel'}|${e
|
|
87
|
+
const key = `${e.relation || 'rel'}|${compileKind(e) || 'runtime'}|${e.id}`
|
|
84
88
|
const cur = grouped.get(key)
|
|
85
89
|
if (cur) cur.count += 1
|
|
86
|
-
else grouped.set(key, {id: e.id, relation: e.relation, typeOnly: e.typeOnly === true, count: 1})
|
|
90
|
+
else grouped.set(key, {id: e.id, relation: e.relation, typeOnly: e.typeOnly === true, compileOnly: e.compileOnly === true, count: 1})
|
|
87
91
|
}
|
|
88
92
|
return [...grouped.values()]
|
|
89
93
|
}
|
|
@@ -102,7 +106,7 @@ export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
|
102
106
|
const outs = dedupeEdges(outsRaw)
|
|
103
107
|
const ins = dedupeEdges(insRaw)
|
|
104
108
|
const line = (e, dir) =>
|
|
105
|
-
` ${dir === 'out' ? '→' : '←'} ${e
|
|
109
|
+
` ${dir === 'out' ? '→' : '←'} ${compileKind(e) ? `${compileKind(e)} ` : ''}${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]${e.count > 1 ? ` (${e.count} sites)` : ''}`
|
|
106
110
|
return [
|
|
107
111
|
note,
|
|
108
112
|
`Neighbors of ${n.label ?? id}${rf ? ` (relation=${rf})` : ''}: ${outs.length + ins.length} unique (${outsRaw.length + insRaw.length} edges)`,
|
|
@@ -114,55 +118,8 @@ export function tGetNeighbors(g, {label, relation_filter} = {}, ctx) {
|
|
|
114
118
|
].filter(Boolean).join('\n')
|
|
115
119
|
}
|
|
116
120
|
|
|
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
|
-
}
|
|
121
|
+
export {tGodNodes} from './tools-graph-hubs.mjs'
|
|
122
|
+
|
|
166
123
|
|
|
167
124
|
export function tGetCommunity(g, {community_id} = {}) {
|
|
168
125
|
const groups = new Map()
|
package/src/mcp/tools-health.mjs
CHANGED
|
@@ -113,7 +113,7 @@ export async function tRunAudit(g, args, ctx) {
|
|
|
113
113
|
return [
|
|
114
114
|
`Internal audit of ${audit.repo} (${audit.scanned.files} files, ${audit.scanned.symbols} symbols, ${audit.scanned.externalImports} external imports; malware scan: ${audit.scanned.malwareScanMode}).`,
|
|
115
115
|
`Severity: critical ${sev.critical}, high ${sev.high}, medium ${sev.medium}, low ${sev.low}, info ${sev.info}. Categories: unused ${bycat.unused}, structure ${bycat.structure}, vulnerability ${bycat.vulnerability}, malware ${bycat.malware}.`,
|
|
116
|
-
`Structure: ${audit.structureReport?.runtimeCycles ?? audit.structureReport?.cycles ?? 0} runtime cycle(s), ${audit.structureReport?.typeCouplings ?? 0}
|
|
116
|
+
`Structure: ${audit.structureReport?.runtimeCycles ?? audit.structureReport?.cycles ?? 0} runtime cycle(s), ${audit.structureReport?.compileTimeCouplings ?? audit.structureReport?.typeCouplings ?? 0} compile-time coupling group(s), ${audit.structureReport?.orphans ?? 0} orphan(s); import edges: ${audit.structureReport?.runtimeImportEdges ?? audit.structureReport?.importEdges ?? 0} runtime + ${audit.structureReport?.typeOnlyImportEdges ?? 0} type-only + ${audit.structureReport?.compileOnlyImportEdges ?? 0} compile-only. Dead: ${audit.deadReport.deadFiles} file(s), ${audit.deadReport.unusedExports} unused export(s).`,
|
|
117
117
|
`Checks: ${check('OSV', audit.checks?.osv)}; ${check('malware', audit.checks?.malware)}. A NOT_CHECKED/PARTIAL/ERROR check is incomplete or unknown, never a clean zero.`,
|
|
118
118
|
``,
|
|
119
119
|
`Showing ${shown.length} of ${filtered.length} finding(s)${cat ? ` in category "${cat}"` : ''}${args.min_severity ? ` at ≥${args.min_severity}` : ''}:`,
|
|
@@ -140,16 +140,28 @@ export function tModuleMap(g, args, ctx) {
|
|
|
140
140
|
const topN = Math.max(1, Math.min(60, Number(args.top_n) || 25))
|
|
141
141
|
const mods = agg.modules.slice(0, topN)
|
|
142
142
|
const edges = agg.moduleEdges.slice(0, Math.min(50, topN * 2))
|
|
143
|
-
const
|
|
143
|
+
const compileEdges = new Map()
|
|
144
|
+
const collectCompileEdges = (list, kind) => {
|
|
145
|
+
for (const edge of list || []) {
|
|
146
|
+
const key = `${edge.from}\0${edge.to}`
|
|
147
|
+
const current = compileEdges.get(key) || {from: edge.from, to: edge.to, count: 0, typeOnly: 0, compileOnly: 0}
|
|
148
|
+
current.count += edge.count
|
|
149
|
+
current[kind] += edge.count
|
|
150
|
+
compileEdges.set(key, current)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
collectCompileEdges(agg.typeOnlyModuleEdges, 'typeOnly')
|
|
154
|
+
collectCompileEdges(agg.compileOnlyModuleEdges, 'compileOnly')
|
|
155
|
+
const compiled = [...compileEdges.values()].sort((a, b) => b.count - a.count).slice(0, Math.min(50, topN * 2))
|
|
144
156
|
return [
|
|
145
|
-
`Module map: ${agg.totals.files} files in ${agg.modules.length} folder-modules, ${agg.totals.moduleEdges} runtime module dependencies and ${agg.totals.typeOnlyModuleEdges || 0} type-only
|
|
157
|
+
`Module map: ${agg.totals.files} files in ${agg.modules.length} folder-modules, ${agg.totals.moduleEdges} runtime module dependencies and ${agg.totals.compileTimeModuleEdges || 0} compile-time dependencies (${agg.totals.typeOnlyModuleEdges || 0} type-only, ${agg.totals.compileOnlyModuleEdges || 0} compile-only). Top ${mods.length}:`,
|
|
146
158
|
...mods.map((m) => ` ${m.name} — ${m.fileCount} files, ${m.symbolCount} symbols`),
|
|
147
159
|
``,
|
|
148
160
|
`Strongest runtime module dependencies:`,
|
|
149
161
|
...edges.map((e) => ` ${e.from} → ${e.to} (${e.count})`),
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
...
|
|
162
|
+
compiled.length ? `` : null,
|
|
163
|
+
compiled.length ? `Compile-time module dependencies (not runtime coupling):` : null,
|
|
164
|
+
...compiled.map((e) => ` ${e.from} → ${e.to} (${e.count}; ${e.typeOnly} type-only, ${e.compileOnly} compile-only)`),
|
|
153
165
|
].filter((line) => line != null).join('\n')
|
|
154
166
|
}
|
|
155
167
|
|
package/src/mcp/tools-impact.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
rawGraph, prevGraphPathFor, edgeEndpoint, diffGraphs, formatGraphDiff,
|
|
10
10
|
} from './graph-context.mjs'
|
|
11
11
|
import {readCoverageForRepo} from '../analysis/coverage-reports.js'
|
|
12
|
+
import {isStructuralRelation} from '../graph/relations.js'
|
|
12
13
|
|
|
13
14
|
function reverseReach(g, seeds, maxDepth) {
|
|
14
15
|
const states = new Map([...seeds].map((id) => [String(id), {
|
|
@@ -19,9 +20,9 @@ function reverseReach(g, seeds, maxDepth) {
|
|
|
19
20
|
const current = frontier[cursor]
|
|
20
21
|
if (current.depth >= maxDepth) continue
|
|
21
22
|
for (const e of g.inn.get(current.id) || []) {
|
|
22
|
-
if (e.relation
|
|
23
|
+
if (isStructuralRelation(e.relation)) continue
|
|
23
24
|
const id = String(e.id)
|
|
24
|
-
const compileOnly = current.compileOnly || e.typeOnly === true
|
|
25
|
+
const compileOnly = current.compileOnly || e.typeOnly === true || e.compileOnly === true
|
|
25
26
|
const depth = current.depth + 1
|
|
26
27
|
const entry = states.get(id) || {
|
|
27
28
|
runtimeDepth: null, runtimeRelation: null, compileDepth: null, compileRelation: null,
|
|
@@ -74,12 +75,14 @@ export function tGetDependents(g, {label, depth = 3, max_nodes = 40} = {}) {
|
|
|
74
75
|
const ranked = [...reached.entries()]
|
|
75
76
|
.filter(([nid]) => !seeds.has(nid))
|
|
76
77
|
.map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid)}))
|
|
77
|
-
.sort((a, b) => a.d - b.d || b.deg - a.deg)
|
|
78
|
+
.sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
|
|
78
79
|
if (!ranked.length) return [note, `No dependents found for ${n.label ?? id} within depth ${maxDepth} — nothing in the graph calls, imports, or inherits it.`].filter(Boolean).join('\n')
|
|
79
80
|
const shown = ranked.slice(0, cap)
|
|
81
|
+
const runtimeCount = ranked.filter((entry) => !entry.entry.compileOnly).length
|
|
82
|
+
const compileCount = ranked.length - runtimeCount
|
|
80
83
|
return [
|
|
81
84
|
note,
|
|
82
|
-
`Dependents of ${n.label ?? id} (reverse calls/imports/inherits, depth ≤${maxDepth}): ${ranked.length} found, showing ${shown.length} by proximity + connectivity.`,
|
|
85
|
+
`Dependents of ${n.label ?? id} (reverse calls/imports/inherits, depth ≤${maxDepth}): ${ranked.length} found (${runtimeCount} runtime, ${compileCount} compile-time-only), showing ${shown.length} by proximity + connectivity.`,
|
|
83
86
|
containingFile ? `Includes importers of its containing file ${labelOf(g, containingFile)}.` : null,
|
|
84
87
|
...shown.map((r) => ` [d${r.d} ${impactKind(r.entry)}] ${r.entry.relation || 'rel'} ${labelOf(g, r.id)} (deg ${r.deg}) [${r.id}]`),
|
|
85
88
|
].filter(Boolean).join('\n')
|
|
@@ -174,7 +177,7 @@ export function tChangeImpact(g, args, ctx) {
|
|
|
174
177
|
const impacted = [...reached.entries()]
|
|
175
178
|
.filter(([nid]) => !seeds.has(nid) && !changedSet.has(fileOfNode(nid)))
|
|
176
179
|
.map(([nid, entry]) => ({id: nid, d: entry.depth, entry, deg: degreeOf(g, nid), file: fileOfNode(nid)}))
|
|
177
|
-
.sort((a, b) => a.d - b.d || b.deg - a.deg)
|
|
180
|
+
.sort((a, b) => a.d - b.d || Number(a.entry.compileOnly) - Number(b.entry.compileOnly) || b.deg - a.deg)
|
|
178
181
|
|
|
179
182
|
// coverage overlay from EXISTING reports (fractions 0..1) — see coverage_map for details
|
|
180
183
|
const knownFiles = (rawGraph(ctx).nodes || []).filter((n) => !String(n.id).includes('#') && n.source_file).map((n) => n.source_file)
|
|
@@ -184,11 +187,13 @@ export function tChangeImpact(g, args, ctx) {
|
|
|
184
187
|
const hasCoverage = coverage.size > 0
|
|
185
188
|
|
|
186
189
|
const shown = impacted.slice(0, cap)
|
|
190
|
+
const runtimeImpacted = impacted.filter((entry) => !entry.entry.compileOnly).length
|
|
191
|
+
const compileImpacted = impacted.length - runtimeImpacted
|
|
187
192
|
const untestedHotspots = shown.filter((n) => { const c = covOf(n.file); return c != null && c < 0.5 && n.deg >= 5 })
|
|
188
193
|
return [
|
|
189
194
|
`Change impact ${sourceLabel}: ${changed.length} changed file(s) → ${seeds.size} graph seed(s) incl. their symbols${unmapped.length ? `; ${unmapped.length} file(s) not in the graph (new/non-code — rebuild_graph refreshes)` : ''}.`,
|
|
190
195
|
impacted.length
|
|
191
|
-
? `${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s), showing ${shown.length}:`
|
|
196
|
+
? `${impacted.length} impacted node(s) within ${maxDepth} reverse hop(s) (${runtimeImpacted} runtime, ${compileImpacted} compile-time-only), showing ${shown.length}:`
|
|
192
197
|
: `Nothing else in the graph depends on the changed code within ${maxDepth} hop(s).`,
|
|
193
198
|
hasCoverage ? `Coverage: existing report mapped where available.` : `Coverage: unavailable (no supported report found); per-node coverage labels omitted.`,
|
|
194
199
|
...shown.map((n) => ` [d${n.d} ${impactKind(n.entry)}] ${n.entry.relation || 'rel'} ${labelOf(g, n.id)} (deg ${n.deg}${hasCoverage ? `, ${pctStr(covOf(n.file))}` : ''}) [${n.id}]`),
|
package/src/mcp-server.mjs
CHANGED
|
@@ -131,8 +131,8 @@ async function main() {
|
|
|
131
131
|
if (!graph && tool.cap !== 'build' && tool.cap !== 'retarget') return reply(id, {content: [{type: 'text', text: `Graph unavailable: ${graphError}`}], isError: true})
|
|
132
132
|
try {
|
|
133
133
|
let text = String(await tool.run(graph, params?.arguments || {}, ctx))
|
|
134
|
-
if (graph && graph.edgeTypesV <
|
|
135
|
-
text += '\n\nWarning: this saved graph predates
|
|
134
|
+
if (graph && graph.edgeTypesV < 2 && (tool.cap === 'graph' || tool.cap === 'health')) {
|
|
135
|
+
text += '\n\nWarning: this saved graph predates compile-only edge metadata (edge schema v2), so Rust module/use dependencies may be absent or misclassified. Call rebuild_graph once before acting on cycle, boundary, dependency, or blast-radius findings.'
|
|
136
136
|
}
|
|
137
137
|
// Graph answers silently reflect a point-in-time build — surface staleness on every graph tool.
|
|
138
138
|
if (tool.cap === 'graph') {
|