@safetnsr/vet 1.13.0 → 1.15.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/dist/categories.js +29 -5
- package/dist/checks/bloat.d.ts +3 -0
- package/dist/checks/bloat.js +369 -0
- package/dist/checks/completeness.d.ts +9 -0
- package/dist/checks/completeness.js +178 -0
- package/dist/checks/debt.js +10 -5
- package/dist/checks/deps.js +5 -2
- package/dist/checks/integrity.js +9 -6
- package/dist/checks/ready.js +4 -1
- package/dist/checks/tests.js +7 -4
- package/dist/checks/verify.js +5 -1
- package/dist/cli.js +20 -4
- package/package.json +1 -1
package/dist/categories.js
CHANGED
|
@@ -16,10 +16,10 @@ export function toGrade(score) {
|
|
|
16
16
|
}
|
|
17
17
|
// ── Category weights ─────────────────────────────────────────────────────────
|
|
18
18
|
const WEIGHTS = {
|
|
19
|
-
security: 0.
|
|
20
|
-
integrity: 0.
|
|
21
|
-
debt: 0.
|
|
22
|
-
deps: 0.
|
|
19
|
+
security: 0.25,
|
|
20
|
+
integrity: 0.35,
|
|
21
|
+
debt: 0.30,
|
|
22
|
+
deps: 0.10,
|
|
23
23
|
};
|
|
24
24
|
// ── Scoring floor for non-security checks ────────────────────────────────────
|
|
25
25
|
const SECURITY_CHECKS = new Set(['scan', 'secrets', 'permissions', 'owasp']);
|
|
@@ -37,6 +37,28 @@ function averageScore(checks) {
|
|
|
37
37
|
const total = checks.reduce((sum, c) => sum + applyScoreFloor(c), 0);
|
|
38
38
|
return Math.round(total / checks.length);
|
|
39
39
|
}
|
|
40
|
+
// ── Completeness multiplier ─────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Extract completeness score and apply it as a multiplier to the overall score.
|
|
43
|
+
* A repo with completeness=0 (no JS/TS source) gets heavily penalized.
|
|
44
|
+
* completeness 0-30 → multiplier 0.3-0.6
|
|
45
|
+
* completeness 30-70 → multiplier 0.6-0.85
|
|
46
|
+
* completeness 70-100 → multiplier 0.85-1.0
|
|
47
|
+
*/
|
|
48
|
+
function completenessMultiplier(categories) {
|
|
49
|
+
const integrity = categories.find(c => c.name === 'integrity');
|
|
50
|
+
if (!integrity)
|
|
51
|
+
return 1.0;
|
|
52
|
+
const comp = integrity.checks.find(c => c.name === 'completeness');
|
|
53
|
+
if (!comp)
|
|
54
|
+
return 1.0;
|
|
55
|
+
const s = comp.score;
|
|
56
|
+
if (s >= 70)
|
|
57
|
+
return 0.85 + (s - 70) * (0.15 / 30);
|
|
58
|
+
if (s >= 30)
|
|
59
|
+
return 0.6 + (s - 30) * (0.25 / 40);
|
|
60
|
+
return 0.3 + s * (0.3 / 30);
|
|
61
|
+
}
|
|
40
62
|
// ── Group checks into categories ─────────────────────────────────────────────
|
|
41
63
|
export function buildCategories(checkMap) {
|
|
42
64
|
const categories = [];
|
|
@@ -63,7 +85,9 @@ export function buildVetResult(project, categories) {
|
|
|
63
85
|
weightedSum += cat.score * cat.weight;
|
|
64
86
|
totalWeight += cat.weight;
|
|
65
87
|
}
|
|
66
|
-
const
|
|
88
|
+
const rawScore = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
|
89
|
+
const compMult = completenessMultiplier(categories);
|
|
90
|
+
const overallScore = Math.round(rawScore * compMult);
|
|
67
91
|
const grade = toGrade(overallScore);
|
|
68
92
|
const allIssues = categories.flatMap(c => c.issues);
|
|
69
93
|
// Read version from package.json
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { join, extname } from 'node:path';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { walkFiles, readFile, gitExec, c } from '../util.js';
|
|
4
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
+
const AI_PATTERNS = /\b(claude|copilot|ai|agent)\b|\[(claude|ai)\]/i;
|
|
6
|
+
const AI_FILE_MARKERS = ['.claude/', 'CLAUDE.md', 'AGENTS.md'];
|
|
7
|
+
const NON_CODE_EXTS = new Set(['.txt', '.log', '.csv', '.json']);
|
|
8
|
+
const SOURCE_EXTS = new Set(['.ts', '.js', '.tsx', '.jsx']);
|
|
9
|
+
const TEN_MB = 10 * 1024 * 1024;
|
|
10
|
+
const BRANCH_RE = /\b(if|else|switch|case)\b|&&|\|\||\?[^?.:]/g;
|
|
11
|
+
// ── Baseline detection ───────────────────────────────────────────────────────
|
|
12
|
+
function findBaseline(cwd) {
|
|
13
|
+
// 1. Search git log for AI-pattern commits
|
|
14
|
+
const log = gitExec(['log', '--oneline', '--all', '--reverse'], cwd);
|
|
15
|
+
if (!log) {
|
|
16
|
+
return { sha: '', message: '', method: 'empty' };
|
|
17
|
+
}
|
|
18
|
+
const lines = log.split('\n').filter(Boolean);
|
|
19
|
+
if (lines.length === 0) {
|
|
20
|
+
return { sha: '', message: '', method: 'empty' };
|
|
21
|
+
}
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const spaceIdx = line.indexOf(' ');
|
|
24
|
+
if (spaceIdx === -1)
|
|
25
|
+
continue;
|
|
26
|
+
const msg = line.substring(spaceIdx + 1);
|
|
27
|
+
if (AI_PATTERNS.test(msg)) {
|
|
28
|
+
const sha = line.substring(0, spaceIdx);
|
|
29
|
+
// Use parent of this commit as baseline (the commit before AI started)
|
|
30
|
+
const parent = gitExec(['rev-parse', `${sha}~1`], cwd);
|
|
31
|
+
if (parent) {
|
|
32
|
+
const parentMsg = gitExec(['log', '--oneline', '-1', parent], cwd);
|
|
33
|
+
return { sha: parent, message: parentMsg.substring(parentMsg.indexOf(' ') + 1) || parent, method: 'ai-commit-parent' };
|
|
34
|
+
}
|
|
35
|
+
// If no parent (first commit is AI), use this commit itself
|
|
36
|
+
return { sha, message: msg, method: 'ai-commit' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// 2. Fallback: find first commit adding AI marker files
|
|
40
|
+
for (const marker of AI_FILE_MARKERS) {
|
|
41
|
+
const result = gitExec(['log', '--all', '--reverse', '--diff-filter=A', '--', marker], cwd);
|
|
42
|
+
if (result) {
|
|
43
|
+
const firstLine = result.split('\n').find(l => l.startsWith('commit '));
|
|
44
|
+
if (firstLine) {
|
|
45
|
+
const sha = firstLine.replace('commit ', '').trim();
|
|
46
|
+
const shortSha = sha.substring(0, 7);
|
|
47
|
+
const parent = gitExec(['rev-parse', `${sha}~1`], cwd);
|
|
48
|
+
if (parent) {
|
|
49
|
+
const parentLog = gitExec(['log', '--oneline', '-1', parent], cwd);
|
|
50
|
+
return { sha: parent, message: parentLog.substring(parentLog.indexOf(' ') + 1) || parent, method: 'marker-file' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 3. Fallback: initial commit (everything is baseline)
|
|
56
|
+
const initial = lines[0];
|
|
57
|
+
const spaceIdx = initial.indexOf(' ');
|
|
58
|
+
const sha = spaceIdx !== -1 ? initial.substring(0, spaceIdx) : initial;
|
|
59
|
+
const msg = spaceIdx !== -1 ? initial.substring(spaceIdx + 1) : sha;
|
|
60
|
+
return { sha, message: msg, method: 'initial' };
|
|
61
|
+
}
|
|
62
|
+
function analyzeFileGrowth(cwd, baselineSha) {
|
|
63
|
+
if (!baselineSha) {
|
|
64
|
+
return { files: [], baselineLOC: 0, currentLOC: 0 };
|
|
65
|
+
}
|
|
66
|
+
const numstat = gitExec(['diff', baselineSha, 'HEAD', '--numstat'], cwd);
|
|
67
|
+
if (!numstat) {
|
|
68
|
+
return { files: [], baselineLOC: 0, currentLOC: 0 };
|
|
69
|
+
}
|
|
70
|
+
const files = [];
|
|
71
|
+
let totalBaselineLOC = 0;
|
|
72
|
+
let totalCurrentLOC = 0;
|
|
73
|
+
for (const line of numstat.split('\n').filter(Boolean)) {
|
|
74
|
+
const parts = line.split('\t');
|
|
75
|
+
if (parts.length < 3)
|
|
76
|
+
continue;
|
|
77
|
+
const [addStr, delStr, file] = parts;
|
|
78
|
+
// Binary files show as '-'
|
|
79
|
+
if (addStr === '-' || delStr === '-')
|
|
80
|
+
continue;
|
|
81
|
+
const additions = parseInt(addStr, 10) || 0;
|
|
82
|
+
const deletions = parseInt(delStr, 10) || 0;
|
|
83
|
+
// Get baseline line count for this file
|
|
84
|
+
const baselineContent = gitExec(['show', `${baselineSha}:${file}`], cwd);
|
|
85
|
+
const baselineLines = baselineContent ? baselineContent.split('\n').length : 0;
|
|
86
|
+
const isNew = baselineLines === 0;
|
|
87
|
+
const currentLines = Math.max(0, baselineLines + additions - deletions);
|
|
88
|
+
const denominator = Math.max(baselineLines, 1);
|
|
89
|
+
const growthPct = ((currentLines / denominator) - 1) * 100;
|
|
90
|
+
totalBaselineLOC += baselineLines;
|
|
91
|
+
totalCurrentLOC += currentLines;
|
|
92
|
+
files.push({ file, additions, deletions, baselineLines, currentLines, growthPct, isNew });
|
|
93
|
+
}
|
|
94
|
+
// If baseline LOC is 0 but we have current LOC, count all current files
|
|
95
|
+
if (totalBaselineLOC === 0) {
|
|
96
|
+
// Count current LOC from all tracked files
|
|
97
|
+
const lsFiles = gitExec(['ls-files'], cwd);
|
|
98
|
+
if (lsFiles) {
|
|
99
|
+
for (const f of lsFiles.split('\n').filter(Boolean)) {
|
|
100
|
+
const content = readFile(join(cwd, f));
|
|
101
|
+
if (content)
|
|
102
|
+
totalCurrentLOC += content.split('\n').length;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { files, baselineLOC: totalBaselineLOC, currentLOC: totalCurrentLOC };
|
|
107
|
+
}
|
|
108
|
+
function findNonCodeBombs(cwd) {
|
|
109
|
+
const bombs = [];
|
|
110
|
+
const allFiles = walkFiles(cwd);
|
|
111
|
+
for (const file of allFiles) {
|
|
112
|
+
const ext = extname(file).toLowerCase();
|
|
113
|
+
if (!NON_CODE_EXTS.has(ext))
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const stat = statSync(join(cwd, file));
|
|
117
|
+
if (stat.size > TEN_MB) {
|
|
118
|
+
bombs.push({ file, sizeBytes: stat.size });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch { /* skip */ }
|
|
122
|
+
}
|
|
123
|
+
return bombs;
|
|
124
|
+
}
|
|
125
|
+
function findPaddingFiles(cwd) {
|
|
126
|
+
const paddings = [];
|
|
127
|
+
const allFiles = walkFiles(cwd);
|
|
128
|
+
for (const file of allFiles) {
|
|
129
|
+
const ext = extname(file).toLowerCase();
|
|
130
|
+
if (!SOURCE_EXTS.has(ext))
|
|
131
|
+
continue;
|
|
132
|
+
const content = readFile(join(cwd, file));
|
|
133
|
+
if (!content)
|
|
134
|
+
continue;
|
|
135
|
+
const lines = content.split('\n');
|
|
136
|
+
const loc = lines.length;
|
|
137
|
+
if (loc <= 1000)
|
|
138
|
+
continue;
|
|
139
|
+
// Count branch constructs
|
|
140
|
+
let branches = 0;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const matches = line.match(BRANCH_RE);
|
|
143
|
+
if (matches)
|
|
144
|
+
branches += matches.length;
|
|
145
|
+
}
|
|
146
|
+
const density = branches / loc;
|
|
147
|
+
if (density < 0.02) {
|
|
148
|
+
paddings.push({ file, loc, density });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return paddings;
|
|
152
|
+
}
|
|
153
|
+
// ── Scoring ──────────────────────────────────────────────────────────────────
|
|
154
|
+
function calculateScore(bloatRatio, bloatedFiles, bombs, paddings) {
|
|
155
|
+
let s = 100;
|
|
156
|
+
const penalties = [];
|
|
157
|
+
// Bloat ratio penalties
|
|
158
|
+
if (bloatRatio > 20) {
|
|
159
|
+
s -= 40;
|
|
160
|
+
penalties.push(`bloat ratio ${bloatRatio.toFixed(1)}x (>20x): -40`);
|
|
161
|
+
}
|
|
162
|
+
else if (bloatRatio > 10) {
|
|
163
|
+
s -= 30;
|
|
164
|
+
penalties.push(`bloat ratio ${bloatRatio.toFixed(1)}x (>10x): -30`);
|
|
165
|
+
}
|
|
166
|
+
else if (bloatRatio > 5) {
|
|
167
|
+
s -= 20;
|
|
168
|
+
penalties.push(`bloat ratio ${bloatRatio.toFixed(1)}x (>5x): -20`);
|
|
169
|
+
}
|
|
170
|
+
// Per-file growth penalties
|
|
171
|
+
const growthPenalty = Math.min(30, bloatedFiles.length * 5);
|
|
172
|
+
if (growthPenalty > 0) {
|
|
173
|
+
s -= growthPenalty;
|
|
174
|
+
penalties.push(`${bloatedFiles.length} file(s) >500% growth: -${growthPenalty}`);
|
|
175
|
+
}
|
|
176
|
+
// Non-code bomb penalties
|
|
177
|
+
const bombPenalty = Math.min(20, bombs.length * 10);
|
|
178
|
+
if (bombPenalty > 0) {
|
|
179
|
+
s -= bombPenalty;
|
|
180
|
+
penalties.push(`${bombs.length} non-code bomb(s) >10MB: -${bombPenalty}`);
|
|
181
|
+
}
|
|
182
|
+
// Padding penalties
|
|
183
|
+
const paddingPenalty = Math.min(20, paddings.length * 5);
|
|
184
|
+
if (paddingPenalty > 0) {
|
|
185
|
+
s -= paddingPenalty;
|
|
186
|
+
penalties.push(`${paddings.length} low-complexity padding file(s): -${paddingPenalty}`);
|
|
187
|
+
}
|
|
188
|
+
return { score: Math.max(0, s), penalties };
|
|
189
|
+
}
|
|
190
|
+
// ── Format helpers ───────────────────────────────────────────────────────────
|
|
191
|
+
function formatSize(bytes) {
|
|
192
|
+
if (bytes >= 1024 * 1024 * 1024)
|
|
193
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(0)}GB`;
|
|
194
|
+
if (bytes >= 1024 * 1024)
|
|
195
|
+
return `${(bytes / (1024 * 1024)).toFixed(0)}MB`;
|
|
196
|
+
if (bytes >= 1024)
|
|
197
|
+
return `${(bytes / 1024).toFixed(0)}KB`;
|
|
198
|
+
return `${bytes}B`;
|
|
199
|
+
}
|
|
200
|
+
// ── Main check (for full vet run) ────────────────────────────────────────────
|
|
201
|
+
export async function checkBloat(cwd) {
|
|
202
|
+
const baseline = findBaseline(cwd);
|
|
203
|
+
if (!baseline.sha) {
|
|
204
|
+
return {
|
|
205
|
+
name: 'bloat',
|
|
206
|
+
score: 100,
|
|
207
|
+
maxScore: 100,
|
|
208
|
+
issues: [{ severity: 'info', message: 'empty repo — no bloat analysis possible', fixable: false }],
|
|
209
|
+
summary: 'empty repo',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const { files, baselineLOC, currentLOC } = analyzeFileGrowth(cwd, baseline.sha);
|
|
213
|
+
// Check if repo has any tracked files
|
|
214
|
+
const trackedFiles = gitExec(['ls-files'], cwd);
|
|
215
|
+
if (!trackedFiles || trackedFiles.trim().length === 0) {
|
|
216
|
+
return {
|
|
217
|
+
name: 'bloat',
|
|
218
|
+
score: 100,
|
|
219
|
+
maxScore: 100,
|
|
220
|
+
issues: [{ severity: 'info', message: 'empty repo — no files to analyze', fixable: false }],
|
|
221
|
+
summary: 'empty repo',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const bloatRatio = baselineLOC > 0 ? currentLOC / baselineLOC : 1;
|
|
225
|
+
const bloatedFiles = files.filter(f => f.growthPct > 500);
|
|
226
|
+
const bombs = findNonCodeBombs(cwd);
|
|
227
|
+
const paddings = findPaddingFiles(cwd);
|
|
228
|
+
const { score: finalScore, penalties } = calculateScore(bloatRatio, bloatedFiles, bombs, paddings);
|
|
229
|
+
const issues = [];
|
|
230
|
+
for (const f of bloatedFiles) {
|
|
231
|
+
issues.push({
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
message: `file growth +${Math.round(f.growthPct)}%: ${f.file} (${f.baselineLines} → ${f.currentLines} lines)`,
|
|
234
|
+
file: f.file,
|
|
235
|
+
fixable: false,
|
|
236
|
+
fixHint: 'review for unnecessary code generation',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
for (const b of bombs) {
|
|
240
|
+
issues.push({
|
|
241
|
+
severity: 'warning',
|
|
242
|
+
message: `non-code bomb: ${b.file} (${formatSize(b.sizeBytes)})`,
|
|
243
|
+
file: b.file,
|
|
244
|
+
fixable: true,
|
|
245
|
+
fixHint: 'add to .gitignore or remove large non-code file',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
for (const p of paddings) {
|
|
249
|
+
issues.push({
|
|
250
|
+
severity: 'warning',
|
|
251
|
+
message: `low-complexity padding: ${p.file} (${p.loc} LOC, density ${p.density.toFixed(3)})`,
|
|
252
|
+
file: p.file,
|
|
253
|
+
fixable: false,
|
|
254
|
+
fixHint: 'review for auto-generated boilerplate',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (bloatRatio > 5) {
|
|
258
|
+
issues.push({
|
|
259
|
+
severity: bloatRatio > 20 ? 'error' : 'warning',
|
|
260
|
+
message: `bloat ratio ${bloatRatio.toFixed(1)}x (baseline: ${baselineLOC} LOC → current: ${currentLOC} LOC)`,
|
|
261
|
+
fixable: false,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const parts = [];
|
|
265
|
+
parts.push(`${bloatRatio.toFixed(1)}x bloat ratio`);
|
|
266
|
+
if (bloatedFiles.length > 0)
|
|
267
|
+
parts.push(`${bloatedFiles.length} bloated file(s)`);
|
|
268
|
+
if (bombs.length > 0)
|
|
269
|
+
parts.push(`${bombs.length} non-code bomb(s)`);
|
|
270
|
+
if (paddings.length > 0)
|
|
271
|
+
parts.push(`${paddings.length} padding file(s)`);
|
|
272
|
+
return {
|
|
273
|
+
name: 'bloat',
|
|
274
|
+
score: finalScore,
|
|
275
|
+
maxScore: 100,
|
|
276
|
+
issues,
|
|
277
|
+
summary: parts.join(', '),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// ── Standalone subcommand ────────────────────────────────────────────────────
|
|
281
|
+
export async function runBloatCommand(format) {
|
|
282
|
+
const cwd = process.cwd();
|
|
283
|
+
const baseline = findBaseline(cwd);
|
|
284
|
+
if (!baseline.sha) {
|
|
285
|
+
if (format === 'json') {
|
|
286
|
+
console.log(JSON.stringify({ baseline: null, bloatRatio: 1, score: 100, files: [], message: 'empty repo' }));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.log(`\n ${c.bold}vet bloat${c.reset} — agent code bloat detector\n`);
|
|
290
|
+
console.log(` ${c.dim}empty repo — no bloat analysis possible${c.reset}\n`);
|
|
291
|
+
console.log(` score: 100/100\n`);
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const { files, baselineLOC, currentLOC } = analyzeFileGrowth(cwd, baseline.sha);
|
|
296
|
+
const bloatRatio = baselineLOC > 0 ? currentLOC / baselineLOC : 1;
|
|
297
|
+
const bloatedFiles = files.filter(f => f.growthPct > 500);
|
|
298
|
+
const bombs = findNonCodeBombs(cwd);
|
|
299
|
+
const paddings = findPaddingFiles(cwd);
|
|
300
|
+
const { score: finalScore } = calculateScore(bloatRatio, bloatedFiles, bombs, paddings);
|
|
301
|
+
if (format === 'json') {
|
|
302
|
+
const result = {
|
|
303
|
+
baseline: { sha: baseline.sha, message: baseline.message, method: baseline.method },
|
|
304
|
+
bloatRatio: Math.round(bloatRatio * 10) / 10,
|
|
305
|
+
baselineLOC,
|
|
306
|
+
currentLOC,
|
|
307
|
+
score: finalScore,
|
|
308
|
+
bloatedFiles: bloatedFiles.map(f => ({ file: f.file, growthPct: Math.round(f.growthPct), lines: f.currentLines })),
|
|
309
|
+
nonCodeBombs: bombs.map(b => ({ file: b.file, size: b.sizeBytes })),
|
|
310
|
+
paddingFiles: paddings.map(p => ({ file: p.file, loc: p.loc, density: Math.round(p.density * 1000) / 1000 })),
|
|
311
|
+
};
|
|
312
|
+
console.log(JSON.stringify(result, null, 2));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// ASCII output
|
|
316
|
+
console.log(`\n ${c.bold}vet bloat${c.reset} — agent code bloat detector\n`);
|
|
317
|
+
console.log(` baseline: ${baseline.sha.substring(0, 7)} (${baseline.message})`);
|
|
318
|
+
console.log(` bloat ratio: ${bloatRatio.toFixed(1)}x (baseline: ${baselineLOC} LOC → current: ${currentLOC} LOC)\n`);
|
|
319
|
+
const tableRows = [];
|
|
320
|
+
for (const f of bloatedFiles) {
|
|
321
|
+
tableRows.push({
|
|
322
|
+
file: f.file,
|
|
323
|
+
growth: f.isNew ? 'new' : `+${Math.round(f.growthPct)}%`,
|
|
324
|
+
lines: String(f.currentLines),
|
|
325
|
+
complexity: '',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
for (const b of bombs) {
|
|
329
|
+
tableRows.push({
|
|
330
|
+
file: b.file,
|
|
331
|
+
growth: 'new',
|
|
332
|
+
lines: formatSize(b.sizeBytes),
|
|
333
|
+
complexity: 'non-code bomb',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
for (const p of paddings) {
|
|
337
|
+
// Only add if not already in bloatedFiles
|
|
338
|
+
if (!bloatedFiles.some(f => f.file === p.file)) {
|
|
339
|
+
tableRows.push({
|
|
340
|
+
file: p.file,
|
|
341
|
+
growth: '',
|
|
342
|
+
lines: String(p.loc),
|
|
343
|
+
complexity: 'low (padding)',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
// Annotate existing row
|
|
348
|
+
const row = tableRows.find(r => r.file === p.file);
|
|
349
|
+
if (row)
|
|
350
|
+
row.complexity = 'low (padding)';
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (tableRows.length > 0) {
|
|
354
|
+
console.log(` ${c.dim}# file${' '.repeat(30)}growth lines complexity${c.reset}`);
|
|
355
|
+
for (let i = 0; i < tableRows.length; i++) {
|
|
356
|
+
const r = tableRows[i];
|
|
357
|
+
const num = String(i + 1).padStart(2);
|
|
358
|
+
const file = r.file.padEnd(35).substring(0, 35);
|
|
359
|
+
const growth = r.growth.padEnd(10);
|
|
360
|
+
const lines = r.lines.padEnd(11);
|
|
361
|
+
console.log(` ${num} ${file}${growth}${lines}${r.complexity}`);
|
|
362
|
+
}
|
|
363
|
+
console.log('');
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
console.log(` ${c.green}no bloat detected${c.reset}\n`);
|
|
367
|
+
}
|
|
368
|
+
console.log(` score: ${finalScore}/100\n`);
|
|
369
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CheckResult } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Completeness check — scores repos on presence of good practices.
|
|
4
|
+
*
|
|
5
|
+
* Unlike other checks (penalty-based), this is BONUS-based:
|
|
6
|
+
* starts at 0 and adds points for good signals.
|
|
7
|
+
* This prevents empty/joke repos from scoring 100.
|
|
8
|
+
*/
|
|
9
|
+
export declare function checkCompleteness(cwd: string, ignore: string[]): Promise<CheckResult>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { walkFiles, readFile } from '../util.js';
|
|
5
|
+
/**
|
|
6
|
+
* Completeness check — scores repos on presence of good practices.
|
|
7
|
+
*
|
|
8
|
+
* Unlike other checks (penalty-based), this is BONUS-based:
|
|
9
|
+
* starts at 0 and adds points for good signals.
|
|
10
|
+
* This prevents empty/joke repos from scoring 100.
|
|
11
|
+
*/
|
|
12
|
+
export async function checkCompleteness(cwd, ignore) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
let points = 0;
|
|
15
|
+
const maxPoints = 100;
|
|
16
|
+
const files = walkFiles(cwd, ignore);
|
|
17
|
+
const fileNames = files.map(f => f.toLowerCase());
|
|
18
|
+
// ── Source code presence (0-25 points) ──
|
|
19
|
+
const jstsFiles = files.filter(f => /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(f));
|
|
20
|
+
const srcFiles = jstsFiles.filter(f => !/node_modules|\.test\.|\.spec\.|__tests__/.test(f));
|
|
21
|
+
if (srcFiles.length === 0) {
|
|
22
|
+
issues.push({
|
|
23
|
+
file: '',
|
|
24
|
+
message: 'no JS/TS source files found',
|
|
25
|
+
severity: 'warning',
|
|
26
|
+
fixable: false,
|
|
27
|
+
});
|
|
28
|
+
// No source = max 30 points total (can't be a good JS/TS project)
|
|
29
|
+
return {
|
|
30
|
+
name: 'completeness',
|
|
31
|
+
score: 0,
|
|
32
|
+
maxScore: 100,
|
|
33
|
+
issues,
|
|
34
|
+
summary: 'no JS/TS source files',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (srcFiles.length >= 3)
|
|
38
|
+
points += 25;
|
|
39
|
+
else if (srcFiles.length >= 1)
|
|
40
|
+
points += 15;
|
|
41
|
+
// ── Tests presence (0-20 points) ──
|
|
42
|
+
const testFiles = jstsFiles.filter(f => /\.test\.|\.spec\.|__tests__/.test(f));
|
|
43
|
+
const hasTestDir = fileNames.some(f => f.startsWith('test/') || f.startsWith('tests/') || f.startsWith('__tests__/'));
|
|
44
|
+
if (testFiles.length >= 5) {
|
|
45
|
+
points += 20;
|
|
46
|
+
}
|
|
47
|
+
else if (testFiles.length >= 1 || hasTestDir) {
|
|
48
|
+
points += 10;
|
|
49
|
+
issues.push({
|
|
50
|
+
file: '',
|
|
51
|
+
message: `only ${testFiles.length} test file(s) found`,
|
|
52
|
+
severity: 'info',
|
|
53
|
+
fixable: false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
issues.push({
|
|
58
|
+
file: '',
|
|
59
|
+
message: 'no test files found',
|
|
60
|
+
severity: 'warning',
|
|
61
|
+
fixable: false,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// ── Package.json quality (0-15 points) ──
|
|
65
|
+
const pkgPath = join(cwd, 'package.json');
|
|
66
|
+
if (existsSync(pkgPath)) {
|
|
67
|
+
try {
|
|
68
|
+
const pkg = JSON.parse(readFile(pkgPath) ?? '{}');
|
|
69
|
+
let pkgPoints = 0;
|
|
70
|
+
if (pkg.name)
|
|
71
|
+
pkgPoints += 3;
|
|
72
|
+
if (pkg.description)
|
|
73
|
+
pkgPoints += 3;
|
|
74
|
+
if (pkg.license)
|
|
75
|
+
pkgPoints += 3;
|
|
76
|
+
if (pkg.scripts?.test)
|
|
77
|
+
pkgPoints += 3;
|
|
78
|
+
if (pkg.scripts?.build || pkg.scripts?.compile)
|
|
79
|
+
pkgPoints += 3;
|
|
80
|
+
points += Math.min(15, pkgPoints);
|
|
81
|
+
}
|
|
82
|
+
catch { /* skip */ }
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
issues.push({
|
|
86
|
+
file: 'package.json',
|
|
87
|
+
message: 'no package.json found',
|
|
88
|
+
severity: 'info',
|
|
89
|
+
fixable: false,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// ── TypeScript (0-10 points) ──
|
|
93
|
+
const hasTsConfig = existsSync(join(cwd, 'tsconfig.json'));
|
|
94
|
+
const tsFiles = files.filter(f => /\.tsx?$/.test(f));
|
|
95
|
+
if (hasTsConfig && tsFiles.length > 0) {
|
|
96
|
+
points += 10;
|
|
97
|
+
}
|
|
98
|
+
else if (tsFiles.length > 0) {
|
|
99
|
+
points += 5;
|
|
100
|
+
}
|
|
101
|
+
// ── Documentation (0-10 points) ──
|
|
102
|
+
const hasReadme = fileNames.some(f => f === 'readme.md' || f === 'readme');
|
|
103
|
+
const hasChangelog = fileNames.some(f => f.includes('changelog'));
|
|
104
|
+
const hasContributing = fileNames.some(f => f.includes('contributing'));
|
|
105
|
+
if (hasReadme)
|
|
106
|
+
points += 5;
|
|
107
|
+
if (hasChangelog || hasContributing)
|
|
108
|
+
points += 5;
|
|
109
|
+
// ── CI/CD (0-10 points) ──
|
|
110
|
+
const hasCI = existsSync(join(cwd, '.github/workflows')) ||
|
|
111
|
+
existsSync(join(cwd, '.gitlab-ci.yml')) ||
|
|
112
|
+
existsSync(join(cwd, '.circleci'));
|
|
113
|
+
if (hasCI)
|
|
114
|
+
points += 10;
|
|
115
|
+
// ── Git freshness (0-10 points, negative for very stale) ──
|
|
116
|
+
try {
|
|
117
|
+
const lastCommit = execSync('git log -1 --format=%ct', { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
118
|
+
const ageMonths = (Date.now() / 1000 - parseInt(lastCommit)) / (30 * 24 * 3600);
|
|
119
|
+
if (ageMonths < 6) {
|
|
120
|
+
points += 10; // actively maintained
|
|
121
|
+
}
|
|
122
|
+
else if (ageMonths < 12) {
|
|
123
|
+
points += 5;
|
|
124
|
+
}
|
|
125
|
+
else if (ageMonths < 24) {
|
|
126
|
+
// no bonus, no penalty
|
|
127
|
+
issues.push({
|
|
128
|
+
file: '',
|
|
129
|
+
message: `last commit ${Math.round(ageMonths)} months ago`,
|
|
130
|
+
severity: 'info',
|
|
131
|
+
fixable: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Stale: actively penalize
|
|
136
|
+
points -= 15;
|
|
137
|
+
issues.push({
|
|
138
|
+
file: '',
|
|
139
|
+
message: `last commit ${Math.round(ageMonths)} months ago — likely abandoned`,
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
fixable: false,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch { /* not a git repo or no commits */ }
|
|
146
|
+
// ── Linting/Formatting (0-10 points) ──
|
|
147
|
+
const hasLint = existsSync(join(cwd, '.eslintrc.json')) ||
|
|
148
|
+
existsSync(join(cwd, '.eslintrc.js')) ||
|
|
149
|
+
existsSync(join(cwd, '.eslintrc.cjs')) ||
|
|
150
|
+
existsSync(join(cwd, 'eslint.config.js')) ||
|
|
151
|
+
existsSync(join(cwd, 'eslint.config.mjs')) ||
|
|
152
|
+
existsSync(join(cwd, 'biome.json')) ||
|
|
153
|
+
existsSync(join(cwd, 'biome.jsonc'));
|
|
154
|
+
const hasPrettier = existsSync(join(cwd, '.prettierrc')) ||
|
|
155
|
+
existsSync(join(cwd, '.prettierrc.json')) ||
|
|
156
|
+
existsSync(join(cwd, 'prettier.config.js'));
|
|
157
|
+
if (hasLint)
|
|
158
|
+
points += 5;
|
|
159
|
+
if (hasPrettier || hasLint)
|
|
160
|
+
points += 5;
|
|
161
|
+
const score = Math.min(maxPoints, points);
|
|
162
|
+
const parts = [];
|
|
163
|
+
if (srcFiles.length > 0)
|
|
164
|
+
parts.push(`${srcFiles.length} source files`);
|
|
165
|
+
if (testFiles.length > 0)
|
|
166
|
+
parts.push(`${testFiles.length} test files`);
|
|
167
|
+
if (hasTsConfig)
|
|
168
|
+
parts.push('TypeScript');
|
|
169
|
+
if (hasCI)
|
|
170
|
+
parts.push('CI');
|
|
171
|
+
return {
|
|
172
|
+
name: 'completeness',
|
|
173
|
+
score,
|
|
174
|
+
maxScore: 100,
|
|
175
|
+
issues,
|
|
176
|
+
summary: parts.length > 0 ? parts.join(', ') : 'minimal project',
|
|
177
|
+
};
|
|
178
|
+
}
|
package/dist/checks/debt.js
CHANGED
|
@@ -517,14 +517,19 @@ export async function checkDebt(cwd, ignore) {
|
|
|
517
517
|
// D) Naming drift
|
|
518
518
|
const driftIssues = findNamingDrift(allFuncs);
|
|
519
519
|
issues.push(...driftIssues);
|
|
520
|
-
// ── Scoring
|
|
521
|
-
|
|
520
|
+
// ── Scoring (size-normalized) ─────────────────────────────────────────────
|
|
521
|
+
// Scale penalties by project size: a repo with 200 files should tolerate
|
|
522
|
+
// more absolute issues than one with 10 files. The scaling factor ranges
|
|
523
|
+
// from 1.0 (≤10 files) to 0.3 (500+ files), using log scale.
|
|
524
|
+
const fileCount = sourceFiles.length;
|
|
525
|
+
const sizeScale = fileCount <= 10 ? 1.0 : Math.max(0.3, 1.0 - Math.log10(fileCount / 10) * 0.4);
|
|
526
|
+
const dupPenalty = Math.min(50, dupIssues.length * 8) * sizeScale;
|
|
522
527
|
const orphanWarnings = orphanIssues.filter(i => i.severity === 'warning');
|
|
523
|
-
const orphanPenalty = Math.min(30, orphanWarnings.length * 5);
|
|
528
|
+
const orphanPenalty = Math.min(30, orphanWarnings.length * 5) * sizeScale;
|
|
524
529
|
const wrapperWarnings = wrapperIssues.filter(i => i.severity === 'warning');
|
|
525
530
|
const driftWarnings = driftIssues.filter(i => i.severity === 'warning');
|
|
526
|
-
const wrapperPenalty = Math.min(15, wrapperWarnings.length * 3);
|
|
527
|
-
const driftPenalty = Math.min(10, driftWarnings.length * 2);
|
|
531
|
+
const wrapperPenalty = Math.min(15, wrapperWarnings.length * 3) * sizeScale;
|
|
532
|
+
const driftPenalty = Math.min(10, driftWarnings.length * 2) * sizeScale;
|
|
528
533
|
const rawScore = 100 - dupPenalty - orphanPenalty - wrapperPenalty - driftPenalty;
|
|
529
534
|
const finalScore = Math.max(0, Math.round(rawScore));
|
|
530
535
|
// ── Summary ──────────────────────────────────────────────────────────────
|
package/dist/checks/deps.js
CHANGED
|
@@ -521,8 +521,11 @@ export async function checkDeps(cwd) {
|
|
|
521
521
|
// ── Scoring ────────────────────────────────────────────────────────────────
|
|
522
522
|
const errors = issues.filter(i => i.severity === 'error').length;
|
|
523
523
|
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
524
|
-
|
|
525
|
-
const
|
|
524
|
+
// Scale by dependency count: more deps = more chances for warnings
|
|
525
|
+
const depCount = declaredNames.length;
|
|
526
|
+
const depScale = depCount <= 5 ? 1.0 : Math.max(0.3, 1.0 - Math.log10(depCount / 5) * 0.4);
|
|
527
|
+
const rawScore = 100 - (errors * 20 * depScale) - (warnings * 5 * depScale);
|
|
528
|
+
const finalScore = Math.max(0, Math.min(100, Math.round(rawScore)));
|
|
526
529
|
// ── Summary ────────────────────────────────────────────────────────────────
|
|
527
530
|
const parts = [];
|
|
528
531
|
if (errors > 0)
|
package/dist/checks/integrity.js
CHANGED
|
@@ -534,15 +534,18 @@ export async function checkIntegrity(cwd, ignore) {
|
|
|
534
534
|
...stubbedTestIssues,
|
|
535
535
|
...unhandledAsyncIssues,
|
|
536
536
|
];
|
|
537
|
-
// Scoring: start at 100, penalize per issue type
|
|
537
|
+
// Scoring: start at 100, penalize per issue type (size-normalized)
|
|
538
|
+
const srcFiles = files.filter(f => /\.(ts|tsx|js|jsx|mts|mjs)$/.test(f));
|
|
539
|
+
const fileCount = srcFiles.length;
|
|
540
|
+
const sizeScale = fileCount <= 10 ? 1.0 : Math.max(0.3, 1.0 - Math.log10(fileCount / 10) * 0.4);
|
|
538
541
|
let score = 100;
|
|
539
|
-
score -= hallucinatedIssues.length * 10;
|
|
540
|
-
score -= emptyCatchIssues.filter(i => i.severity === 'error').length * 8;
|
|
541
|
-
score -= emptyCatchIssues.filter(i => i.severity === 'warning').length * 3;
|
|
542
|
-
score -= stubbedTestIssues.filter(i => i.severity === 'error').length * 5;
|
|
542
|
+
score -= hallucinatedIssues.length * 10 * sizeScale;
|
|
543
|
+
score -= emptyCatchIssues.filter(i => i.severity === 'error').length * 8 * sizeScale;
|
|
544
|
+
score -= emptyCatchIssues.filter(i => i.severity === 'warning').length * 3 * sizeScale;
|
|
545
|
+
score -= stubbedTestIssues.filter(i => i.severity === 'error').length * 5 * sizeScale;
|
|
543
546
|
// Unhandled async capped at -15 (only count warnings, not info-downgraded ones)
|
|
544
547
|
const unhandledWarnings = unhandledAsyncIssues.filter(i => i.severity === 'warning').length;
|
|
545
|
-
score -= Math.min(15, unhandledWarnings * 3);
|
|
548
|
+
score -= Math.min(15, unhandledWarnings * 3 * sizeScale);
|
|
546
549
|
score = Math.max(0, Math.round(score));
|
|
547
550
|
// Summary parts
|
|
548
551
|
const parts = [];
|
package/dist/checks/ready.js
CHANGED
|
@@ -141,7 +141,10 @@ function builtinReady(cwd, ignore) {
|
|
|
141
141
|
const errors = issues.filter(i => i.severity === 'error').length;
|
|
142
142
|
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
143
143
|
const infos = issues.filter(i => i.severity === 'info').length;
|
|
144
|
-
|
|
144
|
+
// Scale penalties for monorepos / large projects — more files = more structural issues found
|
|
145
|
+
const totalFiles = files.length;
|
|
146
|
+
const readyScale = totalFiles <= 20 ? 1.0 : Math.max(0.4, 1.0 - Math.log10(totalFiles / 20) * 0.35);
|
|
147
|
+
const score = Math.max(0, Math.min(100, 100 - errors * 30 * readyScale - warnings * 15 * readyScale - infos * 3 * readyScale));
|
|
145
148
|
let summary = issues.length === 0 ? 'codebase is well-structured for AI' : `${issues.length} readiness issues`;
|
|
146
149
|
if (isMonorepo)
|
|
147
150
|
summary += ' (monorepo detected)';
|
package/dist/checks/tests.js
CHANGED
|
@@ -245,16 +245,19 @@ export function checkTests(cwd, ignore) {
|
|
|
245
245
|
issues.push(...findMockOnlyTests(content, rel));
|
|
246
246
|
issues.push(...findDuplicateDescribes(lines, rel));
|
|
247
247
|
}
|
|
248
|
+
// Size-normalized scoring: scale penalties by test file count
|
|
249
|
+
// A repo with 150 test files will have more absolute issues than one with 5
|
|
250
|
+
const testScale = testFiles.length <= 5 ? 1.0 : Math.max(0.15, 1.0 - Math.log10(testFiles.length / 5) * 0.5);
|
|
248
251
|
let score = 100;
|
|
249
252
|
for (const issue of issues) {
|
|
250
253
|
if (issue.severity === 'error')
|
|
251
|
-
score -= 8;
|
|
254
|
+
score -= 8 * testScale;
|
|
252
255
|
else if (issue.severity === 'warning')
|
|
253
|
-
score -= 4;
|
|
256
|
+
score -= 4 * testScale;
|
|
254
257
|
else
|
|
255
|
-
score -= 2;
|
|
258
|
+
score -= 2 * testScale;
|
|
256
259
|
}
|
|
257
|
-
score = Math.max(0, score);
|
|
260
|
+
score = Math.max(0, Math.round(score));
|
|
258
261
|
const summary = issues.length > 0
|
|
259
262
|
? `${issues.length} test anti-pattern${issues.length !== 1 ? 's' : ''} found across ${testFiles.length} test file${testFiles.length !== 1 ? 's' : ''}`
|
|
260
263
|
: 'no test anti-patterns found';
|
package/dist/checks/verify.js
CHANGED
|
@@ -414,7 +414,11 @@ export function checkVerify(cwd, since) {
|
|
|
414
414
|
}
|
|
415
415
|
verified++;
|
|
416
416
|
}
|
|
417
|
-
|
|
417
|
+
// Score based on pass rate rather than absolute deductions
|
|
418
|
+
// This prevents large codebases with many verified claims from being unfairly penalized
|
|
419
|
+
const totalClaims = verified + failed;
|
|
420
|
+
const passRate = totalClaims > 0 ? verified / totalClaims : 1;
|
|
421
|
+
const finalScore = totalClaims === 0 ? 100 : Math.max(0, Math.round(passRate * 100));
|
|
418
422
|
const baseSummary = failed === 0
|
|
419
423
|
? `${verified} agent claim${verified !== 1 ? 's' : ''} verified clean`
|
|
420
424
|
: `${failed} claim${failed !== 1 ? 's' : ''} failed verification (${verified} passed)`;
|
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,8 @@ import { checkPermissions } from './checks/permissions.js';
|
|
|
22
22
|
import { checkCompact, runCompactCommand } from './checks/compact.js';
|
|
23
23
|
import { checkSubsidy, runSubsidyCommand } from './checks/subsidy.js';
|
|
24
24
|
import { checkLoop, runLoopCommand } from './checks/loop.js';
|
|
25
|
+
import { checkBloat, runBloatCommand } from './checks/bloat.js';
|
|
26
|
+
import { checkCompleteness } from './checks/completeness.js';
|
|
25
27
|
import { score } from './scorer.js';
|
|
26
28
|
import { reportPretty, reportJSON, reportBadge } from './reporter.js';
|
|
27
29
|
import { clearCache } from './file-cache.js';
|
|
@@ -72,6 +74,7 @@ if (flags.has('--help') || flags.has('-h')) {
|
|
|
72
74
|
npx @safetnsr/vet compact [log] compaction forensics for claude code sessions
|
|
73
75
|
npx @safetnsr/vet subsidy [--plan tier] [--since date] show AI cost vs subscription
|
|
74
76
|
npx @safetnsr/vet loop [log] /loop session forensics — per-iteration timeline
|
|
77
|
+
npx @safetnsr/vet bloat detect agent-generated code bloat
|
|
75
78
|
|
|
76
79
|
${c.dim}categories:${c.reset}
|
|
77
80
|
security (30%) scan, secrets, config, model usage
|
|
@@ -107,7 +110,7 @@ if (flags.has('--version') || flags.has('-v')) {
|
|
|
107
110
|
}
|
|
108
111
|
process.exit(0);
|
|
109
112
|
}
|
|
110
|
-
const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop'];
|
|
113
|
+
const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat'];
|
|
111
114
|
const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
|
|
112
115
|
const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
|
|
113
116
|
const isCI = flags.has('--ci');
|
|
@@ -227,6 +230,17 @@ if (command === 'loop') {
|
|
|
227
230
|
}
|
|
228
231
|
process.exit(0);
|
|
229
232
|
}
|
|
233
|
+
if (command === 'bloat') {
|
|
234
|
+
try {
|
|
235
|
+
const format = isJSON ? 'json' : 'ascii';
|
|
236
|
+
await runBloatCommand(format);
|
|
237
|
+
}
|
|
238
|
+
catch (e) {
|
|
239
|
+
console.error(`${c.red}bloat failed:${c.reset}`, e instanceof Error ? e.message : e);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
230
244
|
if (!isGitRepo(cwd)) {
|
|
231
245
|
console.error(`${c.red}not a git repository${c.reset}. vet operates on git repos.`);
|
|
232
246
|
process.exit(1);
|
|
@@ -278,7 +292,7 @@ async function runChecks() {
|
|
|
278
292
|
}
|
|
279
293
|
}
|
|
280
294
|
// Run ALL independent checks in parallel
|
|
281
|
-
const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult,] = await Promise.all([
|
|
295
|
+
const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult,] = await Promise.all([
|
|
282
296
|
withTimeout('scan', () => checkScan(cwd)),
|
|
283
297
|
withTimeout('secrets', () => checkSecrets(cwd)),
|
|
284
298
|
withTimeout('config', () => checkConfig(cwd, ignore)),
|
|
@@ -296,6 +310,8 @@ async function runChecks() {
|
|
|
296
310
|
withTimeout('verify', () => checkVerify(cwd, since)),
|
|
297
311
|
withTimeout('tests', () => checkTests(cwd, ignore)),
|
|
298
312
|
withTimeout('loop', () => checkLoop(cwd)),
|
|
313
|
+
withTimeout('completeness', () => checkCompleteness(cwd, ignore)),
|
|
314
|
+
withTimeout('bloat', () => checkBloat(cwd)),
|
|
299
315
|
]);
|
|
300
316
|
// Git-dependent checks (diff + history) — parallel with each other
|
|
301
317
|
const [diffResult, historyResult] = await Promise.all([
|
|
@@ -306,8 +322,8 @@ async function runChecks() {
|
|
|
306
322
|
clearCache();
|
|
307
323
|
return score(cwd, {
|
|
308
324
|
security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, subsidyResult],
|
|
309
|
-
integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult, loopResult],
|
|
310
|
-
debt: [readyResult, historyResult, debtResult],
|
|
325
|
+
integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult],
|
|
326
|
+
debt: [readyResult, historyResult, debtResult, bloatResult],
|
|
311
327
|
deps: [depsResult],
|
|
312
328
|
});
|
|
313
329
|
}
|