@safetnsr/vet 1.8.0 → 1.8.3
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 +43 -9
- package/dist/checks/deps.js +146 -135
- package/dist/checks/diff.js +24 -5
- package/dist/checks/history.js +1 -1
- package/dist/checks/integrity.js +51 -14
- package/dist/checks/memory.js +6 -10
- package/dist/checks/models.js +24 -6
- package/dist/checks/owasp/asi01-goal-hijack.d.ts +5 -0
- package/dist/checks/owasp/asi01-goal-hijack.js +49 -0
- package/dist/checks/owasp/asi02-tool-misuse.d.ts +5 -0
- package/dist/checks/owasp/asi02-tool-misuse.js +98 -0
- package/dist/checks/owasp/asi03-identity-abuse.d.ts +5 -0
- package/dist/checks/owasp/asi03-identity-abuse.js +80 -0
- package/dist/checks/owasp/asi04-supply-chain.d.ts +5 -0
- package/dist/checks/owasp/asi04-supply-chain.js +79 -0
- package/dist/checks/owasp/asi05-code-execution.d.ts +5 -0
- package/dist/checks/owasp/asi05-code-execution.js +67 -0
- package/dist/checks/owasp/asi06-memory-poisoning.d.ts +5 -0
- package/dist/checks/owasp/asi06-memory-poisoning.js +61 -0
- package/dist/checks/owasp/asi07-inter-agent.d.ts +5 -0
- package/dist/checks/owasp/asi07-inter-agent.js +62 -0
- package/dist/checks/owasp/asi08-cascading.d.ts +5 -0
- package/dist/checks/owasp/asi08-cascading.js +36 -0
- package/dist/checks/owasp/asi09-trust-exploitation.d.ts +5 -0
- package/dist/checks/owasp/asi09-trust-exploitation.js +65 -0
- package/dist/checks/owasp/asi10-rogue-agents.d.ts +5 -0
- package/dist/checks/owasp/asi10-rogue-agents.js +31 -0
- package/dist/checks/owasp/index.d.ts +11 -0
- package/dist/checks/owasp/index.js +11 -0
- package/dist/checks/owasp/shared.d.ts +11 -0
- package/dist/checks/owasp/shared.js +61 -0
- package/dist/checks/owasp.js +1 -1
- package/dist/checks/ready.js +9 -4
- package/dist/checks/receipt.js +2 -16
- package/dist/checks/scan.js +30 -0
- package/dist/checks/secrets.js +2 -20
- package/dist/checks/tests.d.ts +3 -0
- package/dist/checks/tests.js +10 -0
- package/dist/checks/verify.js +13 -0
- package/dist/cli.js +117 -75
- package/dist/util.d.ts +0 -1
- package/dist/util.js +1 -1
- 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;
|
|
@@ -189,6 +207,18 @@ function findDuplicates(allFuncs) {
|
|
|
189
207
|
return issues;
|
|
190
208
|
}
|
|
191
209
|
// ── B) Orphaned exports ──────────────────────────────────────────────────────
|
|
210
|
+
function isLibrary(cwd) {
|
|
211
|
+
try {
|
|
212
|
+
const raw = readFile(join(cwd, 'package.json'));
|
|
213
|
+
if (!raw)
|
|
214
|
+
return false;
|
|
215
|
+
const pkg = JSON.parse(raw);
|
|
216
|
+
return !!(pkg.main || pkg.exports || pkg.module || pkg.types || pkg.bin);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
192
222
|
function findOrphanedExports(cwd, files) {
|
|
193
223
|
const issues = [];
|
|
194
224
|
const sourceFiles = files.filter(f => isSourceFile(f) && !isTestFile(f));
|
|
@@ -237,18 +267,19 @@ function findOrphanedExports(cwd, files) {
|
|
|
237
267
|
allContent.push(content);
|
|
238
268
|
}
|
|
239
269
|
const allText = allContent.join('\n');
|
|
270
|
+
const lib = isLibrary(cwd);
|
|
240
271
|
for (const exp of exports) {
|
|
241
272
|
// Check if name appears in import statements across all files
|
|
242
273
|
// import { name } from or import { x, name } from or import { name as y }
|
|
243
274
|
const importPattern = new RegExp(`import\\s+[^;]*\\b${exp.name}\\b[^;]*from\\s+`, 'm');
|
|
244
275
|
if (!importPattern.test(allText)) {
|
|
245
276
|
issues.push({
|
|
246
|
-
severity: 'warning',
|
|
247
|
-
message: `orphaned export: "${exp.name}" is exported but never imported`,
|
|
277
|
+
severity: lib ? 'info' : 'warning',
|
|
278
|
+
message: `orphaned export: "${exp.name}" is exported but never imported${lib ? ' (library detected — exports may be consumed externally)' : ''}`,
|
|
248
279
|
file: exp.file,
|
|
249
280
|
line: exp.line,
|
|
250
281
|
fixable: true,
|
|
251
|
-
fixHint: 'remove the export keyword or delete the function',
|
|
282
|
+
fixHint: lib ? 'may be public API — verify if still needed' : 'remove the export keyword or delete the function',
|
|
252
283
|
});
|
|
253
284
|
}
|
|
254
285
|
}
|
|
@@ -344,10 +375,13 @@ export async function checkDebt(cwd, ignore) {
|
|
|
344
375
|
const driftIssues = findNamingDrift(allFuncs);
|
|
345
376
|
issues.push(...driftIssues);
|
|
346
377
|
// ── Scoring ──────────────────────────────────────────────────────────────
|
|
347
|
-
const dupPenalty = Math.min(
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
378
|
+
const dupPenalty = Math.min(50, dupIssues.length * 8);
|
|
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);
|
|
351
385
|
const rawScore = 100 - dupPenalty - orphanPenalty - wrapperPenalty - driftPenalty;
|
|
352
386
|
const finalScore = Math.max(0, Math.round(rawScore));
|
|
353
387
|
// ── Summary ──────────────────────────────────────────────────────────────
|
package/dist/checks/deps.js
CHANGED
|
@@ -73,7 +73,8 @@ export function extractImports(source) {
|
|
|
73
73
|
while ((match = dynamicImport.exec(source)) !== null) {
|
|
74
74
|
imports.add(match[1]);
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
// Filter out template literal fragments (e.g. "${top}" from fixHint strings)
|
|
77
|
+
return [...imports].filter(s => !s.includes('$'));
|
|
77
78
|
}
|
|
78
79
|
// ── Package name extraction ──────────────────────────────────────────────────
|
|
79
80
|
export function extractPackageName(specifier) {
|
|
@@ -130,10 +131,15 @@ async function checkRegistry(packages) {
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
// Process in batches of 5
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
try {
|
|
135
|
+
const concurrency = 5;
|
|
136
|
+
for (let i = 0; i < queue.length; i += concurrency) {
|
|
137
|
+
const batch = queue.slice(i, i + concurrency);
|
|
138
|
+
await Promise.all(batch.map(checkOne));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
networkError = true;
|
|
137
143
|
}
|
|
138
144
|
if (networkError) {
|
|
139
145
|
results.set('__network_error__', true);
|
|
@@ -142,150 +148,155 @@ async function checkRegistry(packages) {
|
|
|
142
148
|
}
|
|
143
149
|
// ── Main check ───────────────────────────────────────────────────────────────
|
|
144
150
|
export async function checkDeps(cwd) {
|
|
145
|
-
const issues = [];
|
|
146
|
-
// Read package.json
|
|
147
|
-
let declaredDeps = {};
|
|
148
|
-
let hasPkgJson = false;
|
|
149
151
|
try {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
const issues = [];
|
|
153
|
+
// Read package.json
|
|
154
|
+
let declaredDeps = {};
|
|
155
|
+
let hasPkgJson = false;
|
|
156
|
+
try {
|
|
157
|
+
const pkgRaw = readFile(join(cwd, 'package.json'));
|
|
158
|
+
if (pkgRaw) {
|
|
159
|
+
const pkg = JSON.parse(pkgRaw);
|
|
160
|
+
hasPkgJson = true;
|
|
161
|
+
declaredDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
162
|
+
}
|
|
155
163
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (registryResults.get('__network_error__')) {
|
|
171
|
-
issues.push({
|
|
172
|
-
severity: 'info',
|
|
173
|
-
message: 'could not reach npm registry — skipping existence checks',
|
|
174
|
-
fixable: false,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
for (const pkg of declaredNames) {
|
|
178
|
-
if (registryResults.get(pkg) === false) {
|
|
164
|
+
catch { /* skip */ }
|
|
165
|
+
if (!hasPkgJson) {
|
|
166
|
+
return {
|
|
167
|
+
name: 'deps',
|
|
168
|
+
score: 100,
|
|
169
|
+
maxScore: 100,
|
|
170
|
+
issues: [],
|
|
171
|
+
summary: 'no package.json found',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const declaredNames = Object.keys(declaredDeps);
|
|
175
|
+
// ── 1. Registry check (nonexistent packages) ──────────────────────────────
|
|
176
|
+
const registryResults = await checkRegistry(declaredNames);
|
|
177
|
+
if (registryResults.get('__network_error__')) {
|
|
179
178
|
issues.push({
|
|
180
|
-
severity: '
|
|
181
|
-
message:
|
|
182
|
-
|
|
183
|
-
fixable: true,
|
|
184
|
-
fixHint: 'remove from package.json',
|
|
179
|
+
severity: 'info',
|
|
180
|
+
message: 'could not reach npm registry — skipping existence checks',
|
|
181
|
+
fixable: false,
|
|
185
182
|
});
|
|
186
183
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const topSet = new Set(TOP_PACKAGES);
|
|
190
|
-
// Known-legitimate short packages that happen to be close to popular ones
|
|
191
|
-
const TYPOSQUAT_WHITELIST = new Set([
|
|
192
|
-
'ai', 'clsx', 'ws', 'os', 'ms', 'pg', 'ip', 'bn', 'qs', 'co', 'is',
|
|
193
|
-
]);
|
|
194
|
-
for (const pkg of declaredNames) {
|
|
195
|
-
if (topSet.has(pkg))
|
|
196
|
-
continue; // it IS the popular package
|
|
197
|
-
if (pkg.length <= 3)
|
|
198
|
-
continue; // too short, too many false matches
|
|
199
|
-
if (TYPOSQUAT_WHITELIST.has(pkg))
|
|
200
|
-
continue;
|
|
201
|
-
for (const top of TOP_PACKAGES) {
|
|
202
|
-
const dist = levenshtein(pkg, top);
|
|
203
|
-
if (dist >= 1 && dist <= 2) {
|
|
204
|
-
// If the package exists on the registry, it's likely legitimate — downgrade to info
|
|
205
|
-
const existsOnRegistry = registryResults.get(pkg) === true;
|
|
184
|
+
for (const pkg of declaredNames) {
|
|
185
|
+
if (registryResults.get(pkg) === false) {
|
|
206
186
|
issues.push({
|
|
207
|
-
severity:
|
|
208
|
-
message: `
|
|
187
|
+
severity: 'error',
|
|
188
|
+
message: `phantom dependency: "${pkg}" does not exist on npm`,
|
|
209
189
|
file: 'package.json',
|
|
210
190
|
fixable: true,
|
|
211
|
-
fixHint:
|
|
191
|
+
fixHint: 'remove from package.json',
|
|
212
192
|
});
|
|
213
|
-
break; // one match is enough
|
|
214
193
|
}
|
|
215
194
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
195
|
+
// ── 2. Typosquat detection ─────────────────────────────────────────────────
|
|
196
|
+
const topSet = new Set(TOP_PACKAGES);
|
|
197
|
+
// Known-legitimate short packages that happen to be close to popular ones
|
|
198
|
+
const TYPOSQUAT_WHITELIST = new Set([
|
|
199
|
+
'ai', 'clsx', 'ws', 'os', 'ms', 'pg', 'ip', 'bn', 'qs', 'co', 'is',
|
|
200
|
+
]);
|
|
201
|
+
for (const pkg of declaredNames) {
|
|
202
|
+
if (topSet.has(pkg))
|
|
203
|
+
continue; // it IS the popular package
|
|
204
|
+
if (pkg.length <= 3)
|
|
205
|
+
continue; // too short, too many false matches
|
|
206
|
+
if (TYPOSQUAT_WHITELIST.has(pkg))
|
|
207
|
+
continue;
|
|
208
|
+
for (const top of TOP_PACKAGES) {
|
|
209
|
+
const dist = levenshtein(pkg, top);
|
|
210
|
+
if (dist >= 1 && dist <= 2) {
|
|
211
|
+
// If the package exists on the registry, it's likely legitimate — downgrade to info
|
|
212
|
+
const existsOnRegistry = registryResults.get(pkg) === true;
|
|
213
|
+
issues.push({
|
|
214
|
+
severity: existsOnRegistry ? 'info' : 'error',
|
|
215
|
+
message: `possible typosquat: "${pkg}" is ${dist} edit${dist > 1 ? 's' : ''} from "${top}"${existsOnRegistry ? ' (exists on npm)' : ''}`,
|
|
216
|
+
file: 'package.json',
|
|
217
|
+
fixable: true,
|
|
218
|
+
fixHint: `did you mean "${top}"?`,
|
|
219
|
+
});
|
|
220
|
+
break; // one match is enough
|
|
221
|
+
}
|
|
237
222
|
}
|
|
238
223
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
224
|
+
// ── 3 & 4. Dead deps + phantom imports ─────────────────────────────────────
|
|
225
|
+
const sourceExts = new Set(['.ts', '.js', '.tsx', '.jsx', '.mts', '.mjs', '.cts', '.cjs']);
|
|
226
|
+
const allFiles = walkFiles(cwd);
|
|
227
|
+
const isTestFile = (f) => /\.(test|spec)\.[jt]sx?$/.test(f) || f.includes('__tests__') || /^test[/\\]/.test(f);
|
|
228
|
+
const sourceFiles = allFiles.filter(f => {
|
|
229
|
+
const ext = f.substring(f.lastIndexOf('.'));
|
|
230
|
+
// Skip test files — they contain import strings as test fixtures, not real imports
|
|
231
|
+
return sourceExts.has(ext) && !isTestFile(f);
|
|
232
|
+
});
|
|
233
|
+
const importedPackages = new Set();
|
|
234
|
+
for (const file of sourceFiles) {
|
|
235
|
+
try {
|
|
236
|
+
const content = readFileSync(join(cwd, file), 'utf-8');
|
|
237
|
+
const rawImports = extractImports(content);
|
|
238
|
+
for (const imp of rawImports) {
|
|
239
|
+
if (isBuiltin(imp))
|
|
240
|
+
continue;
|
|
241
|
+
const pkg = extractPackageName(imp);
|
|
242
|
+
if (pkg)
|
|
243
|
+
importedPackages.add(pkg);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch { /* skip unreadable files */ }
|
|
254
247
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
248
|
+
// Dead deps: declared but never imported
|
|
249
|
+
const declaredSet = new Set(declaredNames);
|
|
250
|
+
for (const pkg of declaredNames) {
|
|
251
|
+
if (!importedPackages.has(pkg)) {
|
|
252
|
+
// Check if it's a CLI tool / plugin / type package (common false positives)
|
|
253
|
+
// Still flag it, but as info
|
|
254
|
+
issues.push({
|
|
255
|
+
severity: 'info',
|
|
256
|
+
message: `unused dependency: "${pkg}" is declared but never imported`,
|
|
257
|
+
file: 'package.json',
|
|
258
|
+
fixable: true,
|
|
259
|
+
fixHint: 'remove from package.json',
|
|
260
|
+
});
|
|
261
|
+
}
|
|
265
262
|
}
|
|
263
|
+
// Phantom imports: imported but not declared
|
|
264
|
+
for (const pkg of importedPackages) {
|
|
265
|
+
if (!declaredSet.has(pkg)) {
|
|
266
|
+
issues.push({
|
|
267
|
+
severity: 'warning',
|
|
268
|
+
message: `phantom import: "${pkg}" is imported but not in package.json`,
|
|
269
|
+
fixable: true,
|
|
270
|
+
fixHint: `run: npm install ${pkg}`,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// ── Scoring ────────────────────────────────────────────────────────────────
|
|
275
|
+
const errors = issues.filter(i => i.severity === 'error').length;
|
|
276
|
+
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
277
|
+
const rawScore = 100 - (errors * 30) - (warnings * 10);
|
|
278
|
+
const finalScore = Math.max(0, Math.min(100, rawScore));
|
|
279
|
+
// ── Summary ────────────────────────────────────────────────────────────────
|
|
280
|
+
const parts = [];
|
|
281
|
+
if (errors > 0)
|
|
282
|
+
parts.push(`${errors} error${errors !== 1 ? 's' : ''}`);
|
|
283
|
+
if (warnings > 0)
|
|
284
|
+
parts.push(`${warnings} warning${warnings !== 1 ? 's' : ''}`);
|
|
285
|
+
const infos = issues.filter(i => i.severity === 'info').length;
|
|
286
|
+
if (infos > 0)
|
|
287
|
+
parts.push(`${infos} info`);
|
|
288
|
+
const summary = parts.length === 0
|
|
289
|
+
? `${declaredNames.length} dependencies checked, all clean`
|
|
290
|
+
: `${declaredNames.length} dependencies: ${parts.join(', ')}`;
|
|
291
|
+
return {
|
|
292
|
+
name: 'deps',
|
|
293
|
+
score: finalScore,
|
|
294
|
+
maxScore: 100,
|
|
295
|
+
issues,
|
|
296
|
+
summary,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return { name: 'deps', score: 100, maxScore: 100, issues: [], summary: 'deps check failed' };
|
|
266
301
|
}
|
|
267
|
-
// ── Scoring ────────────────────────────────────────────────────────────────
|
|
268
|
-
const errors = issues.filter(i => i.severity === 'error').length;
|
|
269
|
-
const warnings = issues.filter(i => i.severity === 'warning').length;
|
|
270
|
-
const rawScore = 100 - (errors * 30) - (warnings * 10);
|
|
271
|
-
const finalScore = Math.max(0, Math.min(100, rawScore));
|
|
272
|
-
// ── Summary ────────────────────────────────────────────────────────────────
|
|
273
|
-
const parts = [];
|
|
274
|
-
if (errors > 0)
|
|
275
|
-
parts.push(`${errors} error${errors !== 1 ? 's' : ''}`);
|
|
276
|
-
if (warnings > 0)
|
|
277
|
-
parts.push(`${warnings} warning${warnings !== 1 ? 's' : ''}`);
|
|
278
|
-
const infos = issues.filter(i => i.severity === 'info').length;
|
|
279
|
-
if (infos > 0)
|
|
280
|
-
parts.push(`${infos} info`);
|
|
281
|
-
const summary = parts.length === 0
|
|
282
|
-
? `${declaredNames.length} dependencies checked, all clean`
|
|
283
|
-
: `${declaredNames.length} dependencies: ${parts.join(', ')}`;
|
|
284
|
-
return {
|
|
285
|
-
name: 'deps',
|
|
286
|
-
score: finalScore,
|
|
287
|
-
maxScore: 100,
|
|
288
|
-
issues,
|
|
289
|
-
summary,
|
|
290
|
-
};
|
|
291
302
|
}
|
package/dist/checks/diff.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import { git } from '../util.js';
|
|
1
|
+
import { git, readFile } from '../util.js';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
function fileHasVetIgnore(cwd, filePath, checkName) {
|
|
4
|
+
const content = readFile(join(cwd, filePath));
|
|
5
|
+
if (!content)
|
|
6
|
+
return false;
|
|
7
|
+
const lines = content.split('\n').slice(0, 5);
|
|
8
|
+
const re = new RegExp(`(?://|/\\*|#)\\s*vet-ignore:\\s*${checkName}\\b`);
|
|
9
|
+
return lines.some(l => re.test(l));
|
|
10
|
+
}
|
|
2
11
|
// Generic patterns (still useful but not the star)
|
|
3
12
|
const GENERIC_PATTERNS = [
|
|
4
13
|
// Secrets
|
|
@@ -83,6 +92,8 @@ export function checkDiff(cwd, opts = {}) {
|
|
|
83
92
|
const allPatterns = [...GENERIC_PATTERNS, ...AI_PATTERNS];
|
|
84
93
|
// Pattern matching on added lines
|
|
85
94
|
for (const file of files) {
|
|
95
|
+
if (fileHasVetIgnore(cwd, file.path, 'diff'))
|
|
96
|
+
continue;
|
|
86
97
|
for (const { num, text } of file.addedLines) {
|
|
87
98
|
for (const pattern of allPatterns) {
|
|
88
99
|
if (pattern.regex.test(text)) {
|
|
@@ -113,13 +124,21 @@ export function checkDiff(cwd, opts = {}) {
|
|
|
113
124
|
// Extract imported name
|
|
114
125
|
const nameMatch = imp.text.match(/import\s+(?:\{([^}]+)\}|(\w+))/);
|
|
115
126
|
if (nameMatch) {
|
|
116
|
-
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);
|
|
117
128
|
for (const name of names) {
|
|
118
129
|
if (!name || name.length < 2)
|
|
119
130
|
continue;
|
|
120
|
-
// Check if name is used in any other added line
|
|
121
|
-
const
|
|
122
|
-
|
|
131
|
+
// Check if name is used in any other added line OR in unchanged file content
|
|
132
|
+
const usedInAdded = file.addedLines.some(l => l !== imp && l.text.includes(name));
|
|
133
|
+
// Also read the full file to check if name is used in existing (unchanged) code
|
|
134
|
+
const fullContent = readFile(join(cwd, file.path));
|
|
135
|
+
const usedInFile = fullContent ? fullContent.split('\n').some((l, idx) => {
|
|
136
|
+
// Skip the import line itself
|
|
137
|
+
if (l.trim() === imp.text.trim())
|
|
138
|
+
return false;
|
|
139
|
+
return l.includes(name);
|
|
140
|
+
}) : false;
|
|
141
|
+
if (!usedInAdded && !usedInFile && file.addedLines.length > 3) {
|
|
123
142
|
issues.push({
|
|
124
143
|
severity: 'warning',
|
|
125
144
|
message: `[ai] imported "${name}" but never used in new code`,
|
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
|
@@ -296,6 +296,19 @@ function isErrorBoundaryFile(file) {
|
|
|
296
296
|
return true;
|
|
297
297
|
return false;
|
|
298
298
|
}
|
|
299
|
+
/** Next.js server component files where framework handles errors */
|
|
300
|
+
const NEXTJS_SERVER_FILES = /^(page|layout|loading|not-found|template)\.[jt]sx?$/;
|
|
301
|
+
function isNextjsServerComponent(file) {
|
|
302
|
+
const normalized = file.replace(/\\/g, '/');
|
|
303
|
+
const base = basename(normalized);
|
|
304
|
+
// Next.js app directory server components
|
|
305
|
+
if (NEXTJS_SERVER_FILES.test(base))
|
|
306
|
+
return true;
|
|
307
|
+
// Next.js API route handlers (app/api/)
|
|
308
|
+
if (normalized.includes('app/api/') && /^route\.[jt]sx?$/.test(base))
|
|
309
|
+
return true;
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
299
312
|
/** Check if a file has a top-level error handler (global catch-all) */
|
|
300
313
|
function hasGlobalErrorHandling(content) {
|
|
301
314
|
// process.on('unhandledRejection'/'uncaughtException')
|
|
@@ -329,32 +342,56 @@ function checkUnhandledAsync(cwd, files) {
|
|
|
329
342
|
continue;
|
|
330
343
|
const lines = content.split('\n');
|
|
331
344
|
let unhandledCount = 0;
|
|
345
|
+
// Build a map of which lines are inside try blocks using brace tracking
|
|
346
|
+
const insideTry = new Set();
|
|
347
|
+
const tryStack = [];
|
|
348
|
+
let braceDepth = 0;
|
|
349
|
+
for (let i = 0; i < lines.length; i++) {
|
|
350
|
+
const line = lines[i];
|
|
351
|
+
// Detect try {
|
|
352
|
+
if (/\btry\s*\{/.test(line)) {
|
|
353
|
+
tryStack.push({ braceDepth });
|
|
354
|
+
}
|
|
355
|
+
// Count braces
|
|
356
|
+
for (const ch of line) {
|
|
357
|
+
if (ch === '{')
|
|
358
|
+
braceDepth++;
|
|
359
|
+
if (ch === '}') {
|
|
360
|
+
braceDepth--;
|
|
361
|
+
// Check if we're closing a try block
|
|
362
|
+
if (tryStack.length > 0 && braceDepth <= tryStack[tryStack.length - 1].braceDepth) {
|
|
363
|
+
tryStack.pop();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (tryStack.length > 0) {
|
|
368
|
+
insideTry.add(i);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
332
371
|
for (let i = 0; i < lines.length; i++) {
|
|
333
372
|
const line = lines[i];
|
|
334
373
|
// await without try/catch context — detect standalone awaits
|
|
335
|
-
// We look for: const/let/var x = await or just await on its own, not inside try
|
|
336
374
|
const hasAwait = /^\s*(?:const|let|var)\s+\w.*=\s*await\s+/.test(line) || /^\s*await\s+/.test(line);
|
|
337
375
|
if (!hasAwait)
|
|
338
376
|
continue;
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const contextText = contextLines.join('\n');
|
|
344
|
-
// Count try/catch blocks in context
|
|
345
|
-
const tryCount = (contextText.match(/\btry\s*\{/g) || []).length;
|
|
346
|
-
const catchCount = (contextText.match(/\bcatch\s*(?:\([^)]*\))?\s*\{/g) || []).length;
|
|
347
|
-
if (tryCount === 0 || catchCount === 0) {
|
|
377
|
+
// Skip if inside a try block (proper scope tracking)
|
|
378
|
+
if (insideTry.has(i))
|
|
379
|
+
continue;
|
|
380
|
+
{
|
|
348
381
|
// Check for .catch() chained on this or next line
|
|
349
382
|
const hasCatch = /\.catch\s*\(/.test(line) || (i + 1 < lines.length && /\.catch\s*\(/.test(lines[i + 1]));
|
|
350
383
|
// Check for .then(..., errorHandler) pattern
|
|
351
384
|
const hasThenError = /\.then\s*\([^,]+,\s*\w+/.test(line) || (i + 1 < lines.length && /\.then\s*\([^,]+,\s*\w+/.test(lines[i + 1]));
|
|
352
385
|
if (!hasCatch && !hasThenError) {
|
|
353
386
|
unhandledCount++;
|
|
387
|
+
// Downgrade Next.js server components to info (framework handles errors)
|
|
388
|
+
const isServerComp = isNextjsServerComponent(file);
|
|
354
389
|
if (unhandledCount <= 10) {
|
|
355
390
|
issues.push({
|
|
356
|
-
severity: 'warning',
|
|
357
|
-
message:
|
|
391
|
+
severity: isServerComp ? 'info' : 'warning',
|
|
392
|
+
message: isServerComp
|
|
393
|
+
? 'unhandled async: await without try/catch (Next.js server component — framework-managed)'
|
|
394
|
+
: 'unhandled async: await without try/catch',
|
|
358
395
|
file,
|
|
359
396
|
line: i + 1,
|
|
360
397
|
fixable: false,
|
|
@@ -385,8 +422,8 @@ export async function checkIntegrity(cwd, ignore) {
|
|
|
385
422
|
score -= hallucinatedIssues.length * 10;
|
|
386
423
|
score -= emptyCatchIssues.filter(i => i.severity === 'error').length * 8;
|
|
387
424
|
score -= stubbedTestIssues.filter(i => i.severity === 'error').length * 5;
|
|
388
|
-
// Unhandled async capped at -30
|
|
389
|
-
const unhandledErrors = unhandledAsyncIssues.length;
|
|
425
|
+
// Unhandled async capped at -30 (only count warnings, not info-downgraded ones)
|
|
426
|
+
const unhandledErrors = unhandledAsyncIssues.filter(i => i.severity === 'warning').length;
|
|
390
427
|
score -= Math.min(30, unhandledErrors * 3);
|
|
391
428
|
score = Math.max(0, Math.round(score));
|
|
392
429
|
// Summary parts
|