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}}')
|
||||
.replace(/\*/g, '[^/]*')
|
||||
.replace(/\?/g, '[^/]') // must run BEFORE placeholder restore — '(?:' would corrupt
|
||||
.replace(/\{\{GLOBSTAR_SLASH\}\}/g, '(?:/.+/|/)') // **/ matches 0+ intermediate dirs
|
||||
.replace(/\{\{GLOBSTAR\}\}/g, '.*')
|
||||
.replace(/\?/g, '[^/]');
|
||||
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
||||
|
||||
// Handle leading patterns
|
||||
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(' | ')}`);
|
||||
});
|
||||
});
|
||||
|
||||
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