From c35e9fc9d30c6c617e861e581ad78138b5a088cc Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 18 Jun 2026 11:50:04 +0200 Subject: [PATCH] =?UTF-8?q?refactor(linkedin-studio):=20M0-10=20=E2=80=94?= =?UTF-8?q?=20content-filter=20recognizes=20external=20drafts=20(after=20n?= =?UTF-8?q?egatives,=20before=20infraDirs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linkedin-content-filter.test.mjs | 32 +++++++++++++++++++ hooks/scripts/linkedin-content-filter.mjs | 9 +++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/hooks/scripts/__tests__/linkedin-content-filter.test.mjs b/hooks/scripts/__tests__/linkedin-content-filter.test.mjs index 3a88e60..2b2ffb9 100644 --- a/hooks/scripts/__tests__/linkedin-content-filter.test.mjs +++ b/hooks/scripts/__tests__/linkedin-content-filter.test.mjs @@ -85,3 +85,35 @@ describe('isLinkedInContent — code/config/template/meta files (negative)', () assert.equal(isLinkedInContent(undefined), false); }); }); + +// M0-10: after drafts relocate to the external data root, the gate must still +// fire on them. `.claude` is an infraDir, so a naive check rejects any /.claude/ +// path — the external-drafts positive must short-circuit BEFORE that negative, +// while the state file (.local.md) and REMEMBER.md stay excluded. +describe('isLinkedInContent — external data-root drafts (M0-10)', () => { + const H = '/Users/test'; + + test('fires on a relocated draft under ~/.claude/linkedin-studio/drafts/', () => { + assert.equal(isLinkedInContent(`${H}/.claude/linkedin-studio/drafts/2026-06-20-post.md`), true); + }); + + test('fires on a relocated carousel slide under the external drafts dir', () => { + assert.equal(isLinkedInContent(`${H}/.claude/linkedin-studio/drafts/carousel-x/slide-1.png`), true); + }); + + test('does NOT fire on the external state file (.local.md)', () => { + assert.equal(isLinkedInContent(`${H}/.claude/linkedin-studio.local.md`), false); + }); + + test('does NOT fire on the external REMEMBER.md', () => { + assert.equal(isLinkedInContent(`${H}/.claude/linkedin-studio/REMEMBER.md`), false); + }); + + test('does NOT fire on ~/.claude/settings.json', () => { + assert.equal(isLinkedInContent(`${H}/.claude/settings.json`), false); + }); + + test('still fires on in-plugin assets/drafts/ (migration-window back-compat)', () => { + assert.equal(isLinkedInContent('assets/drafts/2026-06-20-post.md'), true); + }); +}); diff --git a/hooks/scripts/linkedin-content-filter.mjs b/hooks/scripts/linkedin-content-filter.mjs index a93bed1..58e3213 100644 --- a/hooks/scripts/linkedin-content-filter.mjs +++ b/hooks/scripts/linkedin-content-filter.mjs @@ -23,9 +23,16 @@ export function isLinkedInContent(filePath) { const nonContent = ['.local.md', 'CLAUDE.md', 'README.md', 'CHANGELOG.md', 'REMEMBER.md', 'BACKLOG.md', 'DEVELOPMENT-LOG.md']; if (nonContent.some(n => base.endsWith(n) || base === n)) return false; + const normalized = filePath.replace(/\\/g, '/'); + + // POSITIVE (external): relocated drafts under the per-user data root are the + // only external data class the Write/Edit quality gate guards (voice-samples / + // analytics / profile are not editor-gated content). Must come BEFORE the + // infraDirs negative below, which would otherwise reject any /.claude/ path. + if (normalized.startsWith('.claude/linkedin-studio/drafts/') || normalized.includes('/.claude/linkedin-studio/drafts/')) return true; + // NEGATIVE: infrastructure paths const infraDirs = ['hooks', 'scripts', 'config', 'commands', 'agents', 'skills', 'references', 'docs', '.claude', '.claude-plugin', 'node_modules']; - const normalized = filePath.replace(/\\/g, '/'); for (const dir of infraDirs) { if (normalized.startsWith(dir + '/') || normalized.includes('/' + dir + '/')) return false; }