@safetnsr/vet 1.14.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.
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
3
4
  import { walkFiles, readFile } from '../util.js';
4
5
  /**
5
6
  * Completeness check — scores repos on presence of good practices.
@@ -111,6 +112,37 @@ export async function checkCompleteness(cwd, ignore) {
111
112
  existsSync(join(cwd, '.circleci'));
112
113
  if (hasCI)
113
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 */ }
114
146
  // ── Linting/Formatting (0-10 points) ──
115
147
  const hasLint = existsSync(join(cwd, '.eslintrc.json')) ||
116
148
  existsSync(join(cwd, '.eslintrc.js')) ||
@@ -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
- const dupPenalty = Math.min(50, dupIssues.length * 8);
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 ──────────────────────────────────────────────────────────────
@@ -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
- const rawScore = 100 - (errors * 20) - (warnings * 5);
525
- const finalScore = Math.max(0, Math.min(100, rawScore));
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)
@@ -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 = [];
@@ -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
- const score = Math.max(0, Math.min(100, 100 - errors * 30 - warnings * 15 - infos * 3));
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)';
@@ -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';
@@ -414,7 +414,11 @@ export function checkVerify(cwd, since) {
414
414
  }
415
415
  verified++;
416
416
  }
417
- const finalScore = Math.max(0, 100 - deductions);
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {