@sellable/mcp 0.1.256 → 0.1.258

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@ export type EngagementPost = {
7
7
  name: string;
8
8
  headline: string;
9
9
  profileUrl: string;
10
+ followerCount: number | null;
10
11
  };
11
12
  engagement: {
12
13
  likes: number;
@@ -14,6 +15,16 @@ export type EngagementPost = {
14
15
  shares: number;
15
16
  total: number;
16
17
  };
18
+ reachSignals: {
19
+ targetFollowerMin: number | null;
20
+ targetFollowerMax: number | null;
21
+ followerBandFit: "in_target_band" | "below_target_band" | "above_target_band" | "unknown";
22
+ weightedEngagement: number;
23
+ engagementPer1kFollowers: number | null;
24
+ weightedEngagementPer1kFollowers: number | null;
25
+ reachPenaltyMultiplier: number;
26
+ reachAdjustedScore: number | null;
27
+ };
17
28
  contentPreview: string;
18
29
  };
19
30
  export type SearchEngagementPostsInput = {
@@ -22,6 +33,8 @@ export type SearchEngagementPostsInput = {
22
33
  maxAgeDays?: number;
23
34
  minTotalEngagement?: number;
24
35
  maxPosts?: number;
36
+ targetFollowerMin?: number;
37
+ targetFollowerMax?: number;
25
38
  excludePostUrls?: string[];
26
39
  };
27
40
  export type SearchEngagementPostsResponse = {
@@ -63,6 +76,14 @@ export declare const engageDiscoveryToolDefinitions: {
63
76
  type: string;
64
77
  description: string;
65
78
  };
79
+ targetFollowerMin: {
80
+ type: string;
81
+ description: string;
82
+ };
83
+ targetFollowerMax: {
84
+ type: string;
85
+ description: string;
86
+ };
66
87
  excludePostUrls: {
67
88
  type: string;
68
89
  items: {
@@ -27,6 +27,14 @@ export const engageDiscoveryToolDefinitions = [
27
27
  type: "number",
28
28
  description: "Max posts to return after filtering (default 25).",
29
29
  },
30
+ targetFollowerMin: {
31
+ type: "number",
32
+ description: "Optional lower bound for the author's follower count when comparing reach-normalized hook performance. Does not hard-filter; adds reach signals and prioritizes the target band when follower data is available.",
33
+ },
34
+ targetFollowerMax: {
35
+ type: "number",
36
+ description: "Optional upper bound for the author's follower count when comparing reach-normalized hook performance. Does not hard-filter; adds reach signals and prioritizes the target band when follower data is available.",
37
+ },
30
38
  excludePostUrls: {
31
39
  type: "array",
32
40
  items: { type: "string" },
@@ -63,6 +71,101 @@ function safeNumber(value) {
63
71
  const n = typeof value === "number" ? value : Number(value);
64
72
  return Number.isFinite(n) ? n : 0;
65
73
  }
74
+ function parseFollowerCount(value) {
75
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
76
+ return Math.round(value);
77
+ }
78
+ if (typeof value !== "string")
79
+ return null;
80
+ const cleaned = value.trim().toLowerCase().replace(/,/g, "");
81
+ if (!cleaned)
82
+ return null;
83
+ const match = cleaned.match(/(\d+(?:\.\d+)?)\s*([kmb])?/);
84
+ if (!match)
85
+ return null;
86
+ const base = Number(match[1]);
87
+ if (!Number.isFinite(base) || base <= 0)
88
+ return null;
89
+ const suffix = match[2] || "";
90
+ const multiplier = suffix === "b" ? 1_000_000_000 : suffix === "m" ? 1_000_000 : suffix === "k" ? 1_000 : 1;
91
+ return Math.round(base * multiplier);
92
+ }
93
+ function extractFollowerCount(post) {
94
+ const candidates = [
95
+ post?.author?.followerCount,
96
+ post?.author?.followersCount,
97
+ post?.author?.followers,
98
+ post?.author?.follower_count,
99
+ post?.author?.linkedinFollowers,
100
+ post?.author?.stats?.followers,
101
+ post?.followerCount,
102
+ post?.followersCount,
103
+ post?.followers,
104
+ post?.authorFollowerCount,
105
+ ];
106
+ for (const candidate of candidates) {
107
+ const parsed = parseFollowerCount(candidate);
108
+ if (parsed !== null)
109
+ return parsed;
110
+ }
111
+ return null;
112
+ }
113
+ function normalizeFollowerBound(value) {
114
+ const parsed = parseFollowerCount(value);
115
+ return parsed && parsed > 0 ? parsed : null;
116
+ }
117
+ function followerBandFit(followerCount, targetFollowerMin, targetFollowerMax) {
118
+ if (!followerCount)
119
+ return "unknown";
120
+ if (targetFollowerMin && followerCount < targetFollowerMin) {
121
+ return "below_target_band";
122
+ }
123
+ if (targetFollowerMax && followerCount > targetFollowerMax) {
124
+ return "above_target_band";
125
+ }
126
+ if (targetFollowerMin || targetFollowerMax)
127
+ return "in_target_band";
128
+ return "unknown";
129
+ }
130
+ function bandMultiplier(fit, followerCount, targetFollowerMax) {
131
+ if (fit === "in_target_band")
132
+ return 1;
133
+ if (fit === "below_target_band")
134
+ return 0.75;
135
+ if (fit === "unknown")
136
+ return 0.4;
137
+ if (!followerCount || !targetFollowerMax)
138
+ return 0.45;
139
+ if (followerCount <= targetFollowerMax * 2)
140
+ return 0.65;
141
+ if (followerCount <= targetFollowerMax * 5)
142
+ return 0.35;
143
+ return 0.15;
144
+ }
145
+ function reachSignals(engagement, followerCount, targetFollowerMin, targetFollowerMax) {
146
+ const weightedEngagement = engagement.likes + engagement.comments * 4 + engagement.shares * 12;
147
+ const fit = followerBandFit(followerCount, targetFollowerMin, targetFollowerMax);
148
+ const engagementPer1kFollowers = followerCount
149
+ ? Number(((engagement.total / followerCount) * 1000).toFixed(3))
150
+ : null;
151
+ const weightedEngagementPer1kFollowers = followerCount
152
+ ? Number(((weightedEngagement / followerCount) * 1000).toFixed(3))
153
+ : null;
154
+ const reachPenaltyMultiplier = bandMultiplier(fit, followerCount, targetFollowerMax);
155
+ const reachAdjustedScore = weightedEngagementPer1kFollowers === null
156
+ ? null
157
+ : Number((weightedEngagementPer1kFollowers * reachPenaltyMultiplier).toFixed(3));
158
+ return {
159
+ targetFollowerMin,
160
+ targetFollowerMax,
161
+ followerBandFit: fit,
162
+ weightedEngagement,
163
+ engagementPer1kFollowers,
164
+ weightedEngagementPer1kFollowers,
165
+ reachPenaltyMultiplier,
166
+ reachAdjustedScore,
167
+ };
168
+ }
66
169
  export async function searchEngagementPosts(input) {
67
170
  const api = getApi();
68
171
  const keywords = (input.keywords || [])
@@ -79,6 +182,10 @@ export async function searchEngagementPosts(input) {
79
182
  const maxPosts = typeof input.maxPosts === "number" && input.maxPosts > 0
80
183
  ? Math.min(50, input.maxPosts)
81
184
  : 25;
185
+ const targetFollowerMin = normalizeFollowerBound(input.targetFollowerMin);
186
+ const targetFollowerMax = normalizeFollowerBound(input.targetFollowerMax);
187
+ const useTargetFollowerBand = Boolean(targetFollowerMin || targetFollowerMax);
188
+ const collectLimit = useTargetFollowerBand ? 50 : maxPosts;
82
189
  const exclude = new Set((input.excludePostUrls || [])
83
190
  .map((u) => normalizePostUrl(u))
84
191
  .filter(Boolean));
@@ -87,11 +194,13 @@ export async function searchEngagementPosts(input) {
87
194
  keywords: keywords.map((keyword) => ({ keyword })),
88
195
  page,
89
196
  });
90
- const rawPosts = Array.isArray(response?.topPostsForLLM)
91
- ? response.topPostsForLLM
92
- : Array.isArray(response?.posts)
93
- ? response.posts
94
- : [];
197
+ const rawPosts = useTargetFollowerBand && Array.isArray(response?.posts)
198
+ ? response.posts
199
+ : Array.isArray(response?.topPostsForLLM)
200
+ ? response.topPostsForLLM
201
+ : Array.isArray(response?.posts)
202
+ ? response.posts
203
+ : [];
95
204
  const now = Date.now();
96
205
  const oldestMs = now - maxAgeDays * 24 * 60 * 60 * 1000;
97
206
  let tooOld = 0;
@@ -111,6 +220,7 @@ export async function searchEngagementPosts(input) {
111
220
  const comments = safeNumber(p?.engagement?.comments);
112
221
  const shares = safeNumber(p?.engagement?.shares);
113
222
  const total = likes + comments + shares;
223
+ const engagement = { likes, comments, shares, total };
114
224
  if (total < minTotalEngagement) {
115
225
  tooLowEngagement += 1;
116
226
  continue;
@@ -121,6 +231,7 @@ export async function searchEngagementPosts(input) {
121
231
  tooOld += 1;
122
232
  continue;
123
233
  }
234
+ const followerCount = extractFollowerCount(p);
124
235
  kept.push({
125
236
  postId: String(p?.id || ""),
126
237
  url,
@@ -132,15 +243,31 @@ export async function searchEngagementPosts(input) {
132
243
  name: String(p?.author?.name || ""),
133
244
  headline: String(p?.author?.headline || ""),
134
245
  profileUrl: String(p?.author?.profileUrl || ""),
246
+ followerCount,
135
247
  },
136
- engagement: { likes, comments, shares, total },
248
+ engagement,
249
+ reachSignals: reachSignals(engagement, followerCount, targetFollowerMin, targetFollowerMax),
137
250
  contentPreview: previewText(String(p?.content || ""), 220),
138
251
  });
139
- if (kept.length >= maxPosts)
252
+ if (kept.length >= collectLimit)
140
253
  break;
141
254
  }
142
- // Sort by total engagement desc for a predictable shortlist.
143
- kept.sort((a, b) => b.engagement.total - a.engagement.total);
255
+ if (useTargetFollowerBand) {
256
+ kept.sort((a, b) => {
257
+ const scoreDelta = (b.reachSignals.reachAdjustedScore ?? -1) -
258
+ (a.reachSignals.reachAdjustedScore ?? -1);
259
+ if (scoreDelta !== 0)
260
+ return scoreDelta;
261
+ return b.engagement.total - a.engagement.total;
262
+ });
263
+ }
264
+ else {
265
+ // Sort by total engagement desc for a predictable shortlist.
266
+ kept.sort((a, b) => b.engagement.total - a.engagement.total);
267
+ }
268
+ if (kept.length > maxPosts) {
269
+ kept.length = maxPosts;
270
+ }
144
271
  return {
145
272
  success: true,
146
273
  posts: kept,
@@ -1742,6 +1742,14 @@ export declare const allTools: ({
1742
1742
  type: string;
1743
1743
  description: string;
1744
1744
  };
1745
+ targetFollowerMin: {
1746
+ type: string;
1747
+ description: string;
1748
+ };
1749
+ targetFollowerMax: {
1750
+ type: string;
1751
+ description: string;
1752
+ };
1745
1753
  excludePostUrls: {
1746
1754
  type: string;
1747
1755
  items: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.256",
3
+ "version": "0.1.258",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -60,8 +60,9 @@ Do:
60
60
 
61
61
  - capture rough ideas, voice memos, freestyle notes, and ad hoc prompts
62
62
  - research currently working LinkedIn hooks
63
+ - reverse engineer why source hooks work, what shape to keep, and what not to copy
63
64
  - develop premise cards with real story/observed tension and reader value
64
- - generate hook candidates
65
+ - explore many hook angle territories before choosing the winning hook
65
66
  - draft a post body that stays true to the source idea
66
67
  - run validation before calling a draft ready
67
68
  - save ideas, hook research, drafts, and published records under `~/.sellable/content/linkedin/**`
@@ -318,9 +319,13 @@ and drafting gate.
318
319
  The research worker must return a compact packet only:
319
320
 
320
321
  - source examples kept and rejected
322
+ - hook reverse-engineering autopsies: why each source hook works, which shape to
323
+ keep, what not to copy, and what payoff the body must deliver
321
324
  - full adapted hook blocks
325
+ - angle territories tested before drafting
322
326
  - market belief map: resonating ideas, implicit beliefs, audience wants, resentments, fears, and credible controversy angles
323
327
  - premise inputs: real scenes, observed tensions, reader value openings, and proof gaps
328
+ - reach-normalized signal notes, including follower-band fit when available
324
329
  - exact phrase patterns and sentence shapes
325
330
  - body structures and exact body language moves
326
331
  - rendered mobile and desktop preview records
@@ -334,20 +339,24 @@ the exact extracted phrase shapes.
334
339
  Default flow:
335
340
 
336
341
  1. Convert the idea into 3-8 search keywords.
337
- 2. Call `mcp__sellable__search_engagement_posts` with an explicit multi-month window. Default to `maxAgeDays: 120`, tightening to 30-60 days only when the topic is trend-sensitive.
338
- 3. Shortlist high-engagement posts by topic fit, hook strength, creator repeat evidence, and weighted engagement quality.
339
- 4. Because search results may only include previews, call `mcp__sellable__fetch_linkedin_posts` for shortlisted authors/profile URLs and match recent posts by URL/activity ID when full text is needed.
342
+ 2. Call `mcp__sellable__search_engagement_posts` with an explicit multi-month window. Default to `maxAgeDays: 120`, tightening to 30-60 days only when the topic is trend-sensitive. When the user gives a follower range, pass it as `targetFollowerMin` and `targetFollowerMax`; for example, "8k-20k followers" means `targetFollowerMin: 8000` and `targetFollowerMax: 20000`.
343
+ 3. Shortlist posts by topic fit, rendered hook strength, content pattern replicability, creator repeat evidence, weighted engagement quality, and reach-normalized signal quality. Do not sort by raw engagement alone, and do not let the numeric reach score choose the final hook by itself.
344
+ 4. Because search results may only include previews, call `mcp__sellable__fetch_linkedin_posts` for shortlisted authors/profile URLs and match recent posts by URL/activity ID when full text is needed. If a promising source is missing follower count and the user requested reach normalization, call `mcp__sellable__fetch_linkedin_profile` on a bounded shortlist before treating the source as a keeper.
340
345
  5. If full text cannot be matched, record `full_text_unavailable` and use only the preview. Do not invent missing body details.
341
- 6. Weigh shares/reposts above comments, comments above reactions, and reactions as weak reach unless paired with stronger signals. If shares/reposts are unavailable, record `repost_data_unavailable`.
346
+ 6. Weigh shares/reposts above comments, comments above reactions, and reactions as weak reach unless paired with stronger signals. Normalize engagement against follower count when available: record `authorFollowerCount`, `targetFollowerBand`, `followerBandFit`, `engagementPer1kFollowers`, `weightedEngagementPer1kFollowers`, `reachPenaltyMultiplier`, `reachAdjustedScore`, and `baselineLift` when repeated creator posts make it possible. If follower count is unavailable, record `follower_count_unavailable`; if shares/reposts are unavailable, record `repost_data_unavailable`.
342
347
  7. Penalize lead-magnet or giveaway mechanics unless the user explicitly asks for a lead magnet post.
343
348
  8. Build a market belief map before hook generation: what the space is rewarding, what the audience implicitly believes, what they want permission to say, what they resent or fear, what they will argue with, and which controversial angle the user can credibly own. If the user's raw idea is internally coherent but not attached to a live market tension, do not draft from the internal idea alone; present stronger directions or rewrite the draft angle around the external tension.
344
349
  9. Extract premise inputs: real story/scene possibilities, observed tensions, useful reader takeaways, and proof gaps. A good hook cannot rescue a premise with no value.
345
350
  10. For story posts, extract the story mechanism that made the post work, not just the first line.
346
351
  11. Extract hook structures plus specific reusable words, phrases, sentence
347
352
  shapes, transitions, and body language patterns.
348
- 12. Render each kept source hook and adapted hook block through the LinkedIn
353
+ 12. For each keeper, create a `hookAutopsy`: the source mechanism, open-loop
354
+ type, click question, promised payoff, specificity anchor, tension opened,
355
+ source shape to keep, source words not to copy, and adaptation rule for the
356
+ user's idea.
357
+ 13. Render each kept source hook and adapted hook block through the LinkedIn
349
358
  preview rendering contract. Character counts alone are not enough.
350
- 13. Save the research with `mcp__sellable__save_hook_research`.
359
+ 14. Save the research with `mcp__sellable__save_hook_research`.
351
360
 
352
361
  Record provenance:
353
362
 
@@ -356,7 +365,9 @@ Record provenance:
356
365
  - search window
357
366
  - source post URLs
358
367
  - authors/profile URLs
368
+ - author follower counts and target follower-band fit when available
359
369
  - engagement totals and available likes/comments/shares breakdown
370
+ - reach-normalized scoring fields and confidence notes
360
371
  - creator repeat evidence
361
372
  - lead-magnet or engagement-bait penalties
362
373
  - story mechanism when relevant
@@ -364,11 +375,19 @@ Record provenance:
364
375
  - source hook rendered preview records and whether they came from full text,
365
376
  authenticated LinkedIn screenshots, or a search preview
366
377
  - selected hook patterns
378
+ - hook reverse-engineering autopsies, including `clickQuestion`,
379
+ `promisedPayoff`, `sourceShapeToKeep`, `sourceWordsNotToCopy`, and
380
+ `adaptationRule`
367
381
  - market belief map and selected controversy
368
382
  - premise cards and selected premise
369
383
  - exact phrase patterns and sentence shapes
370
384
  - body structures and body language patterns
371
385
  - why each pattern fits the user's idea and voice
386
+ - `whyTheHookCarries`: why the selected hook works from the words, tension, and
387
+ content pattern independent of the source creator's reach
388
+ - `whyTheReachEvidenceIsTrustworthy`: follower-band fit, reach-adjusted score,
389
+ baseline lift, share/comment quality, or why a large-account example is only
390
+ secondary pattern evidence
372
391
 
373
392
  ## Step 1.5: Research Learning Report
374
393
 
@@ -394,6 +413,9 @@ Research status:
394
413
  - keywords:
395
414
  - full-text coverage:
396
415
  - repost/share data:
416
+ - target follower band:
417
+ - reach-normalized winners:
418
+ - big-account examples used only as secondary pattern evidence:
397
419
 
398
420
  Best source examples:
399
421
  1. author, URL, engagement, why kept, why not copied
@@ -414,6 +436,11 @@ Premise cards:
414
436
  Hook patterns learned:
415
437
  1. full adapted hook block
416
438
  - source mechanism:
439
+ - open-loop type:
440
+ - click question:
441
+ - promised payoff:
442
+ - source shape to keep:
443
+ - source words not to copy:
417
444
  - rendered preview:
418
445
  - mobile visible block:
419
446
  - desktop visible block:
@@ -445,9 +472,9 @@ Save recommendations:
445
472
  - gold-standard candidates:
446
473
 
447
474
  Recommended draft directions:
448
- 1. premise card + hook block + body structure
449
- 2. premise card + hook block + body structure
450
- 3. premise card + hook block + body structure
475
+ 1. premise card + hook territory + hook block + click question + body structure
476
+ 2. premise card + hook territory + hook block + click question + body structure
477
+ 3. premise card + hook territory + hook block + click question + body structure
451
478
  ```
452
479
 
453
480
  Keep this report concise enough to read, but concrete enough that another agent
@@ -481,16 +508,32 @@ unless the premise still has concrete reader value.
481
508
 
482
509
  ## Step 2: Hook Candidates
483
510
 
484
- Generate at least 12 hook candidates from the selected premise unless the user
485
- requested a smaller set. Do not generate hooks directly from the raw idea before
486
- the premise is selected.
511
+ Generate a hook angle matrix from the selected premise before writing the post
512
+ body. Do not generate hooks directly from the raw idea before the premise is
513
+ selected.
514
+
515
+ Minimum exploration:
516
+
517
+ - normal drafting: at least 8 distinct angle territories and 24 hook candidates
518
+ - hook-critical requests, "nail the hook," "a ton of angles," or space research:
519
+ at least 12 distinct angle territories and 40 hook candidates
520
+ - smaller set only when the user explicitly asks for fewer options
521
+
522
+ Angle territories must be genuinely different. Use options such as enemy/tool
523
+ contrast, hidden asset, first-person build proof, workflow reveal, contrarian
524
+ category claim, mistake/confession, market timing, operator pain,
525
+ asset/free-reveal without comment bait, specific person/company signal,
526
+ before/after transformation, and question hook.
487
527
 
488
528
  Each hook must include:
489
529
 
490
530
  - selected premise
531
+ - angle territory
491
532
  - premise tension opened
492
533
  - reader value implied
493
534
  - source hook pattern
535
+ - source shape kept
536
+ - source words not copied
494
537
  - why it fits this idea
495
538
  - `renderedPreview` using `references/linkedin-preview-rendering.md`
496
539
  - `mobileRenderedPreviewBlock`
@@ -498,6 +541,12 @@ Each hook must include:
498
541
  - `mobileRenderedLines`
499
542
  - `desktopRenderedLines`
500
543
  - `firstScreenPromise`
544
+ - `intentionalOpenLoop`
545
+ - `specificClickQuestionVisible`
546
+ - `seeMoreClickReason`
547
+ - `clickQuestion`
548
+ - `payoffAfterFold`
549
+ - `bodyProofObligation`
501
550
  - `corePainProofOrCuriosityVisibleMobile`
502
551
  - `corePointVisibleMobile`
503
552
  - `pointAfterMobileClamp`
@@ -517,6 +566,8 @@ Each hook must include:
517
566
  - `rewriteIfTruncated`
518
567
  - proof/story dependency
519
568
  - AI-tell risk
569
+ - why it should win
570
+ - why it should lose
520
571
  - score
521
572
 
522
573
  Do not copy source wording. Copy only the structure.
@@ -537,23 +588,28 @@ available:
537
588
  Use the rendered gates from `references/linkedin-preview-rendering.md`:
538
589
 
539
590
  - `pass`: the mobile rendered preview shows the pain, proof, or curiosity by
540
- the end of the first 3 rendered lines, and the core point is understandable
541
- without opening "see more".
591
+ the end of the first 3 rendered lines, and either the core point is
592
+ understandable without opening "see more" or an intentional open loop creates
593
+ a specific click question with an immediate planned payoff.
542
594
  - `warn`: the mobile rendered preview creates useful curiosity but the core
543
- point is slightly softened by wrapping, blank-line rhythm, or one missing
544
- context word. A compact fallback is required.
595
+ point or click question is slightly softened by wrapping, blank-line rhythm,
596
+ or one missing context word. A compact fallback is required.
545
597
  - `fail`: the hook's real point appears after the first 3 mobile rendered
546
- lines, the first rendered line is generic setup, blank lines consume the
547
- preview before the reader sees the point, or desktop fit is the only reason it
548
- looks good.
598
+ lines without an intentional open-loop plan, the visible open loop is vague,
599
+ the first rendered line is generic setup, blank lines consume the preview
600
+ before the reader sees the point, or desktop fit is the only reason it looks
601
+ good.
549
602
 
550
603
  Desktop preview usually has more room. Still record `desktopPreviewFit`, but
551
604
  never let desktop fit compensate for a mobile `fail`.
552
605
 
553
606
  If a hook's point depends on text after the rendered mobile preview clamp,
554
- rewrite it before selecting it. A selected hook may carry a `warn` only when
555
- the warning is explicit and the validation receipt includes a compact fallback.
556
- A hook with no rendered mobile and desktop preview record cannot be selected.
607
+ rewrite it before selecting it unless the hook is intentionally using a
608
+ see-more open loop. Intentional open loops must show a specific click question
609
+ inside the mobile clamp and must define the first payoff line after the fold. A
610
+ selected hook may carry a `warn` only when the warning is explicit and the
611
+ validation receipt includes a compact fallback. A hook with no rendered mobile
612
+ and desktop preview record cannot be selected.
557
613
 
558
614
  ## Step 3: Draft
559
615
 
@@ -583,6 +639,9 @@ Every saved draft needs a validation receipt with:
583
639
  - selected premise card
584
640
  - candidate hooks considered
585
641
  - selected hook and why
642
+ - hook reverse-engineering audit
643
+ - hook angle matrix summary
644
+ - see-more click audit
586
645
  - proof claims used and source
587
646
  - story/proof files consulted
588
647
  - gold standards consulted
@@ -28,6 +28,7 @@ Worker owns:
28
28
  - tracked-person post fetches
29
29
  - full-text matching by URL/activity ID
30
30
  - duplicate removal
31
+ - follower-band and reach-normalized signal scoring
31
32
  - lead-magnet, giveaway, engagement-bait, and off-voice filtering
32
33
  - market belief mapping across kept and rejected examples
33
34
  - premise input extraction: real scenes, observed tensions, reader value, proof gaps
@@ -54,10 +55,13 @@ Research packet:
54
55
  - market belief map: max 8 bullets
55
56
  - controversy candidates: max 8
56
57
  - premise inputs: max 8
58
+ - hook reverse-engineering autopsies: max 12
57
59
  - full adapted hook blocks: max 12
60
+ - angle territories tested: max 12
58
61
  - exact phrase patterns: max 20
59
62
  - body patterns: max 8
60
63
  - source URLs and author profile URLs
64
+ - reach-normalized signal notes
61
65
  - rendered preview records
62
66
  - confidence gaps
63
67
  - save recommendations
@@ -74,6 +78,13 @@ Use `mcp__sellable__search_engagement_posts` with practical constraints:
74
78
 
75
79
  - explicit `maxAgeDays: 120` by default so research covers the past 30-120 days, not only this week's posts
76
80
  - tighten to 30-60 days for trend-sensitive topics; widen only when the topic has low volume
81
+ - when the user gives a follower range, pass it as `targetFollowerMin` and
82
+ `targetFollowerMax` so the tool can expose reach-normalized signals and
83
+ prioritize comparable creators
84
+ - if a source hook looks promising but follower count is missing, use
85
+ `mcp__sellable__fetch_linkedin_profile` on a bounded shortlist before calling
86
+ the source a keeper; if profile fetch is unavailable or too slow, mark
87
+ `follower_count_unavailable`
77
88
  - high enough engagement to matter
78
89
  - narrow enough to match the idea
79
90
  - enough result depth to see whether a creator has repeated winners, not just one viral outlier
@@ -82,14 +93,19 @@ Record every keyword, filter, search window, page, and result count.
82
93
 
83
94
  ## Weighted Signals
84
95
 
85
- Rank source posts with a weighted signal view. Do not sort by total engagement alone.
96
+ Rank source posts with a weighted signal view. Do not sort by total engagement
97
+ alone. Use the numeric score to decide which sources deserve study; do not let
98
+ the number choose the final hook by itself.
86
99
 
87
100
  Use this scoring shape:
88
101
 
89
102
  ```text
90
103
  hook_score =
91
104
  topic_fit
105
+ + rendered_mobile_preview_strength
92
106
  + hook_clarity
107
+ + content_pattern_replicability
108
+ + reach_adjusted_evidence
93
109
  + creator_repeat_success
94
110
  + repost_share_strength
95
111
  + comment_quality
@@ -107,6 +123,135 @@ Guidance:
107
123
  - If share or repost data is unavailable, record `repost_data_unavailable` instead of inventing it.
108
124
  - Prefer creators with repeated high-performing posts in the same lane over one-off viral posts.
109
125
 
126
+ ## Reach-Normalized Hook Scoring
127
+
128
+ Raw engagement is not the clearest signal when source creators have wildly
129
+ different audience sizes. A post from a 100k+ follower creator can win on
130
+ distribution even when the hook is ordinary. A post from a creator near the
131
+ user's follower range that overperforms is a clearer signal that LinkedIn and
132
+ readers rewarded the hook/premise.
133
+
134
+ This calculation is a source-quality filter, not the final creative decision.
135
+ After reach is controlled, the LLM still chooses the hooks to steal based on the
136
+ visible hook, rendered first-screen promise, content pattern, premise fit, and
137
+ whether the idea can carry without the source creator's distribution.
138
+
139
+ When the user gives a target range, default to that range. For example, if the
140
+ user says "8k to 20k followers," call `mcp__sellable__search_engagement_posts`
141
+ with:
142
+
143
+ ```text
144
+ targetFollowerMin: 8000
145
+ targetFollowerMax: 20000
146
+ ```
147
+
148
+ If the user gives no range, use the user's known follower count from memory or
149
+ ask only when the decision depends on it. If no follower count is available, use
150
+ raw engagement as a fallback and mark `target_follower_band_unknown`.
151
+
152
+ For every shortlisted source post, record:
153
+
154
+ - `authorFollowerCount`
155
+ - `targetFollowerBand`
156
+ - `followerBandFit`: `in_target_band`, `below_target_band`,
157
+ `above_target_band`, or `unknown`
158
+ - `engagementPer1kFollowers`
159
+ - `weightedEngagementPer1kFollowers`
160
+ - `reachAdjustedScore`
161
+ - `creatorBaselineMedianEngagement` when repeated posts from that creator are
162
+ available
163
+ - `baselineLift`: this post's engagement divided by the creator's recent median
164
+ engagement in the same lane, or `creator_baseline_unavailable`
165
+ - `confidence`: `high`, `medium`, or `low`
166
+ - `normalizationNotes`
167
+
168
+ Use this Harvest-compatible calculation when follower counts are available:
169
+
170
+ ```text
171
+ weightedEngagement =
172
+ likes
173
+ + (comments * 4)
174
+ + (shares * 12)
175
+
176
+ engagementPer1kFollowers =
177
+ totalEngagement / authorFollowerCount * 1000
178
+
179
+ weightedEngagementPer1kFollowers =
180
+ weightedEngagement / authorFollowerCount * 1000
181
+
182
+ reachPenaltyMultiplier =
183
+ 1.00 when in target follower band
184
+ 0.75 when below target band
185
+ 0.65 when above target band but <= 2x target max
186
+ 0.35 when above target band but <= 5x target max
187
+ 0.15 when above target band and > 5x target max
188
+ 0.40 when follower count is unknown
189
+
190
+ reachAdjustedScore =
191
+ weightedEngagementPer1kFollowers * reachPenaltyMultiplier
192
+ ```
193
+
194
+ When repeated posts from the same creator are available, calculate:
195
+
196
+ ```text
197
+ creatorBaselineMedianEngagement =
198
+ median weightedEngagement across recent same-lane posts
199
+
200
+ baselineLift =
201
+ post weightedEngagement / creatorBaselineMedianEngagement
202
+ ```
203
+
204
+ Use this scoring shape when deciding which hooks to study:
205
+
206
+ ```text
207
+ hook_score =
208
+ topic_fit
209
+ + rendered_mobile_preview_strength
210
+ + hook_clarity
211
+ + same_follower_band_bonus
212
+ + weighted_engagement_per_1k_followers
213
+ + creator_baseline_lift
214
+ + repost_share_strength
215
+ + substantive_comment_quality
216
+ + creator_repeat_success
217
+ - celebrity_reach_penalty
218
+ - tiny_sample_penalty
219
+ - lead_magnet_penalty
220
+ - engagement_bait_penalty
221
+ - off_voice_penalty
222
+ ```
223
+
224
+ Rules:
225
+
226
+ - Prefer in-band creators when the hook is strong and the engagement is real.
227
+ - Use near-band creators as secondary evidence.
228
+ - Penalize creators far above the target range, especially accounts above 5x
229
+ the target max or above 100k followers when the target range is 8k-20k.
230
+ - Do not automatically reject large-account posts; they can still teach body
231
+ structure, topic heat, or category language. Do not treat them as primary hook
232
+ proof unless they also overperform their creator baseline.
233
+ - Large-account hooks can be selected only when the hook carries without the
234
+ creator: the rendered mobile opening is strong, the premise is reusable by the
235
+ user, the body pattern does not depend on celebrity authority, and the source
236
+ either beats baseline or has unusually high reach-adjusted quality after the
237
+ penalty.
238
+ - Do not overvalue tiny-account anomalies. Very high engagement per follower
239
+ with low absolute engagement is interesting but lower confidence.
240
+ - When follower data is unavailable, do not invent it. Mark
241
+ `follower_count_unavailable`, lower confidence, and rely more on creator repeat
242
+ evidence, repost/share strength, comment quality, and rendered preview quality.
243
+ - A source hook is strongest when it is in/near the target follower band, has
244
+ real absolute engagement, beats the creator's apparent baseline, has
245
+ share/comment quality, and passes rendered mobile preview.
246
+
247
+ Final hook selection must explain both:
248
+
249
+ - `whyTheHookCarries`: the visible words, specificity, tension, and first-screen
250
+ promise that work independent of reach
251
+ - `whyTheReachEvidenceIsTrustworthy`: follower-band fit, reach-adjusted score,
252
+ baseline lift, share/comment quality, or why a large-account example is only
253
+ secondary pattern evidence
254
+
110
255
  Penalize lead-magnet and engagement-bait mechanics unless the user explicitly asks for that style:
111
256
 
112
257
  - `comment "template"` / `comment "guide"` / `comment "playbook"`
@@ -118,6 +263,104 @@ Penalize lead-magnet and engagement-bait mechanics unless the user explicitly as
118
263
  - "like and comment"
119
264
  - broad giveaway framing that makes engagement inflate without proving the hook/body worked
120
265
 
266
+ ## Hook Reverse Engineering Autopsy
267
+
268
+ For any space or LinkedIn topic, do not stop at "this is a good hook." Reverse
269
+ engineer why the opening earned attention, which shape can be reused, and which
270
+ parts belong to the source and must not be copied.
271
+
272
+ For each keeper source hook, record a `hookAutopsy`:
273
+
274
+ ```text
275
+ hookAutopsy:
276
+ sourceHook: <exact visible opening used for analysis>
277
+ renderedFirstScreen: <mobile and desktop visible blocks>
278
+ sourceMechanism: <what the hook does structurally>
279
+ openLoopType: none | hidden_payoff | contradiction | proof_gap | workflow_reveal | asset_reveal | story_gap
280
+ clickQuestion: <the question a reader has before clicking see more>
281
+ promisedPayoff: <what the body/video must deliver after the click>
282
+ curiosityGap: <what is withheld and why that is fair>
283
+ specificityAnchor: <tool, person, number, object, scene, or enemy named>
284
+ tensionOpened: <belief, resentment, fear, status, cost, or contradiction>
285
+ proofImplied: <what proof the source suggests in the first screen>
286
+ whyItWorks: <specific words and structure, not a vibe label>
287
+ whyItMightBeInflated: <reach, giveaway, celebrity, outrage, or topic heat>
288
+ sourceShapeToKeep: <portable structure, written as a template>
289
+ sourceWordsNotToCopy: <phrases, jokes, proof, or context owned by source>
290
+ adaptationRule: <how to make it true for the user's idea and voice>
291
+ ```
292
+
293
+ Good `sourceShapeToKeep` examples:
294
+
295
+ - "The best [desired asset] I have right now is not in [default tool]."
296
+ - "I gave [system/person] one prompt: [specific request]."
297
+ - "[Common vanity metric] is useless until it becomes [business outcome]."
298
+ - "I built [familiar workflow] inside [new interface]."
299
+ - "Most teams [surface behavior]. Then they miss [hidden opportunity]."
300
+
301
+ Bad autopsies:
302
+
303
+ - "contrarian hook"
304
+ - "strong curiosity"
305
+ - "good storytelling"
306
+ - "viral format"
307
+
308
+ Those labels are allowed only after the concrete shape, click question, and
309
+ payoff obligation are recorded.
310
+
311
+ ## Angle Exploration Before Drafting
312
+
313
+ After selecting a premise, create a hook angle matrix before drafting the post.
314
+ This is mandatory when the user asks to nail the hook, study hooks, explore a
315
+ space, or write from a specific idea.
316
+
317
+ Default matrix:
318
+
319
+ - at least 8 angle territories for normal drafting
320
+ - at least 12 angle territories when the user asks for "a ton of angles" or
321
+ says the hook is the main constraint
322
+ - at least 24 generated hooks across those territories for normal drafting
323
+ - at least 40 generated hooks when the user explicitly asks to optimize hooks
324
+
325
+ Angle territories should be materially different, not synonyms. Use territories
326
+ such as:
327
+
328
+ - enemy/tool contrast
329
+ - hidden asset
330
+ - first-person build proof
331
+ - workflow reveal
332
+ - contrarian category claim
333
+ - mistake/confession
334
+ - market timing
335
+ - customer/operator pain
336
+ - asset/free reveal without comment bait
337
+ - specific person/company signal
338
+ - before/after transformation
339
+ - question hook
340
+
341
+ For each territory, record:
342
+
343
+ ```text
344
+ angleTerritory:
345
+ name:
346
+ sourceShapesUsed:
347
+ premiseTensionOpened:
348
+ readerValueImplied:
349
+ bestHook:
350
+ mobileRenderedPreview:
351
+ desktopRenderedPreview:
352
+ clickQuestion:
353
+ payoffAfterFold:
354
+ whyThisAngleShouldWin:
355
+ whyThisAngleShouldLose:
356
+ proofNeeded:
357
+ ```
358
+
359
+ Do not write the post body until the winning hook territory is chosen. The post
360
+ body must then pay off the selected hook quickly. If the best hook says the list
361
+ is not in Apollo, the next lines must reveal where it is. If the hook says "I
362
+ gave Sellable one prompt," the next lines must show the prompt or the demo.
363
+
121
364
  ## Market Belief Map
122
365
 
123
366
  Before selecting a hook or writing a draft, synthesize a market belief map from
@@ -239,6 +482,12 @@ For each source, record:
239
482
  - `corePainProofOrCuriosityVisibleDesktop`
240
483
  - `corePointVisibleMobile`
241
484
  - `corePointVisibleDesktop`
485
+ - `intentionalOpenLoop`
486
+ - `specificClickQuestionVisible`
487
+ - `payoffPlannedImmediatelyAfterClamp`
488
+ - `seeMoreClickReason`
489
+ - `clickQuestion`
490
+ - `payoffAfterFold`
242
491
  - `pointAfterMobileClamp`
243
492
  - `charCountIncludingNewlines`
244
493
  - `physicalLineCount`
@@ -252,6 +501,7 @@ For each source, record:
252
501
  - `desktopPreviewBudget`: `pass`, `warn`, or `fail`
253
502
  - `blankLineVisualRisk`
254
503
  - `corePointBeforeLikelyTruncation`
504
+ - `openLoopType`
255
505
  - `renderedPreviewVerdict`: `pass`, `warn`, or `fail`
256
506
 
257
507
  If only a search preview is available, do not pretend the opening is complete.
@@ -261,15 +511,17 @@ appears cut off or body context is unavailable.
261
511
  Pass/warn/fail is based on rendered output:
262
512
 
263
513
  - `pass`: the mobile rendered preview shows the pain, proof, or curiosity by
264
- the end of the first 3 rendered lines, and the core point is understandable
265
- without opening "see more".
514
+ the end of the first 3 rendered lines, and either the core point is
515
+ understandable without opening "see more" or an intentional open loop creates
516
+ a specific click question with an immediate planned payoff.
266
517
  - `warn`: the mobile rendered preview creates useful curiosity but the core
267
- point is slightly softened by wrapping, blank-line rhythm, or one missing
268
- context word. A compact fallback is required.
518
+ point or click question is slightly softened by wrapping, blank-line rhythm,
519
+ or one missing context word. A compact fallback is required.
269
520
  - `fail`: the hook's real point appears after the first 3 mobile rendered
270
- lines, the first rendered line is generic setup, blank lines consume the
271
- preview before the reader sees the point, or desktop fit is the only reason it
272
- looks good.
521
+ lines without an intentional open-loop plan, the visible open loop is vague,
522
+ the first rendered line is generic setup, blank lines consume the preview
523
+ before the reader sees the point, or desktop fit is the only reason it looks
524
+ good.
273
525
 
274
526
  ## Hook Extraction
275
527
 
@@ -283,7 +535,10 @@ For each shortlisted source post, record:
283
535
  - URL
284
536
  - author
285
537
  - author profile URL when available
538
+ - author follower count and follower-band fit when available
286
539
  - engagement totals and available likes/comments/shares breakdown
540
+ - reach-normalized metrics: engagement per 1k followers, weighted engagement
541
+ per 1k followers, reach-adjusted score, baseline lift, and confidence
287
542
  - creator repeat evidence
288
543
  - visible hook text or preview
289
544
  - rendered preview fields from the section above
@@ -296,6 +551,7 @@ For each shortlisted source post, record:
296
551
  - proof/story dependency
297
552
  - lead magnet or engagement bait penalty
298
553
  - weighted signal notes
554
+ - reach-normalized signal notes
299
555
  - replicability score
300
556
  - track person recommendation: `yes`, `no`, or `ask_user`
301
557
  - tracking reason when recommended
@@ -63,6 +63,10 @@ renderedPreview:
63
63
  blankLinesBeforeClamp: <number>
64
64
  corePainProofOrCuriosityVisible: true | false
65
65
  corePointVisible: true | false
66
+ intentionalOpenLoop: true | false
67
+ specificClickQuestionVisible: true | false
68
+ payoffPlannedImmediatelyAfterClamp: true | false
69
+ seeMoreClickReason: <why a reader would click see more>
66
70
  seeMoreRisk: pass | warn | fail
67
71
  screenshotPath: <optional local path>
68
72
  desktop:
@@ -88,6 +92,7 @@ renderedPreview:
88
92
  longestNonblankLineChars: <number>
89
93
  blankLineVisualRisk: none | low | medium | high
90
94
  pointAfterMobileClamp: true | false
95
+ openLoopType: none | hidden_payoff | contradiction | proof_gap | workflow_reveal | asset_reveal | story_gap
91
96
  rewriteIfTruncated: <short fallback>
92
97
  ```
93
98
 
@@ -112,31 +117,39 @@ For generated hooks:
112
117
 
113
118
  - Generate the hook from the selected premise first.
114
119
  - Render the hook for mobile and desktop before scoring it.
115
- - Score the rendered first-screen promise before scoring cleverness.
116
- - Rewrite any candidate whose real point appears after the mobile clamp.
120
+ - Score the rendered first-screen click reason before scoring cleverness.
121
+ - If the goal is see-more clicks, the hook may intentionally hide the payoff
122
+ after the mobile clamp, but the visible block must create a specific question
123
+ the target reader wants answered.
124
+ - Rewrite any candidate whose real point appears after the mobile clamp unless
125
+ that point is the planned payoff for an intentional open loop.
117
126
 
118
127
  ## Pass, Warn, Fail
119
128
 
120
129
  Use these rendered gates:
121
130
 
122
131
  - `pass`: the mobile rendered preview shows the pain, proof, or curiosity by the
123
- end of the first 3 rendered lines, and the core point is understandable
124
- without opening "see more".
132
+ end of the first 3 rendered lines, and either the core point is
133
+ understandable without opening "see more" or an intentional open loop creates
134
+ a specific click question with an immediate planned payoff.
125
135
  - `warn`: the mobile rendered preview creates useful curiosity but the core
126
- point is slightly softened by wrapping, blank-line rhythm, or one missing
127
- context word. A compact fallback is required.
136
+ point or click question is slightly softened by wrapping, blank-line rhythm,
137
+ or one missing context word. A compact fallback is required.
128
138
  - `fail`: the hook's real point appears after the first 3 mobile rendered lines,
129
- the first rendered line is generic setup, blank lines consume the preview
130
- before the reader sees the point, or desktop fit is the only reason it looks
131
- good.
139
+ the visible open loop is vague, the first rendered line is generic setup,
140
+ blank lines consume the preview before the reader sees the point, or desktop
141
+ fit is the only reason it looks good.
132
142
 
133
143
  A draft cannot be `ready` when the selected hook has:
134
144
 
135
145
  - no `renderedPreview`
136
146
  - `mobile.seeMoreRisk: fail`
137
147
  - `mobile.corePainProofOrCuriosityVisible: false`
138
- - `mobile.corePointVisible: false`
139
- - `pointAfterMobileClamp: true`
148
+ - `mobile.corePointVisible: false` unless `mobile.intentionalOpenLoop: true`,
149
+ `mobile.specificClickQuestionVisible: true`, and
150
+ `mobile.payoffPlannedImmediatelyAfterClamp: true`
151
+ - `pointAfterMobileClamp: true` unless the point after the clamp is the
152
+ intentional payoff for a specific open loop
140
153
 
141
154
  ## Report Format
142
155
 
@@ -59,6 +59,11 @@ Hook research files must preserve:
59
59
  - source post URLs
60
60
  - author/profile URLs
61
61
  - engagement totals
62
+ - author follower counts when available, target follower band, follower-band
63
+ fit, engagement per 1k followers, weighted engagement per 1k followers,
64
+ reach penalty multiplier, reach-adjusted score, baseline lift when available,
65
+ why the hook carries independent of creator reach, and normalization
66
+ confidence notes
62
67
  - full-text availability
63
68
  - source hook preview measurements, including text basis, char count including
64
69
  newlines, physical/content line counts, longest nonblank line, blank-line
@@ -67,6 +72,12 @@ Hook research files must preserve:
67
72
  including literal mobile/desktop preview blocks, rendered line wraps, render
68
73
  basis, CSS contract version, text widths, first-screen promise visibility,
69
74
  core point visibility, and whether the point lands after the mobile clamp
75
+ - hook reverse-engineering autopsies for kept source hooks, including source
76
+ mechanism, open-loop type, click question, promised payoff, specificity
77
+ anchor, source shape to keep, source words not to copy, and adaptation rule
78
+ - hook angle matrix summary, including angle territories tested, top generated
79
+ hooks, winning territory, runner-up territories, and why the winning angle
80
+ beat the alternatives
70
81
  - extracted hook patterns
71
82
  - selected hook basis
72
83
 
@@ -87,6 +98,9 @@ Draft files must preserve:
87
98
  - draft body
88
99
  - validation receipt, including LinkedIn preview pass/warn/fail status and
89
100
  compact fallback when the selected hook carries a warning
101
+ - selected hook reverse-engineering audit, hook angle matrix summary, and
102
+ see-more click audit when the selected hook intentionally hides the payoff
103
+ after the mobile fold
90
104
  - rendered mobile and desktop preview blocks for the selected hook; drafts
91
105
  cannot be ready when this rendered-preview audit is missing or fails mobile
92
106
  visibility
@@ -11,6 +11,10 @@ Every saved draft needs a validation receipt. A draft without this receipt is no
11
11
  - `candidateHooksConsidered`
12
12
  - `selectedHook`
13
13
  - `selectedHookWhy`
14
+ - `sourceHookReachAudit`
15
+ - `hookReverseEngineeringAudit`
16
+ - `hookAngleMatrix`
17
+ - `seeMoreClickAudit`
14
18
  - `proofClaimsUsed`
15
19
  - `proofClaimSources`
16
20
  - `storyFilesConsulted`
@@ -68,20 +72,44 @@ set the verdict to `revise` or `reject`.
68
72
 
69
73
  ## Candidate Set
70
74
 
71
- Generate multiple hook candidates before drafting. Do not lock onto the first draft.
75
+ Generate many hook candidates before drafting. Do not lock onto the first draft.
76
+
77
+ Minimum candidate set:
78
+
79
+ - normal drafting: at least 8 angle territories and 24 hook candidates
80
+ - hook-critical requests: at least 12 angle territories and 40 hook candidates
81
+ - smaller set only when the user explicitly asks for fewer options
82
+
83
+ An angle territory is a distinct reason the reader might care, such as hidden
84
+ asset, tool/enemy contrast, first-person build proof, workflow reveal,
85
+ contrarian category claim, mistake/confession, market timing, operator pain,
86
+ specific person/company signal, before/after transformation, or question hook.
72
87
 
73
88
  Each candidate should include:
74
89
 
75
90
  - hook text
76
91
  - selected premise
92
+ - angle territory
77
93
  - premise tension opened
78
94
  - reader value implied
79
95
  - source pattern
96
+ - source shape kept
97
+ - source words not copied
98
+ - source pattern reach signals: author follower count when available, target
99
+ follower band, follower-band fit, engagement per 1k followers, weighted
100
+ engagement per 1k followers, reach-adjusted score, baseline lift, and
101
+ confidence
80
102
  - score
81
103
  - `renderedPreview` using `references/linkedin-preview-rendering.md`
82
104
  - literal mobile and desktop rendered preview blocks
83
105
  - mobile and desktop rendered line wraps
84
106
  - first-screen promise: pain, proof, or curiosity visible by the mobile clamp
107
+ - `intentionalOpenLoop`
108
+ - `specificClickQuestionVisible`
109
+ - `seeMoreClickReason`
110
+ - `clickQuestion`
111
+ - `payoffAfterFold`
112
+ - `bodyProofObligation`
85
113
  - whether the core point is visible in the mobile rendered preview
86
114
  - whether the point lands after the mobile clamp
87
115
  - char count including newlines and first-line / first-two-line preview measurements
@@ -92,6 +120,55 @@ Each candidate should include:
92
120
  - AI-tell risk
93
121
  - why it should win or lose
94
122
 
123
+ ## Hook Reverse Engineering Audit
124
+
125
+ Before a draft can be `ready`, validate that the selected hook came from a real
126
+ source-shape analysis, not from generic brainstorming.
127
+
128
+ Record:
129
+
130
+ - `sourceHooksStudied`
131
+ - `sourceHookAutopsies`
132
+ - `selectedSourceShape`
133
+ - `sourceShapeKept`
134
+ - `sourceWordsNotCopied`
135
+ - `whyItWorks`
136
+ - `openLoopType`: `none`, `hidden_payoff`, `contradiction`, `proof_gap`,
137
+ `workflow_reveal`, `asset_reveal`, or `story_gap`
138
+ - `clickQuestion`
139
+ - `promisedPayoff`
140
+ - `specificityAnchor`
141
+ - `tensionOpened`
142
+ - `proofImplied`
143
+ - `adaptationRule`
144
+ - `bodyPayoffObligation`
145
+
146
+ The selected draft cannot be `ready` if the hook was copied verbatim from
147
+ another creator, if the receipt only says "contrarian" or "curiosity" without
148
+ the concrete shape, or if the body does not pay off the hook quickly.
149
+
150
+ ## Hook Angle Matrix Audit
151
+
152
+ Before a draft can be `ready`, validate that the hook was chosen after exploring
153
+ materially different angles.
154
+
155
+ Record:
156
+
157
+ - `angleTerritoriesTested`
158
+ - `hookCandidatesGenerated`
159
+ - `winningTerritory`
160
+ - `runnerUpTerritories`
161
+ - `whyWinningAngleBeatsAlternatives`
162
+ - `whyRejectedHooksLost`
163
+ - `mobileRenderedPreviewBlocksForTopCandidates`
164
+ - `desktopRenderedPreviewBlocksForTopCandidates`
165
+ - `proofNeededByWinningAngle`
166
+
167
+ If the user asked to "nail the hook," "focus on hooks," or explore "a ton of
168
+ angles," the draft cannot be `ready` unless at least 12 territories and 40 hook
169
+ candidates were considered or the receipt explains why the user explicitly
170
+ requested fewer.
171
+
95
172
  ## Finalizer Pass
96
173
 
97
174
  After the first draft:
@@ -153,6 +230,35 @@ If `selectedControversy` is missing, if the audience belief is only a generic
153
230
  label like "founders want growth," or if `credibleWhyUs` depends on borrowed
154
231
  proof from another creator, save as `needs_revision`.
155
232
 
233
+ ## Source Hook Reach Audit
234
+
235
+ Before a draft can be `ready`, validate that the selected source hook pattern
236
+ was chosen for a reach-normalized reason, not raw audience size.
237
+
238
+ Record:
239
+
240
+ - `targetFollowerBand`
241
+ - `authorFollowerCount` for each source pattern when available
242
+ - `followerBandFit`: `in_target_band`, `below_target_band`,
243
+ `above_target_band`, or `unknown`
244
+ - `engagementPer1kFollowers`
245
+ - `weightedEngagementPer1kFollowers`
246
+ - `reachAdjustedScore`
247
+ - `reachPenaltyMultiplier`
248
+ - `creatorBaselineMedianEngagement` or `creator_baseline_unavailable`
249
+ - `baselineLift` or `creator_baseline_unavailable`
250
+ - `bigAccountPenaltyApplied`
251
+ - `whyTheHookCarries`
252
+ - `followerCountUnavailableSources`
253
+ - `whyThisHookIsAReachAdjustedSignal`
254
+
255
+ If the user asked for a follower range and the selected source pattern is from a
256
+ large account above the target range, the receipt must explain why it survived
257
+ the celebrity reach penalty. Use large-account hooks as secondary pattern
258
+ evidence unless they clearly overperform the creator's baseline or the hook
259
+ itself carries after reach is controlled. A draft cannot be `ready` when the
260
+ selected hook is justified only by raw total engagement or follower count.
261
+
156
262
  ## LinkedIn Preview Audit
157
263
 
158
264
  Audit the selected hook and top candidates against
@@ -182,15 +288,17 @@ review clamp: first 3 rendered text lines
182
288
  Use rendered gates:
183
289
 
184
290
  - `pass`: the mobile rendered preview shows the pain, proof, or curiosity by
185
- the end of the first 3 rendered lines, and the core point is understandable
186
- without opening "see more".
291
+ the end of the first 3 rendered lines, and either the core point is
292
+ understandable without opening "see more" or an intentional open loop creates
293
+ a specific click question with an immediate planned payoff.
187
294
  - `warn`: the mobile rendered preview creates useful curiosity but the core
188
- point is slightly softened by wrapping, blank-line rhythm, or one missing
189
- context word. A compact fallback is required.
295
+ point or click question is slightly softened by wrapping, blank-line rhythm,
296
+ or one missing context word. A compact fallback is required.
190
297
  - `fail`: the hook's real point appears after the first 3 mobile rendered
191
- lines, the first rendered line is generic setup, blank lines consume the
192
- preview before the reader sees the point, or desktop fit is the only reason it
193
- looks good.
298
+ lines without an intentional open-loop plan, the visible open loop is vague,
299
+ the first rendered line is generic setup, blank lines consume the preview
300
+ before the reader sees the point, or desktop fit is the only reason it looks
301
+ good.
194
302
 
195
303
  Desktop preview usually has more room. Still record desktop fit, but never let
196
304
  desktop fit compensate for a mobile `fail`.
@@ -213,6 +321,12 @@ Record:
213
321
  - `corePainProofOrCuriosityVisibleDesktop`
214
322
  - `corePointVisibleMobile`
215
323
  - `corePointVisibleDesktop`
324
+ - `intentionalOpenLoop`
325
+ - `specificClickQuestionVisible`
326
+ - `payoffPlannedImmediatelyAfterClamp`
327
+ - `seeMoreClickReason`
328
+ - `clickQuestion`
329
+ - `payoffAfterFold`
216
330
  - `pointAfterMobileClamp`
217
331
  - `charCount`
218
332
  - `charCountIncludingNewlines`
@@ -236,10 +350,33 @@ If the hook only works after the rendered mobile clamp, rewrite it. A draft
236
350
  cannot be `ready` with `previewBudgetStatus: fail`,
237
351
  `mobilePreviewFit: fail`, missing `renderedPreview`,
238
352
  `corePainProofOrCuriosityVisibleMobile: false`,
239
- `corePointVisibleMobile: false`, or `pointAfterMobileClamp: true`. A draft may
240
- be `ready` with `previewBudgetStatus: warn` only when the warning is explicit,
241
- usually because the user prefers blank-line rhythm, and the receipt includes a
242
- compact fallback.
353
+ `corePointVisibleMobile: false` unless the receipt has
354
+ `intentionalOpenLoop: true`, `specificClickQuestionVisible: true`, and
355
+ `payoffPlannedImmediatelyAfterClamp: true`, or `pointAfterMobileClamp: true`
356
+ unless the post is intentionally hiding the payoff for a specific see-more
357
+ click. A draft may be `ready` with `previewBudgetStatus: warn` only when the
358
+ warning is explicit, usually because the user prefers blank-line rhythm, and
359
+ the receipt includes a compact fallback.
360
+
361
+ ## See-More Click Audit
362
+
363
+ When the selected hook intentionally hides the payoff after the mobile fold,
364
+ record:
365
+
366
+ - `intentionalOpenLoop: true`
367
+ - `openLoopType`
368
+ - `visibleBeforeFold`
369
+ - `clickQuestion`
370
+ - `payoffAfterFold`
371
+ - `firstPayoffLine`
372
+ - `payoffArrivesWithinFirstBodySection`
373
+ - `readerValueAfterClick`
374
+ - `baitRisk`
375
+ - `baitRiskMitigation`
376
+
377
+ Open loops are allowed only when the body actually delivers the promised payoff.
378
+ If the hook withholds the answer but the body switches topics, save as
379
+ `needs_revision`.
243
380
 
244
381
  ## Simplifier / Concrete-Language Audit
245
382