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
@@ -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 with Weavatrix 0.1.3+)`,
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: ${outs.length + ins.length} (out ${outs.length}, in ${ins.length})`,
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.typeOnly === true ? 'type' : 'runtime'}|${e.id}`
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.typeOnly ? 'type-only ' : ''}${e.relation || 'rel'} ${labelOf(g, e.id)} [${e.id}]${e.count > 1 ? ` (${e.count} sites)` : ''}`
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 function tGodNodes(g, {top_n = 10} = {}) {
118
- const n = Math.max(1, Math.min(100, Number(top_n) || 10))
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 seeds = findSeeds(g, question, 6)
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 [s, r, t] of edges) {
250
- if (!shownIds.has(s) || !shownIds.has(t)) continue
251
- const key = `${s}|${r}|${t}`
252
- if (edgeSeen.has(key)) continue
253
- edgeSeen.add(key)
254
- shownEdges.push([s, r, t])
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
  }