diff --git a/src/commands/sources.ts b/src/commands/sources.ts index b08ebb2..4f8be68 100644 --- a/src/commands/sources.ts +++ b/src/commands/sources.ts @@ -47,17 +47,23 @@ function validateSourceId(id: string): void { // - Single-level glob: 'wedding-planning/*' (cosmetic; same as literal) // // Rules: -// - Allowed chars: lowercase a-z, 0-9, '-' (hyphen), '/' (slash) +// - Allowed chars: lowercase a-z, 0-9, '-' (hyphen), '_' (underscore), '/' (slash) // - Optional single trailing '*' (no other position) // - Length 1..64 (excluding the trailing '*') -// - Reject underscores ('_'), uppercase, whitespace, mid-string '*', -// multi-level globs ('**'), any other punctuation +// - Reject uppercase, whitespace, mid-string '*', multi-level globs +// ('**'), any other punctuation // -// Why fail-fast at write time: a typo'd rule (`memory_dashboard/`) writes -// successfully into config_jsonb but never matches anything at routing -// time — the put_page silently falls to brain-default. Catching at -// CLI-write moment surfaces "this is a typo" before bad data lands. -const SLUG_PREFIX_RULE_RE = /^[a-z0-9](?:[a-z0-9-/]{0,62}[a-z0-9-/])?\*?$/; +// Why fail-fast at write time: a typo'd rule writes successfully into +// config_jsonb but never matches anything at routing time — the put_page +// silently falls to brain-default. Catching at CLI-write moment surfaces +// the typo before bad data lands. +// +// Underscore note (v0.18.2.fork.1 Phase 6 fix): chezmoi-managed prefixes +// like `dot_claude/` are legitimate slugs — chezmoi conventionally maps +// `~/.claude/` → `dot_claude/` in source. Earlier draft of this validator +// rejected underscores, which broke Phase 4 source taxonomy. Underscore +// is now first-class. +const SLUG_PREFIX_RULE_RE = /^[a-z0-9_](?:[a-z0-9_\-/]{0,62}[a-z0-9_\-/])?\*?$/; function validateSlugPrefix(rule: string): void { if (!rule || rule.length === 0) { throw new Error('Empty slug-prefix rule. Each --slug-prefix entry must be a non-empty string.'); diff --git a/test/sources-update-slug-prefix.test.ts b/test/sources-update-slug-prefix.test.ts index 68b386d..93156d6 100644 --- a/test/sources-update-slug-prefix.test.ts +++ b/test/sources-update-slug-prefix.test.ts @@ -106,8 +106,10 @@ describe('Prefix grammar validator (Issue #6 — fail-fast at write time)', () = if (hint) expect(msg).toContain(hint); }; - test('reject underscore', async () => { - await expectReject('memory_dashboard/', 'Invalid slug-prefix'); + test('accept underscore (chezmoi-style prefixes like dot_claude/)', async () => { + await runSources(engine, ['add', 'test-accept-under', '--slug-prefix', 'dot_claude/,foo_bar/']); + const cfg = await readConfig('test-accept-under'); + expect(cfg.slug_prefix_rules).toEqual(['dot_claude/', 'foo_bar/']); }); test('reject uppercase', async () => { await expectReject('MemoryDashboard/', 'Invalid slug-prefix');