refactor(linkedin)!: rename plugin linkedin-thought-leadership → linkedin-studio (v3.0.0)
BREAKING CHANGE: the marketplace slug, the agent namespace (linkedin-studio:<agent>), and the runtime state-file path (~/.claude/linkedin-studio.local.md) all change. Reinstall required; existing state migrated in place (post metrics, streak, history preserved). The /linkedin:* commands are unchanged — the command namespace is set per-command in frontmatter and was always independent of the plugin slug. Functionality is byte-identical to v2.4.0; this release is pure identity. - dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json - agent namespace updated in commands/newsletter.md (only functional invoker) - state path updated in 4 hook scripts + topic-rotation prompt + state template - catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged) - docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md - historical records (CHANGELOG past entries, docs/ build artifacts, config-audit v5.0.0 snapshots) intentionally retain the old slug Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9df3de795c
commit
b6bb61246b
196 changed files with 164 additions and 138 deletions
|
|
@ -0,0 +1,87 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Lint-test for the editorial-reviewer fasit fixture.
|
||||
// Mirrors the structure-only discipline of persona-reviewer-fixture.test.mjs and
|
||||
// fact-checker-fixture.test.mjs: this test asserts the SHAPE of the fixture —
|
||||
// the two judging axes, all ten checks (P1–P5 + A1–A5), the three severities,
|
||||
// and the eight Del 4 cases that form the gold standard. Whether the agent's
|
||||
// live flags actually reproduce the fasit directions is [GATE]/[OPERATØR],
|
||||
// never self-certified here.
|
||||
|
||||
const FIXTURE_PATH = fileURLToPath(
|
||||
new URL('../fixtures/editorial-reviewer-cases.md', import.meta.url)
|
||||
);
|
||||
|
||||
const fixture = readFileSync(FIXTURE_PATH, 'utf8');
|
||||
|
||||
// The ten checks: five prose-craft (P1–P5) + five narrative-architecture (A1–A5).
|
||||
const PROSE_CHECKS = ['P1', 'P2', 'P3', 'P4', 'P5'];
|
||||
const ARCH_CHECKS = ['A1', 'A2', 'A3', 'A4', 'A5'];
|
||||
|
||||
// The two axis names (Norwegian, as the agent and the writing contract use them).
|
||||
const AXES = ['prosa-håndverk', 'narrativ-arkitektur'];
|
||||
|
||||
// The three-rung severity scale.
|
||||
const SEVERITIES = ['BLOCK', 'REWORK', 'NICE'];
|
||||
|
||||
describe('editorial-reviewer fixture structure', () => {
|
||||
test('names both judging axes', () => {
|
||||
for (const axis of AXES) {
|
||||
assert.ok(
|
||||
new RegExp(axis, 'i').test(fixture),
|
||||
`fixture must name the axis "${axis}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('documents all ten checks (P1–P5 + A1–A5)', () => {
|
||||
for (const check of [...PROSE_CHECKS, ...ARCH_CHECKS]) {
|
||||
assert.ok(
|
||||
fixture.includes(check),
|
||||
`fixture must reference the check "${check}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('defines the three-rung severity scale', () => {
|
||||
for (const sev of SEVERITIES) {
|
||||
assert.ok(
|
||||
fixture.includes(sev),
|
||||
`fixture must define the severity "${sev}"`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('documents the eight Del 4 cases', () => {
|
||||
const cases = fixture.match(/^###\s+Case\s+\d+\b/gim) || [];
|
||||
assert.equal(
|
||||
cases.length,
|
||||
8,
|
||||
`fixture must document exactly 8 Del 4 cases (found ${cases.length})`
|
||||
);
|
||||
});
|
||||
|
||||
test('ties the checklist to the Maskinrommet §C2 truth source', () => {
|
||||
assert.ok(
|
||||
/§C2|C2/.test(fixture),
|
||||
'fixture must reference the §C2 writing-contract truth source'
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps the jury-judges-writer-writes boundary (direction, not copy)', () => {
|
||||
assert.ok(
|
||||
/direction, not rewritten copy/i.test(fixture),
|
||||
'fixture must state the direction-not-copy boundary'
|
||||
);
|
||||
});
|
||||
|
||||
test('records the persona-blindspot rationale (≈6/8 editorial-only)', () => {
|
||||
assert.ok(
|
||||
/blindsone/i.test(fixture) && /6\/8/.test(fixture),
|
||||
'fixture must record why the gate exists (blind spots, ~6/8 editorial-only)'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Lint-test for the fact-checker fasit fixture.
|
||||
// Mirrors the structure-only discipline of state-updater.test.mjs: this test
|
||||
// asserts the SHAPE of the fixture (exactly 3 cases, one of each verdict, a
|
||||
// non-empty fasit per case). The accuracy comparison — does the agent's live
|
||||
// output actually match the fasit verdicts — is [GATE]/[OPERATØR], never
|
||||
// self-certified here.
|
||||
|
||||
const FIXTURE_PATH = fileURLToPath(
|
||||
new URL('../fixtures/fact-checker-cases.md', import.meta.url)
|
||||
);
|
||||
const VERDICTS = ['🟢', '🔴', '🟡'];
|
||||
|
||||
const fixture = readFileSync(FIXTURE_PATH, 'utf8');
|
||||
|
||||
// Split on "## Case N" headings; drop the preamble before the first case.
|
||||
const blocks = fixture
|
||||
.split(/^##\s+Case\s+\d+\b.*$/m)
|
||||
.slice(1)
|
||||
.map((b) => b.trim());
|
||||
|
||||
describe('fact-checker fixture structure', () => {
|
||||
test('contains exactly 3 cases', () => {
|
||||
assert.equal(blocks.length, 3, `expected 3 cases, found ${blocks.length}`);
|
||||
});
|
||||
|
||||
test('each case carries exactly one verdict emoji', () => {
|
||||
for (const [i, block] of blocks.entries()) {
|
||||
const present = VERDICTS.filter((v) => block.includes(v));
|
||||
assert.equal(
|
||||
present.length,
|
||||
1,
|
||||
`case ${i + 1} must have exactly one of 🟢/🔴/🟡, found: [${present.join(', ')}]`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the three verdicts are one each of 🟢/🔴/🟡', () => {
|
||||
const seen = blocks.map((b) => VERDICTS.find((v) => b.includes(v)));
|
||||
assert.deepEqual(
|
||||
[...seen].sort(),
|
||||
[...VERDICTS].sort(),
|
||||
`fixture must cover one true (🟢), one false (🔴), one unverifiable (🟡); saw ${JSON.stringify(seen)}`
|
||||
);
|
||||
});
|
||||
|
||||
test('each case has a non-empty Fasit field', () => {
|
||||
for (const [i, block] of blocks.entries()) {
|
||||
const m = block.match(/\*\*Fasit:\*\*\s*(.+)/);
|
||||
assert.ok(m, `case ${i + 1} is missing a **Fasit:** field`);
|
||||
assert.ok(
|
||||
m[1].trim().length > 0,
|
||||
`case ${i + 1} has an empty **Fasit:** field`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import { describe, test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Lint-test for the persona-reviewer fasit fixture.
|
||||
// Mirrors the structure-only discipline of state-updater.test.mjs and
|
||||
// fact-checker-fixture.test.mjs: this test asserts the SHAPE of the fixture —
|
||||
// one reader persona carrying all five library fields, a non-empty sample
|
||||
// draft, the six judging axes, and both review modes documented. Whether the
|
||||
// agent's live flags actually match the fasit directions is [GATE]/[OPERATØR],
|
||||
// never self-certified here.
|
||||
|
||||
const FIXTURE_PATH = fileURLToPath(
|
||||
new URL('../fixtures/persona-reviewer-cases.md', import.meta.url)
|
||||
);
|
||||
|
||||
const fixture = readFileSync(FIXTURE_PATH, 'utf8');
|
||||
|
||||
// The five persona field keys, lowercase to match config/personas.template.md.
|
||||
const PERSONA_FIELDS = ['rolle', 'avkobler', 'overbeviser', 'ekspertise', 'sjargong'];
|
||||
|
||||
// The six judging axes (plan Step 6 / fasit §6.3).
|
||||
const AXES = [
|
||||
'Krok', // hook holds?
|
||||
'Resonans', // does the point land?
|
||||
'Tone', // tone fit for this reader
|
||||
'Troverdighet', // credibility
|
||||
'Leder-takeaway', // leader takeaway + concrete action
|
||||
'Lengde', // length / drive
|
||||
];
|
||||
|
||||
// Both review modes must be documented (resonance + conversion).
|
||||
const MODES = ['resonans', 'konverter'];
|
||||
|
||||
describe('persona-reviewer fixture structure', () => {
|
||||
test('documents one persona with all five library fields', () => {
|
||||
for (const field of PERSONA_FIELDS) {
|
||||
assert.ok(
|
||||
new RegExp(`\\*\\*${field}\\*\\*`).test(fixture),
|
||||
`fixture must document the persona field **${field}**`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('contains a non-empty sample-text section', () => {
|
||||
const m = fixture.match(/##\s+Sample-tekst\b([\s\S]*?)(?=\n##\s|$)/i);
|
||||
assert.ok(m, 'fixture must have a "## Sample-tekst" section');
|
||||
assert.ok(
|
||||
m[1].trim().length > 80,
|
||||
'the sample-text section must contain a real draft excerpt, not a stub'
|
||||
);
|
||||
});
|
||||
|
||||
test('documents all six judging axes', () => {
|
||||
for (const axis of AXES) {
|
||||
assert.ok(fixture.includes(axis), `fixture must name the axis "${axis}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test('documents both review modes (resonance + conversion)', () => {
|
||||
for (const mode of MODES) {
|
||||
assert.ok(
|
||||
new RegExp(mode, 'i').test(fixture),
|
||||
`fixture must document the "${mode}" mode`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue