mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Skip hidden dirs in the detector walker and vendored paths in scan targets
When impeccable (or any agent tool) is installed into a project's .claude/.cursor/.codex tree, a root scan descended into the vendored skill code and reported the detector's own example strings as findings, and context-signals returned installed-skill files as scan candidates whenever the harness tree appeared in the branch diff (issue #303). Rather than growing SKIP_DIRS by a denylist of harness names that drifts as new tools appear, the walker now skips every hidden directory during recursion — which already covered .git/.next/.nuxt/.svelte-kit/.turbo/ .vercel, and covers all present and future harness installs plus .impeccable itself. SKIP_DIRS shrinks to the four non-hidden entries. An explicitly passed hidden target still scans: only child entries are name-checked, never the root the walker is given. scanTargets() applies the same rule to git-changed files (directory segments only, so root dotfiles keep their existing behavior), and falls through to source-dir targeting when the only dirty files are vendored. Prepared with AI assistance (Claude Code), directed by @pbakaus. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
af78b1e512
commit
9f008ebf82
@@ -5,9 +5,15 @@ import path from 'node:path';
|
||||
// File walker
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Hidden directories are skipped wholesale during recursion (below), which
|
||||
// covers .git / .next / .nuxt / .svelte-kit / .turbo / .vercel and — the
|
||||
// issue #303 class — every vendored AI-harness install (.claude, .cursor,
|
||||
// .codex, .agents, .impeccable, ...) whose bundled detector source would
|
||||
// otherwise be reported as findings on a root scan. Only the non-hidden
|
||||
// build/dependency dirs need naming. An explicitly passed hidden target
|
||||
// still scans: walkDir name-checks children, never the root it's given.
|
||||
const SKIP_DIRS = new Set([
|
||||
'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.output',
|
||||
'.svelte-kit', '__pycache__', '.turbo', '.vercel',
|
||||
'node_modules', 'dist', 'build', '__pycache__',
|
||||
]);
|
||||
|
||||
const SCANNABLE_EXTENSIONS = new Set([
|
||||
@@ -24,6 +30,7 @@ function walkDir(dir) {
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }
|
||||
for (const entry of entries) {
|
||||
if (SKIP_DIRS.has(entry.name)) continue;
|
||||
if (entry.isDirectory() && entry.name.startsWith('.')) continue;
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) files.push(...walkDir(full));
|
||||
else if (SCANNABLE_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) files.push(full);
|
||||
|
||||
@@ -156,9 +156,21 @@ const SCANNABLE_EXT = new Set([
|
||||
'.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte', '.astro',
|
||||
]);
|
||||
// Where UI source typically lives. The detector walks these and skips
|
||||
// node_modules / dist / build / .next / .nuxt automatically.
|
||||
// node_modules / dist / build and all hidden dirs automatically.
|
||||
const SOURCE_DIRS = ['src', 'app', 'components', 'pages', 'public'];
|
||||
|
||||
// A changed file under a hidden or dependency/build directory is not app
|
||||
// source — it's a vendored AI-harness install (.claude/skills/..., .cursor/,
|
||||
// .impeccable/, issue #303), a build artifact, or a dependency. Mirrors the
|
||||
// engine walkDir's skip rule so git-changes targeting can't resurface paths
|
||||
// the walker would never visit.
|
||||
function isVendoredPath(rel) {
|
||||
const dirSegments = rel.split(/[\\/]/).slice(0, -1);
|
||||
return dirSegments.some(
|
||||
(seg) => seg.startsWith('.') || seg === 'node_modules' || seg === 'dist' || seg === 'build' || seg === '__pycache__',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Local paths the agent should point the bundled detector at — never a URL.
|
||||
* A URL means a costly Puppeteer browser render, and a probed dev-server port
|
||||
@@ -173,6 +185,7 @@ function scanTargets(cwd, git) {
|
||||
if (git.isRepo && git.changedFiles.length) {
|
||||
const changed = git.changedFiles
|
||||
.filter((f) => SCANNABLE_EXT.has(path.extname(f).toLowerCase()))
|
||||
.filter((f) => !isVendoredPath(f))
|
||||
.filter((f) => fs.existsSync(path.join(cwd, f)));
|
||||
if (changed.length) return { targets: changed.slice(0, 50), via: 'git-changes' };
|
||||
}
|
||||
|
||||
@@ -135,6 +135,39 @@ describe('gatherSignals', () => {
|
||||
assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // README.md filtered out
|
||||
});
|
||||
|
||||
it('filters harness-dir files out of git-changes scan targets (#303)', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
git('init', '-q');
|
||||
git('config', 'user.email', 't@example.com');
|
||||
git('config', 'user.name', 'Test');
|
||||
write('src/Hero.tsx', 'export const Hero = () => null;\n');
|
||||
write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 1;\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'init');
|
||||
write('src/Hero.tsx', 'export const Hero = () => 2;\n'); // dirty app code
|
||||
write('.claude/skills/impeccable/scripts/detector.js', 'export const x = 2;\n'); // dirty vendored skill
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.equal(s.scan.via, 'git-changes');
|
||||
assert.deepEqual(s.scan.targets, ['src/Hero.tsx']); // harness path filtered out
|
||||
});
|
||||
|
||||
it('falls through to source dirs when only harness files changed (#303)', async () => {
|
||||
const { execFileSync } = await import('node:child_process');
|
||||
const git = (...args) => execFileSync('git', args, { cwd: scratch, stdio: 'ignore' });
|
||||
git('init', '-q');
|
||||
git('config', 'user.email', 't@example.com');
|
||||
git('config', 'user.name', 'Test');
|
||||
write('src/Hero.tsx', 'export const Hero = () => null;\n');
|
||||
write('.cursor/skills/impeccable/example.css', 'a{}\n');
|
||||
git('add', '.');
|
||||
git('commit', '-qm', 'init');
|
||||
write('.cursor/skills/impeccable/example.css', 'a{color:red}\n'); // only harness dirty
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.equal(s.scan.via, 'source-dir');
|
||||
assert.deepEqual(s.scan.targets, ['src']);
|
||||
});
|
||||
|
||||
it('has empty scan.targets only when there is no code at all', async () => {
|
||||
const s = await gatherSignals(scratch);
|
||||
assert.deepEqual(s.scan.targets, []);
|
||||
|
||||
@@ -1828,6 +1828,44 @@ describe('walkDir', () => {
|
||||
test('returns empty for nonexistent dir', () => {
|
||||
expect(walkDir('/nonexistent/path/12345')).toHaveLength(0);
|
||||
});
|
||||
|
||||
// Issue #303: when impeccable (or any agent tool) is installed into a
|
||||
// project's .claude/.cursor/etc. tree, a root scan descended into the
|
||||
// vendored skill code and reported the detector's own example strings as
|
||||
// findings. Hidden directories are never app source — skip them all.
|
||||
test('skips hidden dirs (AI-harness installs) during recursion', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-walk-'));
|
||||
try {
|
||||
const write = (rel) => {
|
||||
const abs = path.join(tmp, rel);
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, '/* fixture */');
|
||||
};
|
||||
write('src/app.css');
|
||||
write('.claude/skills/impeccable/scripts/detector.js');
|
||||
write('.cursor/skills/impeccable/example.css');
|
||||
write('.impeccable/live/preview.html');
|
||||
write('node_modules/pkg/index.js');
|
||||
const files = walkDir(tmp);
|
||||
expect(files).toEqual([path.join(tmp, 'src', 'app.css')]);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('still scans a hidden dir passed as the explicit target', () => {
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-walk-'));
|
||||
try {
|
||||
const abs = path.join(tmp, '.claude', 'page.html');
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, '<html></html>');
|
||||
// Only child entries are name-checked; naming the hidden dir directly
|
||||
// is an explicit user intent and must keep working.
|
||||
expect(walkDir(path.join(tmp, '.claude'))).toEqual([abs]);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user