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.
Files changed (78) hide show
  1. package/README.md +168 -63
  2. package/SECURITY.md +25 -8
  3. package/package.json +2 -2
  4. package/skill/SKILL.md +108 -39
  5. package/src/analysis/architecture-contract.js +343 -0
  6. package/src/analysis/audit-debt.js +94 -0
  7. package/src/analysis/change-classification.js +509 -0
  8. package/src/analysis/cycle-route.js +14 -0
  9. package/src/analysis/dead-check.js +13 -6
  10. package/src/analysis/dead-code-review.js +245 -0
  11. package/src/analysis/dep-check-ecosystems.js +163 -0
  12. package/src/analysis/dep-check.js +69 -147
  13. package/src/analysis/dep-rules.js +47 -31
  14. package/src/analysis/duplicates.compute.js +47 -7
  15. package/src/analysis/endpoints-java.js +186 -0
  16. package/src/analysis/endpoints-rust.js +124 -0
  17. package/src/analysis/endpoints.js +18 -5
  18. package/src/analysis/findings.js +8 -1
  19. package/src/analysis/git-history.js +549 -0
  20. package/src/analysis/git-ref-graph.js +74 -0
  21. package/src/analysis/graph-analysis.aggregate.js +29 -28
  22. package/src/analysis/graph-analysis.edges.js +24 -0
  23. package/src/analysis/graph-analysis.js +1 -1
  24. package/src/analysis/graph-analysis.summaries.js +4 -1
  25. package/src/analysis/http-contracts.js +581 -0
  26. package/src/analysis/internal-audit.collect.js +43 -2
  27. package/src/analysis/internal-audit.reach.js +52 -1
  28. package/src/analysis/internal-audit.run.js +66 -13
  29. package/src/analysis/java-source.js +36 -0
  30. package/src/analysis/package-reachability.js +39 -0
  31. package/src/analysis/static-test-reachability.js +133 -0
  32. package/src/build-graph.js +72 -13
  33. package/src/graph/build-worker.js +3 -4
  34. package/src/graph/builder/lang-java.js +177 -14
  35. package/src/graph/builder/lang-js.js +71 -12
  36. package/src/graph/builder/lang-rust.js +129 -6
  37. package/src/graph/community.js +27 -0
  38. package/src/graph/file-lock.js +69 -0
  39. package/src/graph/freshness-probe.js +141 -0
  40. package/src/graph/graph-filter.js +28 -11
  41. package/src/graph/incremental-refresh.js +232 -0
  42. package/src/graph/internal-builder.barrels.js +169 -0
  43. package/src/graph/internal-builder.build.js +111 -16
  44. package/src/graph/internal-builder.java.js +33 -0
  45. package/src/graph/internal-builder.langs.js +2 -1
  46. package/src/graph/internal-builder.resolvers.js +109 -2
  47. package/src/graph/layout.js +21 -5
  48. package/src/graph/relations.js +4 -0
  49. package/src/graph/repo-registry.js +124 -0
  50. package/src/mcp/catalog.mjs +64 -21
  51. package/src/mcp/evidence-snapshot.architecture.mjs +80 -0
  52. package/src/mcp/evidence-snapshot.common.mjs +160 -0
  53. package/src/mcp/evidence-snapshot.duplicates.mjs +217 -0
  54. package/src/mcp/evidence-snapshot.health.mjs +95 -0
  55. package/src/mcp/evidence-snapshot.inventory.mjs +148 -0
  56. package/src/mcp/evidence-snapshot.mjs +50 -0
  57. package/src/mcp/evidence-snapshot.package-graph.mjs +249 -0
  58. package/src/mcp/evidence-snapshot.structure.mjs +81 -0
  59. package/src/mcp/graph-context.mjs +106 -181
  60. package/src/mcp/graph-diff.mjs +217 -0
  61. package/src/mcp/staleness-notice.mjs +20 -0
  62. package/src/mcp/sync-evidence.mjs +418 -0
  63. package/src/mcp/sync-payload.mjs +194 -8
  64. package/src/mcp/tool-result.mjs +51 -0
  65. package/src/mcp/tools-actions.mjs +114 -33
  66. package/src/mcp/tools-architecture.mjs +144 -0
  67. package/src/mcp/tools-company.mjs +273 -0
  68. package/src/mcp/tools-graph-hubs.mjs +58 -0
  69. package/src/mcp/tools-graph.mjs +36 -68
  70. package/src/mcp/tools-health.mjs +354 -20
  71. package/src/mcp/tools-history.mjs +22 -0
  72. package/src/mcp/tools-impact-change.mjs +261 -0
  73. package/src/mcp/tools-impact.mjs +35 -11
  74. package/src/mcp-server.mjs +100 -17
  75. package/src/mcp-source-tools.mjs +11 -3
  76. package/src/path-classification.js +201 -0
  77. package/src/path-ignore.js +69 -0
  78. 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}/` : "") + folderOf(node.source_file);
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 targetFileEdges = link.typeOnly === true ? typeOnlyFileEdges : fileEdges;
112
- let fe = targetFileEdges.get(key);
113
- if (!fe) targetFileEdges.set(key, (fe = { count: 0, rels: {} }));
114
- fe.count++;
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 = parts.length > 1 ? parts.slice(0, 2).join("/") : "(root)";
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);