@safetnsr/vet 1.21.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/checks/split.d.ts +3 -0
- package/dist/checks/split.js +325 -0
- package/dist/cli.js +17 -3
- package/package.json +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { gitExec, c } from '../util.js';
|
|
2
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
3
|
+
const CONFIG_FILES = new Set([
|
|
4
|
+
'package.json', 'package-lock.json', 'tsconfig.json', 'tsconfig.build.json',
|
|
5
|
+
'.eslintrc', '.eslintrc.json', '.eslintrc.js', '.prettierrc', '.prettierrc.json',
|
|
6
|
+
'.env', '.env.example', '.env.local', '.gitignore', '.npmignore',
|
|
7
|
+
'jest.config.js', 'jest.config.ts', 'vitest.config.ts', 'vite.config.ts',
|
|
8
|
+
'webpack.config.js', 'rollup.config.js', 'esbuild.config.js',
|
|
9
|
+
'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
|
|
10
|
+
'.dockerignore', 'Makefile', '.editorconfig',
|
|
11
|
+
]);
|
|
12
|
+
const TEST_PATTERNS = [/^test\//, /^tests\//, /^__tests__\//, /\.test\./, /\.spec\./];
|
|
13
|
+
const FIX_INDICATORS = /\bfix(es|ed)?\b|\bbug\b|\berror\b|\bcrash\b|\bpatch\b/i;
|
|
14
|
+
// ── Diff parsing ─────────────────────────────────────────────────────────────
|
|
15
|
+
function parseDiff(diffOutput) {
|
|
16
|
+
if (!diffOutput.trim())
|
|
17
|
+
return [];
|
|
18
|
+
const hunks = [];
|
|
19
|
+
const fileDiffs = diffOutput.split(/^diff --git /m).filter(Boolean);
|
|
20
|
+
for (const fileDiff of fileDiffs) {
|
|
21
|
+
const lines = fileDiff.split('\n');
|
|
22
|
+
// Parse file paths
|
|
23
|
+
const headerMatch = lines[0]?.match(/a\/(.+?) b\/(.+)/);
|
|
24
|
+
if (!headerMatch)
|
|
25
|
+
continue;
|
|
26
|
+
const file = headerMatch[2];
|
|
27
|
+
// Detect binary
|
|
28
|
+
if (fileDiff.includes('Binary files')) {
|
|
29
|
+
hunks.push({
|
|
30
|
+
file, oldStart: 0, oldCount: 0, newStart: 0, newCount: 0,
|
|
31
|
+
content: '', isNew: false, isDeleted: false, isRenamed: false, isBinary: true,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const isNew = fileDiff.includes('new file mode');
|
|
36
|
+
const isDeleted = fileDiff.includes('deleted file mode');
|
|
37
|
+
const isRenamed = fileDiff.includes('rename from') || fileDiff.includes('similarity index');
|
|
38
|
+
// Parse individual hunks within the file
|
|
39
|
+
const hunkHeaderRE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
40
|
+
let currentHunkLines = [];
|
|
41
|
+
let currentMatch = null;
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
const match = line.match(hunkHeaderRE);
|
|
44
|
+
if (match) {
|
|
45
|
+
// Save previous hunk
|
|
46
|
+
if (currentMatch) {
|
|
47
|
+
hunks.push({
|
|
48
|
+
file,
|
|
49
|
+
oldStart: parseInt(currentMatch[1], 10),
|
|
50
|
+
oldCount: parseInt(currentMatch[2] || '1', 10),
|
|
51
|
+
newStart: parseInt(currentMatch[3], 10),
|
|
52
|
+
newCount: parseInt(currentMatch[4] || '1', 10),
|
|
53
|
+
content: currentHunkLines.join('\n'),
|
|
54
|
+
isNew, isDeleted, isRenamed, isBinary: false,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
currentMatch = match;
|
|
58
|
+
currentHunkLines = [];
|
|
59
|
+
}
|
|
60
|
+
else if (currentMatch) {
|
|
61
|
+
currentHunkLines.push(line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Save last hunk
|
|
65
|
+
if (currentMatch) {
|
|
66
|
+
hunks.push({
|
|
67
|
+
file,
|
|
68
|
+
oldStart: parseInt(currentMatch[1], 10),
|
|
69
|
+
oldCount: parseInt(currentMatch[2] || '1', 10),
|
|
70
|
+
newStart: parseInt(currentMatch[3], 10),
|
|
71
|
+
newCount: parseInt(currentMatch[4] || '1', 10),
|
|
72
|
+
content: currentHunkLines.join('\n'),
|
|
73
|
+
isNew, isDeleted, isRenamed, isBinary: false,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else if (isNew || isDeleted) {
|
|
77
|
+
// File with no hunks (e.g., empty new file)
|
|
78
|
+
hunks.push({
|
|
79
|
+
file, oldStart: 0, oldCount: 0, newStart: 0, newCount: 0,
|
|
80
|
+
content: '', isNew, isDeleted, isRenamed, isBinary: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return hunks;
|
|
85
|
+
}
|
|
86
|
+
// ── Clustering ───────────────────────────────────────────────────────────────
|
|
87
|
+
function isTestFile(file) {
|
|
88
|
+
return TEST_PATTERNS.some(p => p.test(file));
|
|
89
|
+
}
|
|
90
|
+
function isConfigFile(file) {
|
|
91
|
+
const basename = file.split('/').pop() || file;
|
|
92
|
+
return CONFIG_FILES.has(basename) || basename.startsWith('.');
|
|
93
|
+
}
|
|
94
|
+
function getClusterKey(file) {
|
|
95
|
+
if (isTestFile(file))
|
|
96
|
+
return 'test';
|
|
97
|
+
if (isConfigFile(file))
|
|
98
|
+
return 'config';
|
|
99
|
+
// Group by first directory
|
|
100
|
+
const parts = file.split('/');
|
|
101
|
+
if (parts.length > 1)
|
|
102
|
+
return `src:${parts[0]}`;
|
|
103
|
+
return 'src:root';
|
|
104
|
+
}
|
|
105
|
+
function generateCommitMessage(cluster) {
|
|
106
|
+
const fileList = cluster.files.length <= 3
|
|
107
|
+
? cluster.files.map(f => f.split('/').pop()).join(', ')
|
|
108
|
+
: `${cluster.files.length} files`;
|
|
109
|
+
// Test cluster
|
|
110
|
+
if (cluster.prefix === 'test') {
|
|
111
|
+
return `test: update ${fileList}`;
|
|
112
|
+
}
|
|
113
|
+
// Config cluster
|
|
114
|
+
if (cluster.prefix === 'config') {
|
|
115
|
+
return `chore: update ${fileList}`;
|
|
116
|
+
}
|
|
117
|
+
// Check if all files are new
|
|
118
|
+
const allNew = cluster.hunks.every(h => h.isNew);
|
|
119
|
+
if (allNew) {
|
|
120
|
+
return `feat: add ${fileList}`;
|
|
121
|
+
}
|
|
122
|
+
// Check if all files are deleted
|
|
123
|
+
const allDeleted = cluster.hunks.every(h => h.isDeleted);
|
|
124
|
+
if (allDeleted) {
|
|
125
|
+
return `refactor: remove ${fileList}`;
|
|
126
|
+
}
|
|
127
|
+
// Check hunk content for fix indicators
|
|
128
|
+
const allContent = cluster.hunks.map(h => h.content).join('\n');
|
|
129
|
+
if (FIX_INDICATORS.test(allContent)) {
|
|
130
|
+
return `fix: update ${fileList}`;
|
|
131
|
+
}
|
|
132
|
+
return `refactor: update ${fileList}`;
|
|
133
|
+
}
|
|
134
|
+
function clusterHunks(hunks) {
|
|
135
|
+
// Filter out binary files
|
|
136
|
+
const nonBinary = hunks.filter(h => !h.isBinary);
|
|
137
|
+
if (nonBinary.length === 0)
|
|
138
|
+
return [];
|
|
139
|
+
const groups = new Map();
|
|
140
|
+
for (const hunk of nonBinary) {
|
|
141
|
+
const key = getClusterKey(hunk.file);
|
|
142
|
+
if (!groups.has(key))
|
|
143
|
+
groups.set(key, []);
|
|
144
|
+
groups.get(key).push(hunk);
|
|
145
|
+
}
|
|
146
|
+
const clusters = [];
|
|
147
|
+
for (const [key, groupHunks] of groups) {
|
|
148
|
+
const files = [...new Set(groupHunks.map(h => h.file))];
|
|
149
|
+
const cluster = {
|
|
150
|
+
name: key,
|
|
151
|
+
prefix: key.startsWith('src:') ? 'src' : key,
|
|
152
|
+
files,
|
|
153
|
+
hunks: groupHunks,
|
|
154
|
+
commitMessage: '',
|
|
155
|
+
};
|
|
156
|
+
cluster.commitMessage = generateCommitMessage(cluster);
|
|
157
|
+
clusters.push(cluster);
|
|
158
|
+
}
|
|
159
|
+
// Sort: config first, then src, then test
|
|
160
|
+
clusters.sort((a, b) => {
|
|
161
|
+
const order = (c) => c.prefix === 'config' ? 0 : c.prefix === 'src' ? 1 : 2;
|
|
162
|
+
return order(a) - order(b);
|
|
163
|
+
});
|
|
164
|
+
return clusters;
|
|
165
|
+
}
|
|
166
|
+
// ── Score calculation ────────────────────────────────────────────────────────
|
|
167
|
+
function analyzeCommit(cwd, sha) {
|
|
168
|
+
const diff = gitExec(['diff', `${sha}~1`, sha], cwd);
|
|
169
|
+
if (!diff)
|
|
170
|
+
return { fileCount: 0, clusterCount: 0, totalHunks: 0 };
|
|
171
|
+
const hunks = parseDiff(diff);
|
|
172
|
+
const nonBinary = hunks.filter(h => !h.isBinary);
|
|
173
|
+
const clusters = clusterHunks(nonBinary);
|
|
174
|
+
const files = new Set(nonBinary.map(h => h.file));
|
|
175
|
+
return { fileCount: files.size, clusterCount: clusters.length, totalHunks: nonBinary.length };
|
|
176
|
+
}
|
|
177
|
+
// ── Main check (for scorecard) ───────────────────────────────────────────────
|
|
178
|
+
export function checkSplit(cwd) {
|
|
179
|
+
const issues = [];
|
|
180
|
+
// Get recent commits (last 10)
|
|
181
|
+
const log = gitExec(['log', '--oneline', '-10', '--format=%H'], cwd);
|
|
182
|
+
if (!log) {
|
|
183
|
+
return {
|
|
184
|
+
name: 'split',
|
|
185
|
+
score: 100,
|
|
186
|
+
maxScore: 100,
|
|
187
|
+
issues: [{ severity: 'info', message: 'no commits to analyze', fixable: false }],
|
|
188
|
+
summary: 'no commits',
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const shas = log.split('\n').filter(Boolean);
|
|
192
|
+
let totalPenalty = 0;
|
|
193
|
+
let analyzedCount = 0;
|
|
194
|
+
for (const sha of shas) {
|
|
195
|
+
// Check if commit has a parent
|
|
196
|
+
const parent = gitExec(['rev-parse', `${sha}~1`], cwd);
|
|
197
|
+
if (!parent)
|
|
198
|
+
continue;
|
|
199
|
+
const analysis = analyzeCommit(cwd, sha);
|
|
200
|
+
analyzedCount++;
|
|
201
|
+
if (analysis.fileCount === 0)
|
|
202
|
+
continue;
|
|
203
|
+
// Penalty for large multi-concern commits
|
|
204
|
+
if (analysis.clusterCount > 1 && analysis.fileCount > 5) {
|
|
205
|
+
const severity = analysis.clusterCount > 3 ? 'warning' : 'info';
|
|
206
|
+
const shortSha = sha.substring(0, 7);
|
|
207
|
+
const penalty = Math.min(20, (analysis.clusterCount - 1) * 5);
|
|
208
|
+
totalPenalty += penalty;
|
|
209
|
+
issues.push({
|
|
210
|
+
severity,
|
|
211
|
+
message: `commit ${shortSha} touches ${analysis.fileCount} files across ${analysis.clusterCount} concerns`,
|
|
212
|
+
fixable: true,
|
|
213
|
+
fixHint: `run: vet split --since ${shortSha}~1`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (analysis.fileCount > 20) {
|
|
217
|
+
totalPenalty += 15;
|
|
218
|
+
issues.push({
|
|
219
|
+
severity: 'warning',
|
|
220
|
+
message: `commit ${sha.substring(0, 7)} modifies ${analysis.fileCount} files — likely needs splitting`,
|
|
221
|
+
fixable: true,
|
|
222
|
+
fixHint: 'run: vet split',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const score = Math.max(0, 100 - totalPenalty);
|
|
227
|
+
const summary = issues.length === 0
|
|
228
|
+
? 'all recent commits are atomic'
|
|
229
|
+
: `${issues.length} commit(s) could be split into smaller atomic commits`;
|
|
230
|
+
return { name: 'split', score, maxScore: 100, issues, summary };
|
|
231
|
+
}
|
|
232
|
+
// ── Subcommand ───────────────────────────────────────────────────────────────
|
|
233
|
+
export async function runSplitCommand(format, cwd, since, apply, force) {
|
|
234
|
+
const ref = since || 'HEAD~1';
|
|
235
|
+
// Get the diff
|
|
236
|
+
const diff = gitExec(['diff', ref, 'HEAD'], cwd);
|
|
237
|
+
if (!diff.trim()) {
|
|
238
|
+
if (format === 'json') {
|
|
239
|
+
console.log(JSON.stringify({ clusters: [], message: 'no changes to split' }));
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.log(`\n ${c.bold}vet split${c.reset} — commit surgery\n`);
|
|
243
|
+
console.log(` ${c.dim}no changes between ${ref} and HEAD${c.reset}\n`);
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const hunks = parseDiff(diff);
|
|
248
|
+
const clusters = clusterHunks(hunks);
|
|
249
|
+
if (clusters.length <= 1) {
|
|
250
|
+
if (format === 'json') {
|
|
251
|
+
console.log(JSON.stringify({
|
|
252
|
+
clusters: clusters.map(cl => ({
|
|
253
|
+
name: cl.name, prefix: cl.prefix, files: cl.files,
|
|
254
|
+
hunkCount: cl.hunks.length, commitMessage: cl.commitMessage,
|
|
255
|
+
})),
|
|
256
|
+
message: 'commit is already atomic',
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log(`\n ${c.bold}vet split${c.reset} — commit surgery\n`);
|
|
261
|
+
console.log(` ${c.green}commit is already atomic — no split needed${c.reset}\n`);
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
// JSON output
|
|
266
|
+
if (format === 'json') {
|
|
267
|
+
const output = {
|
|
268
|
+
ref,
|
|
269
|
+
clusterCount: clusters.length,
|
|
270
|
+
clusters: clusters.map(cl => ({
|
|
271
|
+
name: cl.name,
|
|
272
|
+
prefix: cl.prefix,
|
|
273
|
+
files: cl.files,
|
|
274
|
+
hunkCount: cl.hunks.length,
|
|
275
|
+
commitMessage: cl.commitMessage,
|
|
276
|
+
})),
|
|
277
|
+
};
|
|
278
|
+
console.log(JSON.stringify(output, null, 2));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// ASCII table output
|
|
282
|
+
console.log(`\n ${c.bold}vet split${c.reset} — commit surgery\n`);
|
|
283
|
+
console.log(` analyzing changes since ${c.cyan}${ref}${c.reset}\n`);
|
|
284
|
+
console.log(` ${c.dim}# commit message${' '.repeat(35)}files hunks${c.reset}`);
|
|
285
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
286
|
+
const cl = clusters[i];
|
|
287
|
+
const num = String(i + 1).padStart(2);
|
|
288
|
+
const msg = cl.commitMessage.padEnd(50).substring(0, 50);
|
|
289
|
+
const files = String(cl.files.length).padStart(5);
|
|
290
|
+
const hunkCount = String(cl.hunks.length).padStart(6);
|
|
291
|
+
console.log(` ${num} ${msg}${files}${hunkCount}`);
|
|
292
|
+
for (const file of cl.files) {
|
|
293
|
+
console.log(` ${c.dim}${file}${c.reset}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
console.log(`\n ${c.bold}${clusters.length} atomic commits${c.reset} proposed\n`);
|
|
297
|
+
if (!apply) {
|
|
298
|
+
console.log(` ${c.dim}dry run — use --apply to execute${c.reset}\n`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// Apply mode: safety checks
|
|
302
|
+
const currentBranch = gitExec(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
303
|
+
if ((currentBranch === 'main' || currentBranch === 'master') && !force) {
|
|
304
|
+
console.log(` ${c.red}refusing to rewrite history on ${currentBranch}${c.reset}`);
|
|
305
|
+
console.log(` ${c.dim}use --force to override${c.reset}\n`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// Create backup branch
|
|
309
|
+
const backupBranch = `vet-split-backup-${Date.now()}`;
|
|
310
|
+
gitExec(['branch', backupBranch], cwd);
|
|
311
|
+
console.log(` ${c.dim}backup branch: ${backupBranch}${c.reset}`);
|
|
312
|
+
// Soft reset to the ref point
|
|
313
|
+
gitExec(['reset', '--soft', ref], cwd);
|
|
314
|
+
gitExec(['reset', 'HEAD'], cwd);
|
|
315
|
+
// Apply each cluster as a separate commit
|
|
316
|
+
for (const cl of clusters) {
|
|
317
|
+
for (const file of cl.files) {
|
|
318
|
+
gitExec(['add', file], cwd);
|
|
319
|
+
}
|
|
320
|
+
gitExec(['commit', '-m', cl.commitMessage], cwd);
|
|
321
|
+
console.log(` ${c.green}committed:${c.reset} ${cl.commitMessage}`);
|
|
322
|
+
}
|
|
323
|
+
console.log(`\n ${c.green}split complete${c.reset} — ${clusters.length} atomic commits created`);
|
|
324
|
+
console.log(` ${c.dim}backup: ${backupBranch}${c.reset}\n`);
|
|
325
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -32,6 +32,7 @@ import { checkBloat, runBloatCommand } from './checks/bloat.js';
|
|
|
32
32
|
import { checkGuard, runGuardCommand } from './checks/guard.js';
|
|
33
33
|
import { checkExplain, runExplainCommand } from './checks/explain.js';
|
|
34
34
|
import { checkContext, runContextCommand } from './checks/context.js';
|
|
35
|
+
import { checkSplit, runSplitCommand } from './checks/split.js';
|
|
35
36
|
import { checkCompleteness } from './checks/completeness.js';
|
|
36
37
|
import { score } from './scorer.js';
|
|
37
38
|
import { reportPretty, reportJSON, reportBadge } from './reporter.js';
|
|
@@ -88,6 +89,7 @@ if (flags.has('--help') || flags.has('-h')) {
|
|
|
88
89
|
npx @safetnsr/vet guard [dir] scan for destructive operation bomb sites
|
|
89
90
|
npx @safetnsr/vet explain [--since REF] [--verbose] [--json] risk-tier agent changes
|
|
90
91
|
npx @safetnsr/vet context [dir] audit agent context files for token cost + stale sections
|
|
92
|
+
npx @safetnsr/vet split [--since HEAD~1] [--apply] [--force] [--json] split AI mega-commits into atomic commits
|
|
91
93
|
|
|
92
94
|
${c.dim}categories:${c.reset}
|
|
93
95
|
security (30%) scan, secrets, config, model usage
|
|
@@ -123,7 +125,7 @@ if (flags.has('--version') || flags.has('-v')) {
|
|
|
123
125
|
}
|
|
124
126
|
process.exit(0);
|
|
125
127
|
}
|
|
126
|
-
const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context'];
|
|
128
|
+
const COMMANDS = ['init', 'receipt', 'map', 'permissions', 'compact', 'subsidy', 'loop', 'bloat', 'guard', 'explain', 'context', 'split'];
|
|
127
129
|
const command = COMMANDS.includes(positional[0]) ? positional[0] : undefined;
|
|
128
130
|
const cwd = resolve(positional.find(p => !COMMANDS.includes(p)) || '.');
|
|
129
131
|
const isCI = flags.has('--ci');
|
|
@@ -276,6 +278,17 @@ if (command === 'context') {
|
|
|
276
278
|
}
|
|
277
279
|
process.exit(0);
|
|
278
280
|
}
|
|
281
|
+
if (command === 'split') {
|
|
282
|
+
try {
|
|
283
|
+
const format = isJSON ? 'json' : 'ascii';
|
|
284
|
+
await runSplitCommand(format, cwd, since, flags.has('--apply'), flags.has('--force'));
|
|
285
|
+
}
|
|
286
|
+
catch (e) {
|
|
287
|
+
console.error(`${c.red}split failed:${c.reset}`, e instanceof Error ? e.message : e);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
process.exit(0);
|
|
291
|
+
}
|
|
279
292
|
if (command === 'explain') {
|
|
280
293
|
try {
|
|
281
294
|
const format = isJSON ? 'json' : 'ascii';
|
|
@@ -340,7 +353,7 @@ async function runChecks() {
|
|
|
340
353
|
}
|
|
341
354
|
}
|
|
342
355
|
// Run ALL independent checks in parallel
|
|
343
|
-
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, clonesResult, contextResult,] = await Promise.all([
|
|
356
|
+
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, clonesResult, contextResult, splitResult,] = await Promise.all([
|
|
344
357
|
withTimeout('scan', () => checkScan(cwd)),
|
|
345
358
|
withTimeout('secrets', () => checkSecrets(cwd)),
|
|
346
359
|
withTimeout('config', () => checkConfig(cwd, ignore)),
|
|
@@ -369,6 +382,7 @@ async function runChecks() {
|
|
|
369
382
|
withTimeout('hotspots', () => checkHotspots(cwd), 30_000),
|
|
370
383
|
withTimeout('clones', () => checkClones(cwd), 60_000),
|
|
371
384
|
withTimeout('context', () => checkContext(cwd)),
|
|
385
|
+
withTimeout('split', () => checkSplit(cwd)),
|
|
372
386
|
]);
|
|
373
387
|
// Git-dependent checks (diff + history) — parallel with each other
|
|
374
388
|
const [diffResult, historyResult] = await Promise.all([
|
|
@@ -380,7 +394,7 @@ async function runChecks() {
|
|
|
380
394
|
return score(cwd, {
|
|
381
395
|
security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, subsidyResult, guardResult],
|
|
382
396
|
integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult, loopResult, completenessResult, explainResult],
|
|
383
|
-
debt: [readyResult, historyResult, debtResult, bloatResult, clonesResult],
|
|
397
|
+
debt: [readyResult, historyResult, debtResult, bloatResult, clonesResult, splitResult],
|
|
384
398
|
deps: [depsResult],
|
|
385
399
|
architecture: [architectureResult],
|
|
386
400
|
aiready: [aireadyResult, deepResult, semanticResult, contextResult],
|