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.
- package/README.md +138 -53
- package/SECURITY.md +25 -8
- package/package.json +2 -2
- package/skill/SKILL.md +92 -30
- 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 +5 -3
- package/src/analysis/dead-code-review.js +245 -0
- package/src/analysis/dep-check-ecosystems.js +6 -0
- package/src/analysis/dep-check.js +40 -6
- package/src/analysis/dep-rules.js +12 -9
- package/src/analysis/duplicates.compute.js +47 -7
- package/src/analysis/endpoints-java.js +186 -0
- package/src/analysis/endpoints.js +8 -2
- 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 +2 -2
- package/src/analysis/graph-analysis.edges.js +3 -0
- package/src/analysis/graph-analysis.summaries.js +1 -1
- package/src/analysis/http-contracts.js +581 -0
- package/src/analysis/internal-audit.collect.js +3 -2
- package/src/analysis/internal-audit.reach.js +52 -1
- package/src/analysis/internal-audit.run.js +58 -10
- 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-js.js +71 -12
- package/src/graph/community.js +10 -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 +82 -11
- package/src/graph/internal-builder.langs.js +2 -1
- package/src/graph/layout.js +21 -5
- package/src/graph/repo-registry.js +124 -0
- package/src/mcp/catalog.mjs +62 -19
- 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 +100 -16
- package/src/mcp/graph-diff.mjs +74 -20
- package/src/mcp/staleness-notice.mjs +20 -0
- package/src/mcp/sync-evidence.mjs +418 -0
- package/src/mcp/sync-payload.mjs +164 -0
- package/src/mcp/tool-result.mjs +51 -0
- package/src/mcp/tools-actions.mjs +111 -30
- package/src/mcp/tools-architecture.mjs +144 -0
- package/src/mcp/tools-company.mjs +273 -0
- package/src/mcp/tools-graph.mjs +25 -14
- package/src/mcp/tools-health.mjs +336 -14
- package/src/mcp/tools-history.mjs +22 -0
- package/src/mcp/tools-impact-change.mjs +261 -0
- package/src/mcp/tools-impact.mjs +25 -6
- 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
|
@@ -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);
|
package/src/analysis/findings.js
CHANGED
|
@@ -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([
|
|
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: "",
|