fix(linkedin-studio): close v4.0.0 audit review findings (S8)
Close the 5 findings from the S7 /trekreview release gate (review.md, verdict BLOCK):
- BLOCKER: comment-multiplier "5x" reconciled to the canonical order-only framing
(no fixed multiplier) in agents/engagement-coach.md, linkedin-growth-playbook,
linkedin-formats.md — per algorithm-signals-reference.md ("do not quote a comment
multiplier").
- BLOCKER: carousel rate "6.60%/6.6% (highest)" reconciled to "~7% top organic
format" in linkedin-formats.md:42 (was self-contradicting :50) and
assets/templates/carousel-templates.md.
- Lint hardening: test-runner.sh STALE_STATS now matches 6.60% + the 5x comment
folklore and scans agents/ + assets/templates/ — the grep that defines the
Phase-0 criterion now catches both BLOCKERs.
- MAJOR: onboarding.md command count 26 -> 27.
- MAJOR: add section-append-branch (production-path) tests for recordFirstHourPlan
+ recordOutreachContact against a template-layout fixture.
- MINOR: move date-scalar changes.push inside the write branch in state-updater.mjs.
Verify: node --test hooks/scripts/__tests__/*.test.mjs -> 92/92; bash
scripts/test-runner.sh -> 66/0/0. NO push until /trekreview re-confirms ALLOW/WARN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1fa2cc945e
commit
18b198f655
8 changed files with 104 additions and 13 deletions
|
|
@ -46,7 +46,7 @@ Help creators:
|
|||
|
||||
**The math that most creators ignore:**
|
||||
- Comments rank above likes in the engagement order (see `references/algorithm-signals-reference.md`)
|
||||
- Comments drive 5x more reach than reshares
|
||||
- Substantive comments (15+ words) outweigh short ones and rank above plain reactions — but below saves and shares (no fixed comment-vs-reshare multiplier)
|
||||
- Posts with 15+ engagements in first hour unlock 2nd/3rd degree distribution
|
||||
- Your comments on others' posts expose you to their audience
|
||||
- Commenting within 30 minutes of a post = 64% more follow-up engagement on your comment
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Carousel Templates
|
||||
|
||||
Slide-by-slide blueprints for LinkedIn carousels (PDF document posts). Carousels have the highest engagement rate of all LinkedIn formats (6.6%) because they maximize dwell time and encourage swipe completion.
|
||||
Slide-by-slide blueprints for LinkedIn carousels (PDF document posts). Carousels/documents are the top organic format on LinkedIn (~7%; see `references/algorithm-signals-reference.md`) because they maximize dwell time and encourage swipe completion.
|
||||
|
||||
## Universal Design Specs
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ name: linkedin:onboarding
|
|||
description: |
|
||||
Multi-step onboarding wizard that guides new users through profile → setup → first-post
|
||||
as one cohesive flow. Designed for users who have just installed the plugin and want a
|
||||
single guided path instead of navigating 26 commands on their own.
|
||||
single guided path instead of navigating 27 commands on their own.
|
||||
Triggers on: "onboarding", "get started", "new user", "setup wizard", "start from scratch",
|
||||
"just installed", "how do I start", "walk me through", "linkedin onboarding".
|
||||
allowed-tools:
|
||||
|
|
@ -214,4 +214,4 @@ First post: [Published DATE / Pending — run /linkedin:first-post]
|
|||
- `/linkedin:batch` — Plan a full week of content in one session
|
||||
- `/linkedin:react` — Turn articles and news into posts
|
||||
- `/linkedin:strategy` — Growth strategy tailored to your follower level
|
||||
- `/linkedin` — See all 26 commands anytime
|
||||
- `/linkedin` — See all 27 commands anytime
|
||||
|
|
|
|||
|
|
@ -36,6 +36,42 @@ growth_rate_needed: 0
|
|||
## Milestone Log
|
||||
`;
|
||||
|
||||
// Mirrors config/state-file.template.md: every template-initialized state file
|
||||
// ships the ## First-Hour Plans and ## Outreach Pipeline sections pre-created,
|
||||
// each already containing its two <!-- --> format comments. Real
|
||||
// /linkedin:firsthour and /linkedin:outreach invocations therefore hit the
|
||||
// section-APPEND branch — not the section-CREATE branch that SAMPLE_STATE (which
|
||||
// omits the sections) exercises. These fixtures cover the production code path.
|
||||
const TEMPLATE_STATE = `---
|
||||
last_post_date: "2026-04-05"
|
||||
first_post_date: "2026-01-15"
|
||||
last_post_topic: "AI strategy"
|
||||
posts_this_week: 2
|
||||
weekly_goal: 3
|
||||
current_streak: 5
|
||||
longest_streak: 12
|
||||
follower_count: 850
|
||||
follower_target: 10000
|
||||
target_date: "2026-12-31"
|
||||
---
|
||||
|
||||
# LinkedIn Session State
|
||||
|
||||
## Recent Posts
|
||||
|
||||
- [2026-04-05] "AI governance is not about..." (1450) - AI strategy
|
||||
|
||||
## First-Hour Plans
|
||||
|
||||
<!-- First-hour / reply-loop plans, newest first. Written by /linkedin:firsthour. -->
|
||||
<!-- Format: ### [YYYY-MM-DD HH:MM] topic -->
|
||||
|
||||
## Outreach Pipeline
|
||||
|
||||
<!-- Outreach contacts / pipeline rows, newest first. Written by /linkedin:outreach. -->
|
||||
<!-- Format: ### [YYYY-MM-DD HH:MM] partner — track -->
|
||||
`;
|
||||
|
||||
describe('updatePostTracking', () => {
|
||||
test('sets last_post_date to provided date', () => {
|
||||
const result = updatePostTracking(SAMPLE_STATE, {
|
||||
|
|
@ -364,6 +400,28 @@ describe('recordFirstHourPlan', () => {
|
|||
assert.ok(Array.isArray(result.changes));
|
||||
assert.ok(result.changes.length > 0);
|
||||
});
|
||||
|
||||
test('appends into the pre-existing First-Hour Plans section (production path) without duplicating it or dropping its format comments', () => {
|
||||
// TEMPLATE_STATE ships the section + its two <!-- --> comments → the
|
||||
// section-APPEND branch must fire (the path every real invocation takes),
|
||||
// NOT section creation.
|
||||
assert.ok(TEMPLATE_STATE.includes('## First-Hour Plans'));
|
||||
const result = recordFirstHourPlan(TEMPLATE_STATE, PLAN_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
// Heading not duplicated (the create branch did not also run)
|
||||
const headings = result.content.match(/^## First-Hour Plans$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be duplicated');
|
||||
// Both template format comments survive the append
|
||||
assert.ok(result.content.includes('<!-- First-hour / reply-loop plans, newest first. Written by /linkedin:firsthour. -->'));
|
||||
assert.ok(result.content.includes('<!-- Format: ### [YYYY-MM-DD HH:MM] topic -->'));
|
||||
// Entry lands INSIDE the section (between its heading and the next section)
|
||||
const section = result.content.slice(
|
||||
result.content.indexOf('## First-Hour Plans'),
|
||||
result.content.indexOf('## Outreach Pipeline')
|
||||
);
|
||||
assert.ok(section.includes('[2026-05-30 09:00]'), 'entry must land inside the First-Hour Plans section');
|
||||
assert.ok(section.includes('AI governance'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('recordOutreachContact', () => {
|
||||
|
|
@ -438,4 +496,22 @@ describe('recordOutreachContact', () => {
|
|||
assert.ok(Array.isArray(result.changes));
|
||||
assert.ok(result.changes.length > 0);
|
||||
});
|
||||
|
||||
test('appends into the pre-existing Outreach Pipeline section (production path) without duplicating it or dropping its format comments', () => {
|
||||
// TEMPLATE_STATE ships the section + its two <!-- --> comments → the
|
||||
// section-APPEND branch must fire, NOT section creation.
|
||||
assert.ok(TEMPLATE_STATE.includes('## Outreach Pipeline'));
|
||||
const result = recordOutreachContact(TEMPLATE_STATE, CONTACT_OPTS);
|
||||
assert.notEqual(result, null);
|
||||
// Heading not duplicated (the create branch did not also run)
|
||||
const headings = result.content.match(/^## Outreach Pipeline$/gm) || [];
|
||||
assert.equal(headings.length, 1, 'section must not be duplicated');
|
||||
// Both template format comments survive the append
|
||||
assert.ok(result.content.includes('<!-- Outreach contacts / pipeline rows, newest first. Written by /linkedin:outreach. -->'));
|
||||
assert.ok(result.content.includes('<!-- Format: ### [YYYY-MM-DD HH:MM] partner — track -->'));
|
||||
// Entry lands INSIDE the section (it is the last section in the template)
|
||||
const section = result.content.slice(result.content.indexOf('## Outreach Pipeline'));
|
||||
assert.ok(section.includes('[2026-05-30 14:00]'), 'entry must land inside the Outreach Pipeline section');
|
||||
assert.ok(section.includes('@bigvoice'));
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -219,13 +219,16 @@ export function recordFirstHourPlan(stateContent, { planDate, postTopic = '', ta
|
|||
let content = stateContent;
|
||||
const changes = [];
|
||||
|
||||
// 1. last_firsthour_date — replace in place, else insert after last_post_date (additive)
|
||||
// 1. last_firsthour_date — replace in place, else insert after last_post_date (additive).
|
||||
// Report the change only inside the branch that actually writes it: if neither
|
||||
// anchor field exists, the scalar is not inserted and must not be reported as changed.
|
||||
if (/^last_firsthour_date: .*/m.test(content)) {
|
||||
content = replaceField(content, 'last_firsthour_date', `"${planDate}"`);
|
||||
changes.push(`last_firsthour_date → ${planDate}`);
|
||||
} else if (/^last_post_date: .*/m.test(content)) {
|
||||
content = content.replace(/^(last_post_date: .*)$/m, `$1\nlast_firsthour_date: "${planDate}"`);
|
||||
}
|
||||
changes.push(`last_firsthour_date → ${planDate}`);
|
||||
}
|
||||
|
||||
// 2. firsthour_active flag — only touch if the field is declared (additive)
|
||||
if (/^firsthour_active: .*/m.test(content)) {
|
||||
|
|
@ -276,15 +279,18 @@ export function recordOutreachContact(stateContent, { contactDate, track = '', p
|
|||
const changes = [];
|
||||
|
||||
// 1. last_outreach_date — replace in place, else insert after last_firsthour_date
|
||||
// if present, else after last_post_date (additive — never required up front)
|
||||
// if present, else after last_post_date (additive — never required up front).
|
||||
// Report the change only inside the branch that actually writes it.
|
||||
if (/^last_outreach_date: .*/m.test(content)) {
|
||||
content = replaceField(content, 'last_outreach_date', `"${contactDate}"`);
|
||||
changes.push(`last_outreach_date → ${contactDate}`);
|
||||
} else if (/^last_firsthour_date: .*/m.test(content)) {
|
||||
content = content.replace(/^(last_firsthour_date: .*)$/m, `$1\nlast_outreach_date: "${contactDate}"`);
|
||||
changes.push(`last_outreach_date → ${contactDate}`);
|
||||
} else if (/^last_post_date: .*/m.test(content)) {
|
||||
content = content.replace(/^(last_post_date: .*)$/m, `$1\nlast_outreach_date: "${contactDate}"`);
|
||||
}
|
||||
changes.push(`last_outreach_date → ${contactDate}`);
|
||||
}
|
||||
|
||||
// 2. outreach_active flag — only touch if the field is declared (additive)
|
||||
if (/^outreach_active: .*/m.test(content)) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ Choosing the right format isn't just about engagement rates—it's about underst
|
|||
|
||||
### Engagement Rates by Format (With Strategic Context)
|
||||
|
||||
**1. Multi-image carousels: 6.60% engagement rate (highest)**
|
||||
**1. Documents / carousels (PDF posts): top organic format (~7%; see `references/algorithm-signals-reference.md`)**
|
||||
- 6-10 slides optimal
|
||||
- 100-150 characters per slide
|
||||
- Caption: 300-500 characters
|
||||
|
|
@ -171,7 +171,7 @@ The first hour after posting determines 70% of your post's total reach. See the
|
|||
2. **Shares** (High signal - people want to show others)
|
||||
3. **Comments 15+ words** (High signal - 2x impact vs short comments)
|
||||
4. **Comments <15 words** (Medium signal)
|
||||
5. **Reactions** (Lower signal - 5x less valuable than comments)
|
||||
5. **Reactions** (Lower signal - baseline engagement unit; see `references/algorithm-signals-reference.md`)
|
||||
|
||||
**AI-generated generic comments reduce reach by 30% and engagement by 55%**
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ Complete reference guide for growing from hundreds to thousands of engaged follo
|
|||
|
||||
**Comment Value:**
|
||||
- Comments rank above likes in the engagement order (saves > shares > quality comments > reactions; see `references/algorithm-signals-reference.md`)
|
||||
- Comments: **5x more effective** than reshares
|
||||
- Comments: high-value engagement signal, but ranked below saves and shares — no verified fixed multiplier
|
||||
- Comments over 15 words: **2x impact** vs shorter ones
|
||||
- Comments from relevant professionals: Significantly higher weight than generic responses
|
||||
|
||||
|
|
|
|||
|
|
@ -187,8 +187,17 @@ echo "--- Algorithm-Stat Consistency ---"
|
|||
# stale/competing magnitudes — and the unpublishable model brand/date — must not
|
||||
# reappear anywhere else (cite the reference, do not restate). This enforces
|
||||
# "one value per effect" by forbidding the known competing values from returning.
|
||||
STALE_STATS='40-50%|25-40%|6\.6%|1\.92%|15x more reach|-40-60%|360Brew|January 2026'
|
||||
STAT_HITS=$(grep -rnE "$STALE_STATS" references/ commands/ skills/ hooks/prompts/ CLAUDE.md README.md .claude-plugin/plugin.json 2>/dev/null | grep -v 'algorithm-signals-reference' || true)
|
||||
#
|
||||
# Pattern + scope must cover every file the criterion's grep covers. The carousel
|
||||
# rate appears as the substring "6.60%" (which "6\.6%" does NOT match), and the
|
||||
# retired comment multiplier as "5x more effective / 5x less valuable / 5x more
|
||||
# reach than" — folklore the canonical reference forbids (it keeps only the
|
||||
# engagement ORDER). Scope includes agents/ and assets/templates/ because the two
|
||||
# v4.0.0 BLOCKERs survived there (agents/engagement-coach.md, assets/templates/
|
||||
# carousel-templates.md). assets/templates/ — not all of assets/ — keeps the scan
|
||||
# off gitignored runtime data (assets/analytics/, assets/drafts/).
|
||||
STALE_STATS='40-50%|25-40%|6\.6%|6\.60%|1\.92%|15x more reach|5x more effective|5x less valuable|5x more reach than|-40-60%|360Brew|January 2026'
|
||||
STAT_HITS=$(grep -rnE "$STALE_STATS" references/ commands/ skills/ hooks/prompts/ agents/ assets/templates/ CLAUDE.md README.md .claude-plugin/plugin.json 2>/dev/null | grep -v 'algorithm-signals-reference' || true)
|
||||
if [ -z "$STAT_HITS" ]; then
|
||||
pass "no stale algorithm magnitudes / model brand outside the canonical reference"
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue