fix(acr): file-discovery skips backups/ dirs — never live config (M-BUG-8)

A directory named `backups` holds backup COPIES, not live config, so walking
it during a config audit produces stale findings. config-audit's own session
backups (~/.claude/config-audit/backups/<ts>/files/.../CLAUDE.md) were the
canonical case: a ~/.claude-scope audit walked 36 frozen config copies as if
live, polluting CPS (C3) and HKV/RUL (C6) results. Same non-live-noise family
as M-BUG-2.

- Add `backups` to SKIP_DIRS (broad, name-based — consistent with vendor/dist/
  .cache): the rule is general, a backups/ dir is never live config.

TDD: 2 failing tests (no config under backups/ discovered + backups/ counted
as skipped) -> green; a third asserts live config beside the backups tree is
still discovered. Full suite 1310/0 (+3). Byte-stable: no fixture is named
`backups` and `backups` appears in zero frozen snapshots, so v5.0.0 + SC-5 +
default-output outputs are unchanged. Dogfooding ~/.claude: files under
/backups/ drop from 36 -> 0; 717 live config files retained.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnUvKEqyEa1m9gy6Aqhdqq
This commit is contained in:
Kjell Tore Guttormsen 2026-06-26 12:23:43 +02:00
commit bfd577aeee
2 changed files with 52 additions and 1 deletions

View file

@ -11,6 +11,11 @@ const SKIP_DIRS = new Set([
'node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__',
'.next', '.nuxt', '.output', '.cache', '.turbo', '.parcel-cache',
'vendor', 'venv', '.venv', '.tox',
// A `backups` dir holds backup COPIES, not live config — auditing it as if
// live produces stale findings. config-audit's own session backups
// (~/.claude/config-audit/backups/<ts>/files/.../CLAUDE.md) are the canonical
// case (M-BUG-8), but the rule is general: backups are never live config.
'backups',
]);
// Path marker for the plugin install cache (~/.claude/plugins/cache).

View file

@ -1,6 +1,6 @@
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { join, sep } from 'node:path';
import { mkdir, writeFile, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import {
@ -509,3 +509,49 @@ describe('discoverConfigFiles — cache-aware filtering', () => {
assert.ok(!keys.has(STALE1) && !keys.has(STALE2), 'stale dropped via Multi');
});
});
// ───────────────────────────────────────────────────────────────
// Group 8: backups/ skip (M-BUG-8) — a directory named `backups`
// holds backup COPIES, not live config. config-audit's own session
// backups (~/.claude/config-audit/backups/<ts>/files/plugins/*/CLAUDE.md)
// were walked as if live, producing stale findings on a ~/.claude audit.
// ───────────────────────────────────────────────────────────────
describe('discoverConfigFiles — skips backups/ (M-BUG-8)', () => {
let dir;
before(async () => {
dir = tempDir('backups');
// A live CLAUDE.md at the root (must still be discovered).
await mkdir(dir, { recursive: true });
await writeFile(join(dir, 'CLAUDE.md'), '# Live config');
// config-audit's own backup tree — frozen copies, NOT live config.
const backupClaude = join(dir, 'config-audit', 'backups', '20260518_103233', 'files', 'plugins', 'voyage');
await mkdir(backupClaude, { recursive: true });
await writeFile(join(backupClaude, 'CLAUDE.md'), '# Frozen backup copy');
const backupSettings = join(dir, 'config-audit', 'backups', '20260518_103233', 'files', '.claude');
await mkdir(backupSettings, { recursive: true });
await writeFile(join(backupSettings, 'settings.json'), '{}');
});
after(async () => {
await rm(dir, { recursive: true, force: true });
});
it('discovers the live CLAUDE.md', async () => {
const { files } = await discoverConfigFiles(dir);
const live = files.find(f => f.relPath === 'CLAUDE.md');
assert.ok(live, 'live CLAUDE.md at root should be discovered');
});
it('does NOT discover config files under a backups/ directory', async () => {
const { files } = await discoverConfigFiles(dir);
const inBackups = files.filter(f => f.absPath.includes(`backups${sep}`));
assert.equal(inBackups.length, 0, 'no files under backups/ should be discovered');
});
it('counts backups/ as a skipped directory', async () => {
const { skipped } = await discoverConfigFiles(dir);
assert.ok(skipped >= 1, 'backups/ should be counted as skipped');
});
});