weavatrix 0.3.13 → 0.3.14
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/docs/releases/v0.3.14.md +37 -0
- package/package.json +2 -1
- package/src/analysis/coverage-reports-tarpaulin.js +112 -0
- package/src/analysis/coverage-reports.js +13 -2
- package/src/analysis/dead-code-review/policy.js +1 -1
- package/src/analysis/duplicate-groups.js +1 -1
- package/src/analysis/endpoints/common.js +18 -1
- package/src/analysis/endpoints/extract.js +13 -0
- package/src/analysis/graph-analysis.aggregate.js +4 -3
- package/src/analysis/graph-analysis.refs.js +48 -0
- package/src/analysis/hot-path-review.js +1 -1
- package/src/analysis/internal-audit.reach.js +28 -0
- package/src/analysis/internal-audit.run.js +1 -1
- package/src/analysis/static-test-reachability.js +1 -1
- package/src/mcp/architecture-starter.mjs +1 -1
- package/src/mcp/catalog.mjs +5 -5
- package/src/mcp/evidence-snapshot.duplicates.mjs +1 -1
- package/src/mcp/graph/tools-query.mjs +1 -1
- package/src/mcp/health/audit-format.mjs +2 -2
- package/src/mcp/health/audit.mjs +29 -4
- package/src/mcp/health/duplicates.mjs +43 -7
- package/src/mcp/health/endpoints.mjs +1 -1
- package/src/mcp/health/structure.mjs +4 -3
- package/src/mcp/tools-context.mjs +1 -1
- package/src/mcp/tools-endpoints.mjs +1 -1
- package/src/mcp/tools-graph-hubs.mjs +1 -1
- package/src/path-classification.js +7 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Weavatrix 0.3.14
|
|
2
|
+
|
|
3
|
+
0.3.14 is a Repo Lens dogfood hardening release. It keeps the 34-tool,
|
|
4
|
+
network-free Core surface while making large JavaScript graphs practical for
|
|
5
|
+
UI consumers and reducing production-analysis noise.
|
|
6
|
+
|
|
7
|
+
## Structured results and performance
|
|
8
|
+
|
|
9
|
+
- `find_duplicates` and ordinary `run_audit` results now include bounded
|
|
10
|
+
structured groups/findings alongside their existing text summaries, so MCP
|
|
11
|
+
clients can render native views without parsing prose.
|
|
12
|
+
- Local symbol references are indexed once per file and declaration ranges are
|
|
13
|
+
subtracted with binary search instead of rescanning the whole file for every
|
|
14
|
+
symbol. On the 8,117-node Repo Lens graph, aggregation fell from more than
|
|
15
|
+
64 seconds to about 0.93 seconds while preserving graph totals.
|
|
16
|
+
|
|
17
|
+
## Production-first precision
|
|
18
|
+
|
|
19
|
+
- `vendor`, `vendored`, and `third-party` paths are classified explicitly and
|
|
20
|
+
suppressed consistently across duplicates, audit, endpoints, context,
|
|
21
|
+
architecture, dead-code, hot-path, and graph-query surfaces.
|
|
22
|
+
- Electron preload files are treated as entry points only when another source
|
|
23
|
+
contains a bounded `BrowserWindow` `preload:` option.
|
|
24
|
+
- Endpoint extraction masks JavaScript regex literals and comments correctly
|
|
25
|
+
and recognizes bounded `node:http` method plus `url.pathname` guards. Repo
|
|
26
|
+
Lens now reports its four production control routes without comment/test
|
|
27
|
+
false positives in the production view.
|
|
28
|
+
|
|
29
|
+
## Coverage interoperability
|
|
30
|
+
|
|
31
|
+
- `coverage_map` reads Cargo Tarpaulin's official JSON report shape, including
|
|
32
|
+
component paths and `traces[].stats.Line`, and advertises both Tarpaulin JSON
|
|
33
|
+
and `cargo llvm-cov` LCOV workflows for Rust repositories.
|
|
34
|
+
|
|
35
|
+
This Core release is paired with `weavatrix-refactor` 0.1.2 and
|
|
36
|
+
`weavatrix-online` 0.2.0. Each layer keeps its own package, license, version,
|
|
37
|
+
and release provenance.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weavatrix",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local repository intelligence MCP for AI coding agents: an always-fresh architecture graph with production-first evidence — blast radius, dead code, endpoints, clones, history, Health and architecture safeguards, with honestly labeled precision.",
|
|
6
6
|
"author": "Sergii Ziborov <sergii.ziborov@gmail.com>",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"docs/releases/v0.3.11.md",
|
|
49
49
|
"docs/releases/v0.3.12.md",
|
|
50
50
|
"docs/releases/v0.3.13.md",
|
|
51
|
+
"docs/releases/v0.3.14.md",
|
|
51
52
|
"docs/releases/v0.2.19.md",
|
|
52
53
|
"README.md",
|
|
53
54
|
"SECURITY.md",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
function pct01(value) {
|
|
4
|
+
const n = Number(value);
|
|
5
|
+
if (!Number.isFinite(n)) return null;
|
|
6
|
+
return Math.max(0, Math.min(1, n > 1 ? n / 100 : n));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function pctFromCounts(covered, total) {
|
|
10
|
+
const c = Number(covered);
|
|
11
|
+
const t = Number(total);
|
|
12
|
+
if (!Number.isFinite(c) || !Number.isFinite(t) || t <= 0) return null;
|
|
13
|
+
return Math.max(0, Math.min(1, c / t));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function addLineHit(lines, line, hit) {
|
|
17
|
+
const value = Number(line);
|
|
18
|
+
if (!Number.isFinite(value) || value <= 0) return;
|
|
19
|
+
const n = Math.round(value);
|
|
20
|
+
if (n < 1) return;
|
|
21
|
+
const current = lines.get(n);
|
|
22
|
+
lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function numericLineList(value) {
|
|
26
|
+
const input = Array.isArray(value) ? value : [];
|
|
27
|
+
return input
|
|
28
|
+
.map((line) => Number(line))
|
|
29
|
+
.filter((line) => Number.isFinite(line) && line > 0)
|
|
30
|
+
.map((line) => Math.round(line));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function legacyLineGroups(record) {
|
|
34
|
+
if (!record || typeof record !== "object") return { covered: [], uncovered: [] };
|
|
35
|
+
const lines = record.lines && typeof record.lines === "object" ? record.lines : {};
|
|
36
|
+
return {
|
|
37
|
+
covered: [
|
|
38
|
+
...numericLineList(record.covered),
|
|
39
|
+
...numericLineList(record.covered_lines),
|
|
40
|
+
...numericLineList(record.coveredLines),
|
|
41
|
+
...numericLineList(lines.covered),
|
|
42
|
+
...numericLineList(lines.covered_lines),
|
|
43
|
+
],
|
|
44
|
+
uncovered: [
|
|
45
|
+
...numericLineList(record.uncovered),
|
|
46
|
+
...numericLineList(record.uncovered_lines),
|
|
47
|
+
...numericLineList(record.uncoveredLines),
|
|
48
|
+
...numericLineList(record.missing),
|
|
49
|
+
...numericLineList(record.missing_lines),
|
|
50
|
+
...numericLineList(lines.uncovered),
|
|
51
|
+
...numericLineList(lines.uncovered_lines),
|
|
52
|
+
...numericLineList(lines.missing),
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recordPath(record) {
|
|
58
|
+
const raw = record?.path || record?.file || record?.filename || record?.name;
|
|
59
|
+
if (Array.isArray(raw)) return raw.map(String).filter(Boolean).join("/");
|
|
60
|
+
return raw;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reportEntries(json) {
|
|
64
|
+
const files = json?.files || json?.coverage || json;
|
|
65
|
+
if (Array.isArray(files)) {
|
|
66
|
+
return files.map((record) => [recordPath(record), record]).filter(([raw]) => raw);
|
|
67
|
+
}
|
|
68
|
+
if (files && typeof files === "object") return Object.entries(files);
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function coveragePct(record, covered, total) {
|
|
73
|
+
const summary = record?.summary && typeof record.summary === "object" ? record.summary : {};
|
|
74
|
+
return pct01(
|
|
75
|
+
record?.coverage
|
|
76
|
+
?? record?.coverage_percentage
|
|
77
|
+
?? record?.line_coverage
|
|
78
|
+
?? summary.coverage
|
|
79
|
+
?? summary.coverage_percentage
|
|
80
|
+
?? summary.percent
|
|
81
|
+
)
|
|
82
|
+
?? pctFromCounts(record?.covered, record?.coverable)
|
|
83
|
+
?? pctFromCounts(covered, total);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readTarpaulinCoverage(filePath) {
|
|
87
|
+
const json = JSON.parse(readFileSync(filePath, "utf8"));
|
|
88
|
+
return reportEntries(json).map(([path, record]) => {
|
|
89
|
+
const lines = new Map();
|
|
90
|
+
for (const trace of Array.isArray(record?.traces) ? record.traces : []) {
|
|
91
|
+
if (!trace?.stats || typeof trace.stats !== "object" || !("Line" in trace.stats)) continue;
|
|
92
|
+
addLineHit(lines, trace.line, trace.stats.Line);
|
|
93
|
+
}
|
|
94
|
+
const legacy = legacyLineGroups(record);
|
|
95
|
+
for (const line of legacy.uncovered) addLineHit(lines, line, 0);
|
|
96
|
+
for (const line of legacy.covered) addLineHit(lines, line, 1);
|
|
97
|
+
const measuredTotal = lines.size;
|
|
98
|
+
const measuredCovered = [...lines.values()].filter((hit) => Number(hit) > 0).length;
|
|
99
|
+
const reportedTotal = Number(record?.coverable);
|
|
100
|
+
const reportedCovered = Number(record?.covered);
|
|
101
|
+
return {
|
|
102
|
+
path,
|
|
103
|
+
entry: {
|
|
104
|
+
pct: coveragePct(record, measuredCovered, measuredTotal),
|
|
105
|
+
total: Number.isFinite(reportedTotal) ? reportedTotal : measuredTotal,
|
|
106
|
+
covered: Number.isFinite(reportedCovered) ? reportedCovered : measuredCovered,
|
|
107
|
+
lines,
|
|
108
|
+
source: "tarpaulin-report.json",
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join, relative, resolve } from "node:path";
|
|
6
6
|
import { createRepoBoundary } from "../repo-path.js";
|
|
7
|
+
import { readTarpaulinCoverage } from "./coverage-reports-tarpaulin.js";
|
|
7
8
|
|
|
8
9
|
export function normRepoPath(value) {
|
|
9
10
|
return String(value || "").replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -84,8 +85,10 @@ function mergeCoverageEntry(map, rawPath, entry, knownFiles, repoRoot) {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
function addLineHit(lines, line, hit) {
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
88
|
+
const value = Number(line);
|
|
89
|
+
if (!Number.isFinite(value) || value <= 0) return;
|
|
90
|
+
const n = Math.round(value);
|
|
91
|
+
if (n < 1) return;
|
|
89
92
|
const current = lines.get(n);
|
|
90
93
|
lines.set(n, Math.max(Number(current) || 0, Number(hit) || 0));
|
|
91
94
|
}
|
|
@@ -208,6 +211,12 @@ function parseGoCoverage(map, filePath, knownFiles, repoRoot) {
|
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
|
|
214
|
+
function parseTarpaulinJson(map, filePath, knownFiles, repoRoot) {
|
|
215
|
+
for (const record of readTarpaulinCoverage(filePath)) {
|
|
216
|
+
mergeCoverageEntry(map, record.path, record.entry, knownFiles, repoRoot);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
211
220
|
export function readCoverageForRepo(repoRoot, knownFiles) {
|
|
212
221
|
const out = new Map();
|
|
213
222
|
if (!repoRoot) return out;
|
|
@@ -219,6 +228,8 @@ export function readCoverageForRepo(repoRoot, knownFiles) {
|
|
|
219
228
|
["lcov.info", parseLcov],
|
|
220
229
|
["coverage/coverage.json", parseCoveragePyJson],
|
|
221
230
|
["coverage.json", parseCoveragePyJson],
|
|
231
|
+
["tarpaulin-report.json", parseTarpaulinJson],
|
|
232
|
+
["coverage/tarpaulin-report.json", parseTarpaulinJson],
|
|
222
233
|
["coverage.out", parseGoCoverage],
|
|
223
234
|
["cover.out", parseGoCoverage]
|
|
224
235
|
];
|
|
@@ -12,7 +12,7 @@ export function hasDynamicCode(source, file = '') {
|
|
|
12
12
|
return DYNAMIC_CODE_RE.test(text) || (JS_LIKE_PATH_RE.test(String(file)) && JS_DYNAMIC_IMPORT_RE.test(text))
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const NON_PRODUCT_CLASSES = Object.freeze(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
15
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
16
16
|
|
|
17
17
|
export function deadCodePathAllowed(info, {includeTests, includeClassified}) {
|
|
18
18
|
if (!includeTests && hasPathClass(info, 'test', 'e2e')) return {ok: false, bucket: 'tests'}
|
|
@@ -6,7 +6,7 @@ import {withGitRefCheckout} from './git-ref-graph.js'
|
|
|
6
6
|
import {buildInternalGraph} from '../graph/internal-builder.js'
|
|
7
7
|
import {filterGraphForMode} from '../graph/graph-filter.js'
|
|
8
8
|
|
|
9
|
-
const NON_PRODUCT = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
9
|
+
const NON_PRODUCT = new Set(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
10
10
|
const eligible = (fragment, settings) => {
|
|
11
11
|
const classes = new Set(fragment.classes || [])
|
|
12
12
|
if (fragment.n < settings.tokMin) return false
|
|
@@ -11,7 +11,15 @@ export function lineAt(text, index) {
|
|
|
11
11
|
// Preserve offsets and literals while hiding comments from regex extractors.
|
|
12
12
|
export function maskComments(text, { hashComments = false } = {}) {
|
|
13
13
|
const chars = String(text || "").split("");
|
|
14
|
-
let quote = "", escaped = false, lineComment = false, blockComment = false;
|
|
14
|
+
let quote = "", escaped = false, lineComment = false, blockComment = false, regex = false, regexClass = false;
|
|
15
|
+
const startsRegex = (index) => {
|
|
16
|
+
let cursor = index - 1;
|
|
17
|
+
while (cursor >= 0 && /\s/.test(chars[cursor])) cursor--;
|
|
18
|
+
if (cursor < 0) return true;
|
|
19
|
+
if (/[=(:,!&|?{}[\];]/.test(chars[cursor])) return true;
|
|
20
|
+
const prefix = chars.slice(Math.max(0, cursor - 12), cursor + 1).join("");
|
|
21
|
+
return /(?:^|\b)(?:return|throw|case|yield|await)\s*$/i.test(prefix) || /=>\s*$/.test(prefix);
|
|
22
|
+
};
|
|
15
23
|
for (let i = 0; i < chars.length; i++) {
|
|
16
24
|
const ch = chars[i], next = chars[i + 1];
|
|
17
25
|
if (lineComment) {
|
|
@@ -24,6 +32,14 @@ export function maskComments(text, { hashComments = false } = {}) {
|
|
|
24
32
|
else if (ch !== "\n" && ch !== "\r") chars[i] = " ";
|
|
25
33
|
continue;
|
|
26
34
|
}
|
|
35
|
+
if (regex) {
|
|
36
|
+
if (escaped) escaped = false;
|
|
37
|
+
else if (ch === "\\") escaped = true;
|
|
38
|
+
else if (ch === "[") regexClass = true;
|
|
39
|
+
else if (ch === "]") regexClass = false;
|
|
40
|
+
else if (ch === "/" && !regexClass) regex = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
27
43
|
if (quote) {
|
|
28
44
|
if (escaped) escaped = false;
|
|
29
45
|
else if (ch === "\\") escaped = true;
|
|
@@ -33,6 +49,7 @@ export function maskComments(text, { hashComments = false } = {}) {
|
|
|
33
49
|
if (ch === '"' || ch === "'" || ch === "`") { quote = ch; continue; }
|
|
34
50
|
if (ch === "/" && next === "/") { chars[i] = chars[i + 1] = " "; i++; lineComment = true; continue; }
|
|
35
51
|
if (ch === "/" && next === "*") { chars[i] = chars[i + 1] = " "; i++; blockComment = true; continue; }
|
|
52
|
+
if (!hashComments && ch === "/" && startsRegex(i)) { regex = true; regexClass = false; escaped = false; continue; }
|
|
36
53
|
if (hashComments && ch === "#") { chars[i] = " "; lineComment = true; }
|
|
37
54
|
}
|
|
38
55
|
return chars.join("");
|
|
@@ -92,6 +92,19 @@ export function extractEndpointsFromText(text, file) {
|
|
|
92
92
|
if (!HTTP_CLIENT_CALLER.test(match[1]) && handler && handler[0] !== "{") add(match[2], match[4], match[5], match.index);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// Bare node:http servers commonly dispatch through request-method and URL-pathname guards rather
|
|
96
|
+
// than a router API. Keep the extraction bounded to files that actually construct an HTTP server;
|
|
97
|
+
// test assertions and arbitrary `object.method` comparisons are not enough to create endpoints.
|
|
98
|
+
if (!py && /\bcreateServer\s*\(|\b(?:node:)?https?\b/.test(scanText)) {
|
|
99
|
+
const guardedRoute = /\bif\s*\(([\s\S]{0,600}?)\)\s*(?:\{|return\b)/g;
|
|
100
|
+
while ((match = guardedRoute.exec(scanText))) {
|
|
101
|
+
const condition = match[1];
|
|
102
|
+
const methodMatch = /\b[A-Za-z_$][\w$]*\.method\s*={2,3}\s*(["'`])(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|TRACE|CONNECT)\1/i.exec(condition);
|
|
103
|
+
const pathMatch = /\b[A-Za-z_$][\w$]*\.pathname\s*={2,3}\s*(["'`])(\/[^"'`]*)\1/i.exec(condition);
|
|
104
|
+
if (methodMatch && pathMatch) add(methodMatch[2], pathMatch[2], "", match.index);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
95
108
|
const go = /\.\s*(?:HandleFunc|Handle)\s*\(\s*(["'`])(\/[^"'`]*)\1\s*,\s*([\s\S]{0,120}?)\)/g;
|
|
96
109
|
while ((match = go.exec(scanText))) add("ANY", match[2], match[3], match.index);
|
|
97
110
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { normalizeRepoParts, readCoverageForRepo, pctFromCounts } from "./coverage-reports.js";
|
|
7
|
-
import { bareSymbolName,
|
|
7
|
+
import { bareSymbolName, computeLocalSymbolRefs, computeSymbolExternalRefs } from "./graph-analysis.refs.js";
|
|
8
8
|
import { createRepoBoundary } from "../repo-path.js";
|
|
9
9
|
import { edgeList, folderModuleOf } from "./graph-analysis.edges.js";
|
|
10
10
|
export { folderModuleOf } from "./graph-analysis.edges.js";
|
|
@@ -223,6 +223,7 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
223
223
|
.map((s) => ({ id: s.id, label: s.label, start: parseInt(s.line, 10), end: parseInt(s.endLine, 10), ref: s }))
|
|
224
224
|
.filter((s) => Number.isFinite(s.start) && s.start > 0)
|
|
225
225
|
.sort((a, b) => a.start - b.start);
|
|
226
|
+
const ranges = [];
|
|
226
227
|
for (let i = 0; i < sorted.length; i++) {
|
|
227
228
|
const start = sorted[i].start;
|
|
228
229
|
const next = i + 1 < sorted.length ? sorted[i + 1].start : total > start ? total + 1 : start + 1;
|
|
@@ -234,9 +235,9 @@ export function aggregateGraph(graph, repoRoot) {
|
|
|
234
235
|
sorted[i].ref.endLine = end;
|
|
235
236
|
const cov = coverageForRange(fid, start, end);
|
|
236
237
|
if (cov != null) symbolCoverage.set(sorted[i].id, cov);
|
|
237
|
-
|
|
238
|
-
if (refs > 0) symbolLocalRefs.set(sorted[i].id, refs);
|
|
238
|
+
ranges.push({id: sorted[i].id, name: bareSymbolName(sorted[i].label), startLine: start, endLine: end});
|
|
239
239
|
}
|
|
240
|
+
for (const [id, refs] of computeLocalSymbolRefs(txt, ranges)) symbolLocalRefs.set(id, refs);
|
|
240
241
|
}
|
|
241
242
|
for (const s of symbols) {
|
|
242
243
|
const fid = id2file.get(s.id);
|
|
@@ -41,6 +41,54 @@ export function countLocalRefsOutsideOwnRange(text, name, startLine, endLine) {
|
|
|
41
41
|
return refs;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
const lowerBound = (values, target) => {
|
|
45
|
+
let low = 0, high = values.length;
|
|
46
|
+
while (low < high) {
|
|
47
|
+
const middle = (low + high) >>> 1;
|
|
48
|
+
if (values[middle] < target) low = middle + 1;
|
|
49
|
+
else high = middle;
|
|
50
|
+
}
|
|
51
|
+
return low;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Count every wanted identifier in a file once, then subtract occurrences inside each declaration
|
|
55
|
+
// with binary searches over occurrence line numbers. The former per-symbol full-file scan became
|
|
56
|
+
// quadratic on large vendored files (thousands of symbols over tens of thousands of lines).
|
|
57
|
+
export function computeLocalSymbolRefs(text, symbols) {
|
|
58
|
+
const candidates = (symbols || []).map((symbol) => ({
|
|
59
|
+
id: symbol.id,
|
|
60
|
+
name: bareSymbolName(symbol.name ?? symbol.label),
|
|
61
|
+
start: Number(symbol.startLine ?? symbol.start),
|
|
62
|
+
end: Number(symbol.endLine ?? symbol.end),
|
|
63
|
+
})).filter((symbol) => symbol.id && isIdentifierName(symbol.name));
|
|
64
|
+
if (!text || !candidates.length) return new Map();
|
|
65
|
+
const wanted = new Set(candidates.map((symbol) => symbol.name));
|
|
66
|
+
const occurrenceLines = new Map();
|
|
67
|
+
const identifier = /[A-Za-z_$][\w$]*/g;
|
|
68
|
+
const lines = String(text).split(/\r?\n/);
|
|
69
|
+
for (let index = 0; index < lines.length; index++) {
|
|
70
|
+
identifier.lastIndex = 0;
|
|
71
|
+
let match;
|
|
72
|
+
while ((match = identifier.exec(lines[index]))) {
|
|
73
|
+
const name = match[0];
|
|
74
|
+
if (!wanted.has(name)) continue;
|
|
75
|
+
const positions = occurrenceLines.get(name) || [];
|
|
76
|
+
positions.push(index + 1);
|
|
77
|
+
occurrenceLines.set(name, positions);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const refs = new Map();
|
|
81
|
+
for (const symbol of candidates) {
|
|
82
|
+
const positions = occurrenceLines.get(symbol.name) || [];
|
|
83
|
+
const start = Number.isFinite(symbol.start) && symbol.start > 0 ? symbol.start : 0;
|
|
84
|
+
const end = Number.isFinite(symbol.end) && symbol.end >= start ? symbol.end : start;
|
|
85
|
+
const inside = start ? lowerBound(positions, end + 1) - lowerBound(positions, start) : 0;
|
|
86
|
+
const outside = Math.max(0, positions.length - inside);
|
|
87
|
+
if (outside > 0) refs.set(symbol.id, outside);
|
|
88
|
+
}
|
|
89
|
+
return refs;
|
|
90
|
+
}
|
|
91
|
+
|
|
44
92
|
function importCandidates(fromFile, spec) {
|
|
45
93
|
const raw = String(spec || "");
|
|
46
94
|
if (!raw.startsWith(".")) return [];
|
|
@@ -7,7 +7,7 @@ import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
|
7
7
|
import {isStructuralRelation} from '../graph/relations.js'
|
|
8
8
|
import {boundedInteger} from '../util.js'
|
|
9
9
|
|
|
10
|
-
const NON_PRODUCT = ['generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
10
|
+
const NON_PRODUCT = ['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
11
11
|
const endpoint = (value) => String(value && typeof value === 'object' ? value.id : value || '')
|
|
12
12
|
const normalize = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
|
|
13
13
|
const boundedInt = boundedInteger
|
|
@@ -53,6 +53,30 @@ export function springConventionEntries(sources, fileSet = null) {
|
|
|
53
53
|
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Electron loads preload scripts from BrowserWindow.webPreferences rather than through a normal
|
|
57
|
+
// import edge. Require both a conventional preload filename and a bounded `preload:` option that
|
|
58
|
+
// names that exact file, so an unrelated helper called preload.js is not silently blessed.
|
|
59
|
+
export function electronPreloadEntries(sources, fileSet = null) {
|
|
60
|
+
const files = fileSet || new Set((sources || new Map()).keys());
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const rawFile of files) {
|
|
63
|
+
const file = String(rawFile || "").replace(/\\/g, "/");
|
|
64
|
+
if (!/(^|\/)preload\.[cm]?[jt]s$/i.test(file)) continue;
|
|
65
|
+
const basename = posix.basename(file).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
66
|
+
const marker = new RegExp(`\\bpreload\\s*:\\s*[\\s\\S]{0,240}?["'\\x60]${basename}["'\\x60]`, "i");
|
|
67
|
+
const owner = [...(sources || [])].find(([other, text]) => String(other).replace(/\\/g, "/") !== file && marker.test(String(text || "")));
|
|
68
|
+
if (!owner) continue;
|
|
69
|
+
out.push({
|
|
70
|
+
file,
|
|
71
|
+
framework: "electron",
|
|
72
|
+
marker: `BrowserWindow preload option in ${String(owner[0]).replace(/\\/g, "/")}`,
|
|
73
|
+
confidence: "high",
|
|
74
|
+
reason: "Electron loads this exact preload script outside the static import graph",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
78
|
+
}
|
|
79
|
+
|
|
56
80
|
// Entry set for reachability: conventional entry names + package.json main/module/browser/bin/exports +
|
|
57
81
|
// html pages (they root classic-script apps) + test files (the runner enters them) + root config files +
|
|
58
82
|
// dynamic-import targets. Anything reachable from here is "used"; the rest corroborates unused-file.
|
|
@@ -136,6 +160,10 @@ export function entryFiles(graph, pkgOrScopes, dynamicTargets = new Set(), { dec
|
|
|
136
160
|
entries.add(evidence.file);
|
|
137
161
|
conventionEvidence.push(evidence);
|
|
138
162
|
}
|
|
163
|
+
for (const evidence of electronPreloadEntries(sources, fileSet)) {
|
|
164
|
+
entries.add(evidence.file);
|
|
165
|
+
conventionEvidence.push(evidence);
|
|
166
|
+
}
|
|
139
167
|
for (const raw of Array.isArray(declaredEntries) ? declaredEntries : [declaredEntries]) {
|
|
140
168
|
const resolved = resolveEntry("", raw);
|
|
141
169
|
if (resolved && fileSet.has(resolved)) entries.add(resolved);
|
|
@@ -61,7 +61,7 @@ export async function runInternalAudit(repoPath, {
|
|
|
61
61
|
};
|
|
62
62
|
const isNonProductPath = (file) => {
|
|
63
63
|
const info = classifyPath(file);
|
|
64
|
-
return info.excluded || hasPathClass(info, "test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp");
|
|
64
|
+
return info.excluded || hasPathClass(info, "test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp");
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
const conventionEvidence = [];
|
|
@@ -47,7 +47,7 @@ export function computeStaticTestReachability(graph, {
|
|
|
47
47
|
const isTest = (file) => hasPathClass(classify(file), "test", "e2e");
|
|
48
48
|
const isProduct = (file) => {
|
|
49
49
|
const info = classify(file);
|
|
50
|
-
return !info.excluded && !hasPathClass(info, "test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp");
|
|
50
|
+
return !info.excluded && !hasPathClass(info, "test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp");
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
const idToFile = new Map();
|
|
@@ -30,7 +30,7 @@ const CODE_EXTENSIONS = new Set([
|
|
|
30
30
|
'.cjs', '.cs', '.css', '.go', '.htm', '.html', '.java', '.js', '.jsx', '.less', '.mjs', '.py', '.pyi',
|
|
31
31
|
'.rs', '.scss', '.sol', '.sql', '.ts', '.tsx',
|
|
32
32
|
])
|
|
33
|
-
const CLASSIFIED = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
33
|
+
const CLASSIFIED = ['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
34
34
|
const EDGE_RELATIONS = new Set(['imports', 're_exports', 'calls', 'references'])
|
|
35
35
|
|
|
36
36
|
const slash = (value) => String(value || '').replace(/\\/g, '/').replace(/^\.\//, '')
|
package/src/mcp/catalog.mjs
CHANGED
|
@@ -124,12 +124,12 @@ function buildTools({tg, ti, th, ts, tb, te, ta, tar, thi, tc, tv, caps}) {
|
|
|
124
124
|
{cap: 'search', name: 'search_code', description: 'Full-text or regex search across the repo source (ripgrep-backed, Node fallback). The graph only stores structure — use this to find literal text/patterns, then get_node/get_neighbors for structure.', inputSchema: {type: 'object', properties: {query: {type: 'string', description: 'text or regex to search for'}, is_regex: {type: 'boolean', default: false}, glob: {type: 'string', description: 'optional path glob, e.g. "*.js" or "src/**"'}, max_results: {type: 'integer', default: 40}}, required: ['query']}, run: (g, a, ctx) => searchCode({repoRoot: ctx.repoRoot, resolveRg}, a)},
|
|
125
125
|
{cap: 'source', name: 'read_source', description: "Read the actual source of a node (by label/ID) or a repo-relative file path — the symbol's lines with context. The graph stores only locations, not source text. For a path read, pass start_line to anchor the window anywhere in the file (otherwise it shows the head).", inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'node label or ID'}, path: {type: 'string', description: 'or a repo-relative file path'}, start_line: {type: 'integer', description: 'anchor line: window = start_line-before .. start_line+after'}, before: {type: 'integer', default: 3}, after: {type: 'integer', default: 40}}}, run: (g, a, ctx) => readSource({repoRoot: ctx.repoRoot, resolveNode, isSymbol}, g, a)},
|
|
126
126
|
{cap: 'source', refreshGraph: true, name: 'inspect_symbol', description: 'Inspect one exact symbol with an on-demand TypeScript/JavaScript LSP reference query, grouped occurrence containers, graph blast radius, complexity facts and bounded local source context. Ambiguous labels fail closed; point queries never replace the broad precision overlay.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_containers: {type: 'integer', minimum: 1, maximum: 50, default: 15}, context_lines: {type: 'integer', minimum: 0, maximum: 40, default: 8}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => ts.tInspectSymbol(g, a, ctx)},
|
|
127
|
-
{cap: 'source', refreshGraph: true, name: 'context_bundle', description: 'Return one compact, bounded source bundle for an exact symbol: definition, production-first inbound/outbound containers, exact re-export sites, on-demand TS/JS reference evidence and diverse excerpts around call sites. Use before an edit when query_graph would be too broad.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_related: {type: 'integer', minimum: 1, maximum: 30, default: 10}, max_reexports: {type: 'integer', minimum: 1, maximum: 100, default: 20}, max_source_files: {type: 'integer', minimum: 1, maximum: 8, default: 4}, context_lines: {type: 'integer', minimum: 0, maximum: 12, default: 4}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp callers after production callers'}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => tb.tContextBundle(g, a, ctx, ts.tInspectSymbol)},
|
|
128
|
-
{cap: 'health', name: 'find_duplicates', description: "Content-based clone detection over production code (MOSS winnowing over method bodies). Supports high-confidence small clones down to 12 tokens when min_tokens is lowered. Tests, classified non-product paths, all-router framework boilerplate and immutable declarative catalogs are excluded by default; opt them in explicitly.", inputSchema: {type: 'object', properties: {min_similarity: {type: 'integer', description: '50-100, default 80 (ignored in semantic mode)'}, min_tokens: {type: 'integer', minimum: 12, maximum: 400, description: 'min fragment size, 12-400; default 50'}, mode: {type: 'string', enum: ['renamed', 'strict', 'semantic'], default: 'renamed'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, include_boilerplate: {type: 'boolean', default: false, description: 'Include clone groups made entirely of conventional *.router.js/ts router symbols'}, include_declarative: {type: 'boolean', default: false, description: 'Include repeated immutable array/object catalogs that contain no executable control flow'}, include_strings: {type: 'boolean', description: 'Also clone-check large multi-line string literals', default: false}, top_n: {type: 'integer', default: 15}}}, run: (g, a, ctx) => th.tFindDuplicates(g, a, ctx)},
|
|
129
|
-
{cap: 'health', name: 'find_dead_code', description: 'Conservative review queue for statically unreferenced files, functions, methods and symbols. Returns confidence, reason, bounded evidence and explicit framework/dynamic/reflection/public-API caveats; never an auto-delete verdict. Tests, generated code, mocks, stories, docs, benchmarks and temporary roots are excluded by default.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repo-relative path prefix'}, kinds: {type: 'array', items: {type: 'string', enum: ['file', 'function', 'method', 'symbol']}, maxItems: 4, uniqueItems: true, description: 'Optional candidate kinds; defaults to all'}, min_confidence: {type: 'string', enum: ['high', 'medium', 'low'], default: 'medium', description: 'Minimum confidence to include. low explicitly includes public/framework/dynamic review candidates'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 30}}}, run: (g, a, ctx) => th.tFindDeadCode(g, a, ctx)},
|
|
130
|
-
{cap: 'health', name: 'run_audit', description: 'Core production-first repository Health review with an explicit capability/completeness matrix for structure, dependencies, bounded runtime-correctness/concurrency patterns, advisories, malware and coverage. Unsupported Maven/Gradle import verification is NOT_SUPPORTED/PARTIAL, never a clean zero. Findings whose evidence is entirely test/e2e/generated/mock/story/docs/benchmark/temp or explicitly excluded are suppressed by default; opt them in with include_classified. category=dependencies is a dedicated dependency-health projection across missing, unused and duplicate declarations while preserving each finding\'s native category. With base_ref, builds and audits an immutable Git checkout and compares stable deterministic finding IDs; debt defaults to genuinely new findings. Supply-chain checks remain explicitly uncomparable across the source-only baseline. changed_files without base_ref is only changed-scope, never a new-debt claim.', inputSchema: {type: 'object', properties: {category: {type: 'string', enum: ['dependencies', 'unused', 'structure', 'vulnerability', 'malware'], description: 'Only findings of this category; dependencies selects dependency manifest/import findings across native categories'}, min_severity: {type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Minimum severity to include'}, max_findings: {type: 'integer', description: 'Max findings to list, default 30'}, include_classified: {type: 'boolean', default: false, description: 'Include findings whose evidence is entirely tests/e2e/generated/mocks/stories/docs/benchmarks/temp or explicitly excluded'}, include_malware_scan: {type: 'boolean', description: 'Also grep installed packages for malware heuristics (slow)', default: false}, base_ref: {type: 'string', maxLength: 200, description: 'Optional immutable Git baseline (for example HEAD~1 or origin/main). Enables honest new/existing/fixed debt comparison'}, changed_files: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 500, description: 'Optional explicit repo-relative scope. Without base_ref this is changed-scope only; when omitted with base_ref, files are derived from the Git diff'}, debt: {type: 'string', enum: ['new', 'existing', 'all'], default: 'new', description: 'Baseline comparison view. Defaults to genuinely new deterministic findings when base_ref is present'}}}, run: (g, a, ctx) => th.tRunAudit(g, a, ctx)},
|
|
127
|
+
{cap: 'source', refreshGraph: true, name: 'context_bundle', description: 'Return one compact, bounded source bundle for an exact symbol: definition, production-first inbound/outbound containers, exact re-export sites, on-demand TS/JS reference evidence and diverse excerpts around call sites. Use before an edit when query_graph would be too broad.', inputSchema: {type: 'object', properties: {label: {type: 'string', description: 'Exact node ID or unambiguous symbol label'}, precision: {type: 'string', enum: ['auto', 'graph', 'lsp'], default: 'auto'}, max_references: {type: 'integer', minimum: 1, maximum: 5000, default: 1000}, max_related: {type: 'integer', minimum: 1, maximum: 30, default: 10}, max_reexports: {type: 'integer', minimum: 1, maximum: 100, default: 20}, max_source_files: {type: 'integer', minimum: 1, maximum: 8, default: 4}, context_lines: {type: 'integer', minimum: 0, maximum: 12, default: 4}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/vendored/mock/story/docs/benchmark/temp callers after production callers'}, timeout_ms: {type: 'integer', minimum: 1000, maximum: 60000, default: 30000}}, required: ['label']}, run: (g, a, ctx) => tb.tContextBundle(g, a, ctx, ts.tInspectSymbol)},
|
|
128
|
+
{cap: 'health', name: 'find_duplicates', description: "Content-based clone detection over production code (MOSS winnowing over method bodies). Supports high-confidence small clones down to 12 tokens when min_tokens is lowered. Tests, classified non-product paths, all-router framework boilerplate and immutable declarative catalogs are excluded by default; opt them in explicitly.", inputSchema: {type: 'object', properties: {min_similarity: {type: 'integer', description: '50-100, default 80 (ignored in semantic mode)'}, min_tokens: {type: 'integer', minimum: 12, maximum: 400, description: 'min fragment size, 12-400; default 50'}, mode: {type: 'string', enum: ['renamed', 'strict', 'semantic'], default: 'renamed'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/vendored/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, include_boilerplate: {type: 'boolean', default: false, description: 'Include clone groups made entirely of conventional *.router.js/ts router symbols'}, include_declarative: {type: 'boolean', default: false, description: 'Include repeated immutable array/object catalogs that contain no executable control flow'}, include_strings: {type: 'boolean', description: 'Also clone-check large multi-line string literals', default: false}, top_n: {type: 'integer', default: 15}}}, run: (g, a, ctx) => th.tFindDuplicates(g, a, ctx)},
|
|
129
|
+
{cap: 'health', name: 'find_dead_code', description: 'Conservative review queue for statically unreferenced files, functions, methods and symbols. Returns confidence, reason, bounded evidence and explicit framework/dynamic/reflection/public-API caveats; never an auto-delete verdict. Tests, generated/vendored code, mocks, stories, docs, benchmarks and temporary roots are excluded by default.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repo-relative path prefix'}, kinds: {type: 'array', items: {type: 'string', enum: ['file', 'function', 'method', 'symbol']}, maxItems: 4, uniqueItems: true, description: 'Optional candidate kinds; defaults to all'}, min_confidence: {type: 'string', enum: ['high', 'medium', 'low'], default: 'medium', description: 'Minimum confidence to include. low explicitly includes public/framework/dynamic review candidates'}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/vendored/mock/story/docs/benchmark/temp and paths explicitly classified as excluded; tests still require include_tests'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 30}}}, run: (g, a, ctx) => th.tFindDeadCode(g, a, ctx)},
|
|
130
|
+
{cap: 'health', name: 'run_audit', description: 'Core production-first repository Health review with an explicit capability/completeness matrix for structure, dependencies, bounded runtime-correctness/concurrency patterns, advisories, malware and coverage. Unsupported Maven/Gradle import verification is NOT_SUPPORTED/PARTIAL, never a clean zero. Findings whose evidence is entirely test/e2e/generated/vendored/mock/story/docs/benchmark/temp or explicitly excluded are suppressed by default; opt them in with include_classified. category=dependencies is a dedicated dependency-health projection across missing, unused and duplicate declarations while preserving each finding\'s native category. With base_ref, builds and audits an immutable Git checkout and compares stable deterministic finding IDs; debt defaults to genuinely new findings. Supply-chain checks remain explicitly uncomparable across the source-only baseline. changed_files without base_ref is only changed-scope, never a new-debt claim.', inputSchema: {type: 'object', properties: {category: {type: 'string', enum: ['dependencies', 'unused', 'structure', 'vulnerability', 'malware'], description: 'Only findings of this category; dependencies selects dependency manifest/import findings across native categories'}, min_severity: {type: 'string', enum: ['critical', 'high', 'medium', 'low', 'info'], description: 'Minimum severity to include'}, max_findings: {type: 'integer', description: 'Max findings to list, default 30'}, include_classified: {type: 'boolean', default: false, description: 'Include findings whose evidence is entirely tests/e2e/generated/vendored/mocks/stories/docs/benchmarks/temp or explicitly excluded'}, include_malware_scan: {type: 'boolean', description: 'Also grep installed packages for malware heuristics (slow)', default: false}, base_ref: {type: 'string', maxLength: 200, description: 'Optional immutable Git baseline (for example HEAD~1 or origin/main). Enables honest new/existing/fixed debt comparison'}, changed_files: {type: 'array', items: {type: 'string'}, minItems: 1, maxItems: 500, description: 'Optional explicit repo-relative scope. Without base_ref this is changed-scope only; when omitted with base_ref, files are derived from the Git diff'}, debt: {type: 'string', enum: ['new', 'existing', 'all'], default: 'new', description: 'Baseline comparison view. Defaults to genuinely new deterministic findings when base_ref is present'}}}, run: (g, a, ctx) => th.tRunAudit(g, a, ctx)},
|
|
131
131
|
{cap: 'health', name: 'coverage_map', description: 'Map a real existing coverage report onto the graph. If no report exists, return clearly labelled static test reachability (a test imports/reaches a source file) with actualCoverage=NOT_AVAILABLE; reachability is never presented as measured coverage.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max risk hotspots to list, default 15'}, path: {type: 'string', description: 'Optional repo-relative path prefix filter, e.g. src/query'}}}, run: (g, a, ctx) => th.tCoverageMap(g, a, ctx)},
|
|
132
|
-
{cap: 'health', name: 'hot_path_review', description: 'Rank a focused production-symbol hot-path queue from parser-derived local complexity, inside-loop allocations/copies/scans/sorts/recursion, graph fan-in/fan-out, and measured coverage or clearly labelled static test reachability. The default score gate is 85 with a narrow strong-local fallback; set min_score=0 for the full diagnostic queue. This is not profiler data or interprocedural Big-O.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repository-relative path prefix'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 20}, min_score: {type: 'integer', minimum: 0, maximum: 100, default: 85, description: 'Focused default is 85; lower explicitly to broaden, or use 0 for every threshold-matching diagnostic candidate'}, cyclomatic_threshold: {type: 'integer', minimum: 2, maximum: 1000, default: 8}, call_threshold: {type: 'integer', minimum: 1, maximum: 10000, default: 12}, loop_depth_threshold: {type: 'integer', minimum: 1, maximum: 10, default: 2}, time_rank_threshold: {type: 'integer', minimum: 0, maximum: 5, default: 2}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/mock/story/docs/benchmark/temp and explicitly excluded paths; tests still require include_tests'}}}, run: (g, a, ctx) => th.tHotPathReview(g, a, ctx)},
|
|
132
|
+
{cap: 'health', name: 'hot_path_review', description: 'Rank a focused production-symbol hot-path queue from parser-derived local complexity, inside-loop allocations/copies/scans/sorts/recursion, graph fan-in/fan-out, and measured coverage or clearly labelled static test reachability. The default score gate is 85 with a narrow strong-local fallback; set min_score=0 for the full diagnostic queue. This is not profiler data or interprocedural Big-O.', inputSchema: {type: 'object', properties: {path: {type: 'string', maxLength: 1024, description: 'Optional repository-relative path prefix'}, top_n: {type: 'integer', minimum: 1, maximum: 100, default: 20}, min_score: {type: 'integer', minimum: 0, maximum: 100, default: 85, description: 'Focused default is 85; lower explicitly to broaden, or use 0 for every threshold-matching diagnostic candidate'}, cyclomatic_threshold: {type: 'integer', minimum: 2, maximum: 1000, default: 8}, call_threshold: {type: 'integer', minimum: 1, maximum: 10000, default: 12}, loop_depth_threshold: {type: 'integer', minimum: 1, maximum: 10, default: 2}, time_rank_threshold: {type: 'integer', minimum: 0, maximum: 5, default: 2}, include_tests: {type: 'boolean', default: false}, include_classified: {type: 'boolean', default: false, description: 'Include generated/vendored/mock/story/docs/benchmark/temp and explicitly excluded paths; tests still require include_tests'}}}, run: (g, a, ctx) => th.tHotPathReview(g, a, ctx)},
|
|
133
133
|
{cap: 'graph', name: 'list_communities', description: 'List graph communities named by their dominant folder (largest first) with sample files — a readable module overview; feed the list position into get_community.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max communities to list, default 20'}}}, run: (g, a, ctx) => th.tListCommunities(g, a, ctx)},
|
|
134
134
|
{cap: 'graph', name: 'module_map', description: 'First orientation view for understanding an unfamiliar application with little context: a production-first folder architecture map with file/symbol counts and strongest module dependencies, separating runtime, TypeScript type-only and language compile-only coupling.', inputSchema: {type: 'object', properties: {top_n: {type: 'integer', description: 'Max modules to list, default 25'}, include_non_product: {type: 'boolean', default: false, description: 'Include tests, fixtures, benchmarks, generated output, docs and other classified non-product files; false by default'}}}, run: (g, a, ctx) => th.tModuleMap(g, a, ctx)},
|
|
135
135
|
{cap: 'source', refreshGraph: true, name: 'list_endpoints', description: 'Inventory of HTTP endpoints defined in the repo (Express/Fastify/Nest/Flask/FastAPI/Go mux/Rust axum and actix-web/Spring MVC and WebFlux): declared and reachable composed paths, static mount provenance, confidence, handler, file:line, and Spring conditional/default-active state.', inputSchema: {type: 'object', properties: {method: {type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT', 'ALL', 'ANY']}, path: {type: 'string', maxLength: 2048, description: 'Optional exact composed path or segment-aligned suffix'}, max_results: {type: 'integer', description: 'Max endpoints to list, default 100'}, include_classified: {type: 'boolean', default: false, description: 'Include test/e2e/generated/mock/story/docs/benchmark/temp and explicitly excluded targets'}}}, run: (g, a, ctx) => th.tListEndpoints(g, a, ctx)},
|
|
@@ -8,7 +8,7 @@ import {compareDuplicateMember} from './evidence/duplicate-member-order.mjs'
|
|
|
8
8
|
const CLONE_MIN_SIMILARITY = 80
|
|
9
9
|
const DIVERGENCE_MAX_SIMILARITY = 45
|
|
10
10
|
const MIN_TOKENS = 50
|
|
11
|
-
const FILTERED_CLASSES = new Set(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
11
|
+
const FILTERED_CLASSES = new Set(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
12
12
|
const SAFE_SYMBOL = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
13
13
|
|
|
14
14
|
const DUPLICATE_EVIDENCE_THRESHOLDS = Object.freeze({
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from '../graph-context.mjs'
|
|
5
5
|
import {createPathClassifier, hasPathClass} from '../../path-classification.js'
|
|
6
6
|
|
|
7
|
-
const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
7
|
+
const QUERY_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
8
8
|
const LOW_SIGNAL_SYMBOL_RE = /^(?:const(?:ant)?|variable|property|field|enum_member)$/i
|
|
9
9
|
const AMBIGUOUS_FILE_BASENAME_RE = /^(mod|lib|main)\.rs$|^index\.[a-z0-9]+$|^__init__\.py$/
|
|
10
10
|
const querySourceFile = (node) => String(node?.source_file || String(node?.id || '').split('#', 1)[0]).replace(/\\/g, '/').replace(/^\.\//, '').toLowerCase()
|
|
@@ -3,7 +3,7 @@ import {runGit} from '../../git-exec.js'
|
|
|
3
3
|
import {createPathClassifier, hasPathClass} from '../../path-classification.js'
|
|
4
4
|
|
|
5
5
|
const SEVERITY_RANK = {critical: 0, high: 1, medium: 2, low: 3, info: 4}
|
|
6
|
-
const AUDIT_NON_PRODUCT_CLASSES = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
6
|
+
const AUDIT_NON_PRODUCT_CLASSES = ['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
7
7
|
|
|
8
8
|
export function formatAuditFinding(f) {
|
|
9
9
|
const where = f.file ? ` (${f.file}${f.symbol ? ` ${f.symbol}` : ''})` : f.package ? ` (pkg ${f.package}${f.version ? `@${f.version}` : ''}${f.manifest ? `; ${f.manifest}` : ''})` : ''
|
|
@@ -168,7 +168,7 @@ export const formatOrdinaryAudit = (audit, args, findings = audit.findings, head
|
|
|
168
168
|
...auditCapabilityLines(audit),
|
|
169
169
|
...auditExtensionLines(audit),
|
|
170
170
|
`Scoped severity: critical ${sev.critical}, high ${sev.high}, medium ${sev.medium}, low ${sev.low}, info ${sev.info}. Scoped categories: unused ${bycat.unused}, structure ${bycat.structure}, vulnerability ${bycat.vulnerability}, malware ${bycat.malware}.`,
|
|
171
|
-
pathScope.suppressed ? `Path policy: production-first; suppressed ${pathScope.suppressed} finding(s) whose evidence is entirely test/e2e/generated/mock/story/docs/benchmark/temp or explicitly excluded. Pass include_classified:true to include them.` : 'Path policy: production-first; no classified-only findings were suppressed.',
|
|
171
|
+
pathScope.suppressed ? `Path policy: production-first; suppressed ${pathScope.suppressed} finding(s) whose evidence is entirely test/e2e/generated/vendored/mock/story/docs/benchmark/temp or explicitly excluded. Pass include_classified:true to include them.` : 'Path policy: production-first; no classified-only findings were suppressed.',
|
|
172
172
|
`Repository-level ${auditChecksLine(audit)}`,
|
|
173
173
|
'',
|
|
174
174
|
`Showing ${shown.length} of ${filtered.length} finding(s)${args.category ? ` in category "${args.category}"` : ''}${args.min_severity ? ` at ≥${args.min_severity}` : ''}:`,
|
package/src/mcp/health/audit.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {buildInternalGraph} from '../../graph/internal-builder.js'
|
|
|
11
11
|
import {resolveGitCommit, withGitRefCheckout} from '../../analysis/git-ref-graph.js'
|
|
12
12
|
import {filterGraphForMode} from '../../graph/graph-filter.js'
|
|
13
13
|
import {toolResult} from '../tool-result.mjs'
|
|
14
|
+
import {summarizeFindings} from '../../analysis/findings.js'
|
|
14
15
|
const auditFormatVersion = new URL(import.meta.url).search
|
|
15
16
|
const {
|
|
16
17
|
auditFilter,
|
|
@@ -147,16 +148,22 @@ async function runAuditWithBaseline(args, ctx, currentGraph) {
|
|
|
147
148
|
// unused deps), structure (import cycles / orphans / boundary rules), supply-chain (offline OSV
|
|
148
149
|
// advisories, typosquat, lockfile drift), optional malware heuristics.
|
|
149
150
|
export async function tRunAudit(g, args, ctx) {
|
|
150
|
-
if (!ctx.repoRoot) return 'Audit needs the repo root (not provided to this server).'
|
|
151
|
+
if (!ctx.repoRoot) return toolResult('Audit needs the repo root (not provided to this server).', {
|
|
152
|
+
status: 'INVALID', findings: [], reason: 'repo root unavailable',
|
|
153
|
+
}, {completeness: {status: 'PARTIAL', reason: 'repo root unavailable'}})
|
|
151
154
|
if (args.base_ref) return runAuditWithBaseline(args, ctx, rawGraph(ctx))
|
|
152
155
|
const graph = effectiveRawGraph(ctx)
|
|
153
156
|
const audit = await runAudit(ctx.repoRoot, graph, args, ctx, {
|
|
154
157
|
skipMalwareScan: !args.include_malware_scan, // greps installed packages — slow, so opt-in
|
|
155
158
|
})
|
|
156
|
-
if (!audit.ok) return `Audit failed: ${audit.error}
|
|
159
|
+
if (!audit.ok) return toolResult(`Audit failed: ${audit.error}`, {
|
|
160
|
+
status: 'ERROR', findings: [], reason: audit.error,
|
|
161
|
+
}, {completeness: {status: 'PARTIAL', reason: audit.error}})
|
|
157
162
|
if (Array.isArray(args.changed_files)) {
|
|
158
163
|
const normalized = normalizeAuditScopeFiles(args.changed_files)
|
|
159
|
-
if (!normalized.ok) return `Changed-scope audit invalid: ${normalized.error}
|
|
164
|
+
if (!normalized.ok) return toolResult(`Changed-scope audit invalid: ${normalized.error}.`, {
|
|
165
|
+
status: 'INVALID', mode: 'changed-scope', findings: [], reason: normalized.error,
|
|
166
|
+
}, {completeness: {status: 'PARTIAL', reason: normalized.error}})
|
|
160
167
|
const scoped = scopeAuditFindings(audit.findings, normalized.files)
|
|
161
168
|
const text = formatOrdinaryAudit(audit, args, scoped,
|
|
162
169
|
`CHANGED-SCOPE ONLY — ${normalized.files.length} explicitly supplied file(s); no baseline was provided, so these findings are not classified as new, existing, or fixed.`, ctx.repoRoot)
|
|
@@ -168,7 +175,25 @@ export async function tRunAudit(g, args, ctx) {
|
|
|
168
175
|
findings: auditFilter(audit, args, scoped, ctx.repoRoot),
|
|
169
176
|
}, {completeness: {status: 'COMPLETE'}})
|
|
170
177
|
}
|
|
171
|
-
|
|
178
|
+
const filtered = auditFilter(audit, args, audit.findings, ctx.repoRoot)
|
|
179
|
+
const max = Math.max(1, Math.min(100, Number(args.max_findings) || 30))
|
|
180
|
+
const shown = filtered.slice(0, max)
|
|
181
|
+
return toolResult(formatOrdinaryAudit(audit, args, audit.findings, null, ctx.repoRoot), {
|
|
182
|
+
status: 'COMPLETE',
|
|
183
|
+
mode: 'ordinary',
|
|
184
|
+
repo: audit.repo,
|
|
185
|
+
scanned: audit.scanned,
|
|
186
|
+
summary: summarizeFindings(filtered),
|
|
187
|
+
findings: shown,
|
|
188
|
+
healthCapabilities: audit.healthCapabilities,
|
|
189
|
+
extensionCapabilities: audit.extensionCapabilities || [],
|
|
190
|
+
dependencyReport: audit.dependencyReport,
|
|
191
|
+
conventionReachability: audit.conventionReachability,
|
|
192
|
+
checks: audit.checks,
|
|
193
|
+
}, {
|
|
194
|
+
page: {shown: shown.length, total: filtered.length, capped: shown.length < filtered.length},
|
|
195
|
+
completeness: {status: 'COMPLETE'},
|
|
196
|
+
})
|
|
172
197
|
}
|
|
173
198
|
|
|
174
199
|
// Named module clusters: graph communities labeled by their dominant folder instead of bare numbers.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {computeDuplicates} from '../../analysis/duplicates.js'
|
|
2
2
|
import {analyzeDuplicateGroups} from '../../analysis/duplicate-groups.js'
|
|
3
|
+
import {toolResult} from '../tool-result.mjs'
|
|
3
4
|
|
|
4
|
-
const NON_PRODUCT_DUPLICATE_CLASSES = new Set(['generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
5
|
+
const NON_PRODUCT_DUPLICATE_CLASSES = new Set(['generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
5
6
|
const fragmentEligible = (fragment, {tokMin, skipTests, includeClassified}) => {
|
|
6
7
|
if (fragment.n < tokMin) return false
|
|
7
8
|
const classes = new Set(fragment.classes || [])
|
|
@@ -11,7 +12,9 @@ const fragmentEligible = (fragment, {tokMin, skipTests, includeClassified}) => {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function tFindDuplicates(g, args, ctx) {
|
|
14
|
-
if (!ctx.repoRoot) return 'Duplicate scan needs the repo root (not provided to this server).'
|
|
15
|
+
if (!ctx.repoRoot) return toolResult('Duplicate scan needs the repo root (not provided to this server).', {
|
|
16
|
+
status: 'INVALID', groups: [], reason: 'repo root unavailable',
|
|
17
|
+
}, {completeness: {status: 'PARTIAL', reason: 'repo root unavailable'}})
|
|
15
18
|
const simMin = Math.min(100, Math.max(50, Number(args.min_similarity) || 80))
|
|
16
19
|
const tokMin = Math.min(400, Math.max(12, Number(args.min_tokens) || 50))
|
|
17
20
|
const mode = args.mode === 'strict' ? 'strict' : 'renamed'
|
|
@@ -40,7 +43,11 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
40
43
|
? b.pair.similarity - a.pair.similarity || b.tokens - a.tokens
|
|
41
44
|
: b.tokens - a.tokens || a.pair.similarity - b.pair.similarity
|
|
42
45
|
})
|
|
43
|
-
if (!candidates.length) return
|
|
46
|
+
if (!candidates.length) return toolResult(
|
|
47
|
+
'No actionable same-name pairs across files (semantic mode; ambiguous middle-similarity pairs are suppressed).',
|
|
48
|
+
{status: 'COMPLETE', mode: 'semantic', total: 0, groups: [], thresholds: {minTokens: tokMin}},
|
|
49
|
+
{page: {shown: 0, total: 0, capped: false}, completeness: {status: 'COMPLETE'}},
|
|
50
|
+
)
|
|
44
51
|
const top = candidates.slice(0, Math.min(30, Math.max(1, Number(args.top_n) || 15)))
|
|
45
52
|
const lines = top.map((item, k) => {
|
|
46
53
|
const a = frags[item.pair.a]
|
|
@@ -54,14 +61,23 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
54
61
|
` ${b.file}:${b.start}-${b.end} (${b.n} tok)`,
|
|
55
62
|
].join('\n')
|
|
56
63
|
})
|
|
57
|
-
|
|
64
|
+
const text = `Found ${candidates.length} actionable same-name pair(s) across files (semantic mode; one closest clone and/or farthest collision per name). Top ${top.length}:\n\n${lines.join('\n\n')}\n\nThese are review candidates, not automatic refactors. Use read_source on both sites before changing code.`
|
|
65
|
+
return toolResult(text, {
|
|
66
|
+
status: 'COMPLETE', mode: 'semantic', total: candidates.length, thresholds: {minTokens: tokMin},
|
|
67
|
+
groups: top.map((item) => ({
|
|
68
|
+
kind: item.kind, label: item.label, similarity: item.pair.similarity,
|
|
69
|
+
members: [frags[item.pair.a], frags[item.pair.b]].map((fragment) => ({
|
|
70
|
+
file: fragment.file, start: fragment.start, end: fragment.end, tokens: fragment.n, label: fragment.label,
|
|
71
|
+
})),
|
|
72
|
+
})),
|
|
73
|
+
}, {page: {shown: top.length, total: candidates.length, capped: top.length < candidates.length}, completeness: {status: 'COMPLETE'}})
|
|
58
74
|
}
|
|
59
75
|
const analysis = analyzeDuplicateGroups(ctx.repoRoot, ctx.graphPath, args)
|
|
60
76
|
const groups = analysis.groups
|
|
61
77
|
const smallPolicy = tokMin < 30 ? ' Small fragments below 30 tokens require at least 95% similarity and two shared bounded fingerprints.' : ''
|
|
62
78
|
const suppressed = analysis.suppressed
|
|
63
79
|
const suppressionNote = suppressed && !includeClassified
|
|
64
|
-
? ` ${suppressed} fragment(s) classified as tests/e2e/generated/mock/story/docs/benchmark/temp or matched by .weavatrix.json exclude were suppressed; pass include_classified:true (and include_tests:true for tests) to inspect them explicitly.`
|
|
80
|
+
? ` ${suppressed} fragment(s) classified as tests/e2e/generated/vendored/mock/story/docs/benchmark/temp or matched by .weavatrix.json exclude were suppressed; pass include_classified:true (and include_tests:true for tests) to inspect them explicitly.`
|
|
65
81
|
: ''
|
|
66
82
|
const boilerplateNote = analysis.boilerplateSuppressed
|
|
67
83
|
? ` ${analysis.boilerplateSuppressed} all-router framework boilerplate group(s) were suppressed; pass include_boilerplate:true to inspect them.`
|
|
@@ -69,7 +85,14 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
69
85
|
const declarativeNote = analysis.declarativeSuppressed
|
|
70
86
|
? ` ${analysis.declarativeSuppressed} immutable declarative catalog group(s) were suppressed; pass include_declarative:true to inspect repeated data shapes.`
|
|
71
87
|
: ''
|
|
72
|
-
if (!groups.length) return
|
|
88
|
+
if (!groups.length) return toolResult(
|
|
89
|
+
`No clones at ≥${simMin}% similarity / ≥${tokMin} tokens (${mode} mode). Try lowering the thresholds.${smallPolicy}${suppressionNote}${boilerplateNote}${declarativeNote}`,
|
|
90
|
+
{
|
|
91
|
+
status: 'COMPLETE', mode, total: 0, groups: [], thresholds: {minSimilarity: simMin, minTokens: tokMin},
|
|
92
|
+
suppressed: {classifiedFragments: suppressed, boilerplateGroups: analysis.boilerplateSuppressed, declarativeGroups: analysis.declarativeSuppressed},
|
|
93
|
+
},
|
|
94
|
+
{page: {shown: 0, total: 0, capped: false}, completeness: {status: 'COMPLETE'}},
|
|
95
|
+
)
|
|
73
96
|
const top = groups.slice(0, Math.min(30, Math.max(1, Number(args.top_n) || 15)))
|
|
74
97
|
const lines = top.map((grp, k) => {
|
|
75
98
|
const isStr = grp.members.some((f) => f.kind === 'string')
|
|
@@ -77,7 +100,20 @@ export function tFindDuplicates(g, args, ctx) {
|
|
|
77
100
|
const sites = grp.members.slice(0, 8).map((f) => ` ${f.file}:${f.start}-${f.end}`)
|
|
78
101
|
return [head, ...sites].join('\n')
|
|
79
102
|
})
|
|
80
|
-
|
|
103
|
+
const text = `Found ${groups.length} clone group(s) (${mode} mode, ≥${simMin}%, ≥${tokMin} tok${includeStrings ? ', incl. large string literals' : ''}). Top ${top.length}:\n\n${lines.join('\n\n')}\n\nUse read_source on any two sites to compare, then extract shared logic.${smallPolicy}${suppressionNote}${boilerplateNote}${declarativeNote}`
|
|
104
|
+
return toolResult(text, {
|
|
105
|
+
status: 'COMPLETE', mode, total: groups.length, thresholds: {minSimilarity: simMin, minTokens: tokMin},
|
|
106
|
+
includeStrings,
|
|
107
|
+
suppressed: {classifiedFragments: suppressed, boilerplateGroups: analysis.boilerplateSuppressed, declarativeGroups: analysis.declarativeSuppressed},
|
|
108
|
+
groups: top.map((group) => ({
|
|
109
|
+
similarity: group.maxSim, duplicatedTokens: group.tokens,
|
|
110
|
+
members: group.members.slice(0, 8).map((fragment) => ({
|
|
111
|
+
file: fragment.file, start: fragment.start, end: fragment.end, tokens: fragment.n,
|
|
112
|
+
label: fragment.label, kind: fragment.kind || 'symbol', classes: fragment.classes || [],
|
|
113
|
+
})),
|
|
114
|
+
memberCount: group.members.length,
|
|
115
|
+
})),
|
|
116
|
+
}, {page: {shown: top.length, total: groups.length, capped: top.length < groups.length}, completeness: {status: 'COMPLETE'}})
|
|
81
117
|
}
|
|
82
118
|
|
|
83
119
|
// Focused dead-code review queue. Unlike the broad run_audit surface, this includes functions and
|
|
@@ -35,7 +35,7 @@ export function tListEndpoints(g, args, ctx) {
|
|
|
35
35
|
const suppressed = includeClassified ? 0 : classified.length
|
|
36
36
|
eps = includeClassified ? [...production, ...classified] : production
|
|
37
37
|
const suppressionNote = suppressed
|
|
38
|
-
? `${suppressed} endpoint(s) in classified test/e2e/generated/mock/story/docs/benchmark/temp or explicitly excluded paths were suppressed; pass include_classified:true to inspect them.`
|
|
38
|
+
? `${suppressed} endpoint(s) in classified test/e2e/generated/vendored/mock/story/docs/benchmark/temp or explicitly excluded paths were suppressed; pass include_classified:true to inspect them.`
|
|
39
39
|
: ''
|
|
40
40
|
if (!eps.length) return suppressed
|
|
41
41
|
? `No production HTTP endpoints detected; ${suppressionNote}`
|
|
@@ -31,7 +31,7 @@ export function tModuleMap(g, args, ctx) {
|
|
|
31
31
|
if (classifiedFiles.has(sourceFile)) continue
|
|
32
32
|
classifiedFiles.add(sourceFile)
|
|
33
33
|
const explanation = classifier.explain(sourceFile)
|
|
34
|
-
if (explanation.excluded || hasPathClass(explanation, 'test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp')) {
|
|
34
|
+
if (explanation.excluded || hasPathClass(explanation, 'test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp')) {
|
|
35
35
|
nonProductFiles.add(sourceFile)
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -173,7 +173,9 @@ export function tCoverageMap(g, args, ctx) {
|
|
|
173
173
|
' JS/TS: npx vitest run --coverage (or jest --coverage)',
|
|
174
174
|
' Python: pytest --cov --cov-report=json',
|
|
175
175
|
' Go: go test ./... -coverprofile=coverage.out',
|
|
176
|
-
'
|
|
176
|
+
' Rust: cargo tarpaulin --out Json --output-dir .',
|
|
177
|
+
' or cargo llvm-cov --lcov --output-path lcov.info',
|
|
178
|
+
'Read locations: coverage/coverage-summary.json, coverage/coverage-final.json, (coverage/)lcov.info, coverage.json, tarpaulin-report.json, coverage.out.',
|
|
177
179
|
].filter((line) => line != null).join('\n')
|
|
178
180
|
}
|
|
179
181
|
const pctStr = (v) => (v == null ? 'n/a' : `${Math.round(v * 100)}%`)
|
|
@@ -216,4 +218,3 @@ export function tCoverageMap(g, args, ctx) {
|
|
|
216
218
|
}
|
|
217
219
|
|
|
218
220
|
// HTTP endpoint inventory: Express/Fastify/Nest/Flask/FastAPI/Go/Rust/Spring route definitions.
|
|
219
|
-
|
|
@@ -6,7 +6,7 @@ import {sourceExcerpt} from './tools-source.mjs'
|
|
|
6
6
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
7
7
|
|
|
8
8
|
const MAX_LINE_SAMPLES = 5
|
|
9
|
-
const CONTEXT_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
9
|
+
const CONTEXT_NON_PRODUCT = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
10
10
|
|
|
11
11
|
const fileOf = (g, id) => {
|
|
12
12
|
const node = g.byId.get(String(id))
|
|
@@ -4,7 +4,7 @@ import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
|
4
4
|
import {sourceExcerpt} from './tools-source.mjs'
|
|
5
5
|
import {toolResult} from './tool-result.mjs'
|
|
6
6
|
|
|
7
|
-
const NON_PRODUCT = ['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
7
|
+
const NON_PRODUCT = ['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp']
|
|
8
8
|
|
|
9
9
|
const symbolLine = (node) => Number(String(node?.source_location || '').match(/L(\d+)/)?.[1]
|
|
10
10
|
|| String(node?.id || '').match(/@(\d+)$/)?.[1] || 0)
|
|
@@ -3,7 +3,7 @@ import {connList} from './graph-context.mjs'
|
|
|
3
3
|
import {createPathClassifier, hasPathClass} from '../path-classification.js'
|
|
4
4
|
|
|
5
5
|
const isCompileTimeEdge = (edge) => edge?.typeOnly === true || edge?.compileOnly === true
|
|
6
|
-
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
6
|
+
const NON_PRODUCT_CLASSES = Object.freeze(['test', 'e2e', 'generated', 'vendored', 'mock', 'story', 'docs', 'benchmark', 'temp'])
|
|
7
7
|
const sourceFileOf = (node) => String(node?.source_file || (node?.file_type === 'code' ? node?.id : '') || '').replace(/\\/g, '/')
|
|
8
8
|
|
|
9
9
|
export function tGodNodes(g, {top_n = 10, include_classified = false} = {}, ctx = {}) {
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
import { openSync, closeSync, readSync, readFileSync, statSync } from "node:fs";
|
|
5
5
|
import { createRepoBoundary } from "./repo-path.js";
|
|
6
6
|
|
|
7
|
-
export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "mock", "story", "docs", "benchmark", "temp"]);
|
|
7
|
+
export const PATH_CLASS_NAMES = Object.freeze(["test", "e2e", "generated", "vendored", "mock", "story", "docs", "benchmark", "temp"]);
|
|
8
8
|
export const PATH_CLASS_QUERY_TERMS = Object.freeze({
|
|
9
9
|
test: Object.freeze(["test", "tests", "testing", "unit"]),
|
|
10
10
|
e2e: Object.freeze(["e2e", "playwright", "cypress"]),
|
|
11
11
|
generated: Object.freeze(["generated", "autogenerated", "dist"]),
|
|
12
|
+
vendored: Object.freeze(["vendor", "vendored", "third-party", "third_party"]),
|
|
12
13
|
mock: Object.freeze(["mock", "mocks", "fixture", "fixtures", "fake"]),
|
|
13
14
|
story: Object.freeze(["story", "stories", "storybook"]),
|
|
14
15
|
docs: Object.freeze(["doc", "docs", "documentation", "readme", "guide"]),
|
|
@@ -72,6 +73,11 @@ const DEFAULT_RULES = [
|
|
|
72
73
|
pattern: "conventional build output path",
|
|
73
74
|
regex: /(^|\/)(?:dist|build|out|coverage|\.next)(\/|$)/i,
|
|
74
75
|
},
|
|
76
|
+
{
|
|
77
|
+
category: "vendored",
|
|
78
|
+
pattern: "vendored or third-party source root",
|
|
79
|
+
regex: /(^|\/)(?:vendor|vendored|third[-_]?party)(\/|$)/i,
|
|
80
|
+
},
|
|
75
81
|
{
|
|
76
82
|
category: "mock",
|
|
77
83
|
pattern: "mock/fixture path or mockData filename",
|