fix(rul): globToRegex corrupts mid-pattern /**/ globs (M-BUG-19)
The ? -> [^/] replacement ran AFTER the {{GLOBSTAR_SLASH}} placeholder was
restored to '(?:/.+/|/)', corrupting the group opener '(?:' into '([^/]:' —
every rule pattern containing a mid-pattern '/**/' silently matched only the
zero-dir branch and live rules were flagged 'matches no files' (CA-RUL).
Found by dogfooding /config-audit implement on a throwaway repo copy: the
implementer agent's correct 'posts/**/post.md' rule was flagged dead.
Fix: run the ? replacement before placeholder restoration. Fixture outcomes
byte-identical; frozen v5.0.0 baselines untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4b7b2d9c48
commit
0cd87e0597
2 changed files with 40 additions and 2 deletions
|
|
@ -251,9 +251,9 @@ function globToRegex(pattern) {
|
||||||
.replace(/\/\*\*\//g, '{{GLOBSTAR_SLASH}}')
|
.replace(/\/\*\*\//g, '{{GLOBSTAR_SLASH}}')
|
||||||
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
||||||
.replace(/\*/g, '[^/]*')
|
.replace(/\*/g, '[^/]*')
|
||||||
|
.replace(/\?/g, '[^/]') // must run BEFORE placeholder restore — '(?:' would corrupt
|
||||||
.replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(?:/.+/|/)') // **/ matches 0+ intermediate dirs
|
.replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(?:/.+/|/)') // **/ matches 0+ intermediate dirs
|
||||||
.replace(/\{\{GLOBSTAR\}\}/g, '.*')
|
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
||||||
.replace(/\?/g, '[^/]');
|
|
||||||
|
|
||||||
// Handle leading patterns
|
// Handle leading patterns
|
||||||
if (!regex.startsWith('.*') && !regex.startsWith('/')) {
|
if (!regex.startsWith('.*') && !regex.startsWith('/')) {
|
||||||
|
|
|
||||||
|
|
@ -291,3 +291,41 @@ describe('RUL — user-global rule is not flagged "matches no files" (M-BUG-9 gu
|
||||||
assert.equal(dead.length, 0, `user-global rule wrongly flagged dead: ${dead.map(f => f.evidence).join(' | ')}`);
|
assert.equal(dead.length, 0, `user-global rule wrongly flagged dead: ${dead.map(f => f.evidence).join(' | ')}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('RUL — mid-pattern /**/ glob matches intermediate dirs (M-BUG-19)', () => {
|
||||||
|
// globToRegex restored the {{GLOBSTAR_SLASH}} placeholder to "(?:/.+/|/)"
|
||||||
|
// BEFORE the ? → [^/] replacement ran, corrupting the group opener "(?:"
|
||||||
|
// into "([^/]:" — so every pattern containing a mid-pattern "/**/" silently
|
||||||
|
// matched nothing but the zero-dir "|/" branch, and live rules like
|
||||||
|
// "posts/**/post.md" were flagged "matches no files".
|
||||||
|
let tmpRoot;
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
resetCounter();
|
||||||
|
tmpRoot = await mkdtemp(join(tmpdir(), 'ca-rul-globstar-'));
|
||||||
|
await mkdir(join(tmpRoot, '.claude', 'rules'), { recursive: true });
|
||||||
|
await mkdir(join(tmpRoot, 'posts', '2026-01-23-slug'), { recursive: true });
|
||||||
|
await writeFile(join(tmpRoot, 'posts', '2026-01-23-slug', 'post.md'), '# Post\n', 'utf8');
|
||||||
|
await writeFile(
|
||||||
|
join(tmpRoot, '.claude', 'rules', 'post-scope.md'),
|
||||||
|
'---\npaths: "posts/**/post.md"\n---\n\n# Post rule\nbody\n',
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const discovery = await discoverConfigFiles(tmpRoot);
|
||||||
|
result = await scan(tmpRoot, discovery);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does NOT flag a live posts/**/post.md rule as "matches no files"', () => {
|
||||||
|
const dead = result.findings.filter(f => f.title.includes('matches no files'));
|
||||||
|
assert.equal(
|
||||||
|
dead.length,
|
||||||
|
0,
|
||||||
|
`/**/ rule matched a real file but was flagged dead: ${dead.map(f => f.evidence).join(' | ')}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue