@sellable/mcp 0.1.255 → 0.1.257
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.
- package/dist/api.d.ts +3 -0
- package/dist/api.js +24 -13
- package/dist/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/server.js +13 -7
- package/dist/tools/campaigns.d.ts +41 -0
- package/dist/tools/campaigns.js +22 -3
- package/dist/tools/csv-dnc.d.ts +0 -36
- package/dist/tools/csv-dnc.js +2 -94
- package/dist/tools/engage-discovery.d.ts +21 -0
- package/dist/tools/engage-discovery.js +136 -9
- package/dist/tools/leads.d.ts +16 -214
- package/dist/tools/leads.js +1 -39
- package/dist/tools/registry.d.ts +514 -606
- package/dist/tools/tables.d.ts +53 -2
- package/dist/tools/tables.js +78 -0
- package/package.json +1 -1
- package/skills/create-campaign/SKILL.md +0 -6
- package/skills/create-campaign-v2/references/filter-leads.md +0 -2
- package/skills/create-campaign-v2/references/lead-validation-preview.md +0 -2
- package/skills/create-campaign-v2/references/step-13-import-leads.md +1 -3
- package/skills/create-post/SKILL.md +67 -21
- package/skills/create-post/references/gold-standard-post-pack.md +11 -0
- package/skills/create-post/references/hook-research-playbook.md +205 -15
- package/skills/create-post/references/linkedin-preview-rendering.md +163 -0
- package/skills/create-post/references/post-file-contract.md +12 -0
- package/skills/create-post/references/post-validation.md +101 -14
- package/skills/find-leads/SKILL.md +0 -6
- package/skills/research/config.json +9 -0
package/dist/tools/tables.d.ts
CHANGED
|
@@ -14,7 +14,13 @@ export interface ListTablesInput {
|
|
|
14
14
|
limit?: number;
|
|
15
15
|
hasSequence?: boolean;
|
|
16
16
|
}
|
|
17
|
-
export
|
|
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
|
+
}>;
|
package/dist/tools/tables.js
CHANGED
|
@@ -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
|
@@ -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.
|
|
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
|
|
@@ -319,9 +321,10 @@ The research worker must return a compact packet only:
|
|
|
319
321
|
- full adapted hook blocks
|
|
320
322
|
- market belief map: resonating ideas, implicit beliefs, audience wants, resentments, fears, and credible controversy angles
|
|
321
323
|
- premise inputs: real scenes, observed tensions, reader value openings, and proof gaps
|
|
324
|
+
- reach-normalized signal notes, including follower-band fit when available
|
|
322
325
|
- exact phrase patterns and sentence shapes
|
|
323
326
|
- body structures and exact body language moves
|
|
324
|
-
- preview
|
|
327
|
+
- rendered mobile and desktop preview records
|
|
325
328
|
- track-person and gold-standard recommendations
|
|
326
329
|
- blocked states or confidence gaps
|
|
327
330
|
|
|
@@ -332,18 +335,20 @@ the exact extracted phrase shapes.
|
|
|
332
335
|
Default flow:
|
|
333
336
|
|
|
334
337
|
1. Convert the idea into 3-8 search keywords.
|
|
335
|
-
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.
|
|
336
|
-
3. Shortlist
|
|
337
|
-
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.
|
|
338
|
+
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`.
|
|
339
|
+
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.
|
|
340
|
+
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.
|
|
338
341
|
5. If full text cannot be matched, record `full_text_unavailable` and use only the preview. Do not invent missing body details.
|
|
339
|
-
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`.
|
|
342
|
+
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`.
|
|
340
343
|
7. Penalize lead-magnet or giveaway mechanics unless the user explicitly asks for a lead magnet post.
|
|
341
344
|
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.
|
|
342
345
|
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.
|
|
343
346
|
10. For story posts, extract the story mechanism that made the post work, not just the first line.
|
|
344
347
|
11. Extract hook structures plus specific reusable words, phrases, sentence
|
|
345
348
|
shapes, transitions, and body language patterns.
|
|
346
|
-
12.
|
|
349
|
+
12. Render each kept source hook and adapted hook block through the LinkedIn
|
|
350
|
+
preview rendering contract. Character counts alone are not enough.
|
|
351
|
+
13. Save the research with `mcp__sellable__save_hook_research`.
|
|
347
352
|
|
|
348
353
|
Record provenance:
|
|
349
354
|
|
|
@@ -352,18 +357,26 @@ Record provenance:
|
|
|
352
357
|
- search window
|
|
353
358
|
- source post URLs
|
|
354
359
|
- authors/profile URLs
|
|
360
|
+
- author follower counts and target follower-band fit when available
|
|
355
361
|
- engagement totals and available likes/comments/shares breakdown
|
|
362
|
+
- reach-normalized scoring fields and confidence notes
|
|
356
363
|
- creator repeat evidence
|
|
357
364
|
- lead-magnet or engagement-bait penalties
|
|
358
365
|
- story mechanism when relevant
|
|
359
366
|
- full-text match status
|
|
360
|
-
- source hook preview
|
|
367
|
+
- source hook rendered preview records and whether they came from full text,
|
|
368
|
+
authenticated LinkedIn screenshots, or a search preview
|
|
361
369
|
- selected hook patterns
|
|
362
370
|
- market belief map and selected controversy
|
|
363
371
|
- premise cards and selected premise
|
|
364
372
|
- exact phrase patterns and sentence shapes
|
|
365
373
|
- body structures and body language patterns
|
|
366
374
|
- why each pattern fits the user's idea and voice
|
|
375
|
+
- `whyTheHookCarries`: why the selected hook works from the words, tension, and
|
|
376
|
+
content pattern independent of the source creator's reach
|
|
377
|
+
- `whyTheReachEvidenceIsTrustworthy`: follower-band fit, reach-adjusted score,
|
|
378
|
+
baseline lift, share/comment quality, or why a large-account example is only
|
|
379
|
+
secondary pattern evidence
|
|
367
380
|
|
|
368
381
|
## Step 1.5: Research Learning Report
|
|
369
382
|
|
|
@@ -389,6 +402,9 @@ Research status:
|
|
|
389
402
|
- keywords:
|
|
390
403
|
- full-text coverage:
|
|
391
404
|
- repost/share data:
|
|
405
|
+
- target follower band:
|
|
406
|
+
- reach-normalized winners:
|
|
407
|
+
- big-account examples used only as secondary pattern evidence:
|
|
392
408
|
|
|
393
409
|
Best source examples:
|
|
394
410
|
1. author, URL, engagement, why kept, why not copied
|
|
@@ -409,7 +425,10 @@ Premise cards:
|
|
|
409
425
|
Hook patterns learned:
|
|
410
426
|
1. full adapted hook block
|
|
411
427
|
- source mechanism:
|
|
412
|
-
- preview
|
|
428
|
+
- rendered preview:
|
|
429
|
+
- mobile visible block:
|
|
430
|
+
- desktop visible block:
|
|
431
|
+
- verdict:
|
|
413
432
|
- internal question:
|
|
414
433
|
- why it fits / why it does not:
|
|
415
434
|
|
|
@@ -484,6 +503,15 @@ Each hook must include:
|
|
|
484
503
|
- reader value implied
|
|
485
504
|
- source hook pattern
|
|
486
505
|
- why it fits this idea
|
|
506
|
+
- `renderedPreview` using `references/linkedin-preview-rendering.md`
|
|
507
|
+
- `mobileRenderedPreviewBlock`
|
|
508
|
+
- `desktopRenderedPreviewBlock`
|
|
509
|
+
- `mobileRenderedLines`
|
|
510
|
+
- `desktopRenderedLines`
|
|
511
|
+
- `firstScreenPromise`
|
|
512
|
+
- `corePainProofOrCuriosityVisibleMobile`
|
|
513
|
+
- `corePointVisibleMobile`
|
|
514
|
+
- `pointAfterMobileClamp`
|
|
487
515
|
- `charCount`
|
|
488
516
|
- `charCountIncludingNewlines`
|
|
489
517
|
- `firstLineChars`
|
|
@@ -504,22 +532,39 @@ Each hook must include:
|
|
|
504
532
|
|
|
505
533
|
Do not copy source wording. Copy only the structure.
|
|
506
534
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
-
|
|
513
|
-
-
|
|
514
|
-
-
|
|
535
|
+
Render every candidate before scoring it. The LinkedIn preview rendering
|
|
536
|
+
contract is the gate; character counts are secondary diagnostics only. Use the
|
|
537
|
+
deterministic CSS contract when no authenticated LinkedIn screenshot is
|
|
538
|
+
available:
|
|
539
|
+
|
|
540
|
+
- mobile text width: `308px`
|
|
541
|
+
- desktop text width: `582px`
|
|
542
|
+
- font size: `14px`
|
|
543
|
+
- line height: `21px`
|
|
544
|
+
- white space: `pre-wrap`
|
|
545
|
+
- overflow wrap: `break-word`
|
|
546
|
+
- review clamp: first 3 rendered text lines
|
|
547
|
+
|
|
548
|
+
Use the rendered gates from `references/linkedin-preview-rendering.md`:
|
|
549
|
+
|
|
550
|
+
- `pass`: the mobile rendered preview shows the pain, proof, or curiosity by
|
|
551
|
+
the end of the first 3 rendered lines, and the core point is understandable
|
|
552
|
+
without opening "see more".
|
|
553
|
+
- `warn`: the mobile rendered preview creates useful curiosity but the core
|
|
554
|
+
point is slightly softened by wrapping, blank-line rhythm, or one missing
|
|
555
|
+
context word. A compact fallback is required.
|
|
556
|
+
- `fail`: the hook's real point appears after the first 3 mobile rendered
|
|
557
|
+
lines, the first rendered line is generic setup, blank lines consume the
|
|
558
|
+
preview before the reader sees the point, or desktop fit is the only reason it
|
|
559
|
+
looks good.
|
|
515
560
|
|
|
516
561
|
Desktop preview usually has more room. Still record `desktopPreviewFit`, but
|
|
517
562
|
never let desktop fit compensate for a mobile `fail`.
|
|
518
563
|
|
|
519
|
-
If a hook's point depends on text after the
|
|
520
|
-
selecting it. A selected hook may carry a `warn` only when
|
|
521
|
-
|
|
522
|
-
|
|
564
|
+
If a hook's point depends on text after the rendered mobile preview clamp,
|
|
565
|
+
rewrite it before selecting it. A selected hook may carry a `warn` only when
|
|
566
|
+
the warning is explicit and the validation receipt includes a compact fallback.
|
|
567
|
+
A hook with no rendered mobile and desktop preview record cannot be selected.
|
|
523
568
|
|
|
524
569
|
## Step 3: Draft
|
|
525
570
|
|
|
@@ -553,6 +598,7 @@ Every saved draft needs a validation receipt with:
|
|
|
553
598
|
- story/proof files consulted
|
|
554
599
|
- gold standards consulted
|
|
555
600
|
- LinkedIn preview audit
|
|
601
|
+
- rendered mobile and desktop hook preview blocks
|
|
556
602
|
- premise/value audit findings
|
|
557
603
|
- simplifier/concrete-language audit findings
|
|
558
604
|
- 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:
|