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
|
@@ -6,6 +6,8 @@ import { join } from "node:path";
|
|
|
6
6
|
import { normalizeRepoParts, readCoverageForRepo, pctFromCounts } from "./coverage-reports.js";
|
|
7
7
|
import { bareSymbolName, countLocalRefsOutsideOwnRange, computeSymbolExternalRefs } from "./graph-analysis.refs.js";
|
|
8
8
|
import { createRepoBoundary } from "../repo-path.js";
|
|
9
|
+
import { edgeList, folderModuleOf } from "./graph-analysis.edges.js";
|
|
10
|
+
export { folderModuleOf } from "./graph-analysis.edges.js";
|
|
9
11
|
|
|
10
12
|
// Aggregate a built graph.json into the file- and module-level view the UI needs:
|
|
11
13
|
// - graph-builder nodes are FILES *and* their symbols (functions/methods), linked file→symbol by the
|
|
@@ -13,7 +15,6 @@ import { createRepoBoundary } from "../repo-path.js";
|
|
|
13
15
|
// - Each file is assigned to ONE module via its file-node's community (symbols may cluster apart,
|
|
14
16
|
// but the file itself belongs where its file-node sits) → exact, non-overlapping file counts.
|
|
15
17
|
// - Real edges (everything except "contains") roll up to file→file and module→module relations.
|
|
16
|
-
// Module names match summarizeCommunities (dominant first-two-path-parts), so they line up with cards.
|
|
17
18
|
export function analyzeGraph(graphJsonPath, repoRoot) {
|
|
18
19
|
let graph;
|
|
19
20
|
try {
|
|
@@ -40,15 +41,11 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
40
41
|
// MODULE = the file's own top FOLDER (its "territory"), up to 2 path levels — NOT a dependency
|
|
41
42
|
// cluster. e.g. src/widget/foo.js → "src/widget"; benchmarks/cleanup.py → "benchmarks" (so all
|
|
42
43
|
// benchmarks files are one module); a top-level file like index.js → "(root)".
|
|
43
|
-
const folderOf = (file) => {
|
|
44
|
-
const dirs = String(file || "").split(/[\\/]/).filter(Boolean).slice(0, -1);
|
|
45
|
-
return dirs.length ? dirs.slice(0, 2).join("/") : "(root)";
|
|
46
|
-
};
|
|
47
44
|
// In a merged (cross-repo) graph every node carries a `repo`; qualify file & module identity by it
|
|
48
45
|
// so same-named folders/files in different repos never merge. Single-repo graphs have no `repo`,
|
|
49
46
|
// so identity stays the bare path (unchanged behavior).
|
|
50
47
|
const fileIdOf = (node) => (node.repo ? `${node.repo}::${node.source_file}` : node.source_file);
|
|
51
|
-
const moduleOfNode = (node) => (node.repo ? `${node.repo}/` : "") +
|
|
48
|
+
const moduleOfNode = (node) => (node.repo ? `${node.repo}/` : "") + folderModuleOf(node.source_file);
|
|
52
49
|
|
|
53
50
|
// file → module (folder) + symbols + display path, all keyed by repo-qualified file id
|
|
54
51
|
const fileModule = new Map();
|
|
@@ -102,45 +99,41 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
102
99
|
const moduleEdges = new Map();
|
|
103
100
|
const typeOnlyFileEdges = new Map();
|
|
104
101
|
const typeOnlyModuleEdges = new Map();
|
|
102
|
+
const compileOnlyFileEdges = new Map();
|
|
103
|
+
const compileOnlyModuleEdges = new Map();
|
|
104
|
+
const compileTimeFileEdges = new Map();
|
|
105
|
+
const compileTimeModuleEdges = new Map();
|
|
106
|
+
const addFileEdge = (map, key, link) => {
|
|
107
|
+
let edge = map.get(key);
|
|
108
|
+
if (!edge) map.set(key, (edge = { count: 0, rels: {} }));
|
|
109
|
+
edge.count++;
|
|
110
|
+
if (link.relation) edge.rels[link.relation] = (edge.rels[link.relation] || 0) + 1;
|
|
111
|
+
};
|
|
105
112
|
for (const link of links) {
|
|
106
|
-
if (link.relation === "contains") continue;
|
|
113
|
+
if (link.relation === "contains" || link.barrelProxy === true) continue;
|
|
107
114
|
const fromFile = id2file.get(endpoint(link.source));
|
|
108
115
|
const toFile = id2file.get(endpoint(link.target));
|
|
109
116
|
if (fromFile && toFile && fromFile !== toFile) {
|
|
110
117
|
const key = `${fromFile} ${toFile}`;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (link.relation) fe.rels[link.relation] = (fe.rels[link.relation] || 0) + 1;
|
|
118
|
+
const compileTime = link.typeOnly === true || link.compileOnly === true;
|
|
119
|
+
const targetFileEdges = link.typeOnly === true ? typeOnlyFileEdges : link.compileOnly === true ? compileOnlyFileEdges : fileEdges;
|
|
120
|
+
addFileEdge(targetFileEdges, key, link);
|
|
121
|
+
if (compileTime) addFileEdge(compileTimeFileEdges, key, link);
|
|
116
122
|
const fromMod = fileModule.get(fromFile);
|
|
117
123
|
const toMod = fileModule.get(toFile);
|
|
118
124
|
if (fromMod && toMod && fromMod !== toMod) {
|
|
119
125
|
const mkey = `${fromMod} ${toMod}`;
|
|
120
|
-
const targetModuleEdges = link.typeOnly === true ? typeOnlyModuleEdges : moduleEdges;
|
|
126
|
+
const targetModuleEdges = link.typeOnly === true ? typeOnlyModuleEdges : link.compileOnly === true ? compileOnlyModuleEdges : moduleEdges;
|
|
121
127
|
targetModuleEdges.set(mkey, (targetModuleEdges.get(mkey) || 0) + 1);
|
|
128
|
+
if (compileTime) compileTimeModuleEdges.set(mkey, (compileTimeModuleEdges.get(mkey) || 0) + 1);
|
|
122
129
|
}
|
|
123
130
|
}
|
|
124
131
|
}
|
|
125
|
-
const split = (key) => {
|
|
126
|
-
const i = key.indexOf(" ");
|
|
127
|
-
return [key.slice(0, i), key.slice(i + 1)];
|
|
128
|
-
};
|
|
129
|
-
const edgeList = (map) =>
|
|
130
|
-
[...map.entries()]
|
|
131
|
-
.map(([key, v]) => {
|
|
132
|
-
const [from, to] = split(key);
|
|
133
|
-
if (typeof v === "number") return { from, to, count: v }; // moduleEdges (no relation breakdown)
|
|
134
|
-
const dom = Object.entries(v.rels).sort((a, b) => b[1] - a[1])[0];
|
|
135
|
-
return { from, to, count: v.count, relation: dom ? dom[0] : null };
|
|
136
|
-
})
|
|
137
|
-
.sort((a, b) => b.count - a.count);
|
|
138
|
-
|
|
139
132
|
// symbol-level (function/method) call graph: edges between symbol nodes (calls/method), each symbol
|
|
140
133
|
// tagged with its file's module so the Symbols view can still cluster into module regions.
|
|
141
134
|
const symEdges = new Map();
|
|
142
135
|
for (const link of links) {
|
|
143
|
-
if (link.relation === "contains") continue;
|
|
136
|
+
if (link.relation === "contains" || link.barrelProxy === true) continue;
|
|
144
137
|
const s = endpoint(link.source);
|
|
145
138
|
const t = endpoint(link.target);
|
|
146
139
|
if (s === t || !symbolIds.has(s) || !symbolIds.has(t)) continue;
|
|
@@ -268,8 +261,12 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
268
261
|
.sort((a, b) => b.fileCount - a.fileCount),
|
|
269
262
|
moduleEdges: edgeList(moduleEdges),
|
|
270
263
|
typeOnlyModuleEdges: edgeList(typeOnlyModuleEdges),
|
|
264
|
+
compileOnlyModuleEdges: edgeList(compileOnlyModuleEdges),
|
|
265
|
+
compileTimeModuleEdges: edgeList(compileTimeModuleEdges),
|
|
271
266
|
fileEdges: edgeList(fileEdges),
|
|
272
267
|
typeOnlyFileEdges: edgeList(typeOnlyFileEdges),
|
|
268
|
+
compileOnlyFileEdges: edgeList(compileOnlyFileEdges),
|
|
269
|
+
compileTimeFileEdges: edgeList(compileTimeFileEdges),
|
|
273
270
|
symbols,
|
|
274
271
|
symbolEdges,
|
|
275
272
|
symbolRefs: [...new Set([...symbolLocalRefs.keys(), ...symbolExternalRefs.keys()])].map((id) => {
|
|
@@ -283,8 +280,12 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
283
280
|
nodes: nodes.filter((n) => n.file_type === "code").length,
|
|
284
281
|
fileEdges: fileEdges.size,
|
|
285
282
|
typeOnlyFileEdges: typeOnlyFileEdges.size,
|
|
283
|
+
compileOnlyFileEdges: compileOnlyFileEdges.size,
|
|
284
|
+
compileTimeFileEdges: compileTimeFileEdges.size,
|
|
286
285
|
moduleEdges: moduleEdges.size,
|
|
287
286
|
typeOnlyModuleEdges: typeOnlyModuleEdges.size,
|
|
287
|
+
compileOnlyModuleEdges: compileOnlyModuleEdges.size,
|
|
288
|
+
compileTimeModuleEdges: compileTimeModuleEdges.size,
|
|
288
289
|
symbols: symbols.length,
|
|
289
290
|
symbolEdges: symbolEdges.length
|
|
290
291
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { communityTerritoryOf } from "../graph/community.js";
|
|
2
|
+
|
|
3
|
+
// Stable folder territories used by module_map. Nested package source roots keep the first directory below
|
|
4
|
+
// `src`; otherwise a monorepo's entire crate/app would collapse into one module with no visible dependencies.
|
|
5
|
+
export function folderModuleOf(file) {
|
|
6
|
+
if (/\.java$/i.test(String(file || ""))) return communityTerritoryOf(file);
|
|
7
|
+
const dirs = String(file || "").split(/[\\/]/).filter(Boolean).slice(0, -1);
|
|
8
|
+
if (!dirs.length) return "(root)";
|
|
9
|
+
const sourceRoot = dirs.lastIndexOf("src");
|
|
10
|
+
if (sourceRoot >= 0 && dirs.length > sourceRoot + 1) return dirs.slice(0, sourceRoot + 2).join("/");
|
|
11
|
+
return dirs.slice(0, 2).join("/");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function edgeList(map) {
|
|
15
|
+
return [...map.entries()]
|
|
16
|
+
.map(([key, value]) => {
|
|
17
|
+
const splitAt = key.indexOf(" ");
|
|
18
|
+
const from = key.slice(0, splitAt), to = key.slice(splitAt + 1);
|
|
19
|
+
if (typeof value === "number") return { from, to, count: value };
|
|
20
|
+
const dominant = Object.entries(value.rels).sort((a, b) => b[1] - a[1])[0];
|
|
21
|
+
return { from, to, count: value.count, relation: dominant ? dominant[0] : null };
|
|
22
|
+
})
|
|
23
|
+
.sort((a, b) => b.count - a.count);
|
|
24
|
+
}
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
// Facade: implementation lives in graph-analysis.summaries.js and graph-analysis.aggregate.js
|
|
5
5
|
// (with internal helpers in graph-analysis.refs.js).
|
|
6
6
|
export { summarizeCommunities, summarizeHotspots } from "./graph-analysis.summaries.js";
|
|
7
|
-
export { analyzeGraph, aggregateGraph } from "./graph-analysis.aggregate.js";
|
|
7
|
+
export { analyzeGraph, aggregateGraph, folderModuleOf } from "./graph-analysis.aggregate.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Reading a built graph.json and summarizing it for the UI cards: named communities and degree
|
|
2
2
|
// hotspots. Both read the graph file from disk and return [] on any failure.
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { communityTerritoryOf } from "../graph/community.js";
|
|
5
|
+
import { isStructuralRelation } from "../graph/relations.js";
|
|
4
6
|
|
|
5
7
|
// graph-builder labels communities "Community N" without an LLM. Derive a real name from each
|
|
6
8
|
// community's dominant folder + sample files so the UI shows modules, not bare numbers.
|
|
@@ -16,7 +18,7 @@ export function summarizeCommunities(graphJsonPath, max = 40) {
|
|
|
16
18
|
const entry = byCommunity.get(community);
|
|
17
19
|
entry.size += 1;
|
|
18
20
|
const parts = String(node.source_file || "").split(/[\\/]/).filter(Boolean);
|
|
19
|
-
const dir =
|
|
21
|
+
const dir = communityTerritoryOf(node.source_file);
|
|
20
22
|
entry.dirs.set(dir, (entry.dirs.get(dir) || 0) + 1);
|
|
21
23
|
if (entry.files.length < 4) entry.files.push(parts[parts.length - 1] || node.source_file || "");
|
|
22
24
|
}
|
|
@@ -41,6 +43,7 @@ export function summarizeHotspots(graphJsonPath, max = 15) {
|
|
|
41
43
|
const inDeg = new Map();
|
|
42
44
|
const outDeg = new Map();
|
|
43
45
|
for (const link of graph.links || []) {
|
|
46
|
+
if (isStructuralRelation(link.relation) || link.barrelProxy === true) continue;
|
|
44
47
|
const s = endpoint(link.source);
|
|
45
48
|
const t = endpoint(link.target);
|
|
46
49
|
outDeg.set(s, (outDeg.get(s) || 0) + 1);
|