@safetnsr/vet 1.14.0 → 1.15.1
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.d.ts +1 -1
- package/dist/categories.js +15 -11
- package/dist/checks/completeness.js +32 -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/package.json +1 -1
package/dist/categories.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CheckResult, CategoryResult, VetResult } from './types.js';
|
|
2
2
|
export declare function toGrade(score: number): string;
|
|
3
|
-
/** Apply a floor of
|
|
3
|
+
/** Apply a floor of 25 to non-security checks that have no security-related errors */
|
|
4
4
|
export declare function applyScoreFloor(check: CheckResult): number;
|
|
5
5
|
export declare function buildCategories(checkMap: {
|
|
6
6
|
security: CheckResult[];
|
package/dist/categories.js
CHANGED
|
@@ -23,12 +23,12 @@ const WEIGHTS = {
|
|
|
23
23
|
};
|
|
24
24
|
// ── Scoring floor for non-security checks ────────────────────────────────────
|
|
25
25
|
const SECURITY_CHECKS = new Set(['scan', 'secrets', 'permissions', 'owasp']);
|
|
26
|
-
/** Apply a floor of
|
|
26
|
+
/** Apply a floor of 25 to non-security checks that have no security-related errors */
|
|
27
27
|
export function applyScoreFloor(check) {
|
|
28
28
|
if (SECURITY_CHECKS.has(check.name))
|
|
29
29
|
return check.score;
|
|
30
|
-
// Non-security check: minimum score is
|
|
31
|
-
return Math.max(
|
|
30
|
+
// Non-security check: minimum score is 25
|
|
31
|
+
return Math.max(25, check.score);
|
|
32
32
|
}
|
|
33
33
|
// ── Average scores within a category ────────────────────────────────────────
|
|
34
34
|
function averageScore(checks) {
|
|
@@ -41,9 +41,11 @@ function averageScore(checks) {
|
|
|
41
41
|
/**
|
|
42
42
|
* Extract completeness score and apply it as a multiplier to the overall score.
|
|
43
43
|
* A repo with completeness=0 (no JS/TS source) gets heavily penalized.
|
|
44
|
-
*
|
|
45
|
-
* completeness
|
|
46
|
-
* completeness
|
|
44
|
+
* Steeper curve to better separate quality tiers:
|
|
45
|
+
* completeness 0-25 → multiplier 0.2-0.45
|
|
46
|
+
* completeness 25-50 → multiplier 0.45-0.65
|
|
47
|
+
* completeness 50-75 → multiplier 0.65-0.85
|
|
48
|
+
* completeness 75-100 → multiplier 0.85-1.0
|
|
47
49
|
*/
|
|
48
50
|
function completenessMultiplier(categories) {
|
|
49
51
|
const integrity = categories.find(c => c.name === 'integrity');
|
|
@@ -53,11 +55,13 @@ function completenessMultiplier(categories) {
|
|
|
53
55
|
if (!comp)
|
|
54
56
|
return 1.0;
|
|
55
57
|
const s = comp.score;
|
|
56
|
-
if (s >=
|
|
57
|
-
return 0.85 + (s -
|
|
58
|
-
if (s >=
|
|
59
|
-
return 0.
|
|
60
|
-
|
|
58
|
+
if (s >= 75)
|
|
59
|
+
return 0.85 + (s - 75) * (0.15 / 25);
|
|
60
|
+
if (s >= 50)
|
|
61
|
+
return 0.65 + (s - 50) * (0.20 / 25);
|
|
62
|
+
if (s >= 25)
|
|
63
|
+
return 0.45 + (s - 25) * (0.20 / 25);
|
|
64
|
+
return 0.20 + s * (0.25 / 25);
|
|
61
65
|
}
|
|
62
66
|
// ── Group checks into categories ─────────────────────────────────────────────
|
|
63
67
|
export function buildCategories(checkMap) {
|
|
@@ -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')) ||
|
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)`;
|