weftens 0.0.1 → 0.1.1

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.
Files changed (101) hide show
  1. package/README.md +178 -12
  2. package/agent-cores/amazon-ppc/package.json +17 -0
  3. package/agent-cores/amazon-ppc/src/cli.mjs +66 -0
  4. package/agent-cores/amazon-ppc/src/optimize.mjs +272 -0
  5. package/agent-cores/amazon-ppc/src/parse.mjs +153 -0
  6. package/agent-cores/amazon-ppc/test/fixtures/search-term-report.csv +8 -0
  7. package/agent-cores/amazon-ppc/test/fixtures/search-term-report.json +5 -0
  8. package/agent-cores/amazon-ppc/test/optimize.test.mjs +161 -0
  9. package/agent-cores/content-ops/package.json +17 -0
  10. package/agent-cores/content-ops/src/cli.mjs +51 -0
  11. package/agent-cores/content-ops/src/inventory.mjs +343 -0
  12. package/agent-cores/content-ops/src/parse.mjs +121 -0
  13. package/agent-cores/content-ops/test/fixtures/cms-export.csv +13 -0
  14. package/agent-cores/content-ops/test/fixtures/cms-export.json +31 -0
  15. package/agent-cores/content-ops/test/inventory.test.mjs +177 -0
  16. package/agent-cores/meta-ads/package.json +16 -0
  17. package/agent-cores/meta-ads/src/analyze.mjs +166 -0
  18. package/agent-cores/meta-ads/src/cli.mjs +39 -0
  19. package/agent-cores/meta-ads/src/parse.mjs +123 -0
  20. package/agent-cores/meta-ads/test/analyze.test.js +72 -0
  21. package/agent-cores/meta-ads/test/fixtures/meta-ads.csv +6 -0
  22. package/agent-cores/paid-search/package.json +17 -0
  23. package/agent-cores/paid-search/src/analyze.mjs +303 -0
  24. package/agent-cores/paid-search/src/cli.mjs +57 -0
  25. package/agent-cores/paid-search/src/parse.mjs +158 -0
  26. package/agent-cores/paid-search/test/analyze.test.mjs +120 -0
  27. package/agent-cores/paid-search/test/fixtures/campaigns.csv +7 -0
  28. package/agent-cores/paid-search/test/fixtures/sample.json +9 -0
  29. package/agent-cores/paid-search/test/fixtures/search-terms.csv +11 -0
  30. package/agent-cores/paid-search/test/parse.test.mjs +56 -0
  31. package/agent-cores/seo-content/package.json +17 -0
  32. package/agent-cores/seo-content/src/cli.mjs +41 -0
  33. package/agent-cores/seo-content/src/parse.mjs +141 -0
  34. package/agent-cores/seo-content/src/strategy.mjs +266 -0
  35. package/agent-cores/seo-content/test/fixtures/gsc-sample.csv +11 -0
  36. package/agent-cores/seo-content/test/parse.test.mjs +59 -0
  37. package/agent-cores/seo-content/test/strategy.test.mjs +108 -0
  38. package/agent-cores/seo-technical/package.json +17 -0
  39. package/agent-cores/seo-technical/src/audit.mjs +233 -0
  40. package/agent-cores/seo-technical/src/cli.mjs +48 -0
  41. package/agent-cores/seo-technical/src/fetch.mjs +51 -0
  42. package/agent-cores/seo-technical/test/audit.test.mjs +73 -0
  43. package/agent-cores/seo-technical/test/fixtures/sample.html +22 -0
  44. package/agent-cores/supply-verify/package.json +17 -0
  45. package/agent-cores/supply-verify/src/cli.mjs +44 -0
  46. package/agent-cores/supply-verify/src/verify.mjs +303 -0
  47. package/agent-cores/supply-verify/test/fixtures/placements.csv +8 -0
  48. package/agent-cores/supply-verify/test/fixtures/placements.json +4 -0
  49. package/agent-cores/supply-verify/test/verify.test.mjs +148 -0
  50. package/agents/agent2060.md +99 -0
  51. package/agents/amazon-ppc.md +42 -0
  52. package/agents/claims-content.md +35 -0
  53. package/agents/content-ops.md +46 -0
  54. package/agents/content-strategy.md +13 -0
  55. package/agents/disclosure-qc.md +13 -0
  56. package/agents/feed-optimizer.md +13 -0
  57. package/agents/growth-analyst.md +24 -0
  58. package/agents/hilbert.md +55 -0
  59. package/agents/ideator.md +13 -0
  60. package/agents/lippmann.md +68 -0
  61. package/agents/paid-search.md +35 -0
  62. package/agents/pio.md +34 -0
  63. package/agents/researcher.md +13 -0
  64. package/agents/richy.md +72 -0
  65. package/agents/script-editor.md +13 -0
  66. package/agents/seo-content.md +57 -0
  67. package/agents/seo-technical.md +53 -0
  68. package/agents/showrunner.md +13 -0
  69. package/agents/social-voice.md +38 -0
  70. package/agents/supply-verify.md +38 -0
  71. package/agents/video-editor.md +13 -0
  72. package/agents/writer.md +45 -0
  73. package/bin/weftens.js +234 -0
  74. package/content-org/.preflight/telemetry.jsonl +6 -0
  75. package/content-org/briefs/BRIEFS.md +47 -0
  76. package/content-org/config.mjs +78 -0
  77. package/content-org/cost.mjs +36 -0
  78. package/content-org/lead.js +64 -0
  79. package/content-org/metrics.mjs +166 -0
  80. package/content-org/orchestrator.mjs +246 -0
  81. package/content-org/package.json +14 -0
  82. package/content-org/producer.mjs +59 -0
  83. package/content-org/publish.mjs +34 -0
  84. package/content-org/review.mjs +32 -0
  85. package/content-org/studio/README.md +27 -0
  86. package/content-org/studio/assemble.sh +48 -0
  87. package/content-org/test.mjs +110 -0
  88. package/index.js +32 -0
  89. package/package.json +49 -10
  90. package/providers.json.example +9 -0
  91. package/src/agent-router.js +124 -0
  92. package/src/agent.js +123 -0
  93. package/src/format.js +66 -0
  94. package/src/invoke.js +129 -0
  95. package/src/pio-client.js +142 -0
  96. package/src/provider.js +81 -0
  97. package/src/registry.js +160 -0
  98. package/src/richy-client.js +216 -0
  99. package/src/router.js +34 -0
  100. package/src/synthesize.js +111 -0
  101. 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.