@safetnsr/vet 1.8.2 → 1.8.4

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.
@@ -61,6 +61,10 @@ function analyzeConfig(cwd, configFile, agentName, files) {
61
61
  if (completenessChecks > 0) {
62
62
  completenessScore = Math.round((completenessHits / completenessChecks) * 10);
63
63
  }
64
+ else {
65
+ // No framework dependencies detected — completeness is not applicable, don't penalize
66
+ completenessScore = 10;
67
+ }
64
68
  // Consistency: cross-reference with actual project config
65
69
  let consistencyScore = 10;
66
70
  const tsconfig = readFile(join(cwd, 'tsconfig.json'));
@@ -80,7 +84,9 @@ function analyzeConfig(cwd, configFile, agentName, files) {
80
84
  catch { /* */ }
81
85
  }
82
86
  // Check if config mentions testing but no test framework installed
83
- if ((contentLower.includes('test') || contentLower.includes('spec')) && !deps.vitest && !deps.jest && !deps.mocha && !deps.ava) {
87
+ // Also check if using Node's built-in test runner (node:test)
88
+ const usesNodeTest = contentLower.includes('node:test') || contentLower.includes('node test runner') || contentLower.includes('node built-in test');
89
+ if ((contentLower.includes('test') || contentLower.includes('spec')) && !deps.vitest && !deps.jest && !deps.mocha && !deps.ava && !usesNodeTest) {
84
90
  consistencyScore -= 2;
85
91
  suggestions.push('config mentions tests but no test framework in dependencies');
86
92
  }
@@ -128,6 +128,18 @@ function extractFunctions(source, file) {
128
128
  return fns;
129
129
  }
130
130
  // ── A) Near-duplicate detection ──────────────────────────────────────────────
131
+ /** Check if functions are in a numbered spec implementation pattern (e.g. asi01, asi02...) */
132
+ function isSpecPattern(group) {
133
+ if (group.length < 3)
134
+ return false;
135
+ const dirs = new Set(group.map(f => f.file.substring(0, f.file.lastIndexOf('/'))));
136
+ if (dirs.size !== 1)
137
+ return false; // must be same directory
138
+ // Check if filenames follow a numbered pattern
139
+ const bases = group.map(f => f.file.substring(f.file.lastIndexOf('/') + 1));
140
+ const numbered = bases.filter(b => /\d{2}/.test(b));
141
+ return numbered.length >= 3;
142
+ }
131
143
  function findDuplicates(allFuncs) {
132
144
  const issues = [];
133
145
  const groups = new Map();
@@ -138,15 +150,21 @@ function findDuplicates(allFuncs) {
138
150
  groups.set(fn.hash, existing);
139
151
  }
140
152
  const reported = new Set();
141
- // Exact duplicates
153
+ // Exact duplicates (only flag if normalized body is substantial)
142
154
  for (const [, group] of groups) {
143
155
  if (group.length < 2)
144
156
  continue;
157
+ // Skip if the normalized body is too generic (short functions normalize to same hash easily)
158
+ if (group[0].normalized.length < 65)
159
+ continue;
145
160
  // Deduplicate by name+file
146
161
  const key = group.map(f => `${f.file}:${f.name}`).sort().join('|');
147
162
  if (reported.has(key))
148
163
  continue;
149
164
  reported.add(key);
165
+ // Skip groups that follow a numbered spec pattern (e.g., ASI01-ASI10 checks)
166
+ if (isSpecPattern(group))
167
+ continue;
150
168
  const locations = group.map(f => `${f.name} (${f.file}:${f.line})`).join(', ');
151
169
  issues.push({
152
170
  severity: 'warning',
@@ -170,7 +188,7 @@ function findDuplicates(allFuncs) {
170
188
  if (a.normalized.length < 30 || b.normalized.length < 30)
171
189
  continue;
172
190
  const sim = similarity(a.normalized, b.normalized);
173
- if (sim > 0.85) {
191
+ if (sim > 0.92) {
174
192
  const key = [a.file + ':' + a.name, b.file + ':' + b.name].sort().join('|');
175
193
  if (reported.has(key))
176
194
  continue;
@@ -358,9 +376,12 @@ export async function checkDebt(cwd, ignore) {
358
376
  issues.push(...driftIssues);
359
377
  // ── Scoring ──────────────────────────────────────────────────────────────
360
378
  const dupPenalty = Math.min(50, dupIssues.length * 8);
361
- const orphanPenalty = Math.min(30, orphanIssues.length * 5);
362
- const wrapperPenalty = Math.min(15, wrapperIssues.length * 3);
363
- const driftPenalty = Math.min(10, driftIssues.length * 2);
379
+ const orphanWarnings = orphanIssues.filter(i => i.severity === 'warning');
380
+ const orphanPenalty = Math.min(30, orphanWarnings.length * 5);
381
+ const wrapperWarnings = wrapperIssues.filter(i => i.severity === 'warning');
382
+ const driftWarnings = driftIssues.filter(i => i.severity === 'warning');
383
+ const wrapperPenalty = Math.min(15, wrapperWarnings.length * 3);
384
+ const driftPenalty = Math.min(10, driftWarnings.length * 2);
364
385
  const rawScore = 100 - dupPenalty - orphanPenalty - wrapperPenalty - driftPenalty;
365
386
  const finalScore = Math.max(0, Math.round(rawScore));
366
387
  // ── Summary ──────────────────────────────────────────────────────────────
@@ -124,7 +124,7 @@ export function checkDiff(cwd, opts = {}) {
124
124
  // Extract imported name
125
125
  const nameMatch = imp.text.match(/import\s+(?:\{([^}]+)\}|(\w+))/);
126
126
  if (nameMatch) {
127
- const names = (nameMatch[1] || nameMatch[2] || '').split(',').map(n => n.trim().split(' as ').pop()?.trim()).filter(Boolean);
127
+ const names = (nameMatch[1] || nameMatch[2] || '').split(',').map(n => n.trim().replace(/^type\s+/, '').split(' as ').pop()?.trim()).filter(Boolean);
128
128
  for (const name of names) {
129
129
  if (!name || name.length < 2)
130
130
  continue;
@@ -66,7 +66,7 @@ export function checkHistory(cwd) {
66
66
  const aiPct = commits.length > 0 ? Math.round((aiCommits / commits.length) * 100) : 0;
67
67
  const infos = issues.filter(i => i.severity === 'info').length;
68
68
  const warnings = issues.filter(i => i.severity === 'warning').length;
69
- const score = Math.max(0, Math.min(100, 100 - warnings * 10 - infos * 2));
69
+ const score = Math.max(0, Math.min(100, 100 - warnings * 10));
70
70
  return {
71
71
  name: 'history',
72
72
  score: Math.round(score),
@@ -304,8 +304,14 @@ function isNextjsServerComponent(file) {
304
304
  // Next.js app directory server components
305
305
  if (NEXTJS_SERVER_FILES.test(base))
306
306
  return true;
307
- // Next.js API route handlers (app/api/)
308
- if (normalized.includes('app/api/') && /^route\.[jt]sx?$/.test(base))
307
+ // Next.js route handlers (route.ts/js/tsx/jsx) anywhere in app/
308
+ if (/^route\.[jt]sx?$/.test(base))
309
+ return true;
310
+ // Any file in app/api/ directory
311
+ if (normalized.includes('app/api/'))
312
+ return true;
313
+ // Next.js middleware
314
+ if (/^middleware\.[jt]s$/.test(base))
309
315
  return true;
310
316
  return false;
311
317
  }
@@ -193,6 +193,9 @@ export function checkMemory(cwd) {
193
193
  // 2. Broken path references
194
194
  const pathRefs = extractPaths(content);
195
195
  for (const { path: p, line } of pathRefs) {
196
+ // Skip ../ references — they point to sibling repos and can't be validated locally
197
+ if (p.startsWith('../'))
198
+ continue;
196
199
  const resolved = p.startsWith('/') ? p : resolve(cwd, p);
197
200
  if (!existsSync(resolved)) {
198
201
  issues.push({
@@ -147,8 +147,30 @@ function isPythonProject(cwd) {
147
147
  }
148
148
  /** Directories where small files are expected (examples, demos, docs) */
149
149
  const SMALL_FILE_DIRS = ['examples/', 'example/', 'demos/', 'demo/', 'docs/'];
150
+ /** Next.js app router files that are designed to be small wrappers */
151
+ const NEXTJS_APP_FILES = new Set([
152
+ 'page.tsx', 'page.jsx', 'page.ts', 'page.js',
153
+ 'layout.tsx', 'layout.jsx',
154
+ 'loading.tsx', 'loading.jsx',
155
+ 'not-found.tsx', 'not-found.jsx',
156
+ 'error.tsx', 'error.jsx',
157
+ 'template.tsx', 'template.jsx',
158
+ ]);
159
+ function isNextjsAppFile(filePath) {
160
+ return NEXTJS_APP_FILES.has(basename(filePath));
161
+ }
162
+ /** Config files should never be flagged as test files */
163
+ function isConfigFile(filePath) {
164
+ const base = basename(filePath);
165
+ return /\.config\.[a-z]+$/i.test(base);
166
+ }
167
+ /** Python pattern directories where small files are expected */
168
+ const PYTHON_PATTERN_DIRS = ['profiles/', 'providers/', 'configs/', 'config/', 'tests/', 'test/'];
169
+ /** Python pattern file names that are expected to be small */
170
+ const PYTHON_PATTERN_NAMES = new Set(['version.py', '__version__.py', 'conftest.py']);
150
171
  function isPythonBoilerplate(filePath) {
151
172
  const base = basename(filePath);
173
+ const normalized = filePath.replace(/\\/g, '/');
152
174
  if (base === '__init__.py')
153
175
  return true;
154
176
  if (base === '__main__.py')
@@ -157,7 +179,20 @@ function isPythonBoilerplate(filePath) {
157
179
  return true;
158
180
  if (filePath.endsWith('.pyi'))
159
181
  return true;
160
- if (filePath.replace(/\\/g, '/').includes('__pycache__/'))
182
+ if (normalized.includes('__pycache__/'))
183
+ return true;
184
+ // Pattern-based small Python files
185
+ if (PYTHON_PATTERN_NAMES.has(base))
186
+ return true;
187
+ if (base.endsWith('_utils.py'))
188
+ return true;
189
+ // Test files in test directories (test_*.py, *_test.py)
190
+ if (/^test_.*\.py$/.test(base) || /^.*_test\.py$/.test(base)) {
191
+ if (PYTHON_PATTERN_DIRS.some(d => normalized.includes(d)))
192
+ return true;
193
+ }
194
+ // Files in pattern directories (profiles/, providers/, configs/, config/)
195
+ if (base.endsWith('.py') && PYTHON_PATTERN_DIRS.some(d => normalized.includes(d)))
161
196
  return true;
162
197
  return false;
163
198
  }
@@ -263,6 +298,11 @@ export function checkVerify(cwd, since) {
263
298
  verified++;
264
299
  continue;
265
300
  }
301
+ // Skip thin file check for Next.js app router files (designed as small wrappers)
302
+ if (isNextjsAppFile(relPath)) {
303
+ verified++;
304
+ continue;
305
+ }
266
306
  if (lineCount < 10 && lineCount > 0) {
267
307
  issues.push({
268
308
  severity: 'warning',
@@ -287,8 +327,8 @@ export function checkVerify(cwd, since) {
287
327
  failed++;
288
328
  continue;
289
329
  }
290
- // 3. Test files must have actual assertions
291
- if (isTestFile(relPath)) {
330
+ // 3. Test files must have actual assertions (but not config files)
331
+ if (isTestFile(relPath) && !isConfigFile(relPath)) {
292
332
  if (!hasAssertions(content)) {
293
333
  issues.push({
294
334
  severity: 'error',
package/dist/cli.js CHANGED
@@ -250,13 +250,13 @@ if (isBadge && !isWatch) {
250
250
  }
251
251
  // --watch mode
252
252
  if (isWatch) {
253
- console.clear();
254
- let result = await runChecks();
255
- console.log(reportPretty(result));
256
- console.log(` ${c.dim}watching for changes... (ctrl+c to stop)${c.reset}\n`);
257
- let debounce = null;
258
- const { watch } = await import('node:fs');
259
253
  try {
254
+ console.clear();
255
+ let result = await runChecks();
256
+ console.log(reportPretty(result));
257
+ console.log(` ${c.dim}watching for changes... (ctrl+c to stop)${c.reset}\n`);
258
+ let debounce = null;
259
+ const { watch } = await import('node:fs');
260
260
  const watcher = watch(cwd, { recursive: true }, (event, filename) => {
261
261
  if (!filename)
262
262
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {