@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.
@@ -0,0 +1,3 @@
1
+ import type { CheckResult } from '../types.js';
2
+ export declare function checkReview(cwd: string): Promise<CheckResult>;
3
+ export declare function runReviewCommand(cwd: string, format: string): Promise<void>;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.28.0",
3
+ "version": "1.29.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {