@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.
- package/dist/checks/config.js +7 -1
- package/dist/checks/debt.js +26 -5
- package/dist/checks/diff.js +1 -1
- package/dist/checks/history.js +1 -1
- package/dist/checks/integrity.js +8 -2
- package/dist/checks/memory.js +3 -0
- package/dist/checks/verify.js +43 -3
- package/dist/cli.js +6 -6
- package/package.json +1 -1
package/dist/checks/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/checks/debt.js
CHANGED
|
@@ -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.
|
|
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
|
|
362
|
-
const
|
|
363
|
-
const
|
|
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 ──────────────────────────────────────────────────────────────
|
package/dist/checks/diff.js
CHANGED
|
@@ -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;
|
package/dist/checks/history.js
CHANGED
|
@@ -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
|
|
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),
|
package/dist/checks/integrity.js
CHANGED
|
@@ -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
|
|
308
|
-
if (
|
|
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
|
}
|
package/dist/checks/memory.js
CHANGED
|
@@ -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({
|
package/dist/checks/verify.js
CHANGED
|
@@ -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 (
|
|
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;
|