@sellable/mcp 0.1.255 → 0.1.256

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.
@@ -14,7 +14,13 @@ export interface ListTablesInput {
14
14
  limit?: number;
15
15
  hasSequence?: boolean;
16
16
  }
17
- export declare const tableToolDefinitions: {
17
+ export interface ExportTableCsvInput {
18
+ tableId: string;
19
+ filters?: unknown[];
20
+ sort?: Record<string, unknown>;
21
+ splitRows?: number;
22
+ }
23
+ export declare const tableToolDefinitions: ({
18
24
  name: string;
19
25
  description: string;
20
26
  inputSchema: {
@@ -28,8 +34,53 @@ export declare const tableToolDefinitions: {
28
34
  type: string;
29
35
  description: string;
30
36
  };
37
+ tableId?: undefined;
38
+ filters?: undefined;
39
+ sort?: undefined;
40
+ splitRows?: undefined;
31
41
  };
32
42
  required: never[];
43
+ additionalProperties?: undefined;
44
+ };
45
+ } | {
46
+ name: string;
47
+ description: string;
48
+ inputSchema: {
49
+ type: string;
50
+ properties: {
51
+ tableId: {
52
+ type: string;
53
+ };
54
+ filters: {
55
+ type: string;
56
+ description: string;
57
+ items: {
58
+ type: string;
59
+ };
60
+ };
61
+ sort: {
62
+ type: string;
63
+ description: string;
64
+ };
65
+ splitRows: {
66
+ type: string;
67
+ description: string;
68
+ };
69
+ limit?: undefined;
70
+ hasSequence?: undefined;
71
+ };
72
+ required: string[];
73
+ additionalProperties: boolean;
33
74
  };
34
- }[];
75
+ })[];
35
76
  export declare function listTables(input: ListTablesInput): Promise<WorkflowTableListItem[]>;
77
+ export declare function exportTableCsv(input: ExportTableCsvInput): Promise<{
78
+ tableId: string;
79
+ endpoint: string;
80
+ totalRows: number;
81
+ files: Array<{
82
+ path: string;
83
+ rows: number;
84
+ }>;
85
+ splitRows: number | null;
86
+ }>;
@@ -1,3 +1,7 @@
1
+ import { parse } from "csv-parse/sync";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
1
5
  import { getApi } from "../api.js";
2
6
  export const tableToolDefinitions = [
3
7
  {
@@ -19,6 +23,31 @@ export const tableToolDefinitions = [
19
23
  required: [],
20
24
  },
21
25
  },
26
+ {
27
+ name: "export_table_csv",
28
+ description: "Export a workflow table using the existing Sellable Export CSV endpoint. Optional filters/sort are passed through and splitRows writes deterministic CSV batches.",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ tableId: { type: "string" },
33
+ filters: {
34
+ type: "array",
35
+ description: "Optional workflow-table filters to pass through.",
36
+ items: { type: "object" },
37
+ },
38
+ sort: {
39
+ type: "object",
40
+ description: "Optional workflow-table sort object to pass through.",
41
+ },
42
+ splitRows: {
43
+ type: "number",
44
+ description: "Optional data rows per split file.",
45
+ },
46
+ },
47
+ required: ["tableId"],
48
+ additionalProperties: false,
49
+ },
50
+ },
22
51
  ];
23
52
  export async function listTables(input) {
24
53
  const api = getApi();
@@ -34,3 +63,52 @@ export async function listTables(input) {
34
63
  const result = await api.get(path);
35
64
  return result.tables;
36
65
  }
66
+ const csvEscape = (value) => {
67
+ const text = value == null ? "" : String(value);
68
+ if (/[",\n\r]/.test(text)) {
69
+ return `"${text.replace(/"/g, '""')}"`;
70
+ }
71
+ return text;
72
+ };
73
+ const toCsv = (records) => records.map((row) => row.map(csvEscape).join(",")).join("\n") + "\n";
74
+ export async function exportTableCsv(input) {
75
+ if (!input.tableId) {
76
+ throw new Error("tableId is required");
77
+ }
78
+ const params = new URLSearchParams();
79
+ if (input.filters)
80
+ params.set("filters", JSON.stringify(input.filters));
81
+ if (input.sort)
82
+ params.set("sort", JSON.stringify(input.sort));
83
+ const endpoint = `/api/v3/workflow-tables/${input.tableId}/export-csv${params.toString() ? `?${params.toString()}` : ""}`;
84
+ const api = getApi();
85
+ const csv = await api.getText(endpoint);
86
+ const records = parse(csv, {
87
+ bom: true,
88
+ relax_column_count: true,
89
+ });
90
+ const header = records[0] ?? [];
91
+ const dataRows = records.slice(1);
92
+ const splitRows = Number.isInteger(input.splitRows) && input.splitRows > 0
93
+ ? input.splitRows
94
+ : null;
95
+ const outputDir = path.join(tmpdir(), "sellable-mcp-exports", `${input.tableId}-${Date.now()}`);
96
+ await mkdir(outputDir, { recursive: true });
97
+ const chunks = splitRows && dataRows.length > 0
98
+ ? Array.from({ length: Math.ceil(dataRows.length / splitRows) }, (_, index) => dataRows.slice(index * splitRows, index * splitRows + splitRows))
99
+ : [dataRows];
100
+ const files = [];
101
+ for (let index = 0; index < chunks.length; index++) {
102
+ const chunk = chunks[index];
103
+ const filePath = path.join(outputDir, chunks.length === 1 ? "export.csv" : `export-${index + 1}.csv`);
104
+ await writeFile(filePath, toCsv([header, ...chunk]), "utf8");
105
+ files.push({ path: filePath, rows: chunk.length });
106
+ }
107
+ return {
108
+ tableId: input.tableId,
109
+ endpoint,
110
+ totalRows: dataRows.length,
111
+ files,
112
+ splitRows,
113
+ };
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/mcp",
3
- "version": "0.1.255",
3
+ "version": "0.1.256",
4
4
  "type": "module",
5
5
  "description": "Sellable MCP server for Claude Code and Codex campaign workflows",
6
6
  "main": "dist/index.js",
@@ -58,7 +58,6 @@ allowed-tools:
58
58
  - mcp__sellable__get_rows
59
59
  - mcp__sellable__get_rows_minimal
60
60
  - mcp__sellable__get_table_rows
61
- - mcp__sellable__list_dnc_entries
62
61
  - mcp__sellable__load_csv_dnc_entries
63
62
  - mcp__sellable__load_csv_linkedin_leads
64
63
  - mcp__sellable__load_csv_domains
@@ -102,11 +101,6 @@ are for known-account targeting only, not DNC. Campaign creation already
102
101
  includes a `DNC Check` column that checks domain and LinkedIn profile before
103
102
  message generation.
104
103
 
105
- If the user asks to show/check the current DNC list, count, list names, or first
106
- page before importing, call `list_dnc_entries`. Confirm the active workspace
107
- name and ID in the response before any write. This is Sellable's workspace DNC
108
- list used by DNC Check.
109
-
110
104
  ## Opening Turn Contract
111
105
 
112
106
  On the first visible response after this skill is invoked, do not narrate
@@ -219,8 +219,6 @@ belong in the recommendation as DNC/suppression instructions outside the rubric
219
219
  DNC imports/suppression lists are handled through `load_csv_dnc_entries` outside
220
220
  `leadScoringRubrics`. The import previews the exact Sellable workspace name and
221
221
  ID, then writes to Sellable's workspace-level DNC list only after confirmation.
222
- If the user asks to show the current DNC count, list names, or first page before
223
- import, call `list_dnc_entries` and report the active workspace name and ID.
224
222
  Campaign creation already includes `DNC Check`, which checks domain/profile
225
223
  before message generation. Do not describe provider workarounds, provider
226
224
  search-time limitations, or Prospeo domain filters as the DNC mechanism.
@@ -50,8 +50,6 @@ Supplied-source preview:
50
50
  preview. Preview the exact Sellable workspace name and ID and confirm before
51
51
  writing. This updates Sellable's workspace-level DNC list; it is not a lead
52
52
  source, Prospeo domain filter, or provider workaround.
53
- If the user asks for the existing DNC count, list names, or first page first,
54
- call `list_dnc_entries` and report the active workspace name and ID.
55
53
  - `supplied-linkedin-profiles` uses `load_csv_linkedin_leads` before the
56
54
  15-lead import batch only as preview/source attachment. Pre-import calls
57
55
  must omit provider-import parameters. Use `campaignOfferId` only to attach the
@@ -61,9 +61,7 @@ Supported branches:
61
61
  Sellable workspace name and ID, and confirmation writes only to that
62
62
  workspace's Sellable DNC list. This is not a lead source and does not create a
63
63
  `domainFilterId`; campaign rows are protected later by the existing
64
- `DNC Check` column before message generation. If the user asks for the
65
- current DNC count, list names, or first page before import, call
66
- `list_dnc_entries` first.
64
+ `DNC Check` column before message generation.
67
65
  - **Supplied LinkedIn profile CSV** — confirm `load_csv_linkedin_leads` only
68
66
  after the user approves that supplied-list source. Batch/materialize the
69
67
  uploaded CSV into a Sellable lead-list table. Persist the returned
@@ -84,6 +84,7 @@ Before drafting, load all required assets with `mcp__sellable__get_subskill_asse
84
84
  3. `subskillName: "create-post", assetPath: "references/premise-development.md"`
85
85
  4. `subskillName: "create-post", assetPath: "references/post-validation.md"`
86
86
  5. `subskillName: "create-post", assetPath: "references/gold-standard-post-pack.md"`
87
+ 6. `subskillName: "create-post", assetPath: "references/linkedin-preview-rendering.md"`
87
88
 
88
89
  If any required asset is missing, unreadable, truncated without continuation, or internally inconsistent, return:
89
90
 
@@ -304,7 +305,8 @@ If local idea capture succeeds but auth/workspace is missing, keep the idea and
304
305
 
305
306
  ## Step 1: Hook Research
306
307
 
307
- Use `references/hook-research-playbook.md`.
308
+ Use `references/hook-research-playbook.md` and
309
+ `references/linkedin-preview-rendering.md`.
308
310
 
309
311
  If the host supports background agents, delegate the search/fetch/autopsy work
310
312
  to one bounded `research-worker` before hook candidate generation. The worker
@@ -321,7 +323,7 @@ The research worker must return a compact packet only:
321
323
  - premise inputs: real scenes, observed tensions, reader value openings, and proof gaps
322
324
  - exact phrase patterns and sentence shapes
323
325
  - body structures and exact body language moves
324
- - preview measurements
326
+ - rendered mobile and desktop preview records
325
327
  - track-person and gold-standard recommendations
326
328
  - blocked states or confidence gaps
327
329
 
@@ -343,7 +345,9 @@ Default flow:
343
345
  10. For story posts, extract the story mechanism that made the post work, not just the first line.
344
346
  11. Extract hook structures plus specific reusable words, phrases, sentence
345
347
  shapes, transitions, and body language patterns.
346
- 12. Save the research with `mcp__sellable__save_hook_research`.
348
+ 12. Render each kept source hook and adapted hook block through the LinkedIn
349
+ preview rendering contract. Character counts alone are not enough.
350
+ 13. Save the research with `mcp__sellable__save_hook_research`.
347
351
 
348
352
  Record provenance:
349
353
 
@@ -357,7 +361,8 @@ Record provenance:
357
361
  - lead-magnet or engagement-bait penalties
358
362
  - story mechanism when relevant
359
363
  - full-text match status
360
- - source hook preview measurements and whether they came from full text or a search preview
364
+ - source hook rendered preview records and whether they came from full text,
365
+ authenticated LinkedIn screenshots, or a search preview
361
366
  - selected hook patterns
362
367
  - market belief map and selected controversy
363
368
  - premise cards and selected premise
@@ -409,7 +414,10 @@ Premise cards:
409
414
  Hook patterns learned:
410
415
  1. full adapted hook block
411
416
  - source mechanism:
412
- - preview budget:
417
+ - rendered preview:
418
+ - mobile visible block:
419
+ - desktop visible block:
420
+ - verdict:
413
421
  - internal question:
414
422
  - why it fits / why it does not:
415
423
 
@@ -484,6 +492,15 @@ Each hook must include:
484
492
  - reader value implied
485
493
  - source hook pattern
486
494
  - why it fits this idea
495
+ - `renderedPreview` using `references/linkedin-preview-rendering.md`
496
+ - `mobileRenderedPreviewBlock`
497
+ - `desktopRenderedPreviewBlock`
498
+ - `mobileRenderedLines`
499
+ - `desktopRenderedLines`
500
+ - `firstScreenPromise`
501
+ - `corePainProofOrCuriosityVisibleMobile`
502
+ - `corePointVisibleMobile`
503
+ - `pointAfterMobileClamp`
487
504
  - `charCount`
488
505
  - `charCountIncludingNewlines`
489
506
  - `firstLineChars`
@@ -504,22 +521,39 @@ Each hook must include:
504
521
 
505
522
  Do not copy source wording. Copy only the structure.
506
523
 
507
- Use this conservative mobile-first LinkedIn preview gate. LinkedIn does not
508
- publish exact "see more" cutoff rules, and rendering varies by device, app
509
- version, font, media, and line break. These are v1 safety budgets, not claims
510
- about an official LinkedIn limit:
511
-
512
- - `pass`: hook is <= 110 chars including newlines, every nonblank line is <= 45 chars, and the hook's core point lands before likely truncation.
513
- - `warn`: hook is 111-140 chars including newlines, any nonblank line is 46-55 chars, or blank lines create visual-line risk. Blank lines are allowed, but they count as physical lines.
514
- - `fail`: hook is > 140 chars including newlines, any nonblank line is > 55 chars, or the hook's point depends on text after likely truncation.
524
+ Render every candidate before scoring it. The LinkedIn preview rendering
525
+ contract is the gate; character counts are secondary diagnostics only. Use the
526
+ deterministic CSS contract when no authenticated LinkedIn screenshot is
527
+ available:
528
+
529
+ - mobile text width: `308px`
530
+ - desktop text width: `582px`
531
+ - font size: `14px`
532
+ - line height: `21px`
533
+ - white space: `pre-wrap`
534
+ - overflow wrap: `break-word`
535
+ - review clamp: first 3 rendered text lines
536
+
537
+ Use the rendered gates from `references/linkedin-preview-rendering.md`:
538
+
539
+ - `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".
542
+ - `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.
545
+ - `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.
515
549
 
516
550
  Desktop preview usually has more room. Still record `desktopPreviewFit`, but
517
551
  never let desktop fit compensate for a mobile `fail`.
518
552
 
519
- If a hook's point depends on text after the likely preview, rewrite it before
520
- selecting it. A selected hook may carry a `warn` only when the warning is about
521
- intentional blank-line rhythm or a slight line-length overage; include a compact
522
- fallback in the validation receipt.
553
+ 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.
523
557
 
524
558
  ## Step 3: Draft
525
559
 
@@ -553,6 +587,7 @@ Every saved draft needs a validation receipt with:
553
587
  - story/proof files consulted
554
588
  - gold standards consulted
555
589
  - LinkedIn preview audit
590
+ - rendered mobile and desktop hook preview blocks
556
591
  - premise/value audit findings
557
592
  - simplifier/concrete-language audit findings
558
593
  - voice audit findings
@@ -119,6 +119,8 @@ Show compact candidate cards. Include:
119
119
  - why it might belong in the pack
120
120
  - hook mechanism
121
121
  - hook preview budget status and measurement basis
122
+ - rendered mobile preview block and verdict
123
+ - rendered desktop preview block and verdict
122
124
  - content/body mechanism
123
125
  - rhythm notes
124
126
  - sentence-structure notes
@@ -194,6 +196,15 @@ Tags:
194
196
  - longest nonblank line chars:
195
197
  - blank-line visual risk:
196
198
  - source text basis:
199
+ - render basis:
200
+ - css contract version:
201
+ - mobile rendered preview block:
202
+ - desktop rendered preview block:
203
+ - mobile rendered lines:
204
+ - desktop rendered lines:
205
+ - core pain/proof/curiosity visible on mobile:
206
+ - core point visible on mobile:
207
+ - point after mobile clamp:
197
208
  - preview budget status:
198
209
  - mobile preview fit:
199
210
  - desktop preview fit:
@@ -31,7 +31,7 @@ Worker owns:
31
31
  - lead-magnet, giveaway, engagement-bait, and off-voice filtering
32
32
  - market belief mapping across kept and rejected examples
33
33
  - premise input extraction: real scenes, observed tensions, reader value, proof gaps
34
- - hook opening measurement
34
+ - rendered hook opening measurement
35
35
  - exact phrase-pattern extraction
36
36
  - body-structure extraction
37
37
  - rejected-example notes
@@ -58,7 +58,7 @@ Research packet:
58
58
  - exact phrase patterns: max 20
59
59
  - body patterns: max 8
60
60
  - source URLs and author profile URLs
61
- - preview measurements
61
+ - rendered preview records
62
62
  - confidence gaps
63
63
  - save recommendations
64
64
  ```
@@ -198,24 +198,48 @@ Measure the visible opening for every shortlisted source post before extracting
198
198
  the hook pattern. This makes the study useful for LinkedIn, not just generally
199
199
  "good writing."
200
200
 
201
- LinkedIn does not publish exact "see more" cutoff rules. Treat these as
202
- conservative v1 planning budgets:
201
+ Use `references/linkedin-preview-rendering.md`. LinkedIn does not publish exact
202
+ "see more" cutoff rules. Character counts are diagnostics only. A source post
203
+ or generated hook is not properly studied until it has a rendered mobile and
204
+ desktop preview record.
203
205
 
204
- - `pass`: opening hook is <= 110 chars including newlines, every nonblank line
205
- is <= 45 chars, and the hook's core point lands before likely truncation.
206
- - `warn`: opening hook is 111-140 chars including newlines, any nonblank line is
207
- 46-55 chars, or blank lines create visual-line risk. Blank lines are allowed,
208
- but they count as physical lines.
209
- - `fail`: opening hook is > 140 chars including newlines, any nonblank line is
210
- > 55 chars, or the hook's core point depends on text after likely truncation.
206
+ Default deterministic renderer for unpublished drafts and research comparison:
211
207
 
212
- Desktop preview has more room, so record it separately, but never let desktop
213
- fit compensate for a mobile `fail`.
208
+ ```text
209
+ cssContractVersion: linkedin-preview-rendering/v1
210
+ font size: 14px
211
+ line height: 21px
212
+ white space: pre-wrap
213
+ overflow wrap: break-word
214
+ mobile text width: 308px
215
+ desktop text width: 582px
216
+ review clamp: first 3 rendered text lines
217
+ ```
218
+
219
+ Authenticated LinkedIn feed/composer screenshots are stronger evidence when
220
+ available. If a screenshot is used, still record the wrapped lines and verdicts
221
+ below so another agent can compare hooks without re-opening LinkedIn.
222
+
223
+ Desktop preview has more room, but never let desktop fit compensate for a
224
+ mobile `fail`.
214
225
 
215
226
  For each source, record:
216
227
 
217
228
  - `sourceTextBasis`: `full_text`, `search_preview`, or `manual_user_source`
218
229
  - `openingTextUsed`
230
+ - `renderedPreview`
231
+ - `cssContractVersion`
232
+ - `mobileRenderedPreviewBlock`
233
+ - `desktopRenderedPreviewBlock`
234
+ - `mobileRenderedLines`
235
+ - `desktopRenderedLines`
236
+ - `mobileRenderedLineCount`
237
+ - `desktopRenderedLineCount`
238
+ - `corePainProofOrCuriosityVisibleMobile`
239
+ - `corePainProofOrCuriosityVisibleDesktop`
240
+ - `corePointVisibleMobile`
241
+ - `corePointVisibleDesktop`
242
+ - `pointAfterMobileClamp`
219
243
  - `charCountIncludingNewlines`
220
244
  - `physicalLineCount`
221
245
  - `contentLineCount`
@@ -228,11 +252,25 @@ For each source, record:
228
252
  - `desktopPreviewBudget`: `pass`, `warn`, or `fail`
229
253
  - `blankLineVisualRisk`
230
254
  - `corePointBeforeLikelyTruncation`
255
+ - `renderedPreviewVerdict`: `pass`, `warn`, or `fail`
231
256
 
232
257
  If only a search preview is available, do not pretend the opening is complete.
233
258
  Record `sourceTextBasis: search_preview` and lower confidence when the hook
234
259
  appears cut off or body context is unavailable.
235
260
 
261
+ Pass/warn/fail is based on rendered output:
262
+
263
+ - `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".
266
+ - `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.
269
+ - `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.
273
+
236
274
  ## Hook Extraction
237
275
 
238
276
  Extract structure and reusable language patterns, not copied prose. The goal is
@@ -248,7 +286,7 @@ For each shortlisted source post, record:
248
286
  - engagement totals and available likes/comments/shares breakdown
249
287
  - creator repeat evidence
250
288
  - visible hook text or preview
251
- - opening preview measurement fields from the section above
289
+ - rendered preview fields from the section above
252
290
  - hook mechanism
253
291
  - exact hook language patterns: reusable words, phrase shapes, contrast forms,
254
292
  sentence shapes, and transition moves
@@ -262,6 +300,11 @@ For each shortlisted source post, record:
262
300
  - track person recommendation: `yes`, `no`, or `ask_user`
263
301
  - tracking reason when recommended
264
302
 
303
+ Do not call a source hook a keeper because the full text is strong if the
304
+ rendered mobile opening is weak. Study what a LinkedIn reader sees before
305
+ "see more"; the body structure can still be useful, but the hook should not be
306
+ copied into the hook pattern set.
307
+
265
308
  ## Specific Language Extraction
266
309
 
267
310
  For each keeper, extract the source's specific language mechanics in this
@@ -0,0 +1,163 @@
1
+ # LinkedIn Preview Rendering
2
+
3
+ This asset defines the required rendered-preview contract for create-post hook
4
+ research, hook candidate generation, gold-standard decomposition, and draft
5
+ validation.
6
+
7
+ Character budgets are only diagnostics. They are not the preview gate. A hook is
8
+ not studied, selected, or ready until it has been rendered through this contract
9
+ or through a stricter authenticated LinkedIn screenshot.
10
+
11
+ ## Rendering Basis
12
+
13
+ LinkedIn does not publish exact feed truncation rules, and rendering can vary by
14
+ surface, app version, device, media attachment, and account state. Use this
15
+ deterministic renderer as the MCP review gate for unpublished drafts.
16
+
17
+ When an authenticated LinkedIn feed/composer/browser screenshot is available,
18
+ that screenshot is the strongest evidence. Still record the fields below so
19
+ future agents can compare candidates without redoing the visual inspection.
20
+
21
+ Observed public LinkedIn post text style for feed-style post pages:
22
+
23
+ ```text
24
+ selector basis: p.attributed-text-segment-list__content
25
+ font family: -apple-system, system-ui, "Segoe UI", Roboto, "Helvetica Neue",
26
+ "Fira Sans", Ubuntu, "Oxygen Sans", Cantarell, "Droid Sans",
27
+ "Lucida Grande", Helvetica, Arial, sans-serif
28
+ font size: 14px
29
+ line height: 21px
30
+ font weight: 400
31
+ letter spacing: normal
32
+ white space: pre-wrap
33
+ overflow wrap: break-word
34
+ text color: rgba(0, 0, 0, 0.9)
35
+ mobile text width: 308px
36
+ desktop text width: 582px
37
+ review clamp: first 3 rendered text lines
38
+ ```
39
+
40
+ The `review clamp` is not an official LinkedIn rule. It is the conservative
41
+ apples-to-apples region create-post must use when comparing hooks. Do not let a
42
+ desktop pass rescue a mobile fail.
43
+
44
+ ## Required Rendering Record
45
+
46
+ Every shortlisted source hook, adapted hook block, generated hook candidate, and
47
+ selected hook must include a `renderedPreview` record:
48
+
49
+ ```text
50
+ renderedPreview:
51
+ basis: linkedin_css_contract | authenticated_linkedin_screenshot | manual_user_source
52
+ cssContractVersion: linkedin-preview-rendering/v1
53
+ mobile:
54
+ textWidthPx: 308
55
+ fontSizePx: 14
56
+ lineHeightPx: 21
57
+ visibleTextBlock: <literal first rendered review-clamp block>
58
+ renderedLines:
59
+ - <line 1 exactly as wrapped>
60
+ - <line 2 exactly as wrapped>
61
+ - <line 3 exactly as wrapped>
62
+ lineCountBeforeClamp: <number>
63
+ blankLinesBeforeClamp: <number>
64
+ corePainProofOrCuriosityVisible: true | false
65
+ corePointVisible: true | false
66
+ seeMoreRisk: pass | warn | fail
67
+ screenshotPath: <optional local path>
68
+ desktop:
69
+ textWidthPx: 582
70
+ fontSizePx: 14
71
+ lineHeightPx: 21
72
+ visibleTextBlock: <literal first rendered review-clamp block>
73
+ renderedLines:
74
+ - <line 1 exactly as wrapped>
75
+ - <line 2 exactly as wrapped>
76
+ - <line 3 exactly as wrapped>
77
+ lineCountBeforeClamp: <number>
78
+ blankLinesBeforeClamp: <number>
79
+ corePainProofOrCuriosityVisible: true | false
80
+ corePointVisible: true | false
81
+ seeMoreRisk: pass | warn | fail
82
+ screenshotPath: <optional local path>
83
+ diagnostics:
84
+ charCount: <number>
85
+ charCountIncludingNewlines: <number>
86
+ firstLineChars: <number>
87
+ firstTwoPhysicalLinesChars: <number>
88
+ longestNonblankLineChars: <number>
89
+ blankLineVisualRisk: none | low | medium | high
90
+ pointAfterMobileClamp: true | false
91
+ rewriteIfTruncated: <short fallback>
92
+ ```
93
+
94
+ If a host cannot produce screenshots, it must still produce the literal wrapped
95
+ line blocks using the CSS contract. If it cannot produce either screenshots or
96
+ literal line wraps, return `blocked` or `needs_revision`; do not claim the hook
97
+ passed preview validation from character counts alone.
98
+
99
+ ## Study Rules
100
+
101
+ For source-post research:
102
+
103
+ - Render the exact visible opening from full text when full text is available.
104
+ - If only a search preview is available, render only the preview text and set
105
+ `basis: manual_user_source` or record `sourceTextBasis: search_preview`.
106
+ - Lower confidence when the search preview is cut off or the body is
107
+ unavailable.
108
+ - Extract hook lessons from the rendered first-screen experience, not from the
109
+ full post in isolation.
110
+
111
+ For generated hooks:
112
+
113
+ - Generate the hook from the selected premise first.
114
+ - 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.
117
+
118
+ ## Pass, Warn, Fail
119
+
120
+ Use these rendered gates:
121
+
122
+ - `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".
125
+ - `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.
128
+ - `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.
132
+
133
+ A draft cannot be `ready` when the selected hook has:
134
+
135
+ - no `renderedPreview`
136
+ - `mobile.seeMoreRisk: fail`
137
+ - `mobile.corePainProofOrCuriosityVisible: false`
138
+ - `mobile.corePointVisible: false`
139
+ - `pointAfterMobileClamp: true`
140
+
141
+ ## Report Format
142
+
143
+ Research reports must show rendered preview blocks for the best examples and
144
+ recommended draft directions:
145
+
146
+ ```text
147
+ Rendered preview:
148
+ mobile:
149
+ <line 1>
150
+ <line 2>
151
+ <line 3>
152
+
153
+ desktop:
154
+ <line 1>
155
+ <line 2>
156
+ <line 3>
157
+
158
+ verdict: pass | warn | fail
159
+ why: <what is visible before the fold>
160
+ ```
161
+
162
+ Do not say "it fits on mobile" without showing what the mobile reader actually
163
+ sees.