@safetnsr/vet 1.28.0 → 1.29.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/checks/review.d.ts +3 -0
- package/dist/checks/review.js +189 -0
- package/dist/cli.js +17 -3
- package/package.json +1 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { readFile, c } from '../util.js';
|
|
4
|
+
const SECTIONS = [
|
|
5
|
+
{
|
|
6
|
+
name: 'FOCUS AREAS',
|
|
7
|
+
points: 20,
|
|
8
|
+
patterns: [/focus/i, /priority/i, /look for/i, /check for/i, /pay attention/i, /concentrate on/i],
|
|
9
|
+
missingImpact: 'Claude Code Review will leave generic comments',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'OUT-OF-SCOPE',
|
|
13
|
+
points: 20,
|
|
14
|
+
patterns: [/out of scope/i, /out-of-scope/i, /ignore/i, /skip/i, /don't review/i, /exclude/i, /not relevant/i],
|
|
15
|
+
missingImpact: 'Reviews may flag irrelevant code patterns',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'PERSONA',
|
|
19
|
+
points: 20,
|
|
20
|
+
patterns: [/act as/i, /you are/i, /persona/i, /role/i, /reviewer/i, /behave as/i],
|
|
21
|
+
missingImpact: 'Reviewer has no defined expertise or tone',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'TOOL LIST',
|
|
25
|
+
points: 20,
|
|
26
|
+
patterns: [/tools/i, /allowed tools/i, /disallowed/i, /permitted/i, /use the following/i],
|
|
27
|
+
missingImpact: 'No tool restrictions — agent may use unexpected tools',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'EXAMPLES',
|
|
31
|
+
points: 20,
|
|
32
|
+
patterns: [], // special: checks for fenced code blocks
|
|
33
|
+
missingImpact: 'No example review comments — output style unpredictable',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
37
|
+
function findReviewFiles(dir, maxDepth) {
|
|
38
|
+
const results = [];
|
|
39
|
+
function walk(d, depth) {
|
|
40
|
+
if (depth > maxDepth)
|
|
41
|
+
return;
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = readdirSync(d);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'build')
|
|
51
|
+
continue;
|
|
52
|
+
const full = join(d, entry);
|
|
53
|
+
try {
|
|
54
|
+
const stat = statSync(full);
|
|
55
|
+
if (stat.isFile() && entry === 'REVIEW.md') {
|
|
56
|
+
results.push(full);
|
|
57
|
+
}
|
|
58
|
+
else if (stat.isDirectory()) {
|
|
59
|
+
walk(full, depth + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { /* skip */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
walk(dir, 0);
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
function scoreFile(content) {
|
|
69
|
+
const sections = [];
|
|
70
|
+
for (const section of SECTIONS) {
|
|
71
|
+
let passed = false;
|
|
72
|
+
if (section.name === 'EXAMPLES') {
|
|
73
|
+
// Check for fenced code blocks
|
|
74
|
+
passed = /```/.test(content);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
passed = section.patterns.some(re => re.test(content));
|
|
78
|
+
}
|
|
79
|
+
sections.push({
|
|
80
|
+
name: section.name,
|
|
81
|
+
passed,
|
|
82
|
+
points: section.points,
|
|
83
|
+
missingImpact: section.missingImpact,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const score = sections.reduce((sum, s) => sum + (s.passed ? s.points : 0), 0);
|
|
87
|
+
return { score, sections };
|
|
88
|
+
}
|
|
89
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
90
|
+
export async function checkReview(cwd) {
|
|
91
|
+
const files = findReviewFiles(cwd, 3);
|
|
92
|
+
const issues = [];
|
|
93
|
+
if (files.length === 0) {
|
|
94
|
+
return {
|
|
95
|
+
name: 'review',
|
|
96
|
+
score: 0,
|
|
97
|
+
maxScore: 100,
|
|
98
|
+
issues: [{
|
|
99
|
+
severity: 'info',
|
|
100
|
+
message: 'No REVIEW.md found — create one to enable Claude Code Review',
|
|
101
|
+
fixable: false,
|
|
102
|
+
}],
|
|
103
|
+
summary: 'no REVIEW.md found',
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
let totalScore = 0;
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
const content = readFile(file) || '';
|
|
109
|
+
const result = scoreFile(content);
|
|
110
|
+
totalScore += result.score;
|
|
111
|
+
const rel = relative(cwd, file);
|
|
112
|
+
if (result.score === 0) {
|
|
113
|
+
issues.push({
|
|
114
|
+
severity: 'warning',
|
|
115
|
+
message: `${rel}: score 0/100 — no behavioral sections detected`,
|
|
116
|
+
file: rel,
|
|
117
|
+
fixable: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
else if (result.score < 100) {
|
|
121
|
+
const missing = result.sections.filter(s => !s.passed).map(s => s.name);
|
|
122
|
+
issues.push({
|
|
123
|
+
severity: 'warning',
|
|
124
|
+
message: `${rel}: score ${result.score}/100 — missing: ${missing.join(', ')}`,
|
|
125
|
+
file: rel,
|
|
126
|
+
fixable: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const avgScore = Math.round(totalScore / files.length);
|
|
131
|
+
const summary = files.length === 1
|
|
132
|
+
? `REVIEW.md score ${avgScore}/100`
|
|
133
|
+
: `${files.length} REVIEW.md files — average score ${avgScore}/100`;
|
|
134
|
+
return {
|
|
135
|
+
name: 'review',
|
|
136
|
+
score: avgScore,
|
|
137
|
+
maxScore: 100,
|
|
138
|
+
issues,
|
|
139
|
+
summary,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// ── Subcommand output ────────────────────────────────────────────────────────
|
|
143
|
+
export async function runReviewCommand(cwd, format) {
|
|
144
|
+
const files = findReviewFiles(cwd, 3);
|
|
145
|
+
if (format === 'json') {
|
|
146
|
+
const results = files.map(file => {
|
|
147
|
+
const content = readFile(file) || '';
|
|
148
|
+
const result = scoreFile(content);
|
|
149
|
+
return { file: relative(cwd, file), score: result.score, sections: result.sections };
|
|
150
|
+
});
|
|
151
|
+
console.log(JSON.stringify({
|
|
152
|
+
files: results,
|
|
153
|
+
score: files.length > 0 ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length) : 0,
|
|
154
|
+
}, null, 2));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
console.log(`\n ${c.bold}vet review${c.reset} — REVIEW.md behavioral completeness\n`);
|
|
158
|
+
if (files.length === 0) {
|
|
159
|
+
console.log(` ${c.dim}no REVIEW.md found${c.reset}`);
|
|
160
|
+
console.log(` ${c.dim}create one to guide Claude Code Review behavior${c.reset}\n`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
for (const file of files) {
|
|
164
|
+
const content = readFile(file) || '';
|
|
165
|
+
const result = scoreFile(content);
|
|
166
|
+
const rel = relative(cwd, file);
|
|
167
|
+
console.log(` ${c.bold}${rel}${c.reset}`);
|
|
168
|
+
console.log(` ${c.dim}${'─'.repeat(50)}${c.reset}`);
|
|
169
|
+
for (const section of result.sections) {
|
|
170
|
+
if (section.passed) {
|
|
171
|
+
console.log(` ${c.green}✓${c.reset} ${section.name} ${c.dim}(${section.points}pts)${c.reset}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
console.log(` ${c.red}✗${c.reset} ${section.name} ${c.dim}(0/${section.points}pts)${c.reset}`);
|
|
175
|
+
console.log(` ${c.dim}→ ${section.missingImpact}${c.reset}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const scoreColor = result.score >= 80 ? c.green : result.score >= 40 ? c.yellow : c.red;
|
|
179
|
+
console.log(`\n score ${scoreColor}${result.score}/100${c.reset}\n`);
|
|
180
|
+
}
|
|
181
|
+
if (files.length > 1) {
|
|
182
|
+
const avg = Math.round(files.reduce((sum, f) => {
|
|
183
|
+
const content = readFile(f) || '';
|
|
184
|
+
return sum + scoreFile(content).score;
|
|
185
|
+
}, 0) / files.length);
|
|
186
|
+
const avgColor = avg >= 80 ? c.green : avg >= 40 ? c.yellow : c.red;
|
|
187
|
+
console.log(` ${c.bold}average${c.reset} ${avgColor}${avg}/100${c.reset}\n`);
|
|
188
|
+
}
|
|
189
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,7 @@ import { checkContext, runContextCommand } from './checks/context.js';
|
|
|
36
36
|
import { checkSplit, runSplitCommand } from './checks/split.js';
|
|
37
37
|
import { runTriageCommand } from './checks/triage.js';
|
|
38
38
|
import { runFleetCommand } from './checks/fleet.js';
|
|
39
|
+
import { checkReview, runReviewCommand } from './checks/review.js';
|
|
39
40
|
import { checkSourceSecurity } from './checks/source-security.js';
|
|
40
41
|
import { checkCompleteness } from './checks/completeness.js';
|
|
41
42
|
import { score } from './scorer.js';
|
|
@@ -98,6 +99,7 @@ if (flags.has('--help') || flags.has('-h')) {
|
|
|
98
99
|
npx @safetnsr/vet sandbox [dir] score agent runtime blast radius
|
|
99
100
|
npx @safetnsr/vet triage [--since HEAD~1] [--json] rank diff files by review urgency
|
|
100
101
|
npx @safetnsr/vet fleet [--sessions dir] [--since 8h] [--json] multi-agent session audit
|
|
102
|
+
npx @safetnsr/vet review [dir] score REVIEW.md behavioral completeness
|
|
101
103
|
|
|
102
104
|
${c.dim}categories:${c.reset}
|
|
103
105
|
security (30%) scan, secrets, config, model usage
|
|
@@ -134,7 +136,7 @@ if (flags.has('--version') || flags.has('-v')) {
|
|
|
134
136
|
}
|
|
135
137
|
process.exit(0);
|
|
136
138
|
}
|
|
137
|
-
const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context', 'split', 'sandbox', 'triage', 'fleet'];
|
|
139
|
+
const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context', 'split', 'sandbox', 'triage', 'fleet', 'review'];
|
|
138
140
|
const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
|
|
139
141
|
const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
|
|
140
142
|
const isCI = flags.has('--ci');
|
|
@@ -346,6 +348,17 @@ if (command === 'fleet') {
|
|
|
346
348
|
}
|
|
347
349
|
process.exit(0);
|
|
348
350
|
}
|
|
351
|
+
if (command === 'review') {
|
|
352
|
+
try {
|
|
353
|
+
const format = isJSON ? 'json' : 'ascii';
|
|
354
|
+
await runReviewCommand(cwd, format);
|
|
355
|
+
}
|
|
356
|
+
catch (e) {
|
|
357
|
+
console.error(`${c.red}review failed:${c.reset}`, e instanceof Error ? e.message : e);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
349
362
|
if (!isGitRepo(cwd)) {
|
|
350
363
|
console.error(`${c.red}not a git repository${c.reset}. vet operates on git repos.`);
|
|
351
364
|
process.exit(1);
|
|
@@ -455,7 +468,7 @@ async function runChecks() {
|
|
|
455
468
|
}
|
|
456
469
|
}
|
|
457
470
|
// Run ALL independent checks in parallel
|
|
458
|
-
const [scanResult, sourceSecurityResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult, clonesResult, contextResult, splitResult, sandboxResult,] = await Promise.all([
|
|
471
|
+
const [scanResult, sourceSecurityResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult, clonesResult, contextResult, splitResult, sandboxResult, reviewResult,] = await Promise.all([
|
|
459
472
|
withTimeout('scan', () => checkScan(cwd)),
|
|
460
473
|
withTimeout('source-security', () => checkSourceSecurity(cwd)),
|
|
461
474
|
withTimeout('secrets', () => checkSecrets(cwd)),
|
|
@@ -487,6 +500,7 @@ async function runChecks() {
|
|
|
487
500
|
withTimeout('context', () => checkContext(cwd)),
|
|
488
501
|
withTimeout('split', () => checkSplit(cwd)),
|
|
489
502
|
withTimeout('sandbox', () => checkSandbox(cwd)),
|
|
503
|
+
withTimeout('review', () => checkReview(cwd)),
|
|
490
504
|
]);
|
|
491
505
|
// Git-dependent checks (diff + history) — parallel with each other
|
|
492
506
|
const [diffResult, historyResult] = await Promise.all([
|
|
@@ -501,7 +515,7 @@ async function runChecks() {
|
|
|
501
515
|
debt: [readyResult, historyResult, debtResult, bloatResult, clonesResult, splitResult],
|
|
502
516
|
deps: [depsResult],
|
|
503
517
|
architecture: [architectureResult],
|
|
504
|
-
aiready: [aireadyResult, deepResult, semanticResult, contextResult],
|
|
518
|
+
aiready: [aireadyResult, deepResult, semanticResult, contextResult, reviewResult],
|
|
505
519
|
history: [hotspotsResult],
|
|
506
520
|
});
|
|
507
521
|
}
|