@safetnsr/vet 1.10.1 → 1.11.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/debt.js +120 -35
- package/dist/checks/deps.js +3 -2
- package/dist/checks/memory.js +3 -2
- package/dist/checks/owasp/asi06-memory-poisoning.js +3 -2
- package/dist/checks/owasp/shared.js +3 -2
- package/dist/checks/scan.js +3 -2
- package/dist/checks/secrets.js +3 -2
- package/dist/checks/tests.js +2 -2
- package/dist/checks/verify.js +3 -2
- package/dist/cli.js +27 -40
- package/dist/file-cache.d.ts +4 -0
- package/dist/file-cache.js +22 -0
- package/dist/util.js +3 -7
- package/package.json +1 -1
package/dist/checks/debt.js
CHANGED
|
@@ -38,27 +38,93 @@ function simpleHash(s) {
|
|
|
38
38
|
}
|
|
39
39
|
return h.toString(36);
|
|
40
40
|
}
|
|
41
|
+
/** Levenshtein distance (optimized single-row DP, with early exit) */
|
|
42
|
+
function levenshtein(a, b, maxDist) {
|
|
43
|
+
if (a === b)
|
|
44
|
+
return 0;
|
|
45
|
+
if (a.length === 0)
|
|
46
|
+
return b.length;
|
|
47
|
+
if (b.length === 0)
|
|
48
|
+
return a.length;
|
|
49
|
+
// Ensure a is shorter for memory efficiency
|
|
50
|
+
if (a.length > b.length) {
|
|
51
|
+
const t = a;
|
|
52
|
+
a = b;
|
|
53
|
+
b = t;
|
|
54
|
+
}
|
|
55
|
+
const aLen = a.length;
|
|
56
|
+
const bLen = b.length;
|
|
57
|
+
// For very long strings, use sampled comparison instead of full DP
|
|
58
|
+
if (aLen > 500) {
|
|
59
|
+
return sampledDistance(a, b, maxDist);
|
|
60
|
+
}
|
|
61
|
+
const row = new Uint32Array(aLen + 1);
|
|
62
|
+
for (let i = 0; i <= aLen; i++)
|
|
63
|
+
row[i] = i;
|
|
64
|
+
for (let j = 1; j <= bLen; j++) {
|
|
65
|
+
let prev = row[0];
|
|
66
|
+
row[0] = j;
|
|
67
|
+
let rowMin = j;
|
|
68
|
+
for (let i = 1; i <= aLen; i++) {
|
|
69
|
+
const cur = row[i];
|
|
70
|
+
if (a[i - 1] === b[j - 1]) {
|
|
71
|
+
row[i] = prev;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
row[i] = 1 + Math.min(prev, row[i], row[i - 1]);
|
|
75
|
+
}
|
|
76
|
+
prev = cur;
|
|
77
|
+
if (row[i] < rowMin)
|
|
78
|
+
rowMin = row[i];
|
|
79
|
+
}
|
|
80
|
+
// Early exit if minimum in this row already exceeds threshold
|
|
81
|
+
if (rowMin > maxDist)
|
|
82
|
+
return rowMin;
|
|
83
|
+
}
|
|
84
|
+
return row[aLen];
|
|
85
|
+
}
|
|
86
|
+
/** Fast sampled distance for long strings — compare chunks instead of full DP */
|
|
87
|
+
function sampledDistance(a, b, maxDist) {
|
|
88
|
+
const maxLen = Math.max(a.length, b.length);
|
|
89
|
+
// Sample 5 chunks of 80 chars each from evenly spaced positions
|
|
90
|
+
const chunkSize = 80;
|
|
91
|
+
const samples = 5;
|
|
92
|
+
let totalDiff = 0;
|
|
93
|
+
let totalSampled = 0;
|
|
94
|
+
for (let s = 0; s < samples; s++) {
|
|
95
|
+
const pos = Math.floor((s / samples) * (Math.min(a.length, b.length) - chunkSize));
|
|
96
|
+
if (pos < 0)
|
|
97
|
+
continue;
|
|
98
|
+
const ca = a.substring(pos, pos + chunkSize);
|
|
99
|
+
const cb = b.substring(pos, pos + chunkSize);
|
|
100
|
+
let diff = 0;
|
|
101
|
+
for (let i = 0; i < chunkSize; i++) {
|
|
102
|
+
if (ca[i] !== cb[i])
|
|
103
|
+
diff++;
|
|
104
|
+
}
|
|
105
|
+
totalDiff += diff;
|
|
106
|
+
totalSampled += chunkSize;
|
|
107
|
+
}
|
|
108
|
+
if (totalSampled === 0)
|
|
109
|
+
return maxLen;
|
|
110
|
+
// Extrapolate
|
|
111
|
+
const estDist = Math.round((totalDiff / totalSampled) * maxLen);
|
|
112
|
+
return estDist;
|
|
113
|
+
}
|
|
41
114
|
/** Similarity ratio between two strings (0-1) */
|
|
42
115
|
function similarity(a, b) {
|
|
43
116
|
if (a === b)
|
|
44
117
|
return 1;
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
if (longer.length === 0)
|
|
118
|
+
const maxLen = Math.max(a.length, b.length);
|
|
119
|
+
if (maxLen === 0)
|
|
48
120
|
return 1;
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
used[j] = true;
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return matches / longer.length;
|
|
121
|
+
// Quick reject: if length diff alone makes similarity impossible
|
|
122
|
+
const lenDiff = Math.abs(a.length - b.length);
|
|
123
|
+
if (1 - lenDiff / maxLen < 0.92)
|
|
124
|
+
return 0;
|
|
125
|
+
const maxDist = Math.floor(maxLen * 0.08); // 92% similarity = 8% max distance
|
|
126
|
+
const dist = levenshtein(a, b, maxDist);
|
|
127
|
+
return 1 - dist / maxLen;
|
|
62
128
|
}
|
|
63
129
|
/** Extract function bodies with brace matching */
|
|
64
130
|
function extractBraceBody(source, startIdx) {
|
|
@@ -175,20 +241,26 @@ function findDuplicates(allFuncs) {
|
|
|
175
241
|
fixHint: 'extract shared logic into a single function',
|
|
176
242
|
});
|
|
177
243
|
}
|
|
178
|
-
// Similarity check for non-exact matches
|
|
244
|
+
// Similarity check for non-exact matches — length-bucketed to avoid O(n²) explosion
|
|
245
|
+
// Only consider functions with substantial normalized bodies (>= 65 chars)
|
|
179
246
|
const singles = allFuncs.filter(fn => {
|
|
180
247
|
const g = groups.get(fn.hash);
|
|
181
|
-
return !g || g.length < 2;
|
|
248
|
+
return (!g || g.length < 2) && fn.normalized.length >= 65;
|
|
182
249
|
});
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
250
|
+
// Sort by normalized length so we can break early when lengths diverge
|
|
251
|
+
singles.sort((a, b) => a.normalized.length - b.normalized.length);
|
|
252
|
+
let comparisons = 0;
|
|
253
|
+
const MAX_COMPARISONS = 200_000; // safety cap
|
|
254
|
+
for (let i = 0; i < singles.length && comparisons < MAX_COMPARISONS; i++) {
|
|
255
|
+
const a = singles[i];
|
|
256
|
+
for (let j = i + 1; j < singles.length; j++) {
|
|
188
257
|
const b = singles[j];
|
|
189
|
-
//
|
|
190
|
-
if (
|
|
191
|
-
|
|
258
|
+
// If b is >25% longer than a, skip rest (sorted, so all further are longer)
|
|
259
|
+
if (b.normalized.length > a.normalized.length * 1.25)
|
|
260
|
+
break;
|
|
261
|
+
comparisons++;
|
|
262
|
+
if (comparisons > MAX_COMPARISONS)
|
|
263
|
+
break;
|
|
192
264
|
const sim = similarity(a.normalized, b.normalized);
|
|
193
265
|
if (sim > 0.92) {
|
|
194
266
|
const key = [a.file + ':' + a.name, b.file + ':' + b.name].sort().join('|');
|
|
@@ -261,20 +333,33 @@ function findOrphanedExports(cwd, files) {
|
|
|
261
333
|
}
|
|
262
334
|
}
|
|
263
335
|
}
|
|
264
|
-
// Scan all files for
|
|
265
|
-
const
|
|
336
|
+
// Scan all files for import names — collect all imported identifiers into a Set
|
|
337
|
+
const importedNames = new Set();
|
|
338
|
+
const importRe = /import\s+(?:type\s+)?(?:\{([^}]+)\}|([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*,\s*\{([^}]+)\})?)\s+from\s+/g;
|
|
266
339
|
for (const file of sourceFiles) {
|
|
267
340
|
const content = readFile(join(cwd, file));
|
|
268
|
-
if (content)
|
|
269
|
-
|
|
341
|
+
if (!content)
|
|
342
|
+
continue;
|
|
343
|
+
let match;
|
|
344
|
+
importRe.lastIndex = 0;
|
|
345
|
+
while ((match = importRe.exec(content)) !== null) {
|
|
346
|
+
// Named imports: { a, b as c }
|
|
347
|
+
const namedParts = [match[1], match[3]].filter(Boolean);
|
|
348
|
+
for (const part of namedParts) {
|
|
349
|
+
for (const name of part.split(',')) {
|
|
350
|
+
const trimmed = name.trim().split(/\s+as\s+/)[0].trim();
|
|
351
|
+
if (trimmed)
|
|
352
|
+
importedNames.add(trimmed);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Default import
|
|
356
|
+
if (match[2])
|
|
357
|
+
importedNames.add(match[2]);
|
|
358
|
+
}
|
|
270
359
|
}
|
|
271
|
-
const allText = allContent.join('\n');
|
|
272
360
|
const lib = isLibrary(cwd);
|
|
273
361
|
for (const exp of exports) {
|
|
274
|
-
|
|
275
|
-
// import { name } from or import { x, name } from or import { name as y }
|
|
276
|
-
const importPattern = new RegExp(`import\\s+[^;]*\\b${exp.name}\\b[^;]*from\\s+`, 'm');
|
|
277
|
-
if (!importPattern.test(allText)) {
|
|
362
|
+
if (!importedNames.has(exp.name)) {
|
|
278
363
|
issues.push({
|
|
279
364
|
severity: lib ? 'info' : 'warning',
|
|
280
365
|
message: `orphaned export: "${exp.name}" is exported but never imported${lib ? ' (library detected — exports may be consumed externally)' : ''}`,
|
package/dist/checks/deps.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { walkFiles, readFile } from '../util.js';
|
|
4
|
+
import { cachedRead } from '../file-cache.js';
|
|
4
5
|
// ── Top packages list (~150 popular npm packages) ────────────────────────────
|
|
5
6
|
const TOP_PACKAGES = [
|
|
6
7
|
'react', 'react-dom', 'next', 'vue', 'angular', 'express', 'koa', 'fastify', 'hono',
|
|
@@ -367,7 +368,7 @@ export async function checkDeps(cwd) {
|
|
|
367
368
|
const importedPackages = new Set();
|
|
368
369
|
for (const file of sourceFiles) {
|
|
369
370
|
try {
|
|
370
|
-
const content =
|
|
371
|
+
const content = cachedRead(join(cwd, file));
|
|
371
372
|
const rawImports = extractImports(content);
|
|
372
373
|
for (const imp of rawImports) {
|
|
373
374
|
if (isBuiltin(imp))
|
package/dist/checks/memory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
|
-
import { existsSync, readdirSync, statSync
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { readFile } from '../util.js';
|
|
4
|
+
import { cachedRead } from '../file-cache.js';
|
|
4
5
|
import { detectWorkspacePackages } from './deps.js';
|
|
5
6
|
// ── Memory file targets ──────────────────────────────────────────────────────
|
|
6
7
|
const ROOT_FILES = ['CLAUDE.md', 'AGENTS.md', 'SOUL.md', '.cursorrules', 'codex.md'];
|
|
@@ -153,7 +154,7 @@ export function checkMemory(cwd) {
|
|
|
153
154
|
const allDeps = new Set();
|
|
154
155
|
if (existsSync(pkgPath)) {
|
|
155
156
|
try {
|
|
156
|
-
const pkg = JSON.parse(
|
|
157
|
+
const pkg = JSON.parse(cachedRead(pkgPath));
|
|
157
158
|
// Include the package's own name
|
|
158
159
|
if (pkg.name)
|
|
159
160
|
allDeps.add(pkg.name);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
3
|
import { readTextFile } from './shared.js';
|
|
4
|
+
import { cachedRead } from '../../file-cache.js';
|
|
4
5
|
// ── ASI06 — Memory and Context Poisoning ─────────────────────────────────────
|
|
5
6
|
export function checkASI06(cwd) {
|
|
6
7
|
const findings = [];
|
|
@@ -17,7 +18,7 @@ export function checkASI06(cwd) {
|
|
|
17
18
|
const gitignorePath = join(cwd, '.gitignore');
|
|
18
19
|
let gitignoreContent = '';
|
|
19
20
|
try {
|
|
20
|
-
gitignoreContent =
|
|
21
|
+
gitignoreContent = cachedRead(gitignorePath);
|
|
21
22
|
}
|
|
22
23
|
catch { /* intentional: .gitignore may not exist */ }
|
|
23
24
|
for (const memPath of memoryPaths) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { statSync, existsSync } from 'node:fs';
|
|
3
3
|
import { isTextFile, collectDirFiles } from '../../util.js';
|
|
4
|
+
import { cachedRead } from '../../file-cache.js';
|
|
4
5
|
// ── Agent config file targets ─────────────────────────────────────────────────
|
|
5
6
|
const AGENT_CONFIG_TARGETS = [
|
|
6
7
|
'.claude',
|
|
@@ -54,7 +55,7 @@ export function readTextFile(filePath) {
|
|
|
54
55
|
if (!isTextFile(filePath))
|
|
55
56
|
return null;
|
|
56
57
|
try {
|
|
57
|
-
return
|
|
58
|
+
return cachedRead(filePath);
|
|
58
59
|
}
|
|
59
60
|
catch { /* intentional: resolver may fail on unreadable files */ }
|
|
60
61
|
return null;
|
package/dist/checks/scan.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, relative } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { statSync, existsSync } from 'node:fs';
|
|
3
3
|
import { isTextFile as utilIsTextFile, collectDirFiles as utilCollectDirFiles } from '../util.js';
|
|
4
|
+
import { cachedRead } from '../file-cache.js';
|
|
4
5
|
const CRITICAL_PATTERNS = [
|
|
5
6
|
{
|
|
6
7
|
id: 'base64-url',
|
|
@@ -215,7 +216,7 @@ export function checkScan(cwd) {
|
|
|
215
216
|
if (!isTextFile(filePath))
|
|
216
217
|
continue;
|
|
217
218
|
try {
|
|
218
|
-
const content =
|
|
219
|
+
const content = cachedRead(filePath);
|
|
219
220
|
const relPath = relative(cwd, filePath);
|
|
220
221
|
filesScanned++;
|
|
221
222
|
findings.push(...scanContent(content, relPath));
|
package/dist/checks/secrets.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, readdirSync,
|
|
1
|
+
import { existsSync, readdirSync, statSync, createReadStream } from 'node:fs';
|
|
2
2
|
import { join, relative, extname, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { execSync } from 'node:child_process';
|
|
5
5
|
import { createInterface } from 'node:readline';
|
|
6
6
|
import { collectDirFiles } from '../util.js';
|
|
7
|
+
import { cachedRead } from '../file-cache.js';
|
|
7
8
|
// ── Shannon entropy ──────────────────────────────────────────────────────────
|
|
8
9
|
function calculateEntropy(str) {
|
|
9
10
|
if (str.length === 0)
|
|
@@ -176,7 +177,7 @@ function findEnvFiles(dir, maxDepth = 3, depth = 0) {
|
|
|
176
177
|
function scanEnvFile(filePath) {
|
|
177
178
|
const findings = [];
|
|
178
179
|
try {
|
|
179
|
-
const lines =
|
|
180
|
+
const lines = cachedRead(filePath).split('\n');
|
|
180
181
|
const gitTracked = isGitTracked(filePath);
|
|
181
182
|
for (const line of lines) {
|
|
182
183
|
if (line.trimStart().startsWith('#'))
|
package/dist/checks/tests.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
2
|
import { walkFiles } from '../util.js';
|
|
3
|
+
import { cachedRead } from '../file-cache.js';
|
|
4
4
|
const TEST_FILE_RE = /\.(test|spec)\.(ts|js|tsx|jsx)$/;
|
|
5
5
|
const TEST_DIR_RE = /(?:^|[/\\])(__tests__|tests?)[/\\]/;
|
|
6
6
|
function isTestFile(relPath) {
|
|
@@ -205,7 +205,7 @@ export function checkTests(cwd, ignore) {
|
|
|
205
205
|
for (const rel of testFiles) {
|
|
206
206
|
let content;
|
|
207
207
|
try {
|
|
208
|
-
content =
|
|
208
|
+
content = cachedRead(join(cwd, rel));
|
|
209
209
|
}
|
|
210
210
|
catch {
|
|
211
211
|
continue;
|
package/dist/checks/verify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, basename, extname } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { execSync } from 'node:child_process';
|
|
4
|
+
import { cachedRead } from '../file-cache.js';
|
|
4
5
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
6
|
function safeExec(cmd, cwd) {
|
|
6
7
|
try {
|
|
@@ -288,7 +289,7 @@ export function checkVerify(cwd, since) {
|
|
|
288
289
|
verified++;
|
|
289
290
|
continue;
|
|
290
291
|
}
|
|
291
|
-
content =
|
|
292
|
+
content = cachedRead(absPath);
|
|
292
293
|
}
|
|
293
294
|
catch {
|
|
294
295
|
continue;
|
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { checkPermissions } from './checks/permissions.js';
|
|
|
22
22
|
import { checkCompact, runCompactCommand } from './checks/compact.js';
|
|
23
23
|
import { score } from './scorer.js';
|
|
24
24
|
import { reportPretty, reportJSON, reportBadge } from './reporter.js';
|
|
25
|
+
import { clearCache } from './file-cache.js';
|
|
25
26
|
const args = process.argv.slice(2);
|
|
26
27
|
const flags = new Set(args.filter(a => a.startsWith('-') && !a.startsWith('--since')));
|
|
27
28
|
const flagMap = new Map();
|
|
@@ -79,7 +80,7 @@ if (flags.has('--help') || flags.has('-h')) {
|
|
|
79
80
|
--watch re-run on file changes
|
|
80
81
|
--json JSON output
|
|
81
82
|
--pretty force pretty output (even in pipes)
|
|
82
|
-
--max-files N limit file scanning (default:
|
|
83
|
+
--max-files N limit file scanning (default: unlimited)
|
|
83
84
|
-h, --help show this help
|
|
84
85
|
-v, --version show version
|
|
85
86
|
`);
|
|
@@ -105,7 +106,7 @@ const isWatch = flags.has('--watch');
|
|
|
105
106
|
const isBadge = flags.has('--badge');
|
|
106
107
|
const isJSON = flags.has('--json') || (!process.stdout.isTTY && !flags.has('--pretty') && !isBadge);
|
|
107
108
|
const since = flagMap.get('since');
|
|
108
|
-
const maxFiles =
|
|
109
|
+
const maxFiles = flagMap.has('max-files') ? (parseInt(flagMap.get('max-files'), 10) || 0) : 0;
|
|
109
110
|
// Load config
|
|
110
111
|
let config = {};
|
|
111
112
|
const configContent = readFile(resolve(cwd, '.vetrc'));
|
|
@@ -232,53 +233,39 @@ async function runChecks() {
|
|
|
232
233
|
const GLOBAL_TIMEOUT = 120_000;
|
|
233
234
|
try {
|
|
234
235
|
// Check file count and warn if large
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
236
|
+
if (maxFiles > 0) {
|
|
237
|
+
const { walkFiles: wf } = await import('./util.js');
|
|
238
|
+
const allProjectFiles = wf(cwd, [], maxFiles);
|
|
239
|
+
if (allProjectFiles.length >= maxFiles) {
|
|
240
|
+
if (!isJSON)
|
|
241
|
+
console.log(` ${c.yellow}Large project (${allProjectFiles.length}+ files) — scanning first ${maxFiles} files. Use --max-files to increase.${c.reset}\n`);
|
|
242
|
+
}
|
|
240
243
|
}
|
|
241
|
-
// Run
|
|
242
|
-
|
|
243
|
-
const [scanResult, secretsResult, configResult, modelsResult, owaspResult] = await Promise.all([
|
|
244
|
+
// Run ALL independent checks in parallel
|
|
245
|
+
const [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult, integrityResult, readyResult, debtResult, depsResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult,] = await Promise.all([
|
|
244
246
|
withTimeout('scan', () => checkScan(cwd)),
|
|
245
247
|
withTimeout('secrets', () => checkSecrets(cwd)),
|
|
246
248
|
withTimeout('config', () => checkConfig(cwd, ignore)),
|
|
247
249
|
withTimeout('models', () => checkModels(cwd, ignore)),
|
|
248
250
|
withTimeout('owasp', () => checkOwasp(cwd)),
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (Date.now() - globalStart > GLOBAL_TIMEOUT) {
|
|
252
|
-
if (!isJSON)
|
|
253
|
-
console.error(` ${c.yellow}⚠ global timeout (${GLOBAL_TIMEOUT / 1000}s) reached — returning partial results${c.reset}`);
|
|
254
|
-
return score(cwd, { security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult], integrity: [], debt: [], deps: [] });
|
|
255
|
-
}
|
|
256
|
-
// Integrity: diff, integrity checks
|
|
257
|
-
const diffResult = await withTimeout('diff', () => checkDiff(cwd, { since }));
|
|
258
|
-
const integrityResult = await withTimeout('integrity', () => checkIntegrity(cwd, ignore));
|
|
259
|
-
// Debt: ready, history, debt
|
|
260
|
-
const [readyResult, debtResult] = await Promise.all([
|
|
251
|
+
withTimeout('permissions', () => checkPermissions(cwd)),
|
|
252
|
+
withTimeout('integrity', () => checkIntegrity(cwd, ignore)),
|
|
261
253
|
withTimeout('ready', () => checkReady(cwd, ignore)),
|
|
262
254
|
withTimeout('debt', () => checkDebt(cwd, ignore)),
|
|
255
|
+
withTimeout('deps', () => checkDeps(cwd)),
|
|
256
|
+
withTimeout('receipt', () => checkReceipt(cwd)),
|
|
257
|
+
withTimeout('compact', () => checkCompact(cwd)),
|
|
258
|
+
withTimeout('memory', () => checkMemory(cwd)),
|
|
259
|
+
withTimeout('verify', () => checkVerify(cwd, since)),
|
|
260
|
+
withTimeout('tests', () => checkTests(cwd, ignore)),
|
|
263
261
|
]);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const depsResult = await withTimeout('deps', () => checkDeps(cwd));
|
|
272
|
-
// Receipt is informational — fold into integrity category but keep low weight
|
|
273
|
-
const receiptResult = await withTimeout('receipt', () => checkReceipt(cwd));
|
|
274
|
-
// Compact: compaction forensics
|
|
275
|
-
const compactResult = await withTimeout('compact', () => checkCompact(cwd));
|
|
276
|
-
// Memory: stale facts in agent memory files
|
|
277
|
-
const memoryResult = await withTimeout('memory', () => checkMemory(cwd));
|
|
278
|
-
// Verify: agent claim validation
|
|
279
|
-
const verifyResult = await withTimeout('verify', () => checkVerify(cwd, since));
|
|
280
|
-
// Tests: test theater detection
|
|
281
|
-
const testsResult = await withTimeout('tests', () => checkTests(cwd, ignore));
|
|
262
|
+
// Git-dependent checks (diff + history) — parallel with each other
|
|
263
|
+
const [diffResult, historyResult] = await Promise.all([
|
|
264
|
+
withTimeout('diff', () => checkDiff(cwd, { since })),
|
|
265
|
+
withTimeout('history', () => checkHistory(cwd)),
|
|
266
|
+
]);
|
|
267
|
+
// Clear file cache after all checks complete
|
|
268
|
+
clearCache();
|
|
282
269
|
return score(cwd, {
|
|
283
270
|
security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult],
|
|
284
271
|
integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult],
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
// Singleton file cache — read once, share across all checks
|
|
3
|
+
const cache = new Map();
|
|
4
|
+
export function cachedRead(path) {
|
|
5
|
+
if (cache.has(path))
|
|
6
|
+
return cache.get(path);
|
|
7
|
+
const content = readFileSync(path, 'utf-8');
|
|
8
|
+
cache.set(path, content);
|
|
9
|
+
return content;
|
|
10
|
+
}
|
|
11
|
+
/** Cached readFile that returns null on error (matches util.readFile signature) */
|
|
12
|
+
export function cachedReadFile(path) {
|
|
13
|
+
try {
|
|
14
|
+
return cachedRead(path);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function clearCache() {
|
|
21
|
+
cache.clear();
|
|
22
|
+
}
|
package/dist/util.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { join, relative } from 'node:path';
|
|
4
|
+
import { cachedReadFile } from './file-cache.js';
|
|
4
5
|
// ANSI colors — zero deps
|
|
5
6
|
export const c = {
|
|
6
7
|
reset: '\x1b[0m',
|
|
@@ -30,18 +31,13 @@ export function isGitRepo(cwd) {
|
|
|
30
31
|
return git('rev-parse --is-inside-work-tree', cwd) === 'true';
|
|
31
32
|
}
|
|
32
33
|
export function readFile(path) {
|
|
33
|
-
|
|
34
|
-
return readFileSync(path, 'utf-8');
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
34
|
+
return cachedReadFile(path);
|
|
39
35
|
}
|
|
40
36
|
/** Returns true if the path exists (file or directory). Convenience alias for existsSync. */
|
|
41
37
|
export function fileExists(path) {
|
|
42
38
|
return existsSync(path);
|
|
43
39
|
}
|
|
44
|
-
export function walkFiles(dir, ignore = [], maxFiles =
|
|
40
|
+
export function walkFiles(dir, ignore = [], maxFiles = 0) {
|
|
45
41
|
const results = [];
|
|
46
42
|
const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', 'vendor', '__pycache__', '.venv', 'venv'];
|
|
47
43
|
const allIgnore = [...defaultIgnore, ...ignore];
|