feat(linkedin-thought-leadership): v1.1.0 — Q2 2026 feature release

9 improvements across 3 tracks:

Onboarding: /linkedin:onboarding wizard, README Quick Start rewrite
Content Quality: voice drift scoring, industry angle variants,
  /linkedin:carousel, /linkedin:react multi-URL comparison
Analytics: automated week-rollover, day-of-week heatmap,
  month-over-month reports

25→27 commands. All Q2 ROADMAP items completed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-04-08 06:16:35 +02:00
commit 1a8cc1942c
33 changed files with 1726 additions and 236 deletions

View file

@ -0,0 +1,102 @@
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { applyWeekRollover } from '../week-rollover.mjs';
const SAMPLE_STATE = `---
last_post_date: "2026-04-05"
first_post_date: "2026-01-15"
last_post_topic: "AI strategy"
posts_this_week: 3
weekly_goal: 3
current_streak: 5
longest_streak: 12
current_week: "2026-W14"
last_import_date: "2026-04-01"
follower_count: 850
follower_target: 10000
target_date: "2026-12-31"
---
## Recent Posts
- 2026-04-05: AI strategy post
`;
describe('applyWeekRollover', () => {
test('resets posts_this_week to 0 on week change', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15');
assert.notEqual(result, null);
assert.match(result.content, /^posts_this_week: 0$/m);
});
test('updates current_week to new week', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15');
assert.notEqual(result, null);
assert.match(result.content, /^current_week: "2026-W15"$/m);
});
test('returns descriptive message on rollover', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15');
assert.notEqual(result, null);
assert.ok(result.message.includes('2026-W15'));
assert.ok(result.message.includes('2026-W14'));
});
test('returns null when week matches (no change needed)', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W14');
assert.equal(result, null);
});
test('preserves all other YAML fields unchanged', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15');
assert.notEqual(result, null);
assert.match(result.content, /^last_post_date: "2026-04-05"$/m);
assert.match(result.content, /^current_streak: 5$/m);
assert.match(result.content, /^weekly_goal: 3$/m);
assert.match(result.content, /^follower_count: 850$/m);
});
test('preserves markdown body after frontmatter', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '2026-W15');
assert.notEqual(result, null);
assert.ok(result.content.includes('## Recent Posts'));
assert.ok(result.content.includes('AI strategy post'));
});
test('initializes current_week when empty without resetting posts', () => {
const stateWithEmptyWeek = SAMPLE_STATE.replace(
'current_week: "2026-W14"',
'current_week: ""'
);
const result = applyWeekRollover(stateWithEmptyWeek, '', '2026-W15');
assert.notEqual(result, null);
assert.match(result.content, /^current_week: "2026-W15"$/m);
// posts_this_week should NOT be reset (user may have manually tracked)
assert.match(result.content, /^posts_this_week: 3$/m);
});
test('returns null when actualWeek is empty', () => {
const result = applyWeekRollover(SAMPLE_STATE, '2026-W14', '');
assert.equal(result, null);
});
test('returns null when actualWeek is null/undefined', () => {
assert.equal(applyWeekRollover(SAMPLE_STATE, '2026-W14', null), null);
assert.equal(applyWeekRollover(SAMPLE_STATE, '2026-W14', undefined), null);
});
test('handles year boundary rollover (W52 → W01)', () => {
const yearEndState = SAMPLE_STATE.replace('2026-W14', '2025-W52');
const result = applyWeekRollover(yearEndState, '2025-W52', '2026-W01');
assert.notEqual(result, null);
assert.match(result.content, /^posts_this_week: 0$/m);
assert.match(result.content, /^current_week: "2026-W01"$/m);
});
test('handles posts_this_week already at 0', () => {
const zeroPostsState = SAMPLE_STATE.replace('posts_this_week: 3', 'posts_this_week: 0');
const result = applyWeekRollover(zeroPostsState, '2026-W14', '2026-W15');
assert.notEqual(result, null);
assert.match(result.content, /^posts_this_week: 0$/m);
assert.match(result.content, /^current_week: "2026-W15"$/m);
});
});

View file

@ -7,6 +7,7 @@ import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { calculateScore } from './personalization-score.mjs';
import { queueToday, queueOverdue, queueUpcoming } from './queue-manager.mjs';
import { applyWeekRollover } from './week-rollover.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const PLUGIN_ROOT = join(__dirname, '..', '..');
@ -135,11 +136,17 @@ if (existsSync(STATE_FILE)) {
}
}
// Week rollover check
// Week rollover — auto-reset posts_this_week on week change
const actualWeek = isoWeek();
let weekResetNote = '';
if (currentWeek && currentWeek !== actualWeek) {
weekResetNote = `Note: Week has changed from ${currentWeek} to ${actualWeek}. posts_this_week should be reset to 0.`;
try {
const rollover = applyWeekRollover(stateContent, currentWeek, actualWeek);
if (rollover) {
writeFileSync(STATE_FILE, rollover.content, 'utf-8');
weekResetNote = rollover.message;
}
} catch (err) {
weekResetNote = `Warning: Week rollover failed (${err.message}). Manual reset may be needed.`;
}
// Build status line
@ -253,9 +260,7 @@ if (existsSync(STATE_FILE)) {
}
// Personalization score check
if (pScore !== null && pScore === 0) {
context += '## Quick Win\\nPersonalization: 0%. Run /linkedin:setup (15 min) to unlock voice-matched, audience-specific content.\\n\\n';
} else if (pScore !== null && pScore < 50) {
if (pScore !== null && pScore < 50) {
reminders += `- Personalization score is ${pScore}%. Run /linkedin:setup to improve content quality with your real voice, case studies, and audience data.\\n`;
}
@ -369,13 +374,8 @@ if (existsSync(STATE_FILE)) {
content = content.replace(/^current_week: .*/m, `current_week: "${actualWeek}"`);
writeFileSync(STATE_FILE, content);
context = `LinkedIn state file auto-initialized from template at ${STATE_FILE}.\\n`;
context += `Current ISO week set to ${actualWeek}.\\n\\n`;
context += '## Welcome to LinkedIn Thought Leadership\\n\\n';
context += 'Your state file has been initialized. Here is how to get started:\\n\\n';
context += '1. Run /linkedin:profile — Optimize your LinkedIn profile for 360Brew (critical before first post)\\n';
context += '2. Run /linkedin:setup — Personalize with your voice, case studies, and audience data\\n';
context += '3. Run /linkedin:first-post — Create your first post in under 10 minutes\\n\\n';
context += 'Your personalization score is 0%. Content quality improves as you fill in your profile.\\n';
context += `Current ISO week set to ${actualWeek}.\\n`;
context += 'Edit the file to set your expertise_areas and weekly_goal.\\n';
} else {
context = `No LinkedIn state file found at ${STATE_FILE} and template missing.\\n`;
context += `Expected template at: ${templateFile}\\n`;

View file

@ -0,0 +1,49 @@
// Pure function for week-rollover logic.
// Exported separately for testability.
/**
* Apply week rollover to state file content.
* Returns updated content string if rollover was applied, null otherwise.
*
* @param {string} stateContent - Full state file content (with YAML frontmatter)
* @param {string} currentWeek - Week value from state file (e.g. "2026-W14")
* @param {string} actualWeek - Computed current ISO week (e.g. "2026-W15")
* @returns {{ content: string, message: string } | null}
*/
export function applyWeekRollover(stateContent, currentWeek, actualWeek) {
if (!actualWeek) return null;
// Case 1: current_week is empty — initialize without resetting posts
if (!currentWeek) {
const updated = stateContent.replace(
/^current_week: .*/m,
`current_week: "${actualWeek}"`
);
if (updated === stateContent) return null;
return {
content: updated,
message: `Initialized current_week to ${actualWeek}.`
};
}
// Case 2: week matches — no action needed
if (currentWeek === actualWeek) return null;
// Case 3: week changed — reset posts_this_week and update current_week
let updated = stateContent;
updated = updated.replace(
/^posts_this_week: .*/m,
'posts_this_week: 0'
);
updated = updated.replace(
/^current_week: .*/m,
`current_week: "${actualWeek}"`
);
if (updated === stateContent) return null;
return {
content: updated,
message: `Auto-reset: posts_this_week → 0 for new week ${actualWeek} (was ${currentWeek}).`
};
}