@sellable/mcp 0.1.293 → 0.1.294
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/index-dev.js +0 -0
- package/dist/index.js +0 -0
- package/dist/tools/engage-discovery.d.ts +9 -0
- package/dist/tools/engage-discovery.js +47 -1
- package/dist/tools/leads.d.ts +9 -0
- package/dist/tools/leads.js +56 -17
- package/dist/tools/linkedin.d.ts +18 -1
- package/dist/tools/linkedin.js +92 -1
- package/package.json +1 -1
- package/skills/create-post/SKILL.md +46 -13
- package/skills/create-post/references/gold-standard-post-pack.md +22 -0
- package/skills/create-post/references/hook-research-playbook.md +117 -4
- package/skills/create-post/references/post-file-contract.md +6 -0
- package/skills/create-post/references/post-validation.md +33 -0
package/dist/index-dev.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -9,6 +9,14 @@ type ReachSignals = {
|
|
|
9
9
|
reachAdjustedScore: number;
|
|
10
10
|
confidence: "high" | "medium" | "low";
|
|
11
11
|
};
|
|
12
|
+
type EngagementPostMedia = {
|
|
13
|
+
imageUrls: string[];
|
|
14
|
+
videoThumbnailUrl?: string;
|
|
15
|
+
videoUrl?: string;
|
|
16
|
+
articleImageUrl?: string;
|
|
17
|
+
articleUrl?: string;
|
|
18
|
+
mediaTypes: string[];
|
|
19
|
+
};
|
|
12
20
|
export type EngagementPost = {
|
|
13
21
|
postId: string;
|
|
14
22
|
url: string;
|
|
@@ -27,6 +35,7 @@ export type EngagementPost = {
|
|
|
27
35
|
total: number;
|
|
28
36
|
};
|
|
29
37
|
reachSignals?: ReachSignals;
|
|
38
|
+
media?: EngagementPostMedia;
|
|
30
39
|
contentPreview: string;
|
|
31
40
|
};
|
|
32
41
|
export type SearchEngagementPostsInput = {
|
|
@@ -2,7 +2,7 @@ import { getApi } from "../api.js";
|
|
|
2
2
|
export const engageDiscoveryToolDefinitions = [
|
|
3
3
|
{
|
|
4
4
|
name: "search_engagement_posts",
|
|
5
|
-
description: "Search for high-signal LinkedIn posts by keyword for engagement/comment drafting. Designed for the engage skill (no campaignOfferId required). Filters by recency and engagement,
|
|
5
|
+
description: "Search for high-signal LinkedIn posts by keyword for engagement/comment drafting. Designed for the engage skill (no campaignOfferId required). Filters by recency and engagement, can exclude already-engaged URLs, and returns source media hints when the scraper provides images, video covers, or article images.",
|
|
6
6
|
inputSchema: {
|
|
7
7
|
type: "object",
|
|
8
8
|
properties: {
|
|
@@ -74,6 +74,50 @@ function safeNumber(value) {
|
|
|
74
74
|
function round3(value) {
|
|
75
75
|
return Number(value.toFixed(3));
|
|
76
76
|
}
|
|
77
|
+
function normalizeMedia(raw) {
|
|
78
|
+
if (!raw || typeof raw !== "object")
|
|
79
|
+
return undefined;
|
|
80
|
+
const imageUrls = Array.isArray(raw.imageUrls)
|
|
81
|
+
? raw.imageUrls
|
|
82
|
+
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
: [];
|
|
85
|
+
const videoThumbnailUrl = typeof raw.videoThumbnailUrl === "string" && raw.videoThumbnailUrl.trim()
|
|
86
|
+
? raw.videoThumbnailUrl.trim()
|
|
87
|
+
: undefined;
|
|
88
|
+
const videoUrl = typeof raw.videoUrl === "string" && raw.videoUrl.trim()
|
|
89
|
+
? raw.videoUrl.trim()
|
|
90
|
+
: undefined;
|
|
91
|
+
const articleImageUrl = typeof raw.articleImageUrl === "string" && raw.articleImageUrl.trim()
|
|
92
|
+
? raw.articleImageUrl.trim()
|
|
93
|
+
: undefined;
|
|
94
|
+
const articleUrl = typeof raw.articleUrl === "string" && raw.articleUrl.trim()
|
|
95
|
+
? raw.articleUrl.trim()
|
|
96
|
+
: undefined;
|
|
97
|
+
const mediaTypes = Array.from(new Set([
|
|
98
|
+
...(Array.isArray(raw.mediaTypes)
|
|
99
|
+
? raw.mediaTypes.map((type) => typeof type === "string" ? type.trim() : "")
|
|
100
|
+
: []),
|
|
101
|
+
imageUrls.length > 0 ? "image" : "",
|
|
102
|
+
videoThumbnailUrl || videoUrl ? "video" : "",
|
|
103
|
+
articleImageUrl || articleUrl ? "article" : "",
|
|
104
|
+
].filter(Boolean)));
|
|
105
|
+
if (imageUrls.length === 0 &&
|
|
106
|
+
!videoThumbnailUrl &&
|
|
107
|
+
!videoUrl &&
|
|
108
|
+
!articleImageUrl &&
|
|
109
|
+
!articleUrl) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
imageUrls,
|
|
114
|
+
...(videoThumbnailUrl ? { videoThumbnailUrl } : {}),
|
|
115
|
+
...(videoUrl ? { videoUrl } : {}),
|
|
116
|
+
...(articleImageUrl ? { articleImageUrl } : {}),
|
|
117
|
+
...(articleUrl ? { articleUrl } : {}),
|
|
118
|
+
mediaTypes,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
77
121
|
function parseFollowerCount(author) {
|
|
78
122
|
const direct = safeNumber(author?.followerCount);
|
|
79
123
|
if (direct > 0)
|
|
@@ -218,6 +262,7 @@ export async function searchEngagementPosts(input) {
|
|
|
218
262
|
targetFollowerMin,
|
|
219
263
|
targetFollowerMax,
|
|
220
264
|
});
|
|
265
|
+
const media = normalizeMedia(p?.media);
|
|
221
266
|
kept.push({
|
|
222
267
|
postId: String(p?.id || ""),
|
|
223
268
|
url,
|
|
@@ -233,6 +278,7 @@ export async function searchEngagementPosts(input) {
|
|
|
233
278
|
},
|
|
234
279
|
engagement: { likes, comments, shares, total },
|
|
235
280
|
...(reachSignals ? { reachSignals } : {}),
|
|
281
|
+
...(media ? { media } : {}),
|
|
236
282
|
contentPreview: previewText(String(p?.content || ""), 220),
|
|
237
283
|
});
|
|
238
284
|
}
|
package/dist/tools/leads.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ type SignalPostForImportSelection = {
|
|
|
5
5
|
likes: number;
|
|
6
6
|
comments: number;
|
|
7
7
|
};
|
|
8
|
+
type SignalPostMedia = {
|
|
9
|
+
imageUrls: string[];
|
|
10
|
+
videoThumbnailUrl?: string;
|
|
11
|
+
videoUrl?: string;
|
|
12
|
+
articleImageUrl?: string;
|
|
13
|
+
articleUrl?: string;
|
|
14
|
+
mediaTypes: string[];
|
|
15
|
+
};
|
|
8
16
|
export declare function selectSignalPostsForImport<T extends SignalPostForImportSelection>(posts: T[], options: {
|
|
9
17
|
targetEngagerCount?: number;
|
|
10
18
|
maxPostsToScrape?: number;
|
|
@@ -4406,6 +4414,7 @@ export declare function searchSignals(input: SignalSearchInput): Promise<{
|
|
|
4406
4414
|
selectionTarget: number;
|
|
4407
4415
|
recommendedPostIds: string[];
|
|
4408
4416
|
topPosts: {
|
|
4417
|
+
media?: SignalPostMedia | undefined;
|
|
4409
4418
|
id: string;
|
|
4410
4419
|
url: string | undefined;
|
|
4411
4420
|
matchedKeyword: string | null;
|
package/dist/tools/leads.js
CHANGED
|
@@ -3,8 +3,8 @@ import { dirname, join, resolve } from "path";
|
|
|
3
3
|
import { getApi, SellableApiError } from "../api.js";
|
|
4
4
|
import { resolveSkillsDir } from "../skills.js";
|
|
5
5
|
import { resolveWorkspaceRoot } from "../utils/workspace-root.js";
|
|
6
|
-
import { buildCsvDomainPreview, matchesConfirmationToken, parseConfirmationToken, projectCsvCarryRows, } from "./csv-domains.js";
|
|
7
6
|
import { listDncEntries, loadCsvDncEntries, } from "./csv-dnc.js";
|
|
7
|
+
import { buildCsvDomainPreview, matchesConfirmationToken, parseConfirmationToken, projectCsvCarryRows, } from "./csv-domains.js";
|
|
8
8
|
import { buildCsvLinkedinPreview, matchesLinkedinConfirmationToken, parseLinkedinConfirmationToken, uploadCsvLinkedinFile, } from "./csv-linkedin.js";
|
|
9
9
|
import { assertInteractionApproval } from "./interaction-mode.js";
|
|
10
10
|
import { assertProviderPromptLoaded, markProviderPromptLoaded, } from "./provider-preflight.js";
|
|
@@ -727,6 +727,52 @@ function truncate(text, max = 220) {
|
|
|
727
727
|
return clean;
|
|
728
728
|
return `${clean.slice(0, max - 1)}...`;
|
|
729
729
|
}
|
|
730
|
+
function normalizeSignalPostMedia(raw) {
|
|
731
|
+
if (!raw || typeof raw !== "object")
|
|
732
|
+
return undefined;
|
|
733
|
+
const media = raw;
|
|
734
|
+
const imageUrls = Array.isArray(media.imageUrls)
|
|
735
|
+
? media.imageUrls
|
|
736
|
+
.map((url) => (typeof url === "string" ? url.trim() : ""))
|
|
737
|
+
.filter(Boolean)
|
|
738
|
+
: [];
|
|
739
|
+
const videoThumbnailUrl = typeof media.videoThumbnailUrl === "string" &&
|
|
740
|
+
media.videoThumbnailUrl.trim()
|
|
741
|
+
? media.videoThumbnailUrl.trim()
|
|
742
|
+
: undefined;
|
|
743
|
+
const videoUrl = typeof media.videoUrl === "string" && media.videoUrl.trim()
|
|
744
|
+
? media.videoUrl.trim()
|
|
745
|
+
: undefined;
|
|
746
|
+
const articleImageUrl = typeof media.articleImageUrl === "string" && media.articleImageUrl.trim()
|
|
747
|
+
? media.articleImageUrl.trim()
|
|
748
|
+
: undefined;
|
|
749
|
+
const articleUrl = typeof media.articleUrl === "string" && media.articleUrl.trim()
|
|
750
|
+
? media.articleUrl.trim()
|
|
751
|
+
: undefined;
|
|
752
|
+
const mediaTypes = Array.from(new Set([
|
|
753
|
+
...(Array.isArray(media.mediaTypes)
|
|
754
|
+
? media.mediaTypes.map((type) => typeof type === "string" ? type.trim() : "")
|
|
755
|
+
: []),
|
|
756
|
+
imageUrls.length > 0 ? "image" : "",
|
|
757
|
+
videoThumbnailUrl || videoUrl ? "video" : "",
|
|
758
|
+
articleImageUrl || articleUrl ? "article" : "",
|
|
759
|
+
].filter(Boolean)));
|
|
760
|
+
if (imageUrls.length === 0 &&
|
|
761
|
+
!videoThumbnailUrl &&
|
|
762
|
+
!videoUrl &&
|
|
763
|
+
!articleImageUrl &&
|
|
764
|
+
!articleUrl) {
|
|
765
|
+
return undefined;
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
imageUrls,
|
|
769
|
+
...(videoThumbnailUrl ? { videoThumbnailUrl } : {}),
|
|
770
|
+
...(videoUrl ? { videoUrl } : {}),
|
|
771
|
+
...(articleImageUrl ? { articleImageUrl } : {}),
|
|
772
|
+
...(articleUrl ? { articleUrl } : {}),
|
|
773
|
+
mediaTypes,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
730
776
|
function normalizePostUrl(url) {
|
|
731
777
|
if (!url)
|
|
732
778
|
return null;
|
|
@@ -764,6 +810,7 @@ function summarizeSignalPost(post) {
|
|
|
764
810
|
const likes = post.engagement?.likes ?? 0;
|
|
765
811
|
const comments = post.engagement?.comments ?? 0;
|
|
766
812
|
const shares = post.engagement?.shares ?? 0;
|
|
813
|
+
const media = normalizeSignalPostMedia(post.media);
|
|
767
814
|
return {
|
|
768
815
|
id: post.id,
|
|
769
816
|
url: post.url,
|
|
@@ -784,6 +831,7 @@ function summarizeSignalPost(post) {
|
|
|
784
831
|
},
|
|
785
832
|
score: Number(post._score.toFixed(2)),
|
|
786
833
|
excerpt: truncate(post.content),
|
|
834
|
+
...(media ? { media } : {}),
|
|
787
835
|
};
|
|
788
836
|
}
|
|
789
837
|
function summarizeSignalSearchResponse(response) {
|
|
@@ -2697,9 +2745,7 @@ function normalizeMcpCompanyIcp(filters) {
|
|
|
2697
2745
|
if (!isPlainObject(icp)) {
|
|
2698
2746
|
return;
|
|
2699
2747
|
}
|
|
2700
|
-
const scope = typeof icp.geographic_scope === "string"
|
|
2701
|
-
? icp.geographic_scope.trim()
|
|
2702
|
-
: "";
|
|
2748
|
+
const scope = typeof icp.geographic_scope === "string" ? icp.geographic_scope.trim() : "";
|
|
2703
2749
|
const scopeMarkets = expandMcpCompanyIcpMarket(scope);
|
|
2704
2750
|
if (scopeMarkets.length > 0) {
|
|
2705
2751
|
icp.geographic_scope = "multi_country";
|
|
@@ -2708,9 +2754,7 @@ function normalizeMcpCompanyIcp(filters) {
|
|
|
2708
2754
|
...normalizeMcpCompanyIcpMarkets(icp.geographic_markets),
|
|
2709
2755
|
]);
|
|
2710
2756
|
}
|
|
2711
|
-
else if (scope &&
|
|
2712
|
-
scope !== "single_country" &&
|
|
2713
|
-
scope !== "multi_country") {
|
|
2757
|
+
else if (scope && scope !== "single_country" && scope !== "multi_country") {
|
|
2714
2758
|
delete icp.geographic_scope;
|
|
2715
2759
|
}
|
|
2716
2760
|
if (icp.geographic_markets !== undefined) {
|
|
@@ -2791,8 +2835,7 @@ function normalizeMcpSeedMatchAll(filters, concreteSeedCount, omittedFilters = [
|
|
|
2791
2835
|
if (!isPlainObject(filters.company_lookalike)) {
|
|
2792
2836
|
return;
|
|
2793
2837
|
}
|
|
2794
|
-
if (filters.company_lookalike.match_all === true &&
|
|
2795
|
-
concreteSeedCount < 2) {
|
|
2838
|
+
if (filters.company_lookalike.match_all === true && concreteSeedCount < 2) {
|
|
2796
2839
|
omittedFilters.push({
|
|
2797
2840
|
field: "company_lookalike.match_all",
|
|
2798
2841
|
reason: "Dropped match_all because fewer than two concrete approved lookalike seeds remained after MCP seed normalization.",
|
|
@@ -2900,7 +2943,9 @@ function normalizeStringArray(input) {
|
|
|
2900
2943
|
.filter((value) => value.length > 0);
|
|
2901
2944
|
}
|
|
2902
2945
|
function normalizeMcpSeeds(input, kind) {
|
|
2903
|
-
return uniqueStrings(normalizeStringArray(input).filter((seed) => kind === "domain"
|
|
2946
|
+
return uniqueStrings(normalizeStringArray(input).filter((seed) => kind === "domain"
|
|
2947
|
+
? isLikelyConcreteDomain(seed)
|
|
2948
|
+
: isLikelyConcreteSeedCompany(seed)));
|
|
2904
2949
|
}
|
|
2905
2950
|
function isLikelyConcreteDomain(input) {
|
|
2906
2951
|
return /^[a-z0-9.-]+\.[a-z]{2,}$/i.test(input);
|
|
@@ -3111,13 +3156,7 @@ function buildProspeoPeopleSearchFallbackInput(input, error) {
|
|
|
3111
3156
|
const fallbackFilters = {
|
|
3112
3157
|
person_name_or_job_title: keyword,
|
|
3113
3158
|
person_seniority: {
|
|
3114
|
-
include: [
|
|
3115
|
-
"C-Suite",
|
|
3116
|
-
"Vice President",
|
|
3117
|
-
"Head",
|
|
3118
|
-
"Director",
|
|
3119
|
-
"Manager",
|
|
3120
|
-
],
|
|
3159
|
+
include: ["C-Suite", "Vice President", "Head", "Director", "Manager"],
|
|
3121
3160
|
},
|
|
3122
3161
|
max_person_per_company: typeof filters.max_person_per_company === "number"
|
|
3123
3162
|
? filters.max_person_per_company
|
package/dist/tools/linkedin.d.ts
CHANGED
|
@@ -185,7 +185,15 @@ interface RawLinkedInPost {
|
|
|
185
185
|
authorPublicIdentifier?: string;
|
|
186
186
|
};
|
|
187
187
|
text?: string;
|
|
188
|
-
content?: string
|
|
188
|
+
content?: string | {
|
|
189
|
+
images?: unknown;
|
|
190
|
+
article?: unknown;
|
|
191
|
+
video?: unknown;
|
|
192
|
+
};
|
|
193
|
+
media?: unknown;
|
|
194
|
+
postImages?: unknown;
|
|
195
|
+
postVideo?: unknown;
|
|
196
|
+
article?: unknown;
|
|
189
197
|
activityDate?: string;
|
|
190
198
|
postedAt?: string | {
|
|
191
199
|
date?: string;
|
|
@@ -219,6 +227,15 @@ interface SerializedPost {
|
|
|
219
227
|
comments: number;
|
|
220
228
|
url: string;
|
|
221
229
|
isRepost: boolean;
|
|
230
|
+
media?: SerializedPostMedia;
|
|
231
|
+
}
|
|
232
|
+
interface SerializedPostMedia {
|
|
233
|
+
imageUrls: string[];
|
|
234
|
+
videoThumbnailUrl?: string;
|
|
235
|
+
videoUrl?: string;
|
|
236
|
+
articleImageUrl?: string;
|
|
237
|
+
articleUrl?: string;
|
|
238
|
+
mediaTypes: string[];
|
|
222
239
|
}
|
|
223
240
|
export declare function serializeLinkedInPosts(rawPosts: RawLinkedInPost[] | undefined, context: string): SerializedPost[];
|
|
224
241
|
export declare function fetchLinkedInPosts(linkedinUrl: string, limit?: number): Promise<{
|
package/dist/tools/linkedin.js
CHANGED
|
@@ -157,14 +157,105 @@ function normalizePostUrl(post) {
|
|
|
157
157
|
post.header?.linkedinUrl ||
|
|
158
158
|
"");
|
|
159
159
|
}
|
|
160
|
+
function collectImageUrls(value, urls = new Set()) {
|
|
161
|
+
if (!value)
|
|
162
|
+
return urls;
|
|
163
|
+
if (typeof value === "string" && /^https?:\/\//i.test(value)) {
|
|
164
|
+
urls.add(value);
|
|
165
|
+
return urls;
|
|
166
|
+
}
|
|
167
|
+
if (Array.isArray(value)) {
|
|
168
|
+
for (const item of value)
|
|
169
|
+
collectImageUrls(item, urls);
|
|
170
|
+
return urls;
|
|
171
|
+
}
|
|
172
|
+
if (typeof value !== "object")
|
|
173
|
+
return urls;
|
|
174
|
+
const objectValue = value;
|
|
175
|
+
for (const key of ["url", "imageUrl", "thumbnailUrl", "thumbnail_url"]) {
|
|
176
|
+
const candidate = objectValue[key];
|
|
177
|
+
if (typeof candidate === "string" && /^https?:\/\//i.test(candidate)) {
|
|
178
|
+
urls.add(candidate);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (objectValue.image)
|
|
182
|
+
collectImageUrls(objectValue.image, urls);
|
|
183
|
+
if (objectValue.images)
|
|
184
|
+
collectImageUrls(objectValue.images, urls);
|
|
185
|
+
return urls;
|
|
186
|
+
}
|
|
187
|
+
function pickString(value) {
|
|
188
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
189
|
+
}
|
|
190
|
+
function serializePostMedia(post) {
|
|
191
|
+
const media = post.media && typeof post.media === "object"
|
|
192
|
+
? post.media
|
|
193
|
+
: {};
|
|
194
|
+
const content = post.content && typeof post.content === "object" ? post.content : {};
|
|
195
|
+
const article = post.article && typeof post.article === "object"
|
|
196
|
+
? post.article
|
|
197
|
+
: content.article && typeof content.article === "object"
|
|
198
|
+
? content.article
|
|
199
|
+
: {};
|
|
200
|
+
const video = post.postVideo && typeof post.postVideo === "object"
|
|
201
|
+
? post.postVideo
|
|
202
|
+
: content.video && typeof content.video === "object"
|
|
203
|
+
? content.video
|
|
204
|
+
: {};
|
|
205
|
+
const imageUrls = Array.from(collectImageUrls([
|
|
206
|
+
media.imageUrls,
|
|
207
|
+
post.postImages,
|
|
208
|
+
content.images,
|
|
209
|
+
article.image,
|
|
210
|
+
]));
|
|
211
|
+
const videoThumbnailUrl = pickString(media.videoThumbnailUrl) ||
|
|
212
|
+
pickString(video.thumbnailUrl) ||
|
|
213
|
+
pickString(video.thumbnail_url) ||
|
|
214
|
+
pickString(video.thumbnail);
|
|
215
|
+
const videoUrl = pickString(media.videoUrl) ||
|
|
216
|
+
pickString(video.videoUrl) ||
|
|
217
|
+
pickString(video.video_url);
|
|
218
|
+
const articleImageUrl = pickString(media.articleImageUrl) ||
|
|
219
|
+
Array.from(collectImageUrls(article.image))[0];
|
|
220
|
+
const articleUrl = pickString(media.articleUrl) ||
|
|
221
|
+
pickString(article.link) ||
|
|
222
|
+
pickString(article.article_url) ||
|
|
223
|
+
pickString(article.articleUrl) ||
|
|
224
|
+
pickString(article.url);
|
|
225
|
+
const mediaTypes = Array.from(new Set([
|
|
226
|
+
...(Array.isArray(media.mediaTypes)
|
|
227
|
+
? media.mediaTypes.map((type) => typeof type === "string" ? type.trim() : "")
|
|
228
|
+
: []),
|
|
229
|
+
imageUrls.length > 0 ? "image" : "",
|
|
230
|
+
videoThumbnailUrl || videoUrl ? "video" : "",
|
|
231
|
+
articleImageUrl || articleUrl ? "article" : "",
|
|
232
|
+
].filter(Boolean)));
|
|
233
|
+
if (imageUrls.length === 0 &&
|
|
234
|
+
!videoThumbnailUrl &&
|
|
235
|
+
!videoUrl &&
|
|
236
|
+
!articleImageUrl &&
|
|
237
|
+
!articleUrl) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
imageUrls,
|
|
242
|
+
...(videoThumbnailUrl ? { videoThumbnailUrl } : {}),
|
|
243
|
+
...(videoUrl ? { videoUrl } : {}),
|
|
244
|
+
...(articleImageUrl ? { articleImageUrl } : {}),
|
|
245
|
+
...(articleUrl ? { articleUrl } : {}),
|
|
246
|
+
mediaTypes,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
160
249
|
function serializeLinkedInPost(post) {
|
|
250
|
+
const media = serializePostMedia(post);
|
|
161
251
|
return {
|
|
162
|
-
text: post.text || post.content || "",
|
|
252
|
+
text: post.text || (typeof post.content === "string" ? post.content : "") || "",
|
|
163
253
|
date: normalizePostDate(post),
|
|
164
254
|
reactions: post.reactionsCount ?? post.engagement?.likes ?? 0,
|
|
165
255
|
comments: post.commentsCount ?? post.engagement?.comments ?? 0,
|
|
166
256
|
url: normalizePostUrl(post),
|
|
167
257
|
isRepost: Boolean(post.isRepublishedPost || post.repostedBy || post.repostedAt),
|
|
258
|
+
...(media ? { media } : {}),
|
|
168
259
|
};
|
|
169
260
|
}
|
|
170
261
|
function isUsableSerializedPost(post) {
|
package/package.json
CHANGED
|
@@ -516,6 +516,10 @@ Visible Flow Trace
|
|
|
516
516
|
- calculated mobile/desktop visible blocks from
|
|
517
517
|
`calculate_linkedin_hook_preview` or authenticated LinkedIn screenshot:
|
|
518
518
|
- optional visual artifact from `render_linkedin_post_preview`:
|
|
519
|
+
- source attached media inspected:
|
|
520
|
+
- sourceVisualBasis / mediaType / cover_only:
|
|
521
|
+
- visualHookMechanism / beliefActivated / proofCreated:
|
|
522
|
+
- visualToCopyAlignment:
|
|
519
523
|
- see-more tension:
|
|
520
524
|
- curiosity debt:
|
|
521
525
|
- body promise:
|
|
@@ -727,6 +731,9 @@ The research worker must return a compact packet only:
|
|
|
727
731
|
- viral-post outlines for the best source posts
|
|
728
732
|
- line-to-template conversions that turn source structures into reusable
|
|
729
733
|
templates without source wording
|
|
734
|
+
- visual/media hook analysis: source image URL or local path, visual basis,
|
|
735
|
+
media type, visible image text, visual hook mechanism, belief activated,
|
|
736
|
+
proof created, visual-to-copy alignment, and Sellable adaptation notes
|
|
730
737
|
- hook-to-body promise maps that show how each hook tells the body
|
|
731
738
|
- body structures and exact body language moves
|
|
732
739
|
- preview measurements
|
|
@@ -746,22 +753,31 @@ Default flow:
|
|
|
746
753
|
3. Shortlist high-engagement posts by topic fit, hook strength, creator repeat evidence, and weighted engagement quality.
|
|
747
754
|
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.
|
|
748
755
|
5. If full text cannot be matched, record `full_text_unavailable` and use only the preview. Do not invent missing body details.
|
|
749
|
-
6.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
756
|
+
6. For every shortlisted keeper or near-keeper, inspect attached media when
|
|
757
|
+
available. Prefer authenticated screenshots or downloaded media; otherwise
|
|
758
|
+
use public `og:image`, `twitter:image`, or JSON-LD `image.url` from the
|
|
759
|
+
source post URL. Record `sourceVisualBasis`, `sourceImageUrl`,
|
|
760
|
+
`sourceImageLocalPath` when created, `imageAvailability`, `mediaType`,
|
|
761
|
+
`cover_only` when only a first frame/cover was inspected,
|
|
762
|
+
`visualHookMechanism`, `beliefActivated`, `proofCreated`, and
|
|
763
|
+
`visualToCopyAlignment`. Do not infer hidden video or carousel frames.
|
|
764
|
+
7. 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`.
|
|
765
|
+
8. Penalize lead-magnet or giveaway mechanics unless the user explicitly asks for a lead magnet post.
|
|
766
|
+
9. Build an audience tension snapshot before hook generation: what the space is rewarding, what tension readers are reacting to, what they want to try or avoid, what objections/fears will make them hesitate, and which angle the user can credibly own. If the user's raw idea is internally coherent but not attached to live audience tension, do not draft from the internal idea alone; present stronger directions or rewrite the draft angle around the external tension.
|
|
767
|
+
10. 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.
|
|
768
|
+
11. For story posts, extract the story mechanism that made the post work, not just the first line.
|
|
769
|
+
12. Extract hook structures plus specific reusable words, phrases, sentence
|
|
755
770
|
shapes, transitions, and body language patterns.
|
|
756
|
-
|
|
771
|
+
13. Create a post positioning breakdown for each keeper post: line/phrase,
|
|
757
772
|
category, narrative technique, tension created, reader question opened,
|
|
758
|
-
proof dependency, and reusable
|
|
759
|
-
|
|
773
|
+
proof dependency, source visual basis, visual hook mechanism, and reusable
|
|
774
|
+
template line.
|
|
775
|
+
14. Convert each keeper into a viral-post outline: hook job, see-more trigger,
|
|
760
776
|
body payoff, close job, and beat-by-beat narrative structure.
|
|
761
|
-
|
|
762
|
-
sequences, required story/proof inputs, forbidden borrowing,
|
|
763
|
-
Sellable-specific adaptation instructions.
|
|
764
|
-
|
|
777
|
+
15. Convert the best outlines into reusable post templates with positioning
|
|
778
|
+
sequences, visual engine, required story/proof inputs, forbidden borrowing,
|
|
779
|
+
and Sellable-specific adaptation instructions.
|
|
780
|
+
16. Save the research with `mcp__sellable__save_hook_research`.
|
|
765
781
|
|
|
766
782
|
Record provenance:
|
|
767
783
|
|
|
@@ -775,6 +791,10 @@ Record provenance:
|
|
|
775
791
|
- lead-magnet or engagement-bait penalties
|
|
776
792
|
- story mechanism when relevant
|
|
777
793
|
- full-text match status
|
|
794
|
+
- source visual basis, source image URLs, local image paths when created,
|
|
795
|
+
media type, image availability, `cover_only` status, visible image text,
|
|
796
|
+
visual hook mechanism, belief activated, proof created, visual-to-copy
|
|
797
|
+
alignment, visual risk, and Sellable adaptation notes
|
|
778
798
|
- transcript/content-memory match status and worldview packet
|
|
779
799
|
- source hook preview measurements and whether they came from full text or a search preview
|
|
780
800
|
- selected hook patterns
|
|
@@ -819,6 +839,19 @@ Research status:
|
|
|
819
839
|
Best source examples:
|
|
820
840
|
1. author, URL, engagement, why kept, why not copied
|
|
821
841
|
|
|
842
|
+
Visual hook analysis:
|
|
843
|
+
1. source + sourceImageUrl/sourceImageLocalPath
|
|
844
|
+
- sourceVisualBasis:
|
|
845
|
+
- imageAvailability:
|
|
846
|
+
- mediaType:
|
|
847
|
+
- cover_only:
|
|
848
|
+
- visualHookMechanism:
|
|
849
|
+
- beliefActivated:
|
|
850
|
+
- proofCreated:
|
|
851
|
+
- visualToCopyAlignment:
|
|
852
|
+
- visualRisk:
|
|
853
|
+
- Sellable adaptation:
|
|
854
|
+
|
|
822
855
|
Audience tension snapshot:
|
|
823
856
|
- resonating ideas:
|
|
824
857
|
- visible audience tension:
|
|
@@ -119,6 +119,9 @@ 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
|
+
- source image URL or local path when available
|
|
123
|
+
- source visual basis, media type, visual hook mechanism, belief activated, and
|
|
124
|
+
visual-to-copy alignment
|
|
122
125
|
- content/body mechanism
|
|
123
126
|
- rhythm notes
|
|
124
127
|
- sentence-structure notes
|
|
@@ -154,6 +157,7 @@ Do not write candidates to the approved pack before the user chooses.
|
|
|
154
157
|
- `Primary Use`
|
|
155
158
|
- `Allowed Use`
|
|
156
159
|
- `Hook Pattern`
|
|
160
|
+
- `Visual Pattern`
|
|
157
161
|
- `Body Pattern`
|
|
158
162
|
- `Rhythm Notes`
|
|
159
163
|
- `Sentence Notes`
|
|
@@ -201,6 +205,24 @@ Tags:
|
|
|
201
205
|
- internal question:
|
|
202
206
|
- emotional trigger:
|
|
203
207
|
|
|
208
|
+
## Visual / Media Breakdown
|
|
209
|
+
|
|
210
|
+
- source image URL:
|
|
211
|
+
- source image local path:
|
|
212
|
+
- source visual basis:
|
|
213
|
+
- image availability:
|
|
214
|
+
- media type:
|
|
215
|
+
- cover only:
|
|
216
|
+
- image alt text:
|
|
217
|
+
- visible image text:
|
|
218
|
+
- visual layout:
|
|
219
|
+
- visual hook mechanism:
|
|
220
|
+
- belief activated:
|
|
221
|
+
- proof created:
|
|
222
|
+
- visual-to-copy alignment:
|
|
223
|
+
- visual risk:
|
|
224
|
+
- allowed adaptation:
|
|
225
|
+
|
|
204
226
|
## Content / Body Breakdown
|
|
205
227
|
|
|
206
228
|
- thesis:
|
|
@@ -7,7 +7,7 @@ readers click "see more," decompose each post into a reusable narrative and
|
|
|
7
7
|
positioning template, and then adapt those templates to the user's real story
|
|
8
8
|
without copying source wording.
|
|
9
9
|
|
|
10
|
-
V2 research has
|
|
10
|
+
V2 research has seven outputs:
|
|
11
11
|
|
|
12
12
|
1. weighted source winners: the best recent posts to learn from
|
|
13
13
|
2. hook autopsies: exact preview measurements, open loops, and tension created
|
|
@@ -16,7 +16,9 @@ V2 research has six outputs:
|
|
|
16
16
|
4. viral-post outlines: the reusable narrative structure of each source post
|
|
17
17
|
5. post positioning breakdown templates: line-level positioning and narrative
|
|
18
18
|
technique maps that can be adapted into the user's draft
|
|
19
|
-
6.
|
|
19
|
+
6. visual/media hook analysis: the attached image, video cover, carousel cover,
|
|
20
|
+
screenshot, diagram, or lack of media that changed how the hook landed
|
|
21
|
+
7. thought leader voice variants from the configured active influencer
|
|
20
22
|
list, unless the user explicitly skipped thought leaders or supplied a
|
|
21
23
|
named subset
|
|
22
24
|
|
|
@@ -45,11 +47,15 @@ Worker owns:
|
|
|
45
47
|
- broad keyword search
|
|
46
48
|
- tracked-person post fetches
|
|
47
49
|
- full-text matching by URL/activity ID
|
|
50
|
+
- public post image/media metadata extraction from shortlisted source URLs
|
|
48
51
|
- duplicate removal
|
|
49
52
|
- lead-magnet, giveaway, engagement-bait, and off-voice filtering
|
|
50
53
|
- audience tension extraction across kept and rejected examples
|
|
51
54
|
- premise input extraction: real scenes, observed tensions, reader value, proof gaps
|
|
52
55
|
- hook opening measurement and "see more" tension autopsy
|
|
56
|
+
- visual artifact inspection: first-frame screenshot, `og:image`,
|
|
57
|
+
`twitter:image`, JSON-LD `image.url`, carousel cover, video cover, visual hook
|
|
58
|
+
mechanism, belief activated, what the image proves, and copy/image alignment
|
|
53
59
|
- exact phrase-pattern extraction
|
|
54
60
|
- post positioning breakdown by line and phrase
|
|
55
61
|
- viral-post outline extraction
|
|
@@ -86,6 +92,9 @@ Research packet:
|
|
|
86
92
|
- body patterns: max 8
|
|
87
93
|
- source URLs and author profile URLs
|
|
88
94
|
- preview measurements
|
|
95
|
+
- source visual records: media availability, source image URLs, local image paths
|
|
96
|
+
when downloaded, visual basis, media type, visible image text, visual hook
|
|
97
|
+
mechanism, belief activated, proof created, and visual-to-copy alignment
|
|
89
98
|
- hook-to-body promise maps
|
|
90
99
|
- body expression inputs
|
|
91
100
|
- thought leader voice variants
|
|
@@ -261,6 +270,66 @@ When full hook/body text matters:
|
|
|
261
270
|
|
|
262
271
|
If full text is unavailable, record `full_text_unavailable`. Use only the preview for hook analysis and do not infer missing body details.
|
|
263
272
|
|
|
273
|
+
## Visual / Media Reality
|
|
274
|
+
|
|
275
|
+
For every shortlisted keeper or near-keeper source post, inspect attached media
|
|
276
|
+
when available. Do this before judging why the hook worked. Many high-performing
|
|
277
|
+
LinkedIn hooks are carried by the first image, video cover, carousel cover,
|
|
278
|
+
diagram, terminal screenshot, before/after output, or workflow map. If the text
|
|
279
|
+
is studied without the media, the research can misread the post.
|
|
280
|
+
|
|
281
|
+
Use the best available visual source in this order:
|
|
282
|
+
|
|
283
|
+
1. authenticated LinkedIn screenshot or downloaded media when the host can
|
|
284
|
+
access it
|
|
285
|
+
2. public `og:image` metadata from the source post URL
|
|
286
|
+
3. public `twitter:image` metadata from the source post URL
|
|
287
|
+
4. JSON-LD `image.url` from the source post URL
|
|
288
|
+
5. no visual source available
|
|
289
|
+
|
|
290
|
+
Record `sourceVisualBasis` as one of:
|
|
291
|
+
|
|
292
|
+
- `authenticated_screenshot`
|
|
293
|
+
- `downloaded_image`
|
|
294
|
+
- `public_og_image`
|
|
295
|
+
- `public_twitter_image`
|
|
296
|
+
- `json_ld_image`
|
|
297
|
+
- `none_available`
|
|
298
|
+
|
|
299
|
+
If the source is a video or carousel and only the first frame or cover image is
|
|
300
|
+
available, record `cover_only`. Do not infer hidden video frames, carousel
|
|
301
|
+
slides, hover states, captions, or comments that were not inspected.
|
|
302
|
+
|
|
303
|
+
For each inspected source, preserve:
|
|
304
|
+
|
|
305
|
+
- `sourceImageUrl`
|
|
306
|
+
- `sourceImageLocalPath` when downloaded or screenshotted
|
|
307
|
+
- `imageAvailability`: `present`, `none`, or `blocked`
|
|
308
|
+
- `mediaType`: `image`, `carousel_cover`, `video_cover`, `text_only`, or
|
|
309
|
+
`unknown`
|
|
310
|
+
- `imageAltText` when available
|
|
311
|
+
- `visibleText`: exact visible words in the image when safe to quote briefly,
|
|
312
|
+
otherwise short anchors
|
|
313
|
+
- `visualLayout`: what the viewer sees first, such as file tree, stack diagram,
|
|
314
|
+
terminal output, checklist, before/after table, UI screenshot, founder photo,
|
|
315
|
+
logo grid, or generic placeholder
|
|
316
|
+
- `visualHookMechanism`: how the visual creates curiosity, proof, contrast,
|
|
317
|
+
concreteness, safety, status, or "I can picture this" belief
|
|
318
|
+
- `beliefActivated`: what the audience has to believe for the post to work
|
|
319
|
+
- `proofCreated`: what the visual proves or makes more believable that the text
|
|
320
|
+
alone would not
|
|
321
|
+
- `visualToCopyAlignment`: how the opening text and media reinforce or fight
|
|
322
|
+
each other
|
|
323
|
+
- `visualRisk`: generic stock feel, unreadable text, borrowed proof,
|
|
324
|
+
engagement-bait cover, misleading model/logo claims, account-safety risk, or
|
|
325
|
+
unclear product proof
|
|
326
|
+
- `sellableAdaptation`: what kind of Sellable visual could use the same
|
|
327
|
+
mechanism without copying the source
|
|
328
|
+
|
|
329
|
+
When a post has no media, record `imageAvailability: none` and treat the hook as
|
|
330
|
+
text-led. When the media is blocked, record `imageAvailability: blocked` and
|
|
331
|
+
lower confidence instead of inventing a visual analysis.
|
|
332
|
+
|
|
264
333
|
## Opening Preview Measurement
|
|
265
334
|
|
|
266
335
|
Measure the visible opening for every shortlisted source post before extracting
|
|
@@ -339,6 +408,8 @@ For each shortlisted source post, record:
|
|
|
339
408
|
- author profile URL when available
|
|
340
409
|
- engagement totals and available likes/comments/shares breakdown
|
|
341
410
|
- creator repeat evidence
|
|
411
|
+
- source visual basis, media type, image URL/local path, and visual hook
|
|
412
|
+
analysis
|
|
342
413
|
- visible hook text or preview
|
|
343
414
|
- opening preview measurement fields from the section above
|
|
344
415
|
- hook mechanism
|
|
@@ -348,6 +419,11 @@ For each shortlisted source post, record:
|
|
|
348
419
|
- exact hook language patterns: reusable words, phrase shapes, contrast forms,
|
|
349
420
|
sentence shapes, and transition moves
|
|
350
421
|
- story mechanism when the post is a story
|
|
422
|
+
- visual mechanism when an image, carousel cover, video cover, screenshot,
|
|
423
|
+
diagram, or generic placeholder affects the hook
|
|
424
|
+
- belief activated by the visual
|
|
425
|
+
- proof created by the visual
|
|
426
|
+
- visual-to-copy alignment
|
|
351
427
|
- internal question created
|
|
352
428
|
- emotional trigger
|
|
353
429
|
- proof/story dependency
|
|
@@ -441,6 +517,14 @@ Use this format:
|
|
|
441
517
|
Post positioning breakdown:
|
|
442
518
|
source: <author + URL>
|
|
443
519
|
text_basis: full_text | search_preview | manual_user_source
|
|
520
|
+
source_visual_basis: authenticated_screenshot | downloaded_image | public_og_image | public_twitter_image | json_ld_image | none_available
|
|
521
|
+
media_type: image | carousel_cover | video_cover | text_only | unknown
|
|
522
|
+
source_image_url:
|
|
523
|
+
source_image_local_path:
|
|
524
|
+
visual_hook_mechanism:
|
|
525
|
+
belief_activated:
|
|
526
|
+
proof_created:
|
|
527
|
+
visual_to_copy_alignment:
|
|
444
528
|
overall_positioning_sequence:
|
|
445
529
|
<Category> -> <Category> -> <Category> -> ...
|
|
446
530
|
|
|
@@ -576,6 +660,14 @@ source:
|
|
|
576
660
|
template_name:
|
|
577
661
|
positioning_sequence:
|
|
578
662
|
<Category> -> <Category> -> <Category> -> ...
|
|
663
|
+
visual_engine:
|
|
664
|
+
source_visual_basis:
|
|
665
|
+
media_type:
|
|
666
|
+
visual_hook_mechanism:
|
|
667
|
+
belief_activated:
|
|
668
|
+
proof_created:
|
|
669
|
+
visual_to_copy_alignment:
|
|
670
|
+
sellable_adaptation:
|
|
579
671
|
required_story_inputs:
|
|
580
672
|
- <input the user must actually have>
|
|
581
673
|
required_proof_inputs:
|
|
@@ -682,6 +774,8 @@ why_it_might_fail:
|
|
|
682
774
|
positioning_sequence_to_borrow:
|
|
683
775
|
hook_move_to_borrow:
|
|
684
776
|
body_outline_to_borrow:
|
|
777
|
+
visual_engine_to_borrow:
|
|
778
|
+
visual_guardrails:
|
|
685
779
|
line_shapes_to_test:
|
|
686
780
|
required_user_inputs:
|
|
687
781
|
required_proof:
|
|
@@ -845,8 +939,27 @@ Save the research with `mcp__sellable__save_hook_research` before drafting.
|
|
|
845
939
|
The saved research must contain enough detail to support a user-visible
|
|
846
940
|
`Research Learning Report`: source examples, full adapted hook blocks, exact
|
|
847
941
|
phrase patterns, post positioning breakdowns, viral-post outlines, reusable
|
|
848
|
-
post templates, hook-to-body promise maps, body
|
|
849
|
-
tracked-person recommendations, and
|
|
942
|
+
post templates, visual/media hook analysis, hook-to-body promise maps, body
|
|
943
|
+
structures, rejected examples, tracked-person recommendations, and
|
|
944
|
+
gold-standard recommendations.
|
|
945
|
+
|
|
946
|
+
Save source visual provenance for every keeper or near-keeper:
|
|
947
|
+
|
|
948
|
+
- `sourceVisualBasis`
|
|
949
|
+
- `sourceImageUrl`
|
|
950
|
+
- `sourceImageLocalPath` when created
|
|
951
|
+
- `imageAvailability`
|
|
952
|
+
- `mediaType`
|
|
953
|
+
- `imageAltText` when available
|
|
954
|
+
- `visibleText`
|
|
955
|
+
- `visualLayout`
|
|
956
|
+
- `visualHookMechanism`
|
|
957
|
+
- `beliefActivated`
|
|
958
|
+
- `proofCreated`
|
|
959
|
+
- `visualToCopyAlignment`
|
|
960
|
+
- `visualRisk`
|
|
961
|
+
- `sellableAdaptation`
|
|
962
|
+
- `cover_only` when only the cover or first frame was inspected
|
|
850
963
|
|
|
851
964
|
If the user approves a creator/person as a recurring inspiration source, also
|
|
852
965
|
call `mcp__sellable__upsert_engage_tracked_person` with:
|
|
@@ -67,6 +67,12 @@ Hook research files must preserve:
|
|
|
67
67
|
reach-adjusted score, and normalization confidence notes when reach
|
|
68
68
|
normalization was used
|
|
69
69
|
- full-text availability
|
|
70
|
+
- source image/media URLs, source visual basis, local screenshot/image artifact
|
|
71
|
+
paths when created, image availability (`present`, `none`, or `blocked`),
|
|
72
|
+
media type (`image`, `carousel_cover`, `video_cover`, `text_only`, or
|
|
73
|
+
`unknown`), `cover_only` status, image alt text when available, visible image
|
|
74
|
+
text anchors, visual hook mechanism, visual belief activated, proof created by
|
|
75
|
+
the visual, visual-to-copy alignment, visual risk, and adaptation guards
|
|
70
76
|
- source hook preview measurements, including text basis, char count including
|
|
71
77
|
newlines, physical/content line counts, longest nonblank line, blank-line
|
|
72
78
|
visual risk, and mobile/desktop preview budget status
|
|
@@ -36,6 +36,7 @@ Every saved draft needs a validation receipt. A draft without this receipt is no
|
|
|
36
36
|
- `premiseValueAudit`
|
|
37
37
|
- `mobileScanabilityAudit`
|
|
38
38
|
- `templateAdaptationAudit`
|
|
39
|
+
- `sourceVisualAudit`
|
|
39
40
|
- `abstractionToConcreteRewriteAudit`
|
|
40
41
|
- `linkedinPreviewAudit`
|
|
41
42
|
- `simplifierConcreteLanguageAudit`
|
|
@@ -97,6 +98,8 @@ Each candidate should include:
|
|
|
97
98
|
- premise tension opened
|
|
98
99
|
- reader value implied
|
|
99
100
|
- source pattern
|
|
101
|
+
- source visual mechanism, source visual basis, visual-to-copy alignment, and
|
|
102
|
+
visual belief activated when the source used media
|
|
100
103
|
- hook-to-body promise
|
|
101
104
|
- see-more tension
|
|
102
105
|
- curiosity debt
|
|
@@ -348,6 +351,8 @@ Record:
|
|
|
348
351
|
- `selectedSourceTemplate`: source, template name, and why it fits
|
|
349
352
|
- `positioningSequenceBorrowed`: category sequence being adapted
|
|
350
353
|
- `viralPostOutlineBorrowed`: beat sequence being adapted
|
|
354
|
+
- `visualEngineBorrowed`: visual hook mechanism, source visual basis, and
|
|
355
|
+
visual-to-copy alignment being adapted when the source used media
|
|
351
356
|
- `lineShapesBorrowed`: reusable line shapes, not copied wording
|
|
352
357
|
- `userStoryInputsUsed`: source-backed user facts filling the template
|
|
353
358
|
- `userProofInputsUsed`: source-backed user proof filling the template
|
|
@@ -361,6 +366,34 @@ Save as `needs_revision` when the draft copies outside wording, borrows outside
|
|
|
361
366
|
proof, keeps a source template's status without equivalent user authority, or
|
|
362
367
|
uses a hook whose promised body payoff is not delivered.
|
|
363
368
|
|
|
369
|
+
## Source Visual Audit
|
|
370
|
+
|
|
371
|
+
Before a draft can be `ready`, validate that media from the source examples was
|
|
372
|
+
seen and interpreted, not guessed.
|
|
373
|
+
|
|
374
|
+
Record:
|
|
375
|
+
|
|
376
|
+
- `visual_hook_analysis`: keeper source, source visual basis, source image URL
|
|
377
|
+
or local path, image availability, media type, and whether the inspection was
|
|
378
|
+
`cover_only`
|
|
379
|
+
- `visibleImageText`: brief visible text anchors from the image when safe
|
|
380
|
+
- `visualHookMechanism`: how the source image, video cover, carousel cover,
|
|
381
|
+
screenshot, diagram, or lack of media helped the hook work
|
|
382
|
+
- `beliefActivated`: what belief the visual made easier for the audience to
|
|
383
|
+
accept
|
|
384
|
+
- `proofCreated`: what the visual made more credible than the text alone
|
|
385
|
+
- `visualToCopyAlignment`: how the source copy and visual reinforced or fought
|
|
386
|
+
each other
|
|
387
|
+
- `sellableVisualAdaptation`: the equivalent Sellable visual mechanism planned
|
|
388
|
+
for this draft, if any
|
|
389
|
+
- `visualRisk`: unreadable media, generic stock-like media, misleading logo or
|
|
390
|
+
model claims, borrowed product proof, account-safety concern, or blocked
|
|
391
|
+
media
|
|
392
|
+
|
|
393
|
+
Save as `needs_revision` when a selected source template relied on media but
|
|
394
|
+
the media was not inspected, when the draft borrows visual proof Christian does
|
|
395
|
+
not have, or when the planned Sellable visual contradicts the hook.
|
|
396
|
+
|
|
364
397
|
## Audience Tension Audit
|
|
365
398
|
|
|
366
399
|
Before a draft can be `ready`, validate that it is not merely an internally
|