@aicopycoders/exodus 2026.7.200-beta.7 → 2026.7.200-beta.9
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/assets/docs/CLAUDE.md +2 -1
- package/assets/docs/PIPELINES.md +24 -0
- package/assets/skills/exodus/SKILL.md +1 -0
- package/assets/skills/exodus-brand/SKILL.md +2 -2
- package/assets/skills/exodus-winners/SKILL.md +230 -0
- package/package.json +1 -1
- package/assets/references/kie-video-models.md +0 -207
- package/assets/references/lipsync-alternatives.md +0 -217
- package/assets/references/meta-ads-mcp-probe.md +0 -167
- package/assets/references/native-audio-dialogue.md +0 -178
- package/assets/references/own-brand-enrichment-path.md +0 -160
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
# Own-Brand Winners — enrichment path design (wayfinder #565)
|
|
2
|
-
|
|
3
|
-
Designed 2026-07-14 from the [MCP probe findings](meta-ads-mcp-probe.md) (#563)
|
|
4
|
-
plus a code audit of the live swipe rail (`scout/src/scrapecreators.ts`,
|
|
5
|
-
`scout/src/trigger/swipe-mine.ts`, `swipe-transcribe.ts`, `image-classify.ts`,
|
|
6
|
-
`convex/swipeAds.ts`, `convex/imageClassification.ts`). Answers: what is the
|
|
7
|
-
exact path from a designated winner (ad-account identity) to a complete stored
|
|
8
|
-
package? Feeds the bridge contract (#566) and the build specs (#567).
|
|
9
|
-
|
|
10
|
-
## TL;DR decisions
|
|
11
|
-
|
|
12
|
-
1. **Match server-side on ad copy, not agent-side on the MCP title fingerprint.**
|
|
13
|
-
The bridge payload carries the account-side ground truth (body, title,
|
|
14
|
-
created_time, format, page id); a new import task scrapes the brand's own
|
|
15
|
-
page once via the existing `fetchCompanyAds` and matches winners to library
|
|
16
|
-
ads by **normalized body+title equality** (tiebreak: earliest `start_date`).
|
|
17
|
-
Full ScrapeCreators snapshots include body text — a far stronger key than
|
|
18
|
-
the library-search `title + time` fingerprint the probe had to settle for,
|
|
19
|
-
and it drops `ads_library_search` from the skill entirely.
|
|
20
|
-
2. **One new Trigger task (`own-brand-import`), everything downstream as-is.**
|
|
21
|
-
The task reuses the scrapecreators.ts helpers + the `/api/v2/swipes/batch`
|
|
22
|
-
upsert, stores **only matched winners**, then fires the existing
|
|
23
|
-
`swipe-transcribe` and `image-classify` sweeps, which pick own-brand rows up
|
|
24
|
-
with zero changes (both sweep global "missing transcript" / "unclassified"
|
|
25
|
-
states).
|
|
26
|
-
3. **Gap-filler = agent-pushed assets for unmatched winners** (ad stopped
|
|
27
|
-
running): text is always complete from MCP; images are downloaded by the
|
|
28
|
-
agent immediately (signed CDN URLs expire) and uploaded as bytes via the
|
|
29
|
-
existing Convex `generateUploadUrl` pattern; **video files have no MCP
|
|
30
|
-
source — the skill asks the user for their own creative file** (it's their
|
|
31
|
-
ad), else the package is stored partial.
|
|
32
|
-
4. **Mirror winner media into Convex storage at import.** fbcdn URLs expire;
|
|
33
|
-
competitor rows survive because the daily re-scrape refreshes them, but
|
|
34
|
-
winners are long-lived fuel imported once. Store the storage **serve URL in
|
|
35
|
-
the existing `imageUrl`/`videoUrl` fields** so every downstream consumer
|
|
36
|
-
(pickers, classify, transcribe, gallery) works unchanged.
|
|
37
|
-
5. **Own-brand `brands` rows must be flagged out of the daily swipe-mine cron**
|
|
38
|
-
(`isOwnBrand: true`, mine filters it): otherwise the nightly sweep scrapes
|
|
39
|
-
the user's whole page and stores every active ad — violating winners-only.
|
|
40
|
-
6. **Completeness is an explicit stored field**, not derived: `complete` |
|
|
41
|
-
`partial` (+ what's missing), surfaced amber in the UI tab (the #478
|
|
42
|
-
partial-delivery pattern).
|
|
43
|
-
|
|
44
|
-
## Stage 0 — what the agent holds per winner (input to the bridge)
|
|
45
|
-
|
|
46
|
-
Per the probe, after the winner interview designates ads, the agent can gather
|
|
47
|
-
via MCP (`ads_get_ad_entities` + `ads_get_creatives` + `ads_get_ad_images` /
|
|
48
|
-
`ads_get_ad_videos`, all with explicit `fields`):
|
|
49
|
-
|
|
50
|
-
| Field | Source | Notes |
|
|
51
|
-
|---|---|---|
|
|
52
|
-
| account ad id, name, campaign/adset ids | entities | hierarchy + provenance |
|
|
53
|
-
| `created_time` | entities | matching tiebreak + daysRunning fallback |
|
|
54
|
-
| body (primary text), title (headline), CTA type | creative | ground truth for matching |
|
|
55
|
-
| `effective_object_story_id` (`<pageId>_<postId>`) | creative | **page id prefix** = scrape target |
|
|
56
|
-
| image hash → full-res signed URL | ad_images | **download immediately**, expires |
|
|
57
|
-
| video id, length, poster URL | ad_videos | no source URL — cannot download |
|
|
58
|
-
| metrics as display strings + result-type labels | entities | verdict snapshot material |
|
|
59
|
-
|
|
60
|
-
The bridge payload (#566) should carry all of the above per winner, plus the
|
|
61
|
-
page id once per import.
|
|
62
|
-
|
|
63
|
-
## Stage 1 — matching: winner → `adArchiveId`
|
|
64
|
-
|
|
65
|
-
Runs inside `own-brand-import` (server-side, ScrapeCreators BYOK key snapshot
|
|
66
|
-
exactly like `swipe-mine`'s manual-run pattern):
|
|
67
|
-
|
|
68
|
-
1. `fetchCompanyAds(pageId)` — one paginated scrape of the user's own page.
|
|
69
|
-
Returns full snapshots: `body.text`, `title`, `start_date`, media URLs,
|
|
70
|
-
`collation_count`.
|
|
71
|
-
2. For each winner: **normalize** (trim, collapse whitespace, strip zero-width
|
|
72
|
-
chars, normalize curly quotes) and match on **body equality**; require
|
|
73
|
-
title equality too when both sides have a title.
|
|
74
|
-
3. Ambiguity: multiple library ads with identical copy are creative variants /
|
|
75
|
-
republishes — any is acceptable (same creative); pick the **earliest
|
|
76
|
-
`start_date`** (longest `daysRunning`, best longevity signal for pickers).
|
|
77
|
-
4. Fallback when the account creative has no body (some `SHARE`-type creatives
|
|
78
|
-
keep copy on the story post): match on unique title + `created_time`
|
|
79
|
-
proximity (±72h). Non-unique → treat as unmatched (gap-filler), never guess.
|
|
80
|
-
5. Matched → optionally `fetchAdLibraryAdById(archiveId, get_transcript=true)`
|
|
81
|
-
for the inline transcript (the idea-ad-library / workflow-executor pattern);
|
|
82
|
-
store it at insert so most video winners never wait for the transcribe cron.
|
|
83
|
-
|
|
84
|
-
**Failure modes:** page scrape empty (wrong page id, zero active ads) → all
|
|
85
|
-
winners unmatched → gap-filler; the Ad Library only lists **running** ads for
|
|
86
|
-
non-political advertisers, so any stopped winner is *expected* to be unmatched
|
|
87
|
-
— that's the designed-for case, not an error. One spec-time item:
|
|
88
|
-
`fetchCompanyAds` hardcodes `country: "US"` — the import path must make this
|
|
89
|
-
configurable (or "ALL" if the endpoint accepts it) or non-US advertisers will
|
|
90
|
-
false-unmatch.
|
|
91
|
-
|
|
92
|
-
## Stage 2 — gap-filler protocol (unmatched winners)
|
|
93
|
-
|
|
94
|
-
What MCP can and cannot supply fills the row directly:
|
|
95
|
-
|
|
96
|
-
- **Copy**: body/title/CTA come from the creative — always complete. No
|
|
97
|
-
library `linkUrl`/`linkDescription`; acceptable loss (fields stay empty).
|
|
98
|
-
- **Image**: the agent downloads the signed CDN URL **at interview time** (not
|
|
99
|
-
at push time — it expires) and the CLI uploads bytes: request an upload URL
|
|
100
|
-
(the `creativeSuite.generateUploadUrl` service pattern), PUT the file, record
|
|
101
|
-
the storage id. Import stores the serve URL in `imageUrl`.
|
|
102
|
-
- **Video**: no MCP file. Ladder: (a) the skill asks the user to point at
|
|
103
|
-
their own creative file — they made the ad, they usually have it — CLI
|
|
104
|
-
uploads it; (b) no file → store partial: copy + poster image (poster URL
|
|
105
|
-
*is* downloadable now) and no transcript. Once a video lands in storage, its
|
|
106
|
-
serve URL in `videoUrl` makes the existing `pendingTranscripts` sweep
|
|
107
|
-
transcribe it automatically (note: `transcribeVideo` rejects >25MB —
|
|
108
|
-
acceptable v1 cap, flag oversize uploads as partial-transcript).
|
|
109
|
-
- **Identity**: gap-filled rows get a synthetic archive id `own:<accountAdId>`
|
|
110
|
-
— stable, unique, satisfies the `by_adArchiveId` dedup index, can't collide
|
|
111
|
-
with numeric library ids. `isActive: false`, `startDate` from
|
|
112
|
-
`created_time`.
|
|
113
|
-
|
|
114
|
-
## Stage 3 — reuse audit
|
|
115
|
-
|
|
116
|
-
| Piece | Verdict |
|
|
117
|
-
|---|---|
|
|
118
|
-
| `scrapecreators.ts` helpers (fetch, extract*, classifyFormat, transcribe) | **as-is** — the import task composes them |
|
|
119
|
-
| `/api/v2/swipes/batch` → `swipeAds.upsertBatch` | **as-is** — winners-only array in, dedup by archive id |
|
|
120
|
-
| `swipe-transcribe` (`pendingTranscripts` sweep) | **as-is** — global format=video-missing-transcript scan includes own-brand rows; also fire it at import end |
|
|
121
|
-
| `image-classify` (`getUnclassifiedSwipeAds`) | **as-is** — sweeps `classificationStatus === undefined`; own-brand rows enter automatically |
|
|
122
|
-
| Consumption (`swipeAdToText`, pickers, `listForWorkspacePublic`) | **as-is** — own-brand rows are swipeAds under a workspace brand; they flow into every picker. Spec check (per map): run-dialog top-N ranks by `daysRunning` — matched winners inherit real longevity, gap-filled ones get `created_time`-derived values, and the winner fields below let pickers boost designated winners explicitly |
|
|
123
|
-
| `swipe-mine` task | **not reused** (fetch-everything shape violates winners-only); its building blocks are |
|
|
124
|
-
| Daily `swipe-mine` cron | **needs a filter**: skip `isOwnBrand` brands (decision 5) |
|
|
125
|
-
| New code | one Trigger task `own-brand-import` + one HTTP route to start it (BYOK key snapshot like `swipe.startMineRun`) + the CLI ingestion command (#566's contract) |
|
|
126
|
-
|
|
127
|
-
## Stage 4 — completeness contract
|
|
128
|
-
|
|
129
|
-
A package is **usable generative fuel** when:
|
|
130
|
-
|
|
131
|
-
- **every winner**: verdict snapshot (composed sentence + metric display
|
|
132
|
-
strings) — non-negotiable, it's the point of the feature — plus body or
|
|
133
|
-
title, and provenance (account ad id, campaign/adset ids).
|
|
134
|
-
- **image winner**: + full-res image (storage-mirrored). Missing image →
|
|
135
|
-
`partial` (copy bots still work; ref-match image engines can't use it).
|
|
136
|
-
- **video winner**: + transcript (from library inline, sweep, or uploaded
|
|
137
|
-
file). Video file itself is desired (gallery, future variations) but
|
|
138
|
-
transcript is the fuel; no transcript AND no video → `partial`.
|
|
139
|
-
|
|
140
|
-
Store it explicitly: `enrichmentStatus: "complete" | "partial"` +
|
|
141
|
-
`enrichmentMissing: string[]` (e.g. `["video-file", "transcript"]`), set by the
|
|
142
|
-
import task, re-evaluated when a sweep fills a gap. UI shows amber on partial
|
|
143
|
-
rows with the missing list (the #478 pattern).
|
|
144
|
-
|
|
145
|
-
New optional `swipeAds` columns this implies for #567 (all additive):
|
|
146
|
-
winner verdict text + metrics snapshot, `designatedAt`, source account
|
|
147
|
-
ad/campaign/adset ids, `enrichmentSource: "library-match" | "agent-push"`,
|
|
148
|
-
`enrichmentStatus` / `enrichmentMissing`, `imageStorageId` / `videoStorageId`
|
|
149
|
-
(lifecycle), plus `brands.isOwnBrand`.
|
|
150
|
-
|
|
151
|
-
## Open items handed to #566 / #567
|
|
152
|
-
|
|
153
|
-
- Bridge contract (#566): exact payload shape per winner (Stage 0 table), how
|
|
154
|
-
the CLI transports image/video bytes (upload-URL handshake vs multipart),
|
|
155
|
-
idempotency on re-push (upsert by account ad id → synthetic/matched archive
|
|
156
|
-
id), and where the "ask the user for the video file" step lives in the skill.
|
|
157
|
-
- Spec (#567): country parameter on the import scrape; winner-boost rule in
|
|
158
|
-
top-selection heuristics; amber partial UI in the Swipe Mining tab; whether
|
|
159
|
-
matched winners' media also gets storage-mirrored at import (recommended:
|
|
160
|
-
yes, both paths, decision 4).
|