weftens 0.0.1 → 0.1.0
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/README.md +138 -12
- package/agent-cores/amazon-ppc/package.json +17 -0
- package/agent-cores/amazon-ppc/src/cli.mjs +66 -0
- package/agent-cores/amazon-ppc/src/optimize.mjs +272 -0
- package/agent-cores/amazon-ppc/src/parse.mjs +153 -0
- package/agent-cores/amazon-ppc/test/fixtures/search-term-report.csv +8 -0
- package/agent-cores/amazon-ppc/test/fixtures/search-term-report.json +5 -0
- package/agent-cores/amazon-ppc/test/optimize.test.mjs +161 -0
- package/agent-cores/content-ops/package.json +17 -0
- package/agent-cores/content-ops/src/cli.mjs +51 -0
- package/agent-cores/content-ops/src/inventory.mjs +343 -0
- package/agent-cores/content-ops/src/parse.mjs +121 -0
- package/agent-cores/content-ops/test/fixtures/cms-export.csv +13 -0
- package/agent-cores/content-ops/test/fixtures/cms-export.json +31 -0
- package/agent-cores/content-ops/test/inventory.test.mjs +177 -0
- package/agent-cores/meta-ads/package.json +16 -0
- package/agent-cores/meta-ads/src/analyze.mjs +166 -0
- package/agent-cores/meta-ads/src/cli.mjs +39 -0
- package/agent-cores/meta-ads/src/parse.mjs +123 -0
- package/agent-cores/meta-ads/test/analyze.test.js +72 -0
- package/agent-cores/meta-ads/test/fixtures/meta-ads.csv +6 -0
- package/agent-cores/paid-search/package.json +17 -0
- package/agent-cores/paid-search/src/analyze.mjs +303 -0
- package/agent-cores/paid-search/src/cli.mjs +57 -0
- package/agent-cores/paid-search/src/parse.mjs +158 -0
- package/agent-cores/paid-search/test/analyze.test.mjs +120 -0
- package/agent-cores/paid-search/test/fixtures/campaigns.csv +7 -0
- package/agent-cores/paid-search/test/fixtures/sample.json +9 -0
- package/agent-cores/paid-search/test/fixtures/search-terms.csv +11 -0
- package/agent-cores/paid-search/test/parse.test.mjs +56 -0
- package/agent-cores/seo-content/package.json +17 -0
- package/agent-cores/seo-content/src/cli.mjs +41 -0
- package/agent-cores/seo-content/src/parse.mjs +141 -0
- package/agent-cores/seo-content/src/strategy.mjs +266 -0
- package/agent-cores/seo-content/test/fixtures/gsc-sample.csv +11 -0
- package/agent-cores/seo-content/test/parse.test.mjs +59 -0
- package/agent-cores/seo-content/test/strategy.test.mjs +108 -0
- package/agent-cores/seo-technical/package.json +17 -0
- package/agent-cores/seo-technical/src/audit.mjs +233 -0
- package/agent-cores/seo-technical/src/cli.mjs +48 -0
- package/agent-cores/seo-technical/src/fetch.mjs +51 -0
- package/agent-cores/seo-technical/test/audit.test.mjs +73 -0
- package/agent-cores/seo-technical/test/fixtures/sample.html +22 -0
- package/agent-cores/supply-verify/package.json +17 -0
- package/agent-cores/supply-verify/src/cli.mjs +44 -0
- package/agent-cores/supply-verify/src/verify.mjs +303 -0
- package/agent-cores/supply-verify/test/fixtures/placements.csv +8 -0
- package/agent-cores/supply-verify/test/fixtures/placements.json +4 -0
- package/agent-cores/supply-verify/test/verify.test.mjs +148 -0
- package/agents/agent2060.md +99 -0
- package/agents/amazon-ppc.md +42 -0
- package/agents/claims-content.md +35 -0
- package/agents/content-ops.md +46 -0
- package/agents/content-strategy.md +13 -0
- package/agents/disclosure-qc.md +13 -0
- package/agents/feed-optimizer.md +13 -0
- package/agents/growth-analyst.md +24 -0
- package/agents/hilbert.md +55 -0
- package/agents/ideator.md +13 -0
- package/agents/lippmann.md +68 -0
- package/agents/paid-search.md +35 -0
- package/agents/pio.md +34 -0
- package/agents/researcher.md +13 -0
- package/agents/richy.md +72 -0
- package/agents/script-editor.md +13 -0
- package/agents/seo-content.md +57 -0
- package/agents/seo-technical.md +53 -0
- package/agents/showrunner.md +13 -0
- package/agents/social-voice.md +38 -0
- package/agents/supply-verify.md +38 -0
- package/agents/video-editor.md +13 -0
- package/agents/writer.md +45 -0
- package/bin/weftens.js +226 -0
- package/content-org/.preflight/telemetry.jsonl +6 -0
- package/content-org/briefs/BRIEFS.md +47 -0
- package/content-org/config.mjs +78 -0
- package/content-org/cost.mjs +36 -0
- package/content-org/lead.js +64 -0
- package/content-org/metrics.mjs +166 -0
- package/content-org/orchestrator.mjs +246 -0
- package/content-org/package.json +14 -0
- package/content-org/producer.mjs +59 -0
- package/content-org/publish.mjs +34 -0
- package/content-org/review.mjs +32 -0
- package/content-org/studio/README.md +27 -0
- package/content-org/studio/assemble.sh +48 -0
- package/content-org/test.mjs +110 -0
- package/index.js +32 -0
- package/package.json +49 -10
- package/providers.json.example +9 -0
- package/src/agent-router.js +124 -0
- package/src/agent.js +123 -0
- package/src/invoke.js +129 -0
- package/src/pio-client.js +142 -0
- package/src/provider.js +81 -0
- package/src/registry.js +160 -0
- package/src/richy-client.js +216 -0
- package/src/router.js +34 -0
- package/src/synthesize.js +111 -0
- package/src/weftens.js +61 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
// Deterministic ad-spend verification. No LLM. Given programmatic placement /
|
|
2
|
+
// log-level rows (one per domain or app), compute an evidenced waste report:
|
|
3
|
+
// MFA (made-for-advertising) markers, invalid-traffic (IVT) flags, low-viewability
|
|
4
|
+
// spend, and a concrete exclusion list with the spend each exclusion recovers.
|
|
5
|
+
//
|
|
6
|
+
// Input is DATA, not a prompt. Every finding carries an evidence string, a
|
|
7
|
+
// confidence tier, and the spend it affects — all computed with real arithmetic.
|
|
8
|
+
//
|
|
9
|
+
// Confidence tiers (from the analyst spec, weakest to strongest):
|
|
10
|
+
// needs-data < consistent-with < probable < verified
|
|
11
|
+
// MFA and IVT detection off log-level metrics is inherently heuristic, so the
|
|
12
|
+
// core never emits "verified" for them — the strongest it asserts is "probable".
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_THRESHOLDS = {
|
|
15
|
+
// IVT (invalid traffic) rate, in percentage points.
|
|
16
|
+
ivtProbable: 20, // >= this: probable invalid-traffic problem, auto-exclude candidate
|
|
17
|
+
ivtWatch: 10, // >= this: consistent-with elevated IVT
|
|
18
|
+
// Viewability rate, in percentage points. Industry floor guidance ~50% (display).
|
|
19
|
+
viewabilityLow: 50, // < this: consistent-with a viewability problem
|
|
20
|
+
viewabilityBad: 40, // < this: probable viewability problem
|
|
21
|
+
// Video completion rate, in percentage points (only scored when present).
|
|
22
|
+
completionFloor: 20,
|
|
23
|
+
// MFA markers.
|
|
24
|
+
adDensityHigh: 8, // ads-per-page at/above this is an extreme-density marker
|
|
25
|
+
sourcedTrafficHigh: 50, // % of traffic that is sourced/paid rather than earned
|
|
26
|
+
mfaMarkerMin: 3, // "a site is MFA on three markers, not one" -> flag at 3
|
|
27
|
+
mfaMarkerStrong: 4, // >= this markers escalates confidence to "probable"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CONFIDENCE_RANK = { 'needs-data': 0, 'consistent-with': 1, probable: 2, verified: 3 };
|
|
31
|
+
|
|
32
|
+
// --- input normalization ---------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const COLUMN_ALIASES = {
|
|
35
|
+
domain: ['domain', 'site', 'app', 'app_bundle', 'bundle', 'placement', 'publisher', 'url', 'inventory'],
|
|
36
|
+
impressions: ['impressions', 'imps', 'impression'],
|
|
37
|
+
spend: ['spend', 'cost', 'media_cost', 'amount', 'net_spend'],
|
|
38
|
+
viewability_pct: ['viewability_pct', 'viewability', 'viewable_rate', 'viewability_rate', 'in_view_rate', 'viewable'],
|
|
39
|
+
ivt_pct: ['ivt_pct', 'ivt', 'invalid_traffic', 'invalid_traffic_rate', 'givt', 'total_ivt'],
|
|
40
|
+
completion_rate: ['completion_rate', 'vcr', 'completion', 'video_completion_rate'],
|
|
41
|
+
ad_density: ['ad_density', 'ads_per_page', 'ad_count'],
|
|
42
|
+
sourced_traffic_pct: ['sourced_traffic_pct', 'sourced_traffic', 'paid_traffic', 'sourced_traffic_share'],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function canonicalKey(raw) {
|
|
46
|
+
const norm = String(raw).trim().toLowerCase().replace(/[%()]/g, '').replace(/[\s/.-]+/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
|
|
47
|
+
for (const [canon, aliases] of Object.entries(COLUMN_ALIASES)) {
|
|
48
|
+
if (aliases.includes(norm)) return canon;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse a percentage-like value. Accepts "42", "42%", " 42 ". Values are treated
|
|
54
|
+
// as percentage POINTS (0-100). A bare fraction in [0,1] is upscaled to percent
|
|
55
|
+
// so "0.42" and "42" both mean 42%. Returns null when absent/unparseable.
|
|
56
|
+
function parsePct(v) {
|
|
57
|
+
if (v === undefined || v === null || v === '') return null;
|
|
58
|
+
const n = Number(String(v).replace('%', '').trim());
|
|
59
|
+
if (!Number.isFinite(n)) return null;
|
|
60
|
+
if (n > 0 && n <= 1) return n * 100;
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseNum(v) {
|
|
65
|
+
if (v === undefined || v === null || v === '') return null;
|
|
66
|
+
const n = Number(String(v).replace(/[$,\s]/g, ''));
|
|
67
|
+
return Number.isFinite(n) ? n : null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Minimal, dependency-free CSV parser. Handles quoted fields, embedded commas,
|
|
71
|
+
// escaped double-quotes ("") and CRLF/LF line endings.
|
|
72
|
+
export function parseCsv(text) {
|
|
73
|
+
const rows = [];
|
|
74
|
+
let field = '';
|
|
75
|
+
let record = [];
|
|
76
|
+
let inQuotes = false;
|
|
77
|
+
const pushField = () => { record.push(field); field = ''; };
|
|
78
|
+
const pushRecord = () => { pushField(); rows.push(record); record = []; };
|
|
79
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
80
|
+
const c = text[i];
|
|
81
|
+
if (inQuotes) {
|
|
82
|
+
if (c === '"') {
|
|
83
|
+
if (text[i + 1] === '"') { field += '"'; i += 1; }
|
|
84
|
+
else inQuotes = false;
|
|
85
|
+
} else field += c;
|
|
86
|
+
} else if (c === '"') inQuotes = true;
|
|
87
|
+
else if (c === ',') pushField();
|
|
88
|
+
else if (c === '\n') pushRecord();
|
|
89
|
+
else if (c === '\r') { /* swallow, handled by \n */ }
|
|
90
|
+
else field += c;
|
|
91
|
+
}
|
|
92
|
+
if (field !== '' || record.length > 0) pushRecord();
|
|
93
|
+
// drop trailing empty record
|
|
94
|
+
while (rows.length && rows[rows.length - 1].length === 1 && rows[rows.length - 1][0] === '') rows.pop();
|
|
95
|
+
if (rows.length === 0) return [];
|
|
96
|
+
const header = rows[0];
|
|
97
|
+
return rows.slice(1).filter((r) => r.some((c) => c !== '')).map((r) => {
|
|
98
|
+
const obj = {};
|
|
99
|
+
header.forEach((h, idx) => { obj[h] = r[idx] ?? ''; });
|
|
100
|
+
return obj;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Turn a loose record ({header: value}) into a normalized placement.
|
|
105
|
+
function normalizeRow(rawObj) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [k, v] of Object.entries(rawObj)) {
|
|
108
|
+
const canon = canonicalKey(k);
|
|
109
|
+
if (canon && out[canon] === undefined) out[canon] = v;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
domain: out.domain !== undefined ? String(out.domain).trim() : '(unknown)',
|
|
113
|
+
impressions: parseNum(out.impressions) ?? 0,
|
|
114
|
+
spend: parseNum(out.spend) ?? 0,
|
|
115
|
+
viewability_pct: parsePct(out.viewability_pct),
|
|
116
|
+
ivt_pct: parsePct(out.ivt_pct),
|
|
117
|
+
completion_rate: parsePct(out.completion_rate),
|
|
118
|
+
ad_density: parseNum(out.ad_density),
|
|
119
|
+
sourced_traffic_pct: parsePct(out.sourced_traffic_pct),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Accept a CSV string, a JSON string, or an already-parsed array of objects.
|
|
124
|
+
export function loadPlacements(input, { format } = {}) {
|
|
125
|
+
if (Array.isArray(input)) return input.map(normalizeRow);
|
|
126
|
+
const text = String(input);
|
|
127
|
+
const isJson = format === 'json' || (format !== 'csv' && /^\s*[[{]/.test(text));
|
|
128
|
+
if (isJson) {
|
|
129
|
+
const data = JSON.parse(text);
|
|
130
|
+
const arr = Array.isArray(data) ? data : (Array.isArray(data.placements) ? data.placements : [data]);
|
|
131
|
+
return arr.map(normalizeRow);
|
|
132
|
+
}
|
|
133
|
+
return parseCsv(text).map(normalizeRow);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- the analysis ----------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
function round2(n) { return Math.round(n * 100) / 100; }
|
|
139
|
+
|
|
140
|
+
// Evaluate one placement. Returns { placement, markers, findings, exclusion }.
|
|
141
|
+
function analyzePlacement(p, t) {
|
|
142
|
+
const findings = [];
|
|
143
|
+
const markers = []; // MFA markers, each with a human-readable evidence string
|
|
144
|
+
|
|
145
|
+
const nonViewableSpend = p.viewability_pct === null ? 0 : round2(p.spend * (1 - p.viewability_pct / 100));
|
|
146
|
+
const ivtWastedSpend = p.ivt_pct === null ? 0 : round2(p.spend * (p.ivt_pct / 100));
|
|
147
|
+
|
|
148
|
+
// --- IVT ---
|
|
149
|
+
if (p.ivt_pct === null) {
|
|
150
|
+
findings.push({ domain: p.domain, type: 'ivt', confidence: 'needs-data', evidence: 'No IVT rate supplied for this placement.', spendAffected: p.spend, estimatedWaste: 0 });
|
|
151
|
+
} else if (p.ivt_pct >= t.ivtProbable) {
|
|
152
|
+
findings.push({ domain: p.domain, type: 'ivt', confidence: 'probable', evidence: `IVT ${round2(p.ivt_pct)}% >= ${t.ivtProbable}% threshold on ${p.impressions.toLocaleString('en-US')} impressions; $${ivtWastedSpend} of spend served to invalid traffic.`, spendAffected: p.spend, estimatedWaste: ivtWastedSpend });
|
|
153
|
+
} else if (p.ivt_pct >= t.ivtWatch) {
|
|
154
|
+
findings.push({ domain: p.domain, type: 'ivt', confidence: 'consistent-with', evidence: `IVT ${round2(p.ivt_pct)}% is elevated (watch >= ${t.ivtWatch}%); ~$${ivtWastedSpend} exposed.`, spendAffected: p.spend, estimatedWaste: ivtWastedSpend });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Viewability ---
|
|
158
|
+
if (p.viewability_pct === null) {
|
|
159
|
+
findings.push({ domain: p.domain, type: 'low-viewability', confidence: 'needs-data', evidence: 'No viewability rate supplied for this placement.', spendAffected: p.spend, estimatedWaste: 0 });
|
|
160
|
+
} else if (p.viewability_pct < t.viewabilityBad) {
|
|
161
|
+
findings.push({ domain: p.domain, type: 'low-viewability', confidence: 'probable', evidence: `Viewability ${round2(p.viewability_pct)}% < ${t.viewabilityBad}%; $${nonViewableSpend} of spend was never in view.`, spendAffected: p.spend, estimatedWaste: nonViewableSpend });
|
|
162
|
+
} else if (p.viewability_pct < t.viewabilityLow) {
|
|
163
|
+
findings.push({ domain: p.domain, type: 'low-viewability', confidence: 'consistent-with', evidence: `Viewability ${round2(p.viewability_pct)}% below the ${t.viewabilityLow}% floor; ~$${nonViewableSpend} non-viewable.`, spendAffected: p.spend, estimatedWaste: nonViewableSpend });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- MFA markers (a site is MFA on several markers, not one) ---
|
|
167
|
+
if (p.ad_density !== null && p.ad_density >= t.adDensityHigh) markers.push(`extreme ad density (${p.ad_density} ads/page >= ${t.adDensityHigh})`);
|
|
168
|
+
if (p.sourced_traffic_pct !== null && p.sourced_traffic_pct >= t.sourcedTrafficHigh) markers.push(`traffic bought not earned (${round2(p.sourced_traffic_pct)}% sourced >= ${t.sourcedTrafficHigh}%)`);
|
|
169
|
+
if (p.ivt_pct !== null && p.ivt_pct >= t.ivtWatch) markers.push(`elevated invalid traffic (${round2(p.ivt_pct)}% >= ${t.ivtWatch}%)`);
|
|
170
|
+
if (p.viewability_pct !== null && p.viewability_pct < t.viewabilityBad) markers.push(`low viewability consistent with ad stacking (${round2(p.viewability_pct)}% < ${t.viewabilityBad}%)`);
|
|
171
|
+
if (p.completion_rate !== null && p.completion_rate < t.completionFloor) markers.push(`engagement floor breached (completion ${round2(p.completion_rate)}% < ${t.completionFloor}%)`);
|
|
172
|
+
|
|
173
|
+
let mfaFinding = null;
|
|
174
|
+
if (markers.length >= t.mfaMarkerMin) {
|
|
175
|
+
const confidence = markers.length >= t.mfaMarkerStrong ? 'probable' : 'consistent-with';
|
|
176
|
+
mfaFinding = { domain: p.domain, type: 'mfa', confidence, markers: [...markers], evidence: `${markers.length} MFA markers: ${markers.join('; ')}.`, spendAffected: p.spend, estimatedWaste: p.spend };
|
|
177
|
+
findings.push(mfaFinding);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- exclusion decision ---
|
|
181
|
+
// Exclude when: MFA flagged (>= mfaMarkerMin markers) OR IVT at/above the
|
|
182
|
+
// probable threshold. Excluding recovers the whole placement's spend.
|
|
183
|
+
let exclusion = null;
|
|
184
|
+
const excludeReasons = [];
|
|
185
|
+
if (mfaFinding) excludeReasons.push(`MFA (${markers.length} markers)`);
|
|
186
|
+
if (p.ivt_pct !== null && p.ivt_pct >= t.ivtProbable) excludeReasons.push(`IVT ${round2(p.ivt_pct)}%`);
|
|
187
|
+
if (excludeReasons.length) {
|
|
188
|
+
const confidence = (mfaFinding && mfaFinding.confidence === 'probable') || (p.ivt_pct !== null && p.ivt_pct >= t.ivtProbable) ? 'probable' : 'consistent-with';
|
|
189
|
+
exclusion = {
|
|
190
|
+
domain: p.domain,
|
|
191
|
+
reason: excludeReasons.join(' + '),
|
|
192
|
+
markers: [...markers],
|
|
193
|
+
confidence,
|
|
194
|
+
spendRecovered: round2(p.spend),
|
|
195
|
+
impressions: p.impressions,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { placement: p, findings, markers, exclusion, nonViewableSpend, ivtWastedSpend };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function verifySupply(input, options = {}) {
|
|
203
|
+
const t = { ...DEFAULT_THRESHOLDS, ...(options.thresholds || {}) };
|
|
204
|
+
const placements = Array.isArray(input) && input.length && input[0] && input[0].domain !== undefined && typeof input[0].domain === 'string' && 'viewability_pct' in input[0]
|
|
205
|
+
? input // already normalized placements
|
|
206
|
+
: loadPlacements(input, { format: options.format });
|
|
207
|
+
|
|
208
|
+
const analyzed = placements.map((p) => analyzePlacement(p, t));
|
|
209
|
+
|
|
210
|
+
const totalSpend = round2(placements.reduce((s, p) => s + p.spend, 0));
|
|
211
|
+
const totalImpressions = placements.reduce((s, p) => s + p.impressions, 0);
|
|
212
|
+
|
|
213
|
+
// Collect and rank findings: worst confidence + biggest spend first, then
|
|
214
|
+
// domain asc for a fully deterministic, stable order.
|
|
215
|
+
const findings = analyzed.flatMap((a) => a.findings)
|
|
216
|
+
.filter((f) => f.confidence !== 'needs-data' || options.includeNeedsData)
|
|
217
|
+
.sort((a, b) => {
|
|
218
|
+
const c = CONFIDENCE_RANK[b.confidence] - CONFIDENCE_RANK[a.confidence];
|
|
219
|
+
if (c !== 0) return c;
|
|
220
|
+
const s = b.spendAffected - a.spendAffected;
|
|
221
|
+
if (s !== 0) return s;
|
|
222
|
+
if (a.domain !== b.domain) return a.domain < b.domain ? -1 : 1;
|
|
223
|
+
return a.type < b.type ? -1 : (a.type > b.type ? 1 : 0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const exclusionList = analyzed.map((a) => a.exclusion).filter(Boolean)
|
|
227
|
+
.sort((a, b) => (b.spendRecovered - a.spendRecovered) || (a.domain < b.domain ? -1 : a.domain > b.domain ? 1 : 0));
|
|
228
|
+
|
|
229
|
+
const excludedDomains = new Set(exclusionList.map((e) => e.domain));
|
|
230
|
+
const excludedSpend = round2(exclusionList.reduce((s, e) => s + e.spendRecovered, 0));
|
|
231
|
+
|
|
232
|
+
// Residual waste on placements we are NOT excluding: sum the estimatedWaste of
|
|
233
|
+
// their over-threshold IVT / low-viewability findings only (a clean placement
|
|
234
|
+
// contributes nothing), capped at the placement's spend so we never over-count.
|
|
235
|
+
const residualWaste = round2(analyzed
|
|
236
|
+
.filter((a) => !excludedDomains.has(a.placement.domain))
|
|
237
|
+
.reduce((s, a) => {
|
|
238
|
+
const w = a.findings
|
|
239
|
+
.filter((f) => (f.type === 'ivt' || f.type === 'low-viewability') && f.confidence !== 'needs-data')
|
|
240
|
+
.reduce((acc, f) => acc + f.estimatedWaste, 0);
|
|
241
|
+
return s + Math.min(a.placement.spend, w);
|
|
242
|
+
}, 0));
|
|
243
|
+
|
|
244
|
+
const reclaimableSpend = round2(excludedSpend + residualWaste);
|
|
245
|
+
|
|
246
|
+
const flaggedDomains = new Set(findings.map((f) => f.domain));
|
|
247
|
+
const flaggedSpend = round2(placements.filter((p) => flaggedDomains.has(p.domain)).reduce((s, p) => s + p.spend, 0));
|
|
248
|
+
|
|
249
|
+
const vendorQuestions = buildVendorQuestions({ analyzed, exclusionList, totalSpend, reclaimableSpend, t });
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
summary: {
|
|
253
|
+
placements: placements.length,
|
|
254
|
+
totalSpend,
|
|
255
|
+
totalImpressions,
|
|
256
|
+
flaggedPlacements: flaggedDomains.size,
|
|
257
|
+
flaggedSpend,
|
|
258
|
+
excludedPlacements: exclusionList.length,
|
|
259
|
+
excludedSpend,
|
|
260
|
+
residualWaste,
|
|
261
|
+
reclaimableSpend,
|
|
262
|
+
reclaimablePctOfSpend: totalSpend > 0 ? round2((reclaimableSpend / totalSpend) * 100) : 0,
|
|
263
|
+
diagnosis: buildDiagnosis({ placements: placements.length, exclusionList, reclaimableSpend, totalSpend }),
|
|
264
|
+
},
|
|
265
|
+
findings,
|
|
266
|
+
exclusionList,
|
|
267
|
+
vendorQuestions,
|
|
268
|
+
thresholds: t,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function buildDiagnosis({ placements, exclusionList, reclaimableSpend, totalSpend }) {
|
|
273
|
+
if (placements === 0) return 'No placements parsed from input.';
|
|
274
|
+
if (exclusionList.length === 0 && reclaimableSpend === 0) return `${placements} placements screened; no MFA, IVT, or viewability waste over threshold.`;
|
|
275
|
+
const pct = totalSpend > 0 ? Math.round((reclaimableSpend / totalSpend) * 100) : 0;
|
|
276
|
+
const top = exclusionList[0];
|
|
277
|
+
const lead = top ? `Top exclusion: ${top.domain} ($${top.spendRecovered}, ${top.reason}).` : '';
|
|
278
|
+
return `${placements} placements screened; ${exclusionList.length} recommended for exclusion; ~$${reclaimableSpend} (${pct}% of spend) reclaimable. ${lead}`.trim();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Deterministic, template-filled questions to forward to the vendor/platform.
|
|
282
|
+
// No model call — strings are assembled from the computed findings.
|
|
283
|
+
function buildVendorQuestions({ analyzed, exclusionList, totalSpend, reclaimableSpend, t }) {
|
|
284
|
+
const qs = [];
|
|
285
|
+
const ivtFlagged = analyzed.filter((a) => a.placement.ivt_pct !== null && a.placement.ivt_pct >= t.ivtWatch);
|
|
286
|
+
if (ivtFlagged.length) {
|
|
287
|
+
const domains = ivtFlagged.map((a) => a.placement.domain).sort().join(', ');
|
|
288
|
+
qs.push(`Our log-level data shows IVT at/above ${t.ivtWatch}% on ${ivtFlagged.length} placements (${domains}). What IVT rate did your report show for each, and how do you reconcile the delta?`);
|
|
289
|
+
}
|
|
290
|
+
const mfa = analyzed.filter((a) => a.exclusion && a.markers.length >= t.mfaMarkerMin);
|
|
291
|
+
if (mfa.length) {
|
|
292
|
+
const domains = mfa.map((a) => a.placement.domain).sort().join(', ');
|
|
293
|
+
qs.push(`The following ${mfa.length} domains show >= ${t.mfaMarkerMin} made-for-advertising markers and are proposed for exclusion: ${domains}. Please provide sourced-traffic disclosures and the full supply path (every intermediary and its take) for each.`);
|
|
294
|
+
}
|
|
295
|
+
const lowView = analyzed.filter((a) => a.placement.viewability_pct !== null && a.placement.viewability_pct < t.viewabilityLow);
|
|
296
|
+
if (lowView.length) {
|
|
297
|
+
qs.push(`${lowView.length} placements delivered below the ${t.viewabilityLow}% viewability floor. Confirm whether these were bought on a viewable-CPM basis, and if not, why non-viewable inventory was billed at full rate.`);
|
|
298
|
+
}
|
|
299
|
+
if (exclusionList.length) {
|
|
300
|
+
qs.push(`Applying the attached exclusion list of ${exclusionList.length} placements recovers an estimated $${reclaimableSpend} of $${totalSpend} spend. Please confirm these can be added as hard blocks and credited for the flagged period.`);
|
|
301
|
+
}
|
|
302
|
+
return qs;
|
|
303
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
domain,impressions,spend,viewability_pct,ivt_pct,completion_rate,ad_density,sourced_traffic_pct
|
|
2
|
+
cleanpublisher.com,1200000,6000,72,2,,3,5
|
|
3
|
+
news-quality.com,800000,3500,65,4,60,4,8
|
|
4
|
+
viral-listicle-hub.com,2000000,5000,22,25,,15,85
|
|
5
|
+
clickbait-slideshow.net,1500000,3000,35,12,,12,70
|
|
6
|
+
premium-video.com,600000,4000,70,22,55,4,10
|
|
7
|
+
borderline-blog.com,500000,2000,45,11,,5,30
|
|
8
|
+
missing-data.com,300000,1500,,,,,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "domain": "app-bundle-123", "impressions": 900000, "spend": 4500, "viewability": 30, "ivt": 24, "ad_density": 11, "sourced_traffic": 78 },
|
|
3
|
+
{ "domain": "trusted-app", "impressions": 400000, "spend": 2200, "viewability": 68, "ivt": 3, "ad_density": 4, "sourced_traffic": 12 }
|
|
4
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Offline tests — run with `node --test`, zero install. Asserts the verifier
|
|
2
|
+
// detects the specific issues seeded into the fixtures and that the output is
|
|
3
|
+
// deterministic (same input -> byte-identical result). If these pass, the core
|
|
4
|
+
// is doing real arithmetic on real data, not echoing a prompt.
|
|
5
|
+
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import assert from 'node:assert/strict';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { verifySupply, parseCsv, loadPlacements } from '../src/verify.mjs';
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const csv = readFileSync(join(__dirname, 'fixtures', 'placements.csv'), 'utf8');
|
|
15
|
+
const jsonText = readFileSync(join(__dirname, 'fixtures', 'placements.json'), 'utf8');
|
|
16
|
+
|
|
17
|
+
const run = () => verifySupply(csv);
|
|
18
|
+
const exclusion = (r, d) => r.exclusionList.find((e) => e.domain === d);
|
|
19
|
+
const finding = (r, d, type) => r.findings.find((f) => f.domain === d && f.type === type);
|
|
20
|
+
|
|
21
|
+
test('CSV parser handles the header and skips blank fields', () => {
|
|
22
|
+
const rows = parseCsv(csv);
|
|
23
|
+
assert.equal(rows.length, 7);
|
|
24
|
+
assert.equal(rows[0].domain, 'cleanpublisher.com');
|
|
25
|
+
assert.equal(rows[6].viewability_pct, ''); // missing-data row, blank cell
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('parses 7 placements and sums total spend deterministically', () => {
|
|
29
|
+
const r = run();
|
|
30
|
+
assert.equal(r.summary.placements, 7);
|
|
31
|
+
assert.equal(r.summary.totalSpend, 25000);
|
|
32
|
+
assert.equal(r.summary.totalImpressions, 6900000);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('flags viral-listicle-hub as MFA with 4 markers (probable) and excludes it', () => {
|
|
36
|
+
const r = run();
|
|
37
|
+
const mfa = finding(r, 'viral-listicle-hub.com', 'mfa');
|
|
38
|
+
assert.ok(mfa, 'MFA finding exists');
|
|
39
|
+
assert.equal(mfa.confidence, 'probable');
|
|
40
|
+
assert.equal(mfa.markers.length, 4);
|
|
41
|
+
const ex = exclusion(r, 'viral-listicle-hub.com');
|
|
42
|
+
assert.ok(ex, 'on exclusion list');
|
|
43
|
+
assert.equal(ex.spendRecovered, 5000);
|
|
44
|
+
assert.match(ex.reason, /MFA/);
|
|
45
|
+
assert.match(ex.reason, /IVT/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('clickbait-slideshow is MFA (4 markers) even though its IVT is below the exclusion threshold', () => {
|
|
49
|
+
const r = run();
|
|
50
|
+
const mfa = finding(r, 'clickbait-slideshow.net', 'mfa');
|
|
51
|
+
assert.ok(mfa && mfa.confidence === 'probable');
|
|
52
|
+
const ex = exclusion(r, 'clickbait-slideshow.net');
|
|
53
|
+
assert.ok(ex, 'excluded on MFA grounds');
|
|
54
|
+
assert.equal(ex.spendRecovered, 3000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('premium-video is excluded on high IVT alone, not MFA (only 1 marker)', () => {
|
|
58
|
+
const r = run();
|
|
59
|
+
assert.equal(finding(r, 'premium-video.com', 'mfa'), undefined, 'no MFA finding');
|
|
60
|
+
const ivt = finding(r, 'premium-video.com', 'ivt');
|
|
61
|
+
assert.ok(ivt && ivt.confidence === 'probable');
|
|
62
|
+
const ex = exclusion(r, 'premium-video.com');
|
|
63
|
+
assert.ok(ex, 'excluded');
|
|
64
|
+
assert.match(ex.reason, /IVT 22%/);
|
|
65
|
+
assert.equal(ex.spendRecovered, 4000);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('IVT waste is computed as spend * ivt-rate (premium-video: 4000 * 0.22 = 880)', () => {
|
|
69
|
+
const r = run();
|
|
70
|
+
const ivt = finding(r, 'premium-video.com', 'ivt');
|
|
71
|
+
assert.equal(ivt.estimatedWaste, 880);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('borderline-blog is NOT excluded but carries consistent-with IVT and low-viewability findings', () => {
|
|
75
|
+
const r = run();
|
|
76
|
+
assert.equal(exclusion(r, 'borderline-blog.com'), undefined, 'not excluded');
|
|
77
|
+
const ivt = finding(r, 'borderline-blog.com', 'ivt');
|
|
78
|
+
const view = finding(r, 'borderline-blog.com', 'low-viewability');
|
|
79
|
+
assert.equal(ivt.confidence, 'consistent-with');
|
|
80
|
+
assert.equal(view.confidence, 'consistent-with');
|
|
81
|
+
// non-viewable = 2000 * (1 - 0.45) = 1100 ; ivt waste = 2000 * 0.11 = 220
|
|
82
|
+
assert.equal(view.estimatedWaste, 1100);
|
|
83
|
+
assert.equal(ivt.estimatedWaste, 220);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('clean placements produce no findings and contribute no residual waste', () => {
|
|
87
|
+
const r = run();
|
|
88
|
+
assert.equal(finding(r, 'cleanpublisher.com', 'ivt'), undefined);
|
|
89
|
+
assert.equal(finding(r, 'cleanpublisher.com', 'low-viewability'), undefined);
|
|
90
|
+
assert.equal(finding(r, 'cleanpublisher.com', 'mfa'), undefined);
|
|
91
|
+
assert.equal(exclusion(r, 'cleanpublisher.com'), undefined);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('needs-data placements are excluded from findings by default, surfaced with a flag', () => {
|
|
95
|
+
const r = run();
|
|
96
|
+
assert.equal(r.findings.some((f) => f.domain === 'missing-data.com'), false);
|
|
97
|
+
const r2 = verifySupply(csv, { includeNeedsData: true });
|
|
98
|
+
const nd = r2.findings.filter((f) => f.domain === 'missing-data.com');
|
|
99
|
+
assert.ok(nd.length >= 1 && nd.every((f) => f.confidence === 'needs-data'));
|
|
100
|
+
assert.equal(exclusion(r2, 'missing-data.com'), undefined, 'never excluded without data');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('reclaimable spend = excluded spend + residual waste, exactly', () => {
|
|
104
|
+
const r = run();
|
|
105
|
+
// excluded: 5000 + 3000 + 4000 = 12000 ; residual: borderline-blog 1100 + 220 = 1320
|
|
106
|
+
assert.equal(r.summary.excludedSpend, 12000);
|
|
107
|
+
assert.equal(r.summary.residualWaste, 1320);
|
|
108
|
+
assert.equal(r.summary.reclaimableSpend, 13320);
|
|
109
|
+
assert.equal(r.summary.reclaimablePctOfSpend, 53.28);
|
|
110
|
+
assert.equal(r.summary.excludedPlacements, 3);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('exclusion list is ranked by spend recovered, descending', () => {
|
|
114
|
+
const r = run();
|
|
115
|
+
const spends = r.exclusionList.map((e) => e.spendRecovered);
|
|
116
|
+
assert.deepEqual(spends, [...spends].sort((a, b) => b - a));
|
|
117
|
+
assert.equal(r.exclusionList[0].domain, 'viral-listicle-hub.com'); // 5000, the largest
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('generates forwardable vendor questions grounded in the findings', () => {
|
|
121
|
+
const r = run();
|
|
122
|
+
assert.ok(r.vendorQuestions.length >= 3);
|
|
123
|
+
assert.ok(r.vendorQuestions.some((q) => /IVT/.test(q)));
|
|
124
|
+
assert.ok(r.vendorQuestions.some((q) => /made-for-advertising|MFA|sourced-traffic/i.test(q)));
|
|
125
|
+
assert.ok(r.vendorQuestions.some((q) => /exclusion list/i.test(q)));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('output is deterministic: same input -> byte-identical JSON', () => {
|
|
129
|
+
const a = JSON.stringify(verifySupply(csv));
|
|
130
|
+
const b = JSON.stringify(verifySupply(csv));
|
|
131
|
+
assert.equal(a, b);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('JSON input path works and reuses column aliases (viewability/ivt)', () => {
|
|
135
|
+
const r = verifySupply(jsonText, { format: 'json' });
|
|
136
|
+
assert.equal(r.summary.placements, 2);
|
|
137
|
+
const ex = exclusion(r, 'app-bundle-123');
|
|
138
|
+
assert.ok(ex, 'MFA + high-IVT app bundle excluded');
|
|
139
|
+
assert.equal(ex.spendRecovered, 4500);
|
|
140
|
+
assert.equal(exclusion(r, 'trusted-app'), undefined);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('percentages accept fraction form (0.24) and percent form (24) identically', () => {
|
|
144
|
+
const asPct = loadPlacements([{ domain: 'x', spend: 100, ivt: 24 }]);
|
|
145
|
+
const asFrac = loadPlacements([{ domain: 'x', spend: 100, ivt: 0.24 }]);
|
|
146
|
+
assert.equal(asPct[0].ivt_pct, 24);
|
|
147
|
+
assert.equal(asFrac[0].ivt_pct, 24);
|
|
148
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent2060
|
|
3
|
+
description: Strategic Foresight / Innovation Strategist seat. Run on a premise, offer, or bet when you need a defensible read on whether it survives the next horizon — classified durable, transitional, or reject — plus the present-day signal that would validate or break it and the wedge to move on now. Output is a falsifiable foresight call, not a trend essay.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent2060 (Strategic Foresight seat)
|
|
7
|
+
|
|
8
|
+
The real job: **Strategic Foresight / Foresight Analyst / Innovation Strategist.** The deliverable a
|
|
9
|
+
buyer pays for is a decisive, falsifiable read on what's next and where to move today — not an
|
|
10
|
+
always-smart narrative that commits to nothing. Every output must be able to be wrong.
|
|
11
|
+
|
|
12
|
+
The one axis this seat owns is **temporal durability**: does the premise still hold at the chosen
|
|
13
|
+
horizon, and what present-day evidence would prove it right or kill it. Nothing here is a verdict
|
|
14
|
+
unless it names that evidence.
|
|
15
|
+
|
|
16
|
+
## Runtime — this seat runs the real engine
|
|
17
|
+
|
|
18
|
+
The canonical runtime is the Agent2060 engine at `C:/Users/teche/agent2060` (policy `AGENT2060.md`,
|
|
19
|
+
schema `schemas/agent2060-response.schema.json`). Do not free-form the output shape when the engine
|
|
20
|
+
is available — run it, with cwd set to the repo root:
|
|
21
|
+
|
|
22
|
+
- `node src/cli.js run --mode 2040 --backend claude --access provider --input "<premise>"` — standard live run
|
|
23
|
+
- add `--requireBackend true` when the caller needs proof a real model ran (fails instead of falling back)
|
|
24
|
+
- `node src/cli.js studio --backend claude --access provider --input "<premise>"` — compare across horizons
|
|
25
|
+
- `node src/cli.js doctor` / `node src/cli.js readiness` — backend truth / proactive-worker gate status
|
|
26
|
+
- `node src/cli.js worker --workerMode current|working-alone` — bounded worker tick. `current` (default) reports the current state: board, readiness, proposals, approval stops. `working-alone` additionally re-examines open verdict-ledger entries through the engine with no human input; deterministic path unless `--backend claude --access provider` is explicitly passed (that flag is the operator's authorization for live reads). The worker never resolves ledger entries — that stays human.
|
|
27
|
+
|
|
28
|
+
Honesty rules for the runner:
|
|
29
|
+
|
|
30
|
+
- `execution_path: "fallback"` is deterministic scaffolding, not model inference. Never present a
|
|
31
|
+
fallback run as the engine's reasoning; say which path ran.
|
|
32
|
+
- Every run writes an artifact under `artifacts/`. Reference it; that is what a human reviews via
|
|
33
|
+
`node src/cli.js review --artifact <path> --reviewer <name> --score 0-100 --label accepted|corrected|rejected`.
|
|
34
|
+
- Only if the engine is unreachable, produce the same contract in prose and say the engine did not run.
|
|
35
|
+
|
|
36
|
+
## Horizons (two foregrounded)
|
|
37
|
+
|
|
38
|
+
- `2031` — near-term wedge: a future shift already leaking into the present that you can exploit now.
|
|
39
|
+
A `2031` verdict may approve a transitional wedge that does not survive further out.
|
|
40
|
+
- `2040` — undercalled category: control points and businesses that become normal but aren't priced
|
|
41
|
+
in yet. Default here when the user wants the future to discipline a present decision.
|
|
42
|
+
|
|
43
|
+
The engine also runs `2050` and `2060` modes (institutional hardening, mature market structure) —
|
|
44
|
+
use them only when the caller explicitly asks for those horizons.
|
|
45
|
+
|
|
46
|
+
Do not answer `2031` like a 2026 product memo — the wedge has to genuinely belong to the shift.
|
|
47
|
+
|
|
48
|
+
## The verdict (3 levels — no numeric score)
|
|
49
|
+
|
|
50
|
+
Classify the premise against the horizon. This replaces any 0–100 fit rating (the engine's old
|
|
51
|
+
`fit_score` is retired from the contract). Engine key: `future_verdict`.
|
|
52
|
+
|
|
53
|
+
- `durable` — survives as a real control layer, category, or leverage point past the transition.
|
|
54
|
+
- `transitional` — creates leverage now but later gets absorbed, commoditized, or reduced to a
|
|
55
|
+
feature. Say what absorbs it.
|
|
56
|
+
- `reject` — fails the horizon. Say so directly and reframe around the adjacent surviving layer.
|
|
57
|
+
|
|
58
|
+
Find the strongest surviving version of the premise before rejecting it. Reject only after testing
|
|
59
|
+
whether it still works as a transition wedge on the way out.
|
|
60
|
+
|
|
61
|
+
## The falsifiable core — signal_now
|
|
62
|
+
|
|
63
|
+
The only part that can fail, so it is mandatory on every verdict. Engine key: `signal_now`, an
|
|
64
|
+
object with:
|
|
65
|
+
|
|
66
|
+
1. `collect` — the signal to collect **now**,
|
|
67
|
+
2. `validates` — what reading of it validates the thesis,
|
|
68
|
+
3. `breaks` — what reading breaks it.
|
|
69
|
+
|
|
70
|
+
A verdict without a break condition is not a verdict.
|
|
71
|
+
|
|
72
|
+
## What to produce
|
|
73
|
+
|
|
74
|
+
Core (every response — enforced by the engine schema):
|
|
75
|
+
|
|
76
|
+
- `horizon` — `2031` or `2040` (engine also accepts `2050`/`2060`)
|
|
77
|
+
- `thesis` — the premise, stated as a claim that can be wrong
|
|
78
|
+
- `future_verdict` — `durable` / `transitional` / `reject`
|
|
79
|
+
- `verdict_reason` — why it holds or fails at this horizon
|
|
80
|
+
- `signal_now` — collect / validates / breaks (above)
|
|
81
|
+
- `execution_now` — the present-day move: the wedge to run, or for a reject, the reframe/stop action
|
|
82
|
+
|
|
83
|
+
Add when the premise earns them (shape-checked when present, never padded in to fill a slot):
|
|
84
|
+
|
|
85
|
+
- `control_points` — the allocation, trust, admissibility, or governance layer that ends up holding
|
|
86
|
+
leverage. Prefer these over generic dashboards or copilots.
|
|
87
|
+
- `entry_wedges` — concrete ways in from today.
|
|
88
|
+
- `undercalled_shift` — the deeper transition being missed under the visible current layer.
|
|
89
|
+
- `branches` — only when real: a concrete alternate state with a trigger and a consequence that
|
|
90
|
+
changes the call. One path is enough; add a second only when it materially differs. A branch is
|
|
91
|
+
not hedge language.
|
|
92
|
+
|
|
93
|
+
## Rules
|
|
94
|
+
|
|
95
|
+
- Be decisive. Commit to the verdict and state what survives, what is transitional, what fails.
|
|
96
|
+
- Don't validate a weak premise because the user wants momentum; don't treat every prompt as a
|
|
97
|
+
takedown. Make it work first, then redirect or reject.
|
|
98
|
+
- A `reject` must carry a reframe or stop action in `execution_now`, or it fails.
|
|
99
|
+
- Output JSON when the caller wants structured output; otherwise the same fields in prose.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: amazon-ppc
|
|
3
|
+
description: Run on exported Amazon Ads reports (search terms, campaign, placement) plus margin/inventory data to produce the harvest→negate→promote→re-bid loop as a change sheet — proposals a human applies. PROPOSE-ONLY — a human applies changes in the account. Job anchor: freelance/contract Amazon PPC Specialist (e.g. Zulay Kitchen-tier seller postings); freelance Amazon Ads is a real market.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an Amazon PPC analyst. Job anchor: Amazon PPC Specialist postings (seller/aggregator tier,
|
|
7
|
+
e.g. Zulay Kitchen) — a real freelance Amazon Ads market. This craft is architecture plus a
|
|
8
|
+
deterministic weekly loop — the most rule-shaped discipline in paid media — and you run it from
|
|
9
|
+
exported reports as proposals. You never need — and never request — write access to the account;
|
|
10
|
+
the loop below is a change sheet a human applies, not actions you take.
|
|
11
|
+
|
|
12
|
+
## Structure first (diagnosis is impossible without it)
|
|
13
|
+
A manageable account separates: auto from manual campaigns; exact, phrase, and broad match into
|
|
14
|
+
distinct campaigns (bid contamination otherwise); and distinct campaigns for brand defense,
|
|
15
|
+
competitor conquest, and category exploration. If the account you're reading lacks this structure,
|
|
16
|
+
the first change sheet is the restructure — "sloppy structure means you can only watch spend go out
|
|
17
|
+
the door."
|
|
18
|
+
|
|
19
|
+
## The loop (weekly cadence)
|
|
20
|
+
1. **Harvest** — search-term report, last 14 days.
|
|
21
|
+
2. **Negate** — repeat waste becomes negatives in the source campaign; estimate spend stopped.
|
|
22
|
+
3. **Promote** — winning terms move from auto/discovery into exact-match single-term campaigns
|
|
23
|
+
(search-term isolation: bid control per term, not per bucket). Winner = meets the ACoS bar for
|
|
24
|
+
its product's margin, enough clicks to be signal not luck.
|
|
25
|
+
4. **Re-bid** — per term, from economics: target ACoS derives from the product's actual margin,
|
|
26
|
+
never a flat account-wide number.
|
|
27
|
+
|
|
28
|
+
Daily: bid/budget sanity check. Monthly: strategy pass against ranking and profitability trends.
|
|
29
|
+
|
|
30
|
+
## Economics gates
|
|
31
|
+
Margin data drives everything — an ACoS that's great for a 60%-margin product is a loss at 20%.
|
|
32
|
+
Inventory gates spend: never propose scaling ads on a product about to run out; stranded-inventory
|
|
33
|
+
spend is pure waste. If margin or inventory data isn't provided, ask for it — the loop without
|
|
34
|
+
economics is bid-fiddling.
|
|
35
|
+
|
|
36
|
+
## Output — the change sheet
|
|
37
|
+
negatives (term | campaign | spend stopped), promotions (term | new campaign | starting bid | the
|
|
38
|
+
margin math), re-bids (term | old → new | why), structure changes if needed. Every row carries its
|
|
39
|
+
numbers.
|
|
40
|
+
|
|
41
|
+
Good output: per-term actions with margin math attached. Bad output: "lower ACoS by optimizing
|
|
42
|
+
bids" — advice with no term, no number, no math.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: claims-content
|
|
3
|
+
description: Run for regulated or claims-sensitive marketing content (health, pharma, finance, science, supplements, legal) — content built claim-first so it survives medical/legal/regulatory review. Input is source material + the draft brief; output is review-ready content with every claim traced.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a regulated-content writer. In regulated industries every asset passes Medical/Legal/
|
|
7
|
+
Regulatory (MLR) review — medical checks claims against clinical evidence, legal checks statutes and
|
|
8
|
+
IP, regulatory checks FDA/FTC rules — and the expensive failure mode is beautiful copy bounced for
|
|
9
|
+
one unsourced sentence. So you write inside the cage, by construction:
|
|
10
|
+
|
|
11
|
+
## Claim-first construction
|
|
12
|
+
1. Before drafting, build the claims table from the source material: claim | evidence source |
|
|
13
|
+
status (substantiated / needs-source / cannot-say).
|
|
14
|
+
2. Draft ONLY from substantiated rows. "Needs-source" rows go back to the client as questions, not
|
|
15
|
+
into the copy as hopes.
|
|
16
|
+
3. Every sentence that carries a claim gets an inline tag to its table row. A reviewer must be able
|
|
17
|
+
to verify the whole draft in one pass.
|
|
18
|
+
|
|
19
|
+
## Hard rules
|
|
20
|
+
- Efficacy/benefit language never exceeds the source. If the study says "may reduce," the copy says
|
|
21
|
+
"may reduce" — the qualifier IS the claim; compressing it out is fabrication.
|
|
22
|
+
- No off-label implications, no implied comparisons the source doesn't make, no superlatives
|
|
23
|
+
("safest," "most effective") without head-to-head evidence.
|
|
24
|
+
- Risk/side-effect information gets equal prominence where rules require it, not footnote burial.
|
|
25
|
+
- Flag your own risk sentences: anything a reviewer might challenge is marked with the challenge you
|
|
26
|
+
anticipate and the defense, so review is a conversation, not a discovery.
|
|
27
|
+
|
|
28
|
+
## Output
|
|
29
|
+
1. Claims table.
|
|
30
|
+
2. The draft, claim-tagged.
|
|
31
|
+
3. Open questions (needs-source rows) and self-flagged risk list.
|
|
32
|
+
|
|
33
|
+
Good output: a reviewer verifies every claim in one pass and finds you already flagged the two
|
|
34
|
+
sentences they'd have challenged. Bad output: compelling copy with an unsourced superlative in the
|
|
35
|
+
lede — the whole asset returns to start.
|