weavatrix 0.1.4 → 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 (70) hide show
  1. package/README.md +138 -53
  2. package/SECURITY.md +25 -8
  3. package/package.json +2 -2
  4. package/skill/SKILL.md +92 -30
  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 +5 -3
  10. package/src/analysis/dead-code-review.js +245 -0
  11. package/src/analysis/dep-check-ecosystems.js +6 -0
  12. package/src/analysis/dep-check.js +40 -6
  13. package/src/analysis/dep-rules.js +12 -9
  14. package/src/analysis/duplicates.compute.js +47 -7
  15. package/src/analysis/endpoints-java.js +186 -0
  16. package/src/analysis/endpoints.js +8 -2
  17. package/src/analysis/findings.js +8 -1
  18. package/src/analysis/git-history.js +549 -0
  19. package/src/analysis/git-ref-graph.js +74 -0
  20. package/src/analysis/graph-analysis.aggregate.js +2 -2
  21. package/src/analysis/graph-analysis.edges.js +3 -0
  22. package/src/analysis/graph-analysis.summaries.js +1 -1
  23. package/src/analysis/http-contracts.js +581 -0
  24. package/src/analysis/internal-audit.collect.js +3 -2
  25. package/src/analysis/internal-audit.reach.js +52 -1
  26. package/src/analysis/internal-audit.run.js +58 -10
  27. package/src/analysis/java-source.js +36 -0
  28. package/src/analysis/package-reachability.js +39 -0
  29. package/src/analysis/static-test-reachability.js +133 -0
  30. package/src/build-graph.js +72 -13
  31. package/src/graph/build-worker.js +3 -4
  32. package/src/graph/builder/lang-js.js +71 -12
  33. package/src/graph/community.js +10 -0
  34. package/src/graph/file-lock.js +69 -0
  35. package/src/graph/freshness-probe.js +141 -0
  36. package/src/graph/graph-filter.js +28 -11
  37. package/src/graph/incremental-refresh.js +232 -0
  38. package/src/graph/internal-builder.barrels.js +169 -0
  39. package/src/graph/internal-builder.build.js +82 -11
  40. package/src/graph/internal-builder.langs.js +2 -1
  41. package/src/graph/layout.js +21 -5
  42. package/src/graph/repo-registry.js +124 -0
  43. package/src/mcp/catalog.mjs +62 -19
  44. package/src/mcp/evidence-snapshot.architecture.mjs +80 -0
  45. package/src/mcp/evidence-snapshot.common.mjs +160 -0
  46. package/src/mcp/evidence-snapshot.duplicates.mjs +217 -0
  47. package/src/mcp/evidence-snapshot.health.mjs +95 -0
  48. package/src/mcp/evidence-snapshot.inventory.mjs +148 -0
  49. package/src/mcp/evidence-snapshot.mjs +50 -0
  50. package/src/mcp/evidence-snapshot.package-graph.mjs +249 -0
  51. package/src/mcp/evidence-snapshot.structure.mjs +81 -0
  52. package/src/mcp/graph-context.mjs +100 -16
  53. package/src/mcp/graph-diff.mjs +74 -20
  54. package/src/mcp/staleness-notice.mjs +20 -0
  55. package/src/mcp/sync-evidence.mjs +418 -0
  56. package/src/mcp/sync-payload.mjs +164 -0
  57. package/src/mcp/tool-result.mjs +51 -0
  58. package/src/mcp/tools-actions.mjs +111 -30
  59. package/src/mcp/tools-architecture.mjs +144 -0
  60. package/src/mcp/tools-company.mjs +273 -0
  61. package/src/mcp/tools-graph.mjs +25 -14
  62. package/src/mcp/tools-health.mjs +336 -14
  63. package/src/mcp/tools-history.mjs +22 -0
  64. package/src/mcp/tools-impact-change.mjs +261 -0
  65. package/src/mcp/tools-impact.mjs +25 -6
  66. package/src/mcp-server.mjs +100 -17
  67. package/src/mcp-source-tools.mjs +11 -3
  68. package/src/path-classification.js +201 -0
  69. package/src/path-ignore.js +69 -0
  70. package/src/security/typosquat.js +1 -1
@@ -0,0 +1,186 @@
1
+ // Spring MVC/WebFlux endpoint extraction. This stays source-only and deliberately resolves only
2
+ // literal annotation paths: inventing values for constants would make an architecture inventory look
3
+ // more complete than the evidence permits.
4
+ import { maskJavaNonCode } from "./java-source.js";
5
+
6
+ const SPRING_MAPPING = /@(?:org\.springframework\.web\.bind\.annotation\.)?(RequestMapping|GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping)\b/g;
7
+ const REQUEST_METHOD = /\bRequestMethod\s*\.\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE)\b/g;
8
+
9
+ const lineAt = (text, index) => {
10
+ let line = 1;
11
+ for (let i = 0; i < index && i < text.length; i++) if (text[i] === "\n") line++;
12
+ return line;
13
+ };
14
+
15
+ function skipTrivia(text, start) {
16
+ let i = start;
17
+ while (i < text.length) {
18
+ if (/\s/.test(text[i])) { i++; continue; }
19
+ if (text.startsWith("//", i)) {
20
+ const nl = text.indexOf("\n", i + 2);
21
+ i = nl < 0 ? text.length : nl + 1;
22
+ continue;
23
+ }
24
+ if (text.startsWith("/*", i)) {
25
+ const end = text.indexOf("*/", i + 2);
26
+ i = end < 0 ? text.length : end + 2;
27
+ continue;
28
+ }
29
+ break;
30
+ }
31
+ return i;
32
+ }
33
+
34
+ function balancedEnd(text, start, open = "(", close = ")") {
35
+ if (text[start] !== open) return start;
36
+ let depth = 0;
37
+ let quote = "";
38
+ let escaped = false;
39
+ for (let i = start; i < text.length; i++) {
40
+ const ch = text[i];
41
+ if (quote) {
42
+ if (escaped) escaped = false;
43
+ else if (ch === "\\") escaped = true;
44
+ else if (ch === quote) quote = "";
45
+ continue;
46
+ }
47
+ if (ch === '"' || ch === "'") { quote = ch; continue; }
48
+ if (ch === open) depth++;
49
+ else if (ch === close && --depth === 0) return i + 1;
50
+ }
51
+ return text.length;
52
+ }
53
+
54
+ function annotationInvocation(text, nameEnd) {
55
+ const start = skipTrivia(text, nameEnd);
56
+ if (text[start] !== "(") return { args: "", end: start };
57
+ const end = balancedEnd(text, start);
58
+ return { args: text.slice(start + 1, Math.max(start + 1, end - 1)), end };
59
+ }
60
+
61
+ function skipAnnotations(text, start) {
62
+ let i = skipTrivia(text, start);
63
+ while (text[i] === "@") {
64
+ const name = /^@[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$][\w$]*)*/.exec(text.slice(i));
65
+ if (!name) break;
66
+ i += name[0].length;
67
+ i = annotationInvocation(text, i).end;
68
+ i = skipTrivia(text, i);
69
+ }
70
+ return i;
71
+ }
72
+
73
+ function stringLiterals(value) {
74
+ const out = [];
75
+ const re = /"((?:\\.|[^"\\])*)"/g;
76
+ let match;
77
+ while ((match = re.exec(String(value || "")))) {
78
+ out.push(match[1].replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
79
+ }
80
+ return out;
81
+ }
82
+
83
+ function initializer(args, equalsAt) {
84
+ let i = skipTrivia(args, equalsAt + 1);
85
+ if (args[i] === "{") {
86
+ const end = balancedEnd(args, i, "{", "}");
87
+ return args.slice(i, end);
88
+ }
89
+ if (args[i] === '"') {
90
+ let escaped = false;
91
+ for (let end = i + 1; end < args.length; end++) {
92
+ if (escaped) escaped = false;
93
+ else if (args[end] === "\\") escaped = true;
94
+ else if (args[end] === '"') return args.slice(i, end + 1);
95
+ }
96
+ }
97
+ const comma = args.indexOf(",", i);
98
+ return args.slice(i, comma < 0 ? args.length : comma);
99
+ }
100
+
101
+ function mappingPaths(args) {
102
+ const source = String(args || "");
103
+ const named = /\b(?:path|value)\s*=/g.exec(source);
104
+ if (named) return stringLiterals(initializer(source, source.indexOf("=", named.index)));
105
+ const leading = source.trimStart();
106
+ if (!leading) return [""];
107
+ if (leading[0] === '"' || leading[0] === "{") return stringLiterals(leading);
108
+ // `method=`, `produces=` and friends do not specify a path, so they map the class/method root.
109
+ if (/^(?:method|produces|consumes|headers|params|name)\s*=/.test(leading)) return [""];
110
+ return []; // unresolved positional constant: do not invent a root route
111
+ }
112
+
113
+ function mappingMethods(name, args) {
114
+ if (name !== "RequestMapping") return [name.replace(/Mapping$/, "").toUpperCase()];
115
+ const methods = [];
116
+ let match;
117
+ REQUEST_METHOD.lastIndex = 0;
118
+ while ((match = REQUEST_METHOD.exec(args))) if (!methods.includes(match[1])) methods.push(match[1]);
119
+ return methods.length ? methods : ["ANY"];
120
+ }
121
+
122
+ function joinPaths(prefix, path) {
123
+ const left = String(prefix || "").trim().replace(/^\/+|\/+$/g, "");
124
+ const right = String(path || "").trim().replace(/^\/+|\/+$/g, "");
125
+ return `/${[left, right].filter(Boolean).join("/")}`.replace(/\/{2,}/g, "/");
126
+ }
127
+
128
+ function declarationAfter(text, start) {
129
+ const declarationStart = skipAnnotations(text, start);
130
+ const bounded = text.slice(declarationStart, declarationStart + 2_500);
131
+ const terminators = [bounded.indexOf("{"), bounded.indexOf(";")].filter((i) => i >= 0);
132
+ const end = terminators.length ? Math.min(...terminators) : bounded.length;
133
+ const head = bounded.slice(0, end);
134
+ const classMatch = /\b(class|interface|record|enum)\s+([A-Za-z_$][\w$]*)/.exec(head);
135
+ if (classMatch) {
136
+ const open = bounded.indexOf("{");
137
+ const bodyOpen = open < 0 ? -1 : declarationStart + open;
138
+ return { kind: "class", name: classMatch[2], bodyOpen, bodyClose: bodyOpen < 0 ? text.length : balancedEnd(text, bodyOpen, "{", "}") };
139
+ }
140
+ const methodMatch = /\b([A-Za-z_$][\w$]*)\s*\(/.exec(head);
141
+ return methodMatch ? { kind: "method", name: methodMatch[1] } : { kind: "unknown", name: "" };
142
+ }
143
+
144
+ export function extractSpringEndpoints(text, file) {
145
+ if (!/\.java$/i.test(file)) return [];
146
+ const code = maskJavaNonCode(text);
147
+ if (!/@(?:[\w$]+\.)*(?:Request|Get|Post|Put|Patch|Delete)Mapping\b/.test(code)) return [];
148
+ const mappings = [];
149
+ let match;
150
+ SPRING_MAPPING.lastIndex = 0;
151
+ while ((match = SPRING_MAPPING.exec(code))) {
152
+ const invocation = annotationInvocation(text, SPRING_MAPPING.lastIndex);
153
+ const declaration = declarationAfter(code, invocation.end);
154
+ mappings.push({
155
+ annotation: match[1],
156
+ args: invocation.args,
157
+ index: match.index,
158
+ line: lineAt(text, match.index),
159
+ declaration,
160
+ });
161
+ SPRING_MAPPING.lastIndex = Math.max(SPRING_MAPPING.lastIndex, invocation.end);
162
+ }
163
+
164
+ const classMappings = mappings
165
+ .filter((item) => item.declaration.kind === "class")
166
+ .map((item) => ({ ...item, paths: mappingPaths(item.args) }));
167
+ const out = [];
168
+ for (const item of mappings) {
169
+ if (item.declaration.kind !== "method") continue;
170
+ const owner = classMappings
171
+ .filter((candidate) => candidate.declaration.bodyOpen >= 0
172
+ && item.index > candidate.declaration.bodyOpen
173
+ && item.index < candidate.declaration.bodyClose)
174
+ .sort((a, b) => b.declaration.bodyOpen - a.declaration.bodyOpen)[0];
175
+ if (owner && !owner.paths.length) continue; // unresolved class prefix makes the full route unknowable
176
+ const prefixes = owner?.paths?.length ? owner.paths : [""];
177
+ const paths = mappingPaths(item.args);
178
+ if (!paths.length) continue; // literal path was requested but could not be resolved
179
+ for (const method of mappingMethods(item.annotation, item.args)) {
180
+ for (const prefix of prefixes) for (const path of paths) {
181
+ out.push({ method, path: joinPaths(prefix, path), handler: item.declaration.name, file, line: item.line });
182
+ }
183
+ }
184
+ }
185
+ return out;
186
+ }
@@ -10,6 +10,7 @@
10
10
  import { safeRead } from "../util.js";
11
11
  import { createRepoBoundary } from "../repo-path.js";
12
12
  import { extractRustEndpoints } from "./endpoints-rust.js";
13
+ import { extractSpringEndpoints } from "./endpoints-java.js";
13
14
 
14
15
  const MAX_FILES = 3000;
15
16
  const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "TRACE", "CONNECT", "ALL", "ANY"]);
@@ -73,6 +74,7 @@ export function extractEndpointsFromText(text, file) {
73
74
  const out = [];
74
75
  const py = /\.py$/i.test(file);
75
76
  const rust = /\.rs$/i.test(file);
77
+ const java = /\.java$/i.test(file);
76
78
  const add = (method, path, expr, idx) => {
77
79
  const p = cleanPath(path);
78
80
  if (!looksLikePath(p)) return;
@@ -107,6 +109,10 @@ export function extractEndpointsFromText(text, file) {
107
109
  }
108
110
 
109
111
  if (rust) extractRustEndpoints(text, add);
112
+ if (java) {
113
+ out.push(...extractSpringEndpoints(text, file));
114
+ return out; // generic JS-style method calls would turn Java HTTP clients into fake server routes
115
+ }
110
116
 
111
117
  // ---- object routes: "/path": { GET: fn, POST: fn2 } or "/path": handler --------------------
112
118
  // find each "…": { or "…": expr, where the key looks like a path
@@ -178,11 +184,11 @@ export function detectEndpoints(repoPath, codeFiles) {
178
184
  const boundary = createRepoBoundary(repoPath);
179
185
  for (const f of files) {
180
186
  const rel = f.path || f;
181
- if (!/\.(js|ts|tsx|jsx|cjs|mjs|py|go|rs)$/i.test(rel)) continue;
187
+ if (!/\.(js|ts|tsx|jsx|cjs|mjs|py|go|rs|java)$/i.test(rel)) continue;
182
188
  const resolved = boundary.resolve(rel);
183
189
  if (!resolved.ok) continue;
184
190
  const text = safeRead(resolved.path);
185
- if (!text || (!nextRoutePath(rel) && !/["'`]\/|\.(get|post|put|patch|delete)\s*\(|HandleFunc|@\w*\.?(get|post|put|patch|delete)/i.test(text))) continue;
191
+ if (!text || (!nextRoutePath(rel) && !/["'`]\/|\.(get|post|put|patch|delete)\s*\(|HandleFunc|@\w*\.?(get|post|put|patch|delete)|@(?:[\w$]+\.)*(?:Request|Get|Post|Put|Patch|Delete)Mapping\b/i.test(text))) continue;
186
192
  for (const e of extractEndpointsFromText(text, rel.replace(/\\/g, "/"))) {
187
193
  const key = `${e.method} ${normParamKey(e.path)}`;
188
194
  const prev = byKey.get(key);
@@ -8,8 +8,14 @@ export const FINDING_CATEGORIES = ["unused", "structure", "vulnerability", "malw
8
8
 
9
9
  // Stable id: survives re-runs so the UI can persist expand/dismiss state per finding.
10
10
  export function makeFinding(f) {
11
+ const cycleIdentity = Array.isArray(f.cycleMembers)
12
+ ? [...new Set(f.cycleMembers.map(String))].sort().join("\0")
13
+ : "";
11
14
  const id = createHash("sha1")
12
- .update([f.category, f.rule, f.file || "", f.package || "", f.symbol || "", f.title || ""].join("|"))
15
+ .update([
16
+ f.category, f.rule, f.file || "", f.manifest || "", f.scope || "",
17
+ f.package || "", f.symbol || "", cycleIdentity, f.title || "",
18
+ ].join("|"))
13
19
  .digest("hex")
14
20
  .slice(0, 16);
15
21
  return {
@@ -18,6 +24,7 @@ export function makeFinding(f) {
18
24
  confidence: "medium",
19
25
  title: "",
20
26
  detail: "",
27
+ reason: "",
21
28
  file: "",
22
29
  line: 0,
23
30
  symbol: "",