@safetnsr/vet 1.18.0 → 1.19.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 -0
- package/dist/categories.js +5 -4
- package/dist/checks/deep.d.ts +2 -0
- package/dist/checks/deep.js +276 -0
- package/dist/checks/hotspots.d.ts +2 -0
- package/dist/checks/hotspots.js +228 -0
- package/dist/checks/semantic.d.ts +2 -0
- package/dist/checks/semantic.js +181 -0
- package/dist/cli.js +10 -2
- package/dist/scorer.d.ts +1 -0
- package/dist/types.d.ts +1 -1
- package/package.json +5 -2
package/dist/categories.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export declare function buildCategories(checkMap: {
|
|
|
9
9
|
deps: CheckResult[];
|
|
10
10
|
architecture: CheckResult[];
|
|
11
11
|
aiready: CheckResult[];
|
|
12
|
+
history: CheckResult[];
|
|
12
13
|
}): CategoryResult[];
|
|
13
14
|
export declare function buildVetResult(project: string, categories: CategoryResult[]): VetResult;
|
package/dist/categories.js
CHANGED
|
@@ -16,12 +16,13 @@ export function toGrade(score) {
|
|
|
16
16
|
}
|
|
17
17
|
// ── Category weights ─────────────────────────────────────────────────────────
|
|
18
18
|
const WEIGHTS = {
|
|
19
|
-
security: 0.
|
|
20
|
-
integrity: 0.
|
|
21
|
-
debt: 0.
|
|
19
|
+
security: 0.20,
|
|
20
|
+
integrity: 0.20,
|
|
21
|
+
debt: 0.15,
|
|
22
22
|
deps: 0.10,
|
|
23
23
|
architecture: 0.10,
|
|
24
24
|
aiready: 0.10,
|
|
25
|
+
history: 0.15,
|
|
25
26
|
};
|
|
26
27
|
// ── Scoring floor for non-security checks ────────────────────────────────────
|
|
27
28
|
const SECURITY_CHECKS = new Set(['scan', 'secrets', 'permissions', 'owasp']);
|
|
@@ -68,7 +69,7 @@ function completenessMultiplier(categories) {
|
|
|
68
69
|
// ── Group checks into categories ─────────────────────────────────────────────
|
|
69
70
|
export function buildCategories(checkMap) {
|
|
70
71
|
const categories = [];
|
|
71
|
-
for (const name of ['security', 'integrity', 'debt', 'deps', 'architecture', 'aiready']) {
|
|
72
|
+
for (const name of ['security', 'integrity', 'debt', 'deps', 'architecture', 'aiready', 'history']) {
|
|
72
73
|
const checks = checkMap[name];
|
|
73
74
|
if (!checks || checks.length === 0)
|
|
74
75
|
continue;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { walkFiles, readFile, c } from '../util.js';
|
|
4
|
+
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs']);
|
|
5
|
+
function isSourceFile(f) {
|
|
6
|
+
const dot = f.lastIndexOf('.');
|
|
7
|
+
return dot !== -1 && SOURCE_EXTS.has(f.substring(dot));
|
|
8
|
+
}
|
|
9
|
+
function isTestFile(f) {
|
|
10
|
+
return /\.(test|spec)\.[jt]sx?$/.test(f) || f.includes('__tests__') || /(?:^|[/\\])tests?[/\\]/.test(f);
|
|
11
|
+
}
|
|
12
|
+
// ── AST-based function analysis ─────────────────────────────────────────────
|
|
13
|
+
const BRANCHING_KINDS = new Set([
|
|
14
|
+
ts.SyntaxKind.IfStatement,
|
|
15
|
+
ts.SyntaxKind.ForStatement,
|
|
16
|
+
ts.SyntaxKind.ForInStatement,
|
|
17
|
+
ts.SyntaxKind.ForOfStatement,
|
|
18
|
+
ts.SyntaxKind.WhileStatement,
|
|
19
|
+
ts.SyntaxKind.DoStatement,
|
|
20
|
+
ts.SyntaxKind.ConditionalExpression,
|
|
21
|
+
ts.SyntaxKind.CatchClause,
|
|
22
|
+
ts.SyntaxKind.CaseClause,
|
|
23
|
+
ts.SyntaxKind.BinaryExpression, // for && and || short-circuits
|
|
24
|
+
]);
|
|
25
|
+
function analyzeCatch(node) {
|
|
26
|
+
const block = node.block;
|
|
27
|
+
const stmts = block.statements;
|
|
28
|
+
const line = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
29
|
+
if (stmts.length === 0) {
|
|
30
|
+
return { line, isEmpty: true, isLazy: false, isRethrow: false };
|
|
31
|
+
}
|
|
32
|
+
const text = block.getText();
|
|
33
|
+
const isLazy = stmts.length === 1 && /console\.(log|error|warn)\s*\(/.test(text) && !text.includes('throw');
|
|
34
|
+
const isRethrow = text.includes('throw');
|
|
35
|
+
return { line, isEmpty: false, isLazy, isRethrow };
|
|
36
|
+
}
|
|
37
|
+
function analyzeFunction(node, file, src) {
|
|
38
|
+
let name = '';
|
|
39
|
+
let paramCount = 0;
|
|
40
|
+
let hasReturnType = false;
|
|
41
|
+
let body;
|
|
42
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)) {
|
|
43
|
+
name = node.name?.getText(src) || '(anonymous)';
|
|
44
|
+
paramCount = node.parameters.length;
|
|
45
|
+
hasReturnType = !!node.type;
|
|
46
|
+
body = node.body;
|
|
47
|
+
}
|
|
48
|
+
else if (ts.isMethodDeclaration(node)) {
|
|
49
|
+
name = node.name?.getText(src) || '(method)';
|
|
50
|
+
paramCount = node.parameters.length;
|
|
51
|
+
hasReturnType = !!node.type;
|
|
52
|
+
body = node.body;
|
|
53
|
+
}
|
|
54
|
+
else if (ts.isArrowFunction(node)) {
|
|
55
|
+
// Get name from parent variable declaration
|
|
56
|
+
const parent = node.parent;
|
|
57
|
+
if (ts.isVariableDeclaration(parent) && parent.name) {
|
|
58
|
+
name = parent.name.getText(src);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
name = '(arrow)';
|
|
62
|
+
}
|
|
63
|
+
paramCount = node.parameters.length;
|
|
64
|
+
hasReturnType = !!node.type;
|
|
65
|
+
body = node.body;
|
|
66
|
+
}
|
|
67
|
+
if (!body || !name)
|
|
68
|
+
return null;
|
|
69
|
+
const startLine = src.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
70
|
+
const endLine = src.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
|
|
71
|
+
const lineCount = endLine - startLine + 1;
|
|
72
|
+
if (lineCount < 5)
|
|
73
|
+
return null; // skip trivial functions
|
|
74
|
+
// Calculate cyclomatic complexity + max nesting + catch quality
|
|
75
|
+
let cyclomatic = 1;
|
|
76
|
+
let maxNesting = 0;
|
|
77
|
+
let currentNesting = 0;
|
|
78
|
+
let cognitive = 0;
|
|
79
|
+
const catchBlocks = [];
|
|
80
|
+
function walk(n, nesting) {
|
|
81
|
+
if (BRANCHING_KINDS.has(n.kind)) {
|
|
82
|
+
// Don't count && and || as branching for cyclomatic (too noisy)
|
|
83
|
+
if (n.kind === ts.SyntaxKind.BinaryExpression) {
|
|
84
|
+
const binExpr = n;
|
|
85
|
+
if (binExpr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
86
|
+
binExpr.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
87
|
+
cyclomatic++;
|
|
88
|
+
cognitive += 1; // no nesting increment for logical ops
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
cyclomatic++;
|
|
93
|
+
currentNesting = nesting + 1;
|
|
94
|
+
if (currentNesting > maxNesting)
|
|
95
|
+
maxNesting = currentNesting;
|
|
96
|
+
cognitive += 1 + nesting; // cognitive complexity: increment + nesting bonus
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (ts.isCatchClause(n)) {
|
|
100
|
+
catchBlocks.push(analyzeCatch(n));
|
|
101
|
+
}
|
|
102
|
+
const nextNesting = BRANCHING_KINDS.has(n.kind) && n.kind !== ts.SyntaxKind.BinaryExpression
|
|
103
|
+
? nesting + 1 : nesting;
|
|
104
|
+
ts.forEachChild(n, child => walk(child, nextNesting));
|
|
105
|
+
}
|
|
106
|
+
walk(body, 0);
|
|
107
|
+
return {
|
|
108
|
+
name, file, line: startLine, lineCount, paramCount,
|
|
109
|
+
cyclomatic, maxNesting, hasReturnType, catchBlocks,
|
|
110
|
+
cognitiveComplexity: cognitive,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// ── Naming analysis (heuristic, no ML) ──────────────────────────────────────
|
|
114
|
+
function isDescriptiveName(name) {
|
|
115
|
+
if (name.startsWith('('))
|
|
116
|
+
return 'unclear'; // anonymous
|
|
117
|
+
if (name.length <= 2)
|
|
118
|
+
return 'too-short';
|
|
119
|
+
// Single word with no verb pattern
|
|
120
|
+
if (!/[A-Z]/.test(name) && !name.includes('_') && name.length < 6)
|
|
121
|
+
return 'too-short';
|
|
122
|
+
return 'good';
|
|
123
|
+
}
|
|
124
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
125
|
+
export async function checkDeep(cwd) {
|
|
126
|
+
const allFiles = walkFiles(cwd);
|
|
127
|
+
const sourceFiles = allFiles.filter(f => isSourceFile(f) && !isTestFile(f));
|
|
128
|
+
if (sourceFiles.length < 3) {
|
|
129
|
+
return { name: 'deep', score: 100, maxScore: 100, summary: 'too few files', issues: [] };
|
|
130
|
+
}
|
|
131
|
+
const issues = [];
|
|
132
|
+
const allMetrics = [];
|
|
133
|
+
const t0 = Date.now();
|
|
134
|
+
for (const file of sourceFiles) {
|
|
135
|
+
const content = readFile(join(cwd, file));
|
|
136
|
+
if (!content)
|
|
137
|
+
continue;
|
|
138
|
+
try {
|
|
139
|
+
const src = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
|
|
140
|
+
function visit(node) {
|
|
141
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) ||
|
|
142
|
+
ts.isMethodDeclaration(node) || ts.isArrowFunction(node)) {
|
|
143
|
+
const metrics = analyzeFunction(node, file, src);
|
|
144
|
+
if (metrics)
|
|
145
|
+
allMetrics.push(metrics);
|
|
146
|
+
}
|
|
147
|
+
ts.forEachChild(node, visit);
|
|
148
|
+
}
|
|
149
|
+
visit(src);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Skip files that can't be parsed
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const elapsed = Date.now() - t0;
|
|
156
|
+
if (allMetrics.length === 0) {
|
|
157
|
+
return { name: 'deep', score: 100, maxScore: 100, summary: 'no functions to analyze', issues: [] };
|
|
158
|
+
}
|
|
159
|
+
// ── Cyclomatic complexity ─────────────────────────────────────────────────
|
|
160
|
+
const highComplexity = allMetrics.filter(f => f.cyclomatic > 10);
|
|
161
|
+
const veryHighComplexity = allMetrics.filter(f => f.cyclomatic > 20);
|
|
162
|
+
for (const func of veryHighComplexity.slice(0, 5)) {
|
|
163
|
+
issues.push({
|
|
164
|
+
severity: 'warning',
|
|
165
|
+
message: `high cyclomatic complexity: ${func.name} has complexity ${func.cyclomatic} (threshold: 10) — hard to test and modify`,
|
|
166
|
+
file: func.file, line: func.line,
|
|
167
|
+
fixable: true,
|
|
168
|
+
fixHint: 'break into smaller functions, use strategy pattern or lookup tables',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
for (const func of highComplexity.filter(f => f.cyclomatic <= 20).slice(0, 5)) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: 'info',
|
|
174
|
+
message: `moderate complexity: ${func.name} has complexity ${func.cyclomatic} (threshold: 10)`,
|
|
175
|
+
file: func.file, line: func.line,
|
|
176
|
+
fixable: true,
|
|
177
|
+
fixHint: 'consider extracting helper functions',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// ── Deep nesting ──────────────────────────────────────────────────────────
|
|
181
|
+
const deeplyNested = allMetrics.filter(f => f.maxNesting > 4);
|
|
182
|
+
for (const func of deeplyNested.slice(0, 5)) {
|
|
183
|
+
issues.push({
|
|
184
|
+
severity: 'warning',
|
|
185
|
+
message: `deep nesting: ${func.name} has ${func.maxNesting} levels of nesting — use early returns or extract functions`,
|
|
186
|
+
file: func.file, line: func.line,
|
|
187
|
+
fixable: true,
|
|
188
|
+
fixHint: 'use guard clauses (early return) to flatten nesting',
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// ── Catch block quality ───────────────────────────────────────────────────
|
|
192
|
+
const allCatches = allMetrics.flatMap(f => f.catchBlocks.map(cb => ({ ...cb, func: f })));
|
|
193
|
+
const emptyCatches = allCatches.filter(c => c.isEmpty);
|
|
194
|
+
const lazyCatches = allCatches.filter(c => c.isLazy);
|
|
195
|
+
for (const ec of emptyCatches.slice(0, 3)) {
|
|
196
|
+
issues.push({
|
|
197
|
+
severity: 'error',
|
|
198
|
+
message: `empty catch block in ${ec.func.name} — errors are silently swallowed`,
|
|
199
|
+
file: ec.func.file, line: ec.line,
|
|
200
|
+
fixable: true,
|
|
201
|
+
fixHint: 'at minimum: log the error, or re-throw with context',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
for (const lc of lazyCatches.slice(0, 3)) {
|
|
205
|
+
issues.push({
|
|
206
|
+
severity: 'warning',
|
|
207
|
+
message: `lazy error handling in ${lc.func.name} — catch only console.logs the error without recovery or rethrow`,
|
|
208
|
+
file: lc.func.file, line: lc.line,
|
|
209
|
+
fixable: true,
|
|
210
|
+
fixHint: 'add proper error handling: typed errors, retry logic, or graceful degradation',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// ── Cognitive complexity ──────────────────────────────────────────────────
|
|
214
|
+
const highCognitive = allMetrics.filter(f => f.cognitiveComplexity > 15);
|
|
215
|
+
for (const func of highCognitive.slice(0, 3)) {
|
|
216
|
+
issues.push({
|
|
217
|
+
severity: 'info',
|
|
218
|
+
message: `high cognitive complexity: ${func.name} has cognitive complexity ${func.cognitiveComplexity} — difficult for humans and AI agents to understand`,
|
|
219
|
+
file: func.file, line: func.line,
|
|
220
|
+
fixable: true,
|
|
221
|
+
fixHint: 'simplify control flow, extract well-named helper functions',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
// ── Parameter count ───────────────────────────────────────────────────────
|
|
225
|
+
const manyParams = allMetrics.filter(f => f.paramCount >= 5);
|
|
226
|
+
if (manyParams.length > 0) {
|
|
227
|
+
issues.push({
|
|
228
|
+
severity: 'info',
|
|
229
|
+
message: `${manyParams.length} function${manyParams.length !== 1 ? 's' : ''} with 5+ parameters: ${manyParams.slice(0, 3).map(f => f.name + '(' + f.paramCount + ')').join(', ')}`,
|
|
230
|
+
file: manyParams[0].file, line: manyParams[0].line,
|
|
231
|
+
fixable: true,
|
|
232
|
+
fixHint: 'use an options object instead of many positional parameters',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// ── Naming quality ────────────────────────────────────────────────────────
|
|
236
|
+
const poorNames = allMetrics.filter(f => isDescriptiveName(f.name) !== 'good');
|
|
237
|
+
if (poorNames.length > 3) {
|
|
238
|
+
issues.push({
|
|
239
|
+
severity: 'info',
|
|
240
|
+
message: `${poorNames.length} functions with unclear or too-short names: ${poorNames.slice(0, 3).map(f => '"' + f.name + '"').join(', ')}`,
|
|
241
|
+
file: poorNames[0].file,
|
|
242
|
+
fixable: true,
|
|
243
|
+
fixHint: 'use descriptive verb+noun function names (e.g., calculateTotal, validateUser)',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// ── Scoring ───────────────────────────────────────────────────────────────
|
|
247
|
+
const total = allMetrics.length;
|
|
248
|
+
// Complexity score: % of functions below threshold
|
|
249
|
+
const complexityOk = allMetrics.filter(f => f.cyclomatic <= 10).length;
|
|
250
|
+
const complexityScore = Math.round((complexityOk / total) * 100);
|
|
251
|
+
// Nesting score
|
|
252
|
+
const nestingOk = allMetrics.filter(f => f.maxNesting <= 4).length;
|
|
253
|
+
const nestingScore = Math.round((nestingOk / total) * 100);
|
|
254
|
+
// Error handling score
|
|
255
|
+
const errorScore = allCatches.length === 0 ? 100
|
|
256
|
+
: Math.max(20, 100 - emptyCatches.length * 20 - lazyCatches.length * 10);
|
|
257
|
+
// Naming score
|
|
258
|
+
const namingOk = allMetrics.filter(f => isDescriptiveName(f.name) === 'good').length;
|
|
259
|
+
const namingScore = Math.round((namingOk / total) * 100);
|
|
260
|
+
const score = Math.max(25, Math.round(complexityScore * 0.35 +
|
|
261
|
+
nestingScore * 0.25 +
|
|
262
|
+
errorScore * 0.25 +
|
|
263
|
+
namingScore * 0.15));
|
|
264
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
265
|
+
const parts = [];
|
|
266
|
+
parts.push(`${total} functions analyzed in ${elapsed}ms`);
|
|
267
|
+
if (highComplexity.length > 0)
|
|
268
|
+
parts.push(c.yellow + `${highComplexity.length} complex` + c.reset);
|
|
269
|
+
if (deeplyNested.length > 0)
|
|
270
|
+
parts.push(c.yellow + `${deeplyNested.length} deeply nested` + c.reset);
|
|
271
|
+
if (emptyCatches.length > 0)
|
|
272
|
+
parts.push(c.red + `${emptyCatches.length} empty catches` + c.reset);
|
|
273
|
+
if (lazyCatches.length > 0)
|
|
274
|
+
parts.push(c.yellow + `${lazyCatches.length} lazy catches` + c.reset);
|
|
275
|
+
return { name: 'deep', score, maxScore: 100, summary: parts.join(', '), issues };
|
|
276
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { walkFiles, readFile, c } from '../util.js';
|
|
4
|
+
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.py', '.go', '.rs', '.java']);
|
|
5
|
+
function isSourceFile(f) {
|
|
6
|
+
const dot = f.lastIndexOf('.');
|
|
7
|
+
return dot !== -1 && SOURCE_EXTS.has(f.substring(dot));
|
|
8
|
+
}
|
|
9
|
+
function getGitChurn(cwd, months = 6) {
|
|
10
|
+
const churn = new Map();
|
|
11
|
+
try {
|
|
12
|
+
const since = `--since="${months} months ago"`;
|
|
13
|
+
// Get commit count + authors per file
|
|
14
|
+
const log = execSync(`git log ${since} --format="%H %ae" --name-only --no-merges 2>/dev/null`, { cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 15_000 });
|
|
15
|
+
let currentAuthor = '';
|
|
16
|
+
const fileAuthors = new Map();
|
|
17
|
+
const fileCommits = new Map();
|
|
18
|
+
for (const line of log.split('\n')) {
|
|
19
|
+
if (!line.trim())
|
|
20
|
+
continue;
|
|
21
|
+
if (/^[0-9a-f]{40}\s/.test(line)) {
|
|
22
|
+
currentAuthor = line.split(' ').slice(1).join(' ');
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const file = line.trim();
|
|
26
|
+
fileCommits.set(file, (fileCommits.get(file) || 0) + 1);
|
|
27
|
+
if (!fileAuthors.has(file))
|
|
28
|
+
fileAuthors.set(file, new Set());
|
|
29
|
+
fileAuthors.get(file).add(currentAuthor);
|
|
30
|
+
}
|
|
31
|
+
// Get lines changed per file
|
|
32
|
+
const numstat = execSync(`git log ${since} --numstat --format="" --no-merges 2>/dev/null`, { cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 15_000 });
|
|
33
|
+
const fileLinesChanged = new Map();
|
|
34
|
+
for (const line of numstat.split('\n')) {
|
|
35
|
+
const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
36
|
+
if (!match)
|
|
37
|
+
continue;
|
|
38
|
+
const added = parseInt(match[1], 10);
|
|
39
|
+
const removed = parseInt(match[2], 10);
|
|
40
|
+
const file = match[3];
|
|
41
|
+
fileLinesChanged.set(file, (fileLinesChanged.get(file) || 0) + added + removed);
|
|
42
|
+
}
|
|
43
|
+
for (const [file, commits] of fileCommits) {
|
|
44
|
+
churn.set(file, {
|
|
45
|
+
file,
|
|
46
|
+
commits,
|
|
47
|
+
authors: fileAuthors.get(file)?.size || 1,
|
|
48
|
+
linesChanged: fileLinesChanged.get(file) || 0,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Not a git repo or git not available
|
|
54
|
+
}
|
|
55
|
+
return churn;
|
|
56
|
+
}
|
|
57
|
+
function getTemporalCoupling(cwd, months = 6) {
|
|
58
|
+
const couplings = [];
|
|
59
|
+
try {
|
|
60
|
+
const since = `--since="${months} months ago"`;
|
|
61
|
+
const log = execSync(`git log ${since} --format="COMMIT" --name-only --no-merges 2>/dev/null`, { cwd, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, timeout: 15_000 });
|
|
62
|
+
// Parse commits into file sets
|
|
63
|
+
const commits = [];
|
|
64
|
+
let current = [];
|
|
65
|
+
for (const line of log.split('\n')) {
|
|
66
|
+
if (line === 'COMMIT') {
|
|
67
|
+
if (current.length > 0)
|
|
68
|
+
commits.push(current);
|
|
69
|
+
current = [];
|
|
70
|
+
}
|
|
71
|
+
else if (line.trim()) {
|
|
72
|
+
const f = line.trim();
|
|
73
|
+
if (isSourceFile(f))
|
|
74
|
+
current.push(f);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (current.length > 0)
|
|
78
|
+
commits.push(current);
|
|
79
|
+
// Count co-changes (files that appear in same commit)
|
|
80
|
+
const pairCount = new Map();
|
|
81
|
+
const fileCommitCount = new Map();
|
|
82
|
+
for (const files of commits) {
|
|
83
|
+
if (files.length > 20)
|
|
84
|
+
continue; // skip huge commits (refactors/renames)
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
fileCommitCount.set(f, (fileCommitCount.get(f) || 0) + 1);
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < files.length; i++) {
|
|
89
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
90
|
+
const key = [files[i], files[j]].sort().join('::');
|
|
91
|
+
pairCount.set(key, (pairCount.get(key) || 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Find strong couplings
|
|
96
|
+
for (const [key, count] of pairCount) {
|
|
97
|
+
if (count < 3)
|
|
98
|
+
continue; // minimum 3 co-changes
|
|
99
|
+
const [f1, f2] = key.split('::');
|
|
100
|
+
const minCommits = Math.min(fileCommitCount.get(f1) || 1, fileCommitCount.get(f2) || 1);
|
|
101
|
+
const strength = count / minCommits;
|
|
102
|
+
if (strength > 0.5) { // >50% of the time they change together
|
|
103
|
+
couplings.push({ file1: f1, file2: f2, cochanges: count, couplingStrength: strength });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
couplings.sort((a, b) => b.couplingStrength - a.couplingStrength);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Not a git repo
|
|
110
|
+
}
|
|
111
|
+
return couplings;
|
|
112
|
+
}
|
|
113
|
+
// ── Complexity proxy: indentation depth ─────────────────────────────────────
|
|
114
|
+
function getIndentationComplexity(content) {
|
|
115
|
+
const lines = content.split('\n');
|
|
116
|
+
let totalDepth = 0;
|
|
117
|
+
let maxDepth = 0;
|
|
118
|
+
let measured = 0;
|
|
119
|
+
for (const line of lines) {
|
|
120
|
+
if (!line.trim())
|
|
121
|
+
continue;
|
|
122
|
+
const match = line.match(/^(\s+)/);
|
|
123
|
+
if (match) {
|
|
124
|
+
const depth = match[1].includes('\t')
|
|
125
|
+
? match[1].split('\t').length - 1
|
|
126
|
+
: Math.floor(match[1].length / 2);
|
|
127
|
+
totalDepth += depth;
|
|
128
|
+
if (depth > maxDepth)
|
|
129
|
+
maxDepth = depth;
|
|
130
|
+
}
|
|
131
|
+
measured++;
|
|
132
|
+
}
|
|
133
|
+
return measured > 0 ? totalDepth / measured : 0;
|
|
134
|
+
}
|
|
135
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
136
|
+
export async function checkHotspots(cwd) {
|
|
137
|
+
const issues = [];
|
|
138
|
+
const t0 = Date.now();
|
|
139
|
+
const churn = getGitChurn(cwd);
|
|
140
|
+
if (churn.size === 0) {
|
|
141
|
+
return { name: 'hotspots', score: 100, maxScore: 100, summary: 'no git history', issues: [] };
|
|
142
|
+
}
|
|
143
|
+
const allFiles = walkFiles(cwd);
|
|
144
|
+
const sourceFiles = allFiles.filter(f => isSourceFile(f));
|
|
145
|
+
// Calculate complexity for each file (indentation-based, fast)
|
|
146
|
+
const fileComplexity = new Map();
|
|
147
|
+
for (const file of sourceFiles) {
|
|
148
|
+
const content = readFile(join(cwd, file));
|
|
149
|
+
if (!content)
|
|
150
|
+
continue;
|
|
151
|
+
fileComplexity.set(file, getIndentationComplexity(content));
|
|
152
|
+
}
|
|
153
|
+
const hotspots = [];
|
|
154
|
+
for (const [file, ch] of churn) {
|
|
155
|
+
const complexity = fileComplexity.get(file);
|
|
156
|
+
if (complexity === undefined)
|
|
157
|
+
continue;
|
|
158
|
+
// Normalize: risk = log(commits) × complexity
|
|
159
|
+
const risk = Math.log2(ch.commits + 1) * complexity;
|
|
160
|
+
hotspots.push({ file, commits: ch.commits, complexity, risk, authors: ch.authors });
|
|
161
|
+
}
|
|
162
|
+
hotspots.sort((a, b) => b.risk - a.risk);
|
|
163
|
+
// Top hotspots are issues
|
|
164
|
+
const topHotspots = hotspots.slice(0, 5);
|
|
165
|
+
for (const hs of topHotspots) {
|
|
166
|
+
if (hs.risk < 5)
|
|
167
|
+
continue; // skip low-risk files
|
|
168
|
+
issues.push({
|
|
169
|
+
severity: hs.risk > 20 ? 'warning' : 'info',
|
|
170
|
+
message: `hotspot: ${hs.file} — ${hs.commits} commits, complexity ${hs.complexity.toFixed(1)}, risk score ${hs.risk.toFixed(1)}${hs.authors > 3 ? `, ${hs.authors} authors` : ''}`,
|
|
171
|
+
file: hs.file,
|
|
172
|
+
fixable: false,
|
|
173
|
+
fixHint: 'high-churn complex files are bug magnets — prioritize refactoring and add tests',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ── Temporal coupling ─────────────────────────────────────────────────────
|
|
177
|
+
const couplings = getTemporalCoupling(cwd);
|
|
178
|
+
// Filter out obvious couplings (same directory, test+source)
|
|
179
|
+
const interestingCouplings = couplings.filter(cp => {
|
|
180
|
+
const dir1 = cp.file1.split('/').slice(0, -1).join('/');
|
|
181
|
+
const dir2 = cp.file2.split('/').slice(0, -1).join('/');
|
|
182
|
+
// Same directory coupling is expected
|
|
183
|
+
if (dir1 === dir2)
|
|
184
|
+
return false;
|
|
185
|
+
// Test+source coupling is expected
|
|
186
|
+
if (cp.file1.includes('test') || cp.file2.includes('test'))
|
|
187
|
+
return false;
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
for (const cp of interestingCouplings.slice(0, 3)) {
|
|
191
|
+
issues.push({
|
|
192
|
+
severity: 'info',
|
|
193
|
+
message: `temporal coupling: ${cp.file1} ↔ ${cp.file2} change together ${Math.round(cp.couplingStrength * 100)}% of the time (${cp.cochanges} co-changes) — possible hidden dependency`,
|
|
194
|
+
file: cp.file1,
|
|
195
|
+
fixable: false,
|
|
196
|
+
fixHint: 'investigate if these files share a concept that should be co-located or abstracted',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// ── Multi-author hotfiles ─────────────────────────────────────────────────
|
|
200
|
+
const manyAuthors = hotspots.filter(h => h.authors >= 5).slice(0, 3);
|
|
201
|
+
for (const ma of manyAuthors) {
|
|
202
|
+
issues.push({
|
|
203
|
+
severity: 'info',
|
|
204
|
+
message: `knowledge diffusion: ${ma.file} touched by ${ma.authors} authors — high bus factor risk if not well-documented`,
|
|
205
|
+
file: ma.file,
|
|
206
|
+
fixable: false,
|
|
207
|
+
fixHint: 'ensure this file has clear documentation and tests — many people modify it',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
const elapsed = Date.now() - t0;
|
|
211
|
+
// ── Scoring ───────────────────────────────────────────────────────────────
|
|
212
|
+
// Score based on how many high-risk hotspots exist relative to codebase size
|
|
213
|
+
const highRiskCount = hotspots.filter(h => h.risk > 20).length;
|
|
214
|
+
const riskRatio = sourceFiles.length > 0 ? highRiskCount / sourceFiles.length : 0;
|
|
215
|
+
const hotspotScore = Math.max(25, Math.round(100 - riskRatio * 500));
|
|
216
|
+
// Temporal coupling penalty
|
|
217
|
+
const couplingPenalty = Math.min(30, interestingCouplings.length * 5);
|
|
218
|
+
const score = Math.max(25, hotspotScore - couplingPenalty);
|
|
219
|
+
const parts = [];
|
|
220
|
+
parts.push(`${churn.size} files in git history, ${elapsed}ms`);
|
|
221
|
+
if (topHotspots.length > 0 && topHotspots[0].risk > 5) {
|
|
222
|
+
parts.push(c.yellow + `top hotspot: ${topHotspots[0].file.split('/').pop()} (risk ${topHotspots[0].risk.toFixed(0)})` + c.reset);
|
|
223
|
+
}
|
|
224
|
+
if (interestingCouplings.length > 0) {
|
|
225
|
+
parts.push(`${interestingCouplings.length} temporal couplings`);
|
|
226
|
+
}
|
|
227
|
+
return { name: 'hotspots', score, maxScore: 100, summary: parts.join(', '), issues };
|
|
228
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { walkFiles, readFile, c } from '../util.js';
|
|
3
|
+
const SOURCE_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs']);
|
|
4
|
+
function isSourceFile(f) {
|
|
5
|
+
const dot = f.lastIndexOf('.');
|
|
6
|
+
return dot !== -1 && SOURCE_EXTS.has(f.substring(dot));
|
|
7
|
+
}
|
|
8
|
+
function isTestFile(f) {
|
|
9
|
+
return /\.(test|spec)\.[jt]sx?$/.test(f) || f.includes('__tests__') || /(?:^|[/\\])tests?[/\\]/.test(f);
|
|
10
|
+
}
|
|
11
|
+
const ANTI_PATTERNS = [
|
|
12
|
+
{
|
|
13
|
+
name: 'lazy-error-handling',
|
|
14
|
+
description: 'catches error but only logs it without recovery or rethrow',
|
|
15
|
+
severity: 'warning',
|
|
16
|
+
fixHint: 'add proper error recovery, rethrow with context, or use typed error classes',
|
|
17
|
+
embedding_text: 'try { doSomething(); } catch(e) { console.log(e); } try { await fetch(url); } catch(err) { console.error(err); return null; }',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'any-abuse',
|
|
21
|
+
description: 'excessive use of TypeScript any type to bypass type checking',
|
|
22
|
+
severity: 'warning',
|
|
23
|
+
fixHint: 'replace any with specific types, use unknown for truly unknown types',
|
|
24
|
+
embedding_text: 'function process(data: any, config: any): any { return (data as any).map((x: any) => x); const result: any = {}; }',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'callback-hell',
|
|
28
|
+
description: 'deeply nested callbacks or promise chains',
|
|
29
|
+
severity: 'info',
|
|
30
|
+
fixHint: 'refactor to async/await',
|
|
31
|
+
embedding_text: 'getData(function(a) { getMore(a, function(b) { getEvenMore(b, function(c) { process(c, function(d) { done(d); }); }); }); });',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'empty-function',
|
|
35
|
+
description: 'function with no implementation or only comments/todos',
|
|
36
|
+
severity: 'warning',
|
|
37
|
+
fixHint: 'implement the function or remove it',
|
|
38
|
+
embedding_text: 'function handleSubmit() { /* TODO: implement */ } function processData(input) { // not implemented yet return input; }',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'string-heavy-logic',
|
|
42
|
+
description: 'business logic driven by string comparisons instead of enums or types',
|
|
43
|
+
severity: 'info',
|
|
44
|
+
fixHint: 'use enums, union types, or constants instead of string literals',
|
|
45
|
+
embedding_text: 'if (status === "pending") { } else if (status === "active") { } else if (status === "cancelled") { } else if (status === "completed") { }',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
function extractFunctions(file, content) {
|
|
49
|
+
const funcs = [];
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
const funcStartRe = /^(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/;
|
|
52
|
+
const arrowRe = /^(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(/;
|
|
53
|
+
const methodRe = /^\s+(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*(?::\s*[^{]+)?\s*\{/;
|
|
54
|
+
for (let i = 0; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i];
|
|
56
|
+
const match = line.match(funcStartRe) || line.match(arrowRe) || line.match(methodRe);
|
|
57
|
+
if (!match)
|
|
58
|
+
continue;
|
|
59
|
+
const name = match[1];
|
|
60
|
+
if (!name || ['if', 'for', 'while', 'switch', 'catch'].includes(name))
|
|
61
|
+
continue;
|
|
62
|
+
// Find end of function
|
|
63
|
+
let depth = 0, started = false, endLine = i;
|
|
64
|
+
for (let j = i; j < lines.length && j < i + 200; j++) {
|
|
65
|
+
for (const ch of lines[j]) {
|
|
66
|
+
if (ch === '{') {
|
|
67
|
+
depth++;
|
|
68
|
+
started = true;
|
|
69
|
+
}
|
|
70
|
+
if (ch === '}')
|
|
71
|
+
depth--;
|
|
72
|
+
}
|
|
73
|
+
if (started && depth <= 0) {
|
|
74
|
+
endLine = j;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const lineCount = endLine - i + 1;
|
|
79
|
+
if (lineCount < 8)
|
|
80
|
+
continue; // skip tiny functions
|
|
81
|
+
const body = lines.slice(i, endLine + 1).join('\n').slice(0, 400);
|
|
82
|
+
funcs.push({ name, file, line: i + 1, body });
|
|
83
|
+
}
|
|
84
|
+
return funcs;
|
|
85
|
+
}
|
|
86
|
+
// ── Cosine similarity ───────────────────────────────────────────────────────
|
|
87
|
+
function cosine(a, b) {
|
|
88
|
+
let dot = 0, na = 0, nb = 0;
|
|
89
|
+
for (let i = 0; i < a.length; i++) {
|
|
90
|
+
dot += a[i] * b[i];
|
|
91
|
+
na += a[i] * a[i];
|
|
92
|
+
nb += b[i] * b[i];
|
|
93
|
+
}
|
|
94
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
95
|
+
}
|
|
96
|
+
// ── Main check ───────────────────────────────────────────────────────────────
|
|
97
|
+
export async function checkSemantic(cwd) {
|
|
98
|
+
const allFiles = walkFiles(cwd);
|
|
99
|
+
const sourceFiles = allFiles.filter(f => isSourceFile(f) && !isTestFile(f));
|
|
100
|
+
if (sourceFiles.length < 3) {
|
|
101
|
+
return { name: 'semantic', score: 100, maxScore: 100, summary: 'too few files', issues: [] };
|
|
102
|
+
}
|
|
103
|
+
// Extract functions
|
|
104
|
+
const allFuncs = [];
|
|
105
|
+
for (const file of sourceFiles) {
|
|
106
|
+
const content = readFile(join(cwd, file));
|
|
107
|
+
if (!content)
|
|
108
|
+
continue;
|
|
109
|
+
allFuncs.push(...extractFunctions(file, content));
|
|
110
|
+
}
|
|
111
|
+
if (allFuncs.length === 0) {
|
|
112
|
+
return { name: 'semantic', score: 100, maxScore: 100, summary: 'no functions', issues: [] };
|
|
113
|
+
}
|
|
114
|
+
// Cap at 100 longest functions for performance
|
|
115
|
+
const funcsToAnalyze = allFuncs
|
|
116
|
+
.sort((a, b) => b.body.length - a.body.length)
|
|
117
|
+
.slice(0, 100);
|
|
118
|
+
const issues = [];
|
|
119
|
+
const t0 = Date.now();
|
|
120
|
+
let matchCount = 0;
|
|
121
|
+
try {
|
|
122
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
123
|
+
const extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', {
|
|
124
|
+
dtype: 'q8',
|
|
125
|
+
});
|
|
126
|
+
// Embed anti-patterns once
|
|
127
|
+
const patternEmbeddings = [];
|
|
128
|
+
for (const pattern of ANTI_PATTERNS) {
|
|
129
|
+
const result = await extractor(pattern.embedding_text, { pooling: 'mean', normalize: true });
|
|
130
|
+
patternEmbeddings.push({ pattern, embedding: new Float32Array(result.data) });
|
|
131
|
+
}
|
|
132
|
+
// Embed and compare each function
|
|
133
|
+
const THRESHOLD = 0.40; // similarity threshold — code-to-code embeddings
|
|
134
|
+
for (const func of funcsToAnalyze) {
|
|
135
|
+
const result = await extractor(func.body, { pooling: 'mean', normalize: true });
|
|
136
|
+
const funcEmb = new Float32Array(result.data);
|
|
137
|
+
for (const { pattern, embedding } of patternEmbeddings) {
|
|
138
|
+
const sim = cosine(funcEmb, embedding);
|
|
139
|
+
if (sim > THRESHOLD) {
|
|
140
|
+
matchCount++;
|
|
141
|
+
issues.push({
|
|
142
|
+
severity: pattern.severity,
|
|
143
|
+
message: `semantic match: ${func.name} matches "${pattern.name}" pattern (${Math.round(sim * 100)}% similarity) — ${pattern.description}`,
|
|
144
|
+
file: func.file,
|
|
145
|
+
line: func.line,
|
|
146
|
+
fixable: true,
|
|
147
|
+
fixHint: pattern.fixHint,
|
|
148
|
+
});
|
|
149
|
+
break; // one match per function
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
// transformers.js not available or model download failed
|
|
156
|
+
return {
|
|
157
|
+
name: 'semantic',
|
|
158
|
+
score: 100,
|
|
159
|
+
maxScore: 100,
|
|
160
|
+
summary: `semantic analysis unavailable: ${err instanceof Error ? err.message : 'unknown'}`,
|
|
161
|
+
issues: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const elapsed = Date.now() - t0;
|
|
165
|
+
// Score based on % of functions matching anti-patterns
|
|
166
|
+
const matchRate = funcsToAnalyze.length > 0 ? matchCount / funcsToAnalyze.length : 0;
|
|
167
|
+
const score = Math.max(25, Math.round(100 - matchRate * 200));
|
|
168
|
+
const parts = [];
|
|
169
|
+
parts.push(`${funcsToAnalyze.length} functions scanned in ${elapsed}ms`);
|
|
170
|
+
if (matchCount > 0)
|
|
171
|
+
parts.push(c.yellow + `${matchCount} anti-pattern matches` + c.reset);
|
|
172
|
+
else
|
|
173
|
+
parts.push('no anti-patterns detected');
|
|
174
|
+
return {
|
|
175
|
+
name: 'semantic',
|
|
176
|
+
score,
|
|
177
|
+
maxScore: 100,
|
|
178
|
+
summary: parts.join(', '),
|
|
179
|
+
issues,
|
|
180
|
+
};
|
|
181
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,9 @@ import { checkDebt } from './checks/debt.js';
|
|
|
15
15
|
import { checkIntegrity } from './checks/integrity.js';
|
|
16
16
|
import { checkArchitecture } from './checks/architecture.js';
|
|
17
17
|
import { checkAIReady } from './checks/aiready.js';
|
|
18
|
+
import { checkDeep } from './checks/deep.js';
|
|
19
|
+
import { checkSemantic } from './checks/semantic.js';
|
|
20
|
+
import { checkHotspots } from './checks/hotspots.js';
|
|
18
21
|
import { checkReceipt, runReceiptCommand } from './checks/receipt.js';
|
|
19
22
|
import { checkMemory } from './checks/memory.js';
|
|
20
23
|
import { checkVerify } from './checks/verify.js';
|
|
@@ -33,6 +36,7 @@ import { reportPretty, reportJSON, reportBadge } from './reporter.js';
|
|
|
33
36
|
import { clearCache } from './file-cache.js';
|
|
34
37
|
const args = process.argv.slice(2);
|
|
35
38
|
const flags = new Set(args.filter(a => a.startsWith('-') && !a.startsWith('--since')));
|
|
39
|
+
const deepMode = flags.has('--deep');
|
|
36
40
|
const flagMap = new Map();
|
|
37
41
|
// Parse --since=value or --since value, --max-files=value
|
|
38
42
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -322,7 +326,7 @@ async function runChecks() {
|
|
|
322
326
|
}
|
|
323
327
|
}
|
|
324
328
|
// Run ALL independent checks in parallel
|
|
325
|
-
const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult,] = await Promise.all([
|
|
329
|
+
const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, subsidyResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, bloatResult, guardResult, explainResult, architectureResult, aireadyResult, deepResult, semanticResult, hotspotsResult,] = await Promise.all([
|
|
326
330
|
withTimeout('scan', () => checkScan(cwd)),
|
|
327
331
|
withTimeout('secrets', () => checkSecrets(cwd)),
|
|
328
332
|
withTimeout('config', () => checkConfig(cwd, ignore)),
|
|
@@ -346,6 +350,9 @@ async function runChecks() {
|
|
|
346
350
|
withTimeout('explain', () => checkExplain(cwd, since)),
|
|
347
351
|
withTimeout('architecture', () => checkArchitecture(cwd)),
|
|
348
352
|
withTimeout('aiready', () => checkAIReady(cwd)),
|
|
353
|
+
withTimeout('deep', () => checkDeep(cwd), 60_000),
|
|
354
|
+
withTimeout('semantic', () => checkSemantic(cwd), 60_000),
|
|
355
|
+
withTimeout('hotspots', () => checkHotspots(cwd), 30_000),
|
|
349
356
|
]);
|
|
350
357
|
// Git-dependent checks (diff + history) — parallel with each other
|
|
351
358
|
const [diffResult, historyResult] = await Promise.all([
|
|
@@ -360,7 +367,8 @@ async function runChecks() {
|
|
|
360
367
|
debt: [readyResult, historyResult, debtResult, bloatResult],
|
|
361
368
|
deps: [depsResult],
|
|
362
369
|
architecture: [architectureResult],
|
|
363
|
-
aiready: [aireadyResult],
|
|
370
|
+
aiready: [aireadyResult, deepResult, semanticResult],
|
|
371
|
+
history: [hotspotsResult],
|
|
364
372
|
});
|
|
365
373
|
}
|
|
366
374
|
catch (e) {
|
package/dist/scorer.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface Issue {
|
|
|
14
14
|
fixHint?: string;
|
|
15
15
|
}
|
|
16
16
|
export interface CategoryResult {
|
|
17
|
-
name: 'security' | 'integrity' | 'debt' | 'deps' | 'architecture' | 'aiready';
|
|
17
|
+
name: 'security' | 'integrity' | 'debt' | 'deps' | 'architecture' | 'aiready' | 'history';
|
|
18
18
|
score: number;
|
|
19
19
|
weight: number;
|
|
20
20
|
checks: CheckResult[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@safetnsr/vet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.19.1",
|
|
4
4
|
"description": "vet your AI-generated code — one command, one score card, one letter grade",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^20.0.0",
|
|
37
37
|
"tsx": "^4.21.0",
|
|
38
|
-
"typescript": "^5.
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
39
|
},
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
44
|
"@safetnsr/model-graveyard": "^0.2.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@huggingface/transformers": "^3.8.1"
|
|
45
48
|
}
|
|
46
49
|
}
|