feat(linkedin-studio): short-form de-AI gate via differentiation-checker + voice-guardian

Wire the orphan differentiation-checker (#10) into the five short-form
creation commands (post/quick/react/carousel/video) as a De-AI /
Differentiation Gate at each command's quality-check step: confirm the
LinkedIn-named substance signals (personal substance, original thinking,
concrete specifics, genuine voice) + a soft engagement-bait check, and
delegate an originality pass to linkedin-studio:differentiation-checker
when the angle risks commodity content. Add Task to allowed-tools in
quick/react/carousel (post/video already had it from Step 13).

Extend (not duplicate) hooks/prompts/voice-guardian.md's AI-pattern
section with the same named signals from research/01 D8 + research/03 D4.
Runtime-loaded prompt — no compile-hooks.py, no hooks.json change
(verified: compile-hooks --check reports no drift).

Test: new hooks/scripts/__tests__/linkedin-content-filter.test.mjs pins
the content/non-content boundary the gate is scoped by (14 tests).
Full hook suite 76/76, structure lint 61/61.

Plan Step 14 (Wave 4 S2). Counts unchanged (26 commands / 19 agents).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-30 02:31:41 +02:00
commit e2ed3eb0aa
7 changed files with 131 additions and 0 deletions

View file

@ -0,0 +1,87 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { isLinkedInContent } from '../linkedin-content-filter.mjs';
// The short-form de-AI / differentiation gate (Step 14) is scoped by the same
// content/non-content boundary the content-gatekeeper hook uses. These tests
// pin that boundary so the gate fires on the short-form draft paths the five
// creation commands (post/quick/react/carousel/video) actually write to, and
// stays silent on plugin infrastructure (commands, hooks, references, config).
describe('isLinkedInContent — short-form gate scope (positive)', () => {
test('fires on a draft post under assets/drafts/', () => {
assert.equal(isLinkedInContent('assets/drafts/2026-05-30-ai-governance.md'), true);
});
test('fires on a carousel slide image under assets/drafts/', () => {
assert.equal(isLinkedInContent('assets/drafts/carousel-20260530-ai/slide-1.png'), true);
});
test('fires on a nested absolute assets/drafts path', () => {
assert.equal(
isLinkedInContent('/Users/ktg/work/assets/drafts/video-script.md'),
true
);
});
test('fires on a linkedin-posts content path', () => {
assert.equal(isLinkedInContent('/Users/ktg/linkedin-posts/quick-take.md'), true);
});
test('fires on a linkedin-studio/assets content path', () => {
assert.equal(
isLinkedInContent('/repo/plugins/linkedin-studio/assets/examples/post.md'),
true
);
});
});
describe('isLinkedInContent — gate stays silent on infrastructure (negative)', () => {
test('does not fire on a command file', () => {
assert.equal(isLinkedInContent('commands/post.md'), false);
});
test('does not fire on a hook prompt', () => {
assert.equal(isLinkedInContent('hooks/prompts/voice-guardian.md'), false);
});
test('does not fire on a reference doc', () => {
assert.equal(isLinkedInContent('references/linkedin-formats.md'), false);
});
test('does not fire on an agent definition', () => {
assert.equal(isLinkedInContent('agents/differentiation-checker.md'), false);
});
test('does not fire on a script or its tests', () => {
assert.equal(isLinkedInContent('hooks/scripts/state-updater.mjs'), false);
assert.equal(
isLinkedInContent('hooks/scripts/__tests__/linkedin-content-filter.test.mjs'),
false
);
});
});
describe('isLinkedInContent — code/config/template/meta files (negative)', () => {
test('does not fire on code/config extensions', () => {
for (const p of ['x.json', 'x.mjs', 'x.ts', 'x.yaml', 'x.css', 'x.html']) {
assert.equal(isLinkedInContent(p), false, `${p} should be non-content`);
}
});
test('does not fire on template files', () => {
assert.equal(isLinkedInContent('config/state-file.template.md'), false);
});
test('does not fire on known meta filenames', () => {
for (const p of ['CLAUDE.md', 'README.md', 'CHANGELOG.md', 'notes.local.md']) {
assert.equal(isLinkedInContent(p), false, `${p} should be non-content`);
}
});
test('returns false for empty or missing path', () => {
assert.equal(isLinkedInContent(''), false);
assert.equal(isLinkedInContent(null), false);
assert.equal(isLinkedInContent(undefined), false);
});
});