@safetnsr/vet 1.9.0 → 1.10.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/README.md +4 -0
- package/dist/checks/deps.d.ts +2 -0
- package/dist/checks/deps.js +144 -1
- package/dist/checks/integrity.js +9 -0
- package/dist/checks/memory.js +5 -0
- package/dist/checks/verify.js +18 -0
- package/dist/cli.js +57 -18
- package/dist/util.d.ts +1 -1
- package/dist/util.js +31 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,7 @@ a codebase that scores well here gives AI agents better context, fewer hallucina
|
|
|
40
40
|
| **secrets** | scans dist/, build/, .next/ + .env files for leaked API keys using pattern + entropy analysis |
|
|
41
41
|
| **history** | git commit churn, AI attribution ratios, suspiciously large changes |
|
|
42
42
|
| **receipt** | parses Claude Code session logs — files changed, commands run, packages installed, SHA256 integrity hash |
|
|
43
|
+
| **compact** | compaction forensics — what context got dropped during Claude Code session compaction |
|
|
43
44
|
|
|
44
45
|
plus: **integrity** (hallucinated imports), **deps** (unused/phantom dependencies), **owasp** (OWASP Top 10 for AI agents), **verify** (validates agent claims against actual changes).
|
|
45
46
|
|
|
@@ -91,6 +92,9 @@ npx @safetnsr/vet init
|
|
|
91
92
|
# agent session receipt
|
|
92
93
|
npx @safetnsr/vet receipt
|
|
93
94
|
npx @safetnsr/vet receipt --json
|
|
95
|
+
|
|
96
|
+
# compaction forensics for claude sessions
|
|
97
|
+
npx @safetnsr/vet compact [log]
|
|
94
98
|
```
|
|
95
99
|
|
|
96
100
|
## --fix
|
package/dist/checks/deps.d.ts
CHANGED
|
@@ -3,4 +3,6 @@ export declare function levenshtein(a: string, b: string): number;
|
|
|
3
3
|
export declare function extractImports(source: string): string[];
|
|
4
4
|
export declare function extractPackageName(specifier: string): string | null;
|
|
5
5
|
export declare function isBuiltin(specifier: string): boolean;
|
|
6
|
+
export declare function detectWorkspacePackages(cwd: string): Set<string>;
|
|
7
|
+
export declare function detectProvidedDeps(cwd: string): Set<string>;
|
|
6
8
|
export declare function checkDeps(cwd: string): Promise<CheckResult>;
|
package/dist/checks/deps.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { walkFiles, readFile } from '../util.js';
|
|
4
4
|
// ── Top packages list (~150 popular npm packages) ────────────────────────────
|
|
5
5
|
const TOP_PACKAGES = [
|
|
@@ -146,6 +146,140 @@ async function checkRegistry(packages) {
|
|
|
146
146
|
}
|
|
147
147
|
return results;
|
|
148
148
|
}
|
|
149
|
+
// ── Workspace detection ──────────────────────────────────────────────────────
|
|
150
|
+
export function detectWorkspacePackages(cwd) {
|
|
151
|
+
const names = new Set();
|
|
152
|
+
// Detect workspace globs from package.json, pnpm-workspace.yaml, lerna.json
|
|
153
|
+
const globs = [];
|
|
154
|
+
try {
|
|
155
|
+
const pkgRaw = readFile(join(cwd, 'package.json'));
|
|
156
|
+
if (pkgRaw) {
|
|
157
|
+
const pkg = JSON.parse(pkgRaw);
|
|
158
|
+
if (Array.isArray(pkg.workspaces)) {
|
|
159
|
+
globs.push(...pkg.workspaces);
|
|
160
|
+
}
|
|
161
|
+
else if (pkg.workspaces?.packages) {
|
|
162
|
+
globs.push(...pkg.workspaces.packages);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch { /* skip */ }
|
|
167
|
+
try {
|
|
168
|
+
const pnpmWs = readFile(join(cwd, 'pnpm-workspace.yaml'));
|
|
169
|
+
if (pnpmWs) {
|
|
170
|
+
// Simple YAML parse: extract lines like " - 'packages/*'"
|
|
171
|
+
const matches = pnpmWs.matchAll(/['"]?([^'":\n]+\*[^'":\n]*)['"]?/g);
|
|
172
|
+
for (const m of matches)
|
|
173
|
+
globs.push(m[1].trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { /* skip */ }
|
|
177
|
+
try {
|
|
178
|
+
const lernaRaw = readFile(join(cwd, 'lerna.json'));
|
|
179
|
+
if (lernaRaw) {
|
|
180
|
+
const lerna = JSON.parse(lernaRaw);
|
|
181
|
+
if (Array.isArray(lerna.packages))
|
|
182
|
+
globs.push(...lerna.packages);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch { /* skip */ }
|
|
186
|
+
// Resolve globs to workspace package.json files
|
|
187
|
+
for (const glob of globs) {
|
|
188
|
+
// Handle simple globs like "packages/*"
|
|
189
|
+
const parts = glob.replace(/\/$/, '').split('/');
|
|
190
|
+
const starIdx = parts.indexOf('*');
|
|
191
|
+
if (starIdx === -1) {
|
|
192
|
+
// Exact directory
|
|
193
|
+
try {
|
|
194
|
+
const pkgPath = join(cwd, glob, 'package.json');
|
|
195
|
+
const raw = readFile(pkgPath);
|
|
196
|
+
if (raw) {
|
|
197
|
+
const pkg = JSON.parse(raw);
|
|
198
|
+
if (pkg.name)
|
|
199
|
+
names.add(pkg.name);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* skip */ }
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// Wildcard — list directory at the non-wildcard prefix
|
|
206
|
+
const prefix = parts.slice(0, starIdx).join('/');
|
|
207
|
+
const prefixDir = join(cwd, prefix);
|
|
208
|
+
try {
|
|
209
|
+
if (existsSync(prefixDir) && statSync(prefixDir).isDirectory()) {
|
|
210
|
+
for (const entry of readdirSync(prefixDir)) {
|
|
211
|
+
const entryDir = join(prefixDir, entry);
|
|
212
|
+
try {
|
|
213
|
+
if (!statSync(entryDir).isDirectory())
|
|
214
|
+
continue;
|
|
215
|
+
// If there are more parts after *, recurse
|
|
216
|
+
const suffix = parts.slice(starIdx + 1);
|
|
217
|
+
const pkgDir = suffix.length > 0 ? join(entryDir, ...suffix) : entryDir;
|
|
218
|
+
const pkgPath = join(pkgDir, 'package.json');
|
|
219
|
+
const raw = readFile(pkgPath);
|
|
220
|
+
if (raw) {
|
|
221
|
+
const pkg = JSON.parse(raw);
|
|
222
|
+
if (pkg.name)
|
|
223
|
+
names.add(pkg.name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch { /* skip */ }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch { /* skip */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return names;
|
|
234
|
+
}
|
|
235
|
+
// ── Plugin host-provided deps ────────────────────────────────────────────────
|
|
236
|
+
export function detectProvidedDeps(cwd) {
|
|
237
|
+
const provided = new Set();
|
|
238
|
+
try {
|
|
239
|
+
const pkgRaw = readFile(join(cwd, 'package.json'));
|
|
240
|
+
if (!pkgRaw)
|
|
241
|
+
return provided;
|
|
242
|
+
const pkg = JSON.parse(pkgRaw);
|
|
243
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
244
|
+
// Obsidian plugin
|
|
245
|
+
const hasObsidian = 'obsidian' in (allDeps || {});
|
|
246
|
+
const manifestPath = join(cwd, 'manifest.json');
|
|
247
|
+
let hasManifestId = false;
|
|
248
|
+
try {
|
|
249
|
+
const manifestRaw = readFile(manifestPath);
|
|
250
|
+
if (manifestRaw) {
|
|
251
|
+
const manifest = JSON.parse(manifestRaw);
|
|
252
|
+
if (manifest.id)
|
|
253
|
+
hasManifestId = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch { /* skip */ }
|
|
257
|
+
if (hasObsidian || hasManifestId) {
|
|
258
|
+
provided.add('obsidian');
|
|
259
|
+
provided.add('electron');
|
|
260
|
+
// @codemirror/* is handled by prefix check below
|
|
261
|
+
provided.add('@codemirror/*');
|
|
262
|
+
}
|
|
263
|
+
// VSCode extension
|
|
264
|
+
if (pkg.engines?.vscode) {
|
|
265
|
+
provided.add('vscode');
|
|
266
|
+
}
|
|
267
|
+
// Electron app
|
|
268
|
+
if (allDeps?.electron) {
|
|
269
|
+
provided.add('electron');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch { /* skip */ }
|
|
273
|
+
return provided;
|
|
274
|
+
}
|
|
275
|
+
function isProvidedPackage(pkg, provided) {
|
|
276
|
+
if (provided.has(pkg))
|
|
277
|
+
return true;
|
|
278
|
+
// Handle @codemirror/* wildcard
|
|
279
|
+
if (provided.has('@codemirror/*') && pkg.startsWith('@codemirror/'))
|
|
280
|
+
return true;
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
149
283
|
// ── Main check ───────────────────────────────────────────────────────────────
|
|
150
284
|
export async function checkDeps(cwd) {
|
|
151
285
|
try {
|
|
@@ -260,9 +394,18 @@ export async function checkDeps(cwd) {
|
|
|
260
394
|
});
|
|
261
395
|
}
|
|
262
396
|
}
|
|
397
|
+
// Detect workspace packages and host-provided deps
|
|
398
|
+
const workspacePackages = detectWorkspacePackages(cwd);
|
|
399
|
+
const providedDeps = detectProvidedDeps(cwd);
|
|
263
400
|
// Phantom imports: imported but not declared
|
|
264
401
|
for (const pkg of importedPackages) {
|
|
265
402
|
if (!declaredSet.has(pkg)) {
|
|
403
|
+
// Skip workspace packages
|
|
404
|
+
if (workspacePackages.has(pkg))
|
|
405
|
+
continue;
|
|
406
|
+
// Skip host-provided deps
|
|
407
|
+
if (isProvidedPackage(pkg, providedDeps))
|
|
408
|
+
continue;
|
|
266
409
|
issues.push({
|
|
267
410
|
severity: 'warning',
|
|
268
411
|
message: `phantom import: "${pkg}" is imported but not in package.json`,
|
package/dist/checks/integrity.js
CHANGED
|
@@ -102,6 +102,9 @@ function extractRelativeImports(source) {
|
|
|
102
102
|
}
|
|
103
103
|
return imports;
|
|
104
104
|
}
|
|
105
|
+
function isBuildArtifactImport(importPath) {
|
|
106
|
+
return /(?:^|\/)(?:dist|build)\//.test(importPath);
|
|
107
|
+
}
|
|
105
108
|
function checkHallucinatedImports(cwd, files) {
|
|
106
109
|
const issues = [];
|
|
107
110
|
const sourceExts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cts', '.cjs']);
|
|
@@ -111,11 +114,17 @@ function checkHallucinatedImports(cwd, files) {
|
|
|
111
114
|
continue;
|
|
112
115
|
if (file.includes('node_modules'))
|
|
113
116
|
continue;
|
|
117
|
+
// Skip .d.ts declaration files — they reference build outputs
|
|
118
|
+
if (file.endsWith('.d.ts'))
|
|
119
|
+
continue;
|
|
114
120
|
const content = readFile(join(cwd, file));
|
|
115
121
|
if (!content)
|
|
116
122
|
continue;
|
|
117
123
|
const relImports = extractRelativeImports(content);
|
|
118
124
|
for (const imp of relImports) {
|
|
125
|
+
// Skip build artifact imports (./dist/..., ../build/...)
|
|
126
|
+
if (isBuildArtifactImport(imp.path))
|
|
127
|
+
continue;
|
|
119
128
|
// Skip .js extensions pointing to .ts files (common in ESM TypeScript)
|
|
120
129
|
// The resolver already handles this
|
|
121
130
|
if (!resolveRelativeImport(imp.path, file, cwd)) {
|
package/dist/checks/memory.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from 'node:path';
|
|
2
2
|
import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { readFile } from '../util.js';
|
|
4
|
+
import { detectWorkspacePackages } from './deps.js';
|
|
4
5
|
// ── Memory file targets ──────────────────────────────────────────────────────
|
|
5
6
|
const ROOT_FILES = ['CLAUDE.md', 'AGENTS.md', 'SOUL.md', '.cursorrules', 'codex.md'];
|
|
6
7
|
const MEMORY_DIR = 'memory';
|
|
@@ -166,6 +167,10 @@ export function checkMemory(cwd) {
|
|
|
166
167
|
}
|
|
167
168
|
catch { /* skip */ }
|
|
168
169
|
}
|
|
170
|
+
// Include workspace package names
|
|
171
|
+
const workspacePackages = detectWorkspacePackages(cwd);
|
|
172
|
+
for (const name of workspacePackages)
|
|
173
|
+
allDeps.add(name);
|
|
169
174
|
// Collect all tool mentions across files for contradiction detection
|
|
170
175
|
const globalToolMentions = new Map();
|
|
171
176
|
for (const filePath of memoryFiles) {
|
package/dist/checks/verify.js
CHANGED
|
@@ -38,6 +38,18 @@ function isConfigOrMetaFile(filePath) {
|
|
|
38
38
|
const base = basename(filePath);
|
|
39
39
|
const ext = extname(filePath).toLowerCase();
|
|
40
40
|
const normalized = filePath.replace(/\\/g, '/');
|
|
41
|
+
// All .d.ts declaration files
|
|
42
|
+
if (filePath.endsWith('.d.ts'))
|
|
43
|
+
return true;
|
|
44
|
+
// *.config.* pattern (vite.config.ts, postcss.config.js, tailwind.config.ts, etc.)
|
|
45
|
+
if (/\.config\.[a-z]+$/i.test(base))
|
|
46
|
+
return true;
|
|
47
|
+
// *.rc.* or dotfile rc pattern (.eslintrc.js, .prettierrc.cjs, etc.)
|
|
48
|
+
if (/\.rc\.[a-z]+$/i.test(base) || /^\.[a-z]+rc$/i.test(base) || /^\.[a-z]+rc\.[a-z]+$/i.test(base))
|
|
49
|
+
return true;
|
|
50
|
+
// tsconfig variants (tsconfig.json, tsconfig.build.json, etc.)
|
|
51
|
+
if (/^tsconfig(\..+)?\.json$/i.test(base))
|
|
52
|
+
return true;
|
|
41
53
|
// Dotfiles
|
|
42
54
|
if (CONFIG_DOTFILES.has(base))
|
|
43
55
|
return true;
|
|
@@ -303,6 +315,12 @@ export function checkVerify(cwd, since) {
|
|
|
303
315
|
verified++;
|
|
304
316
|
continue;
|
|
305
317
|
}
|
|
318
|
+
// Skip barrel index files (index.ts/js/tsx/jsx under 15 lines)
|
|
319
|
+
const indexNames = new Set(['index.ts', 'index.js', 'index.tsx', 'index.jsx']);
|
|
320
|
+
if (indexNames.has(basename(relPath)) && lineCount < 15) {
|
|
321
|
+
verified++;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
306
324
|
if (lineCount < 10 && lineCount > 0) {
|
|
307
325
|
issues.push({
|
|
308
326
|
severity: 'warning',
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import { reportPretty, reportJSON, reportBadge } from './reporter.js';
|
|
|
25
25
|
const args = process.argv.slice(2);
|
|
26
26
|
const flags = new Set(args.filter(a => a.startsWith('-') && !a.startsWith('--since')));
|
|
27
27
|
const flagMap = new Map();
|
|
28
|
-
// Parse --since=value or --since value
|
|
28
|
+
// Parse --since=value or --since value, --max-files=value
|
|
29
29
|
for (let i = 0; i < args.length; i++) {
|
|
30
30
|
if (args[i].startsWith('--since=')) {
|
|
31
31
|
flagMap.set('since', args[i].split('=')[1]);
|
|
@@ -34,6 +34,13 @@ for (let i = 0; i < args.length; i++) {
|
|
|
34
34
|
flagMap.set('since', args[i + 1]);
|
|
35
35
|
i++;
|
|
36
36
|
}
|
|
37
|
+
else if (args[i].startsWith('--max-files=')) {
|
|
38
|
+
flagMap.set('max-files', args[i].split('=')[1]);
|
|
39
|
+
}
|
|
40
|
+
else if (args[i] === '--max-files' && args[i + 1]) {
|
|
41
|
+
flagMap.set('max-files', args[i + 1]);
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
37
44
|
}
|
|
38
45
|
const positional = args.filter(a => !a.startsWith('-'));
|
|
39
46
|
if (flags.has('--help') || flags.has('-h')) {
|
|
@@ -72,6 +79,7 @@ if (flags.has('--help') || flags.has('-h')) {
|
|
|
72
79
|
--watch re-run on file changes
|
|
73
80
|
--json JSON output
|
|
74
81
|
--pretty force pretty output (even in pipes)
|
|
82
|
+
--max-files N limit file scanning (default: 2000)
|
|
75
83
|
-h, --help show this help
|
|
76
84
|
-v, --version show version
|
|
77
85
|
`);
|
|
@@ -97,6 +105,7 @@ const isWatch = flags.has('--watch');
|
|
|
97
105
|
const isBadge = flags.has('--badge');
|
|
98
106
|
const isJSON = flags.has('--json') || (!process.stdout.isTTY && !flags.has('--pretty') && !isBadge);
|
|
99
107
|
const since = flagMap.get('since');
|
|
108
|
+
const maxFiles = parseInt(flagMap.get('max-files') || '2000', 10) || 2000;
|
|
100
109
|
// Load config
|
|
101
110
|
let config = {};
|
|
102
111
|
const configContent = readFile(resolve(cwd, '.vetrc'));
|
|
@@ -207,39 +216,69 @@ if (isFix) {
|
|
|
207
216
|
}
|
|
208
217
|
process.exit(0);
|
|
209
218
|
}
|
|
219
|
+
/** Run a check with a per-check timeout (30s). Returns a skip result on timeout. */
|
|
220
|
+
async function withTimeout(name, fn, timeoutMs = 30_000) {
|
|
221
|
+
return new Promise((res) => {
|
|
222
|
+
const timer = setTimeout(() => {
|
|
223
|
+
if (!isJSON)
|
|
224
|
+
console.error(` ${c.yellow}⚠ ${name} check timed out after ${timeoutMs / 1000}s — skipped${c.reset}`);
|
|
225
|
+
res({ name, score: 100, maxScore: 100, issues: [], summary: `skipped (timeout after ${timeoutMs / 1000}s)` });
|
|
226
|
+
}, timeoutMs);
|
|
227
|
+
Promise.resolve(fn()).then((r) => { clearTimeout(timer); res(r); }).catch(() => { clearTimeout(timer); res({ name, score: 100, maxScore: 100, issues: [], summary: 'check failed' }); });
|
|
228
|
+
});
|
|
229
|
+
}
|
|
210
230
|
async function runChecks() {
|
|
231
|
+
const globalStart = Date.now();
|
|
232
|
+
const GLOBAL_TIMEOUT = 120_000;
|
|
211
233
|
try {
|
|
234
|
+
// Check file count and warn if large
|
|
235
|
+
const { walkFiles: wf } = await import('./util.js');
|
|
236
|
+
const allProjectFiles = wf(cwd, [], maxFiles);
|
|
237
|
+
if (allProjectFiles.length >= maxFiles) {
|
|
238
|
+
if (!isJSON)
|
|
239
|
+
console.log(` ${c.yellow}Large project (${allProjectFiles.length}+ files) — scanning first ${maxFiles} files. Use --max-files to increase.${c.reset}\n`);
|
|
240
|
+
}
|
|
212
241
|
// Run all checks, grouped into categories
|
|
213
242
|
// Security: scan, secrets, config, models, owasp, permissions
|
|
214
243
|
const [scanResult, secretsResult, configResult, modelsResult, owaspResult] = await Promise.all([
|
|
215
|
-
|
|
216
|
-
checkSecrets(cwd),
|
|
217
|
-
|
|
218
|
-
checkModels(cwd, ignore),
|
|
219
|
-
|
|
244
|
+
withTimeout('scan', () => checkScan(cwd)),
|
|
245
|
+
withTimeout('secrets', () => checkSecrets(cwd)),
|
|
246
|
+
withTimeout('config', () => checkConfig(cwd, ignore)),
|
|
247
|
+
withTimeout('models', () => checkModels(cwd, ignore)),
|
|
248
|
+
withTimeout('owasp', () => checkOwasp(cwd)),
|
|
220
249
|
]);
|
|
221
|
-
const permissionsResult = checkPermissions(cwd);
|
|
250
|
+
const permissionsResult = await withTimeout('permissions', () => checkPermissions(cwd));
|
|
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
|
+
}
|
|
222
256
|
// Integrity: diff, integrity checks
|
|
223
|
-
const diffResult = checkDiff(cwd, { since });
|
|
224
|
-
const integrityResult = await checkIntegrity(cwd, ignore);
|
|
257
|
+
const diffResult = await withTimeout('diff', () => checkDiff(cwd, { since }));
|
|
258
|
+
const integrityResult = await withTimeout('integrity', () => checkIntegrity(cwd, ignore));
|
|
225
259
|
// Debt: ready, history, debt
|
|
226
260
|
const [readyResult, debtResult] = await Promise.all([
|
|
227
|
-
checkReady(cwd, ignore),
|
|
228
|
-
checkDebt(cwd, ignore),
|
|
261
|
+
withTimeout('ready', () => checkReady(cwd, ignore)),
|
|
262
|
+
withTimeout('debt', () => checkDebt(cwd, ignore)),
|
|
229
263
|
]);
|
|
230
|
-
const historyResult = checkHistory(cwd);
|
|
264
|
+
const historyResult = await withTimeout('history', () => checkHistory(cwd));
|
|
265
|
+
if (Date.now() - globalStart > GLOBAL_TIMEOUT) {
|
|
266
|
+
if (!isJSON)
|
|
267
|
+
console.error(` ${c.yellow}⚠ global timeout (${GLOBAL_TIMEOUT / 1000}s) reached — returning partial results${c.reset}`);
|
|
268
|
+
return score(cwd, { security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult], integrity: [diffResult, integrityResult], debt: [readyResult, historyResult, debtResult], deps: [] });
|
|
269
|
+
}
|
|
231
270
|
// Deps: deps
|
|
232
|
-
const depsResult = await checkDeps(cwd);
|
|
271
|
+
const depsResult = await withTimeout('deps', () => checkDeps(cwd));
|
|
233
272
|
// Receipt is informational — fold into integrity category but keep low weight
|
|
234
|
-
const receiptResult = await checkReceipt(cwd);
|
|
273
|
+
const receiptResult = await withTimeout('receipt', () => checkReceipt(cwd));
|
|
235
274
|
// Compact: compaction forensics
|
|
236
|
-
const compactResult = await checkCompact(cwd);
|
|
275
|
+
const compactResult = await withTimeout('compact', () => checkCompact(cwd));
|
|
237
276
|
// Memory: stale facts in agent memory files
|
|
238
|
-
const memoryResult = checkMemory(cwd);
|
|
277
|
+
const memoryResult = await withTimeout('memory', () => checkMemory(cwd));
|
|
239
278
|
// Verify: agent claim validation
|
|
240
|
-
const verifyResult = checkVerify(cwd, since);
|
|
279
|
+
const verifyResult = await withTimeout('verify', () => checkVerify(cwd, since));
|
|
241
280
|
// Tests: test theater detection
|
|
242
|
-
const testsResult = checkTests(cwd, ignore);
|
|
281
|
+
const testsResult = await withTimeout('tests', () => checkTests(cwd, ignore));
|
|
243
282
|
return score(cwd, {
|
|
244
283
|
security: [scanResult, secretsResult, configResult, modelsResult, owaspResult, permissionsResult],
|
|
245
284
|
integrity: [diffResult, integrityResult, receiptResult, compactResult, memoryResult, verifyResult, testsResult],
|
package/dist/util.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export declare function isGitRepo(cwd: string): boolean;
|
|
|
17
17
|
export declare function readFile(path: string): string | null;
|
|
18
18
|
/** Returns true if the path exists (file or directory). Convenience alias for existsSync. */
|
|
19
19
|
export declare function fileExists(path: string): boolean;
|
|
20
|
-
export declare function walkFiles(dir: string, ignore?: string[]): string[];
|
|
20
|
+
export declare function walkFiles(dir: string, ignore?: string[], maxFiles?: number): string[];
|
|
21
21
|
/** Check if a file is binary by sampling first 512 bytes for null bytes */
|
|
22
22
|
export declare function isTextFile(filePath: string): boolean;
|
|
23
23
|
/** Recursively collect all file paths under a directory */
|
package/dist/util.js
CHANGED
|
@@ -41,11 +41,14 @@ export function readFile(path) {
|
|
|
41
41
|
export function fileExists(path) {
|
|
42
42
|
return existsSync(path);
|
|
43
43
|
}
|
|
44
|
-
export function walkFiles(dir, ignore = []) {
|
|
44
|
+
export function walkFiles(dir, ignore = [], maxFiles) {
|
|
45
45
|
const results = [];
|
|
46
46
|
const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', 'coverage', 'vendor', '__pycache__', '.venv', 'venv'];
|
|
47
47
|
const allIgnore = [...defaultIgnore, ...ignore];
|
|
48
|
+
let stopped = false;
|
|
48
49
|
function walk(d) {
|
|
50
|
+
if (stopped)
|
|
51
|
+
return;
|
|
49
52
|
let entries;
|
|
50
53
|
try {
|
|
51
54
|
entries = readdirSync(d);
|
|
@@ -54,6 +57,8 @@ export function walkFiles(dir, ignore = []) {
|
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
59
|
for (const entry of entries) {
|
|
60
|
+
if (stopped)
|
|
61
|
+
return;
|
|
57
62
|
if (allIgnore.includes(entry))
|
|
58
63
|
continue;
|
|
59
64
|
const full = join(d, entry);
|
|
@@ -61,15 +66,39 @@ export function walkFiles(dir, ignore = []) {
|
|
|
61
66
|
const stat = statSync(full);
|
|
62
67
|
if (stat.isDirectory())
|
|
63
68
|
walk(full);
|
|
64
|
-
else
|
|
69
|
+
else {
|
|
65
70
|
results.push(relative(dir, full));
|
|
71
|
+
if (maxFiles && results.length >= maxFiles) {
|
|
72
|
+
stopped = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
catch { /* skip */ }
|
|
68
78
|
}
|
|
69
79
|
}
|
|
70
80
|
walk(dir);
|
|
81
|
+
// When limited, prioritize src/ files over examples/docs/test
|
|
82
|
+
if (maxFiles && stopped) {
|
|
83
|
+
results.sort((a, b) => {
|
|
84
|
+
const aP = priorityBucket(a);
|
|
85
|
+
const bP = priorityBucket(b);
|
|
86
|
+
if (aP !== bP)
|
|
87
|
+
return aP - bP;
|
|
88
|
+
return a.localeCompare(b);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
71
91
|
return results;
|
|
72
92
|
}
|
|
93
|
+
function priorityBucket(file) {
|
|
94
|
+
if (file.startsWith('src/') || file.startsWith('lib/') || file.startsWith('app/'))
|
|
95
|
+
return 0;
|
|
96
|
+
if (file.startsWith('test/') || file.startsWith('tests/') || file.startsWith('__tests__/'))
|
|
97
|
+
return 2;
|
|
98
|
+
if (file.startsWith('examples/') || file.startsWith('docs/') || file.startsWith('example/'))
|
|
99
|
+
return 3;
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
73
102
|
/** Check if a file is binary by sampling first 512 bytes for null bytes */
|
|
74
103
|
export function isTextFile(filePath) {
|
|
75
104
|
try {
|