@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.
@@ -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 longer = a.length >= b.length ? a : b;
46
- const shorter = a.length >= b.length ? b : a;
47
- if (longer.length === 0)
118
+ const maxLen = Math.max(a.length, b.length);
119
+ if (maxLen === 0)
48
120
  return 1;
49
- // Count matching characters in sequence
50
- let matches = 0;
51
- const used = new Array(longer.length).fill(false);
52
- for (let i = 0; i < shorter.length; i++) {
53
- for (let j = 0; j < longer.length; j++) {
54
- if (!used[j] && shorter[i] === longer[j]) {
55
- matches++;
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 (capped to avoid O(n²) explosion on large repos)
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
- // Cap at 500 functions for similarity O(n²) with levenshtein is too expensive above that
184
- const singlesCapped = singles.length > 500 ? singles.slice(0, 500) : singles;
185
- for (let i = 0; i < singlesCapped.length; i++) {
186
- for (let j = i + 1; j < singlesCapped.length; j++) {
187
- const a = singles[i];
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
- // Skip very short normalized bodies
190
- if (a.normalized.length < 30 || b.normalized.length < 30)
191
- continue;
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 imports of each name
265
- const allContent = [];
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
- allContent.push(content);
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
- // Check if name appears in import statements across all files
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)' : ''}`,
@@ -1,6 +1,7 @@
1
1
  import { join } from 'node:path';
2
- import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
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 = readFileSync(join(cwd, file), 'utf-8');
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))
@@ -1,6 +1,7 @@
1
1
  import { join, resolve } from 'node:path';
2
- import { existsSync, readdirSync, statSync, readFileSync } from 'node:fs';
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(readFileSync(pkgPath, 'utf-8'));
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 { readFileSync, existsSync } from 'node:fs';
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 = readFileSync(gitignorePath, 'utf-8');
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 { readFileSync, statSync, existsSync } from 'node:fs';
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 readFileSync(filePath, 'utf-8');
58
+ return cachedRead(filePath);
58
59
  }
59
60
  catch { /* intentional: resolver may fail on unreadable files */ }
60
61
  return null;
@@ -1,6 +1,7 @@
1
1
  import { join, relative } from 'node:path';
2
- import { readFileSync, statSync, existsSync } from 'node:fs';
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 = readFileSync(filePath, 'utf-8');
219
+ const content = cachedRead(filePath);
219
220
  const relPath = relative(cwd, filePath);
220
221
  filesScanned++;
221
222
  findings.push(...scanContent(content, relPath));
@@ -1,9 +1,10 @@
1
- import { existsSync, readdirSync, readFileSync, statSync, createReadStream } from 'node:fs';
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 = readFileSync(filePath, 'utf-8').split('\n');
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('#'))
@@ -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 = readFileSync(join(cwd, rel), 'utf-8');
208
+ content = cachedRead(join(cwd, rel));
209
209
  }
210
210
  catch {
211
211
  continue;
@@ -1,6 +1,7 @@
1
1
  import { join, basename, extname } from 'node:path';
2
- import { readFileSync, existsSync, statSync } from 'node:fs';
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 = readFileSync(absPath, 'utf-8');
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: 2000)
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 = parseInt(flagMap.get('max-files') || '2000', 10) || 2000;
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
- 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`);
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 all checks, grouped into categories
242
- // Security: scan, secrets, config, models, owasp, permissions
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
- 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
- }
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
- 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
- }
270
- // Deps: deps
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,4 @@
1
+ export declare function cachedRead(path: string): string;
2
+ /** Cached readFile that returns null on error (matches util.readFile signature) */
3
+ export declare function cachedReadFile(path: string): string | null;
4
+ export declare function clearCache(): void;
@@ -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
- try {
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 = 2000) {
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safetnsr/vet",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "vet your AI-generated code — one command, one score card, one letter grade",
5
5
  "type": "module",
6
6
  "bin": {