thumbgate 0.9.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.
Files changed (369) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/rlhf-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1483 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +286 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1195 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-314.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +293 -0
  117. package/scripts/auto-wire-hooks.js +316 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +93 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +231 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +206 -0
  152. package/scripts/ensure-repo-bootstrap.js +129 -0
  153. package/scripts/ephemeral-agent-store.js +219 -0
  154. package/scripts/eval-harness.js +56 -0
  155. package/scripts/evolution-state.js +241 -0
  156. package/scripts/experiment-tracker.js +267 -0
  157. package/scripts/export-databricks-bundle.js +242 -0
  158. package/scripts/export-dpo-pairs.js +344 -0
  159. package/scripts/export-kto-pairs.js +309 -0
  160. package/scripts/export-training.js +450 -0
  161. package/scripts/failure-diagnostics.js +558 -0
  162. package/scripts/feedback-attribution.js +313 -0
  163. package/scripts/feedback-fallback.js +110 -0
  164. package/scripts/feedback-history-distiller.js +391 -0
  165. package/scripts/feedback-inbox-read.js +162 -0
  166. package/scripts/feedback-loop.js +1887 -0
  167. package/scripts/feedback-paths.js +145 -0
  168. package/scripts/feedback-quality.js +139 -0
  169. package/scripts/feedback-root-consolidator.js +238 -0
  170. package/scripts/feedback-schema.js +426 -0
  171. package/scripts/feedback-session.js +286 -0
  172. package/scripts/feedback-to-memory.js +185 -0
  173. package/scripts/feedback-to-rules.js +164 -0
  174. package/scripts/filesystem-search.js +405 -0
  175. package/scripts/funnel-analytics.js +35 -0
  176. package/scripts/gate-satisfy.js +42 -0
  177. package/scripts/gate-stats.js +116 -0
  178. package/scripts/gate-templates.js +70 -0
  179. package/scripts/gates-engine.js +816 -0
  180. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  181. package/scripts/generate-pretool-hook.sh +40 -0
  182. package/scripts/github-about.js +350 -0
  183. package/scripts/github-outreach.js +65 -0
  184. package/scripts/gtm-revenue-loop.js +520 -0
  185. package/scripts/hallucination-detector.js +226 -0
  186. package/scripts/hf-papers.js +317 -0
  187. package/scripts/history-distiller.js +200 -0
  188. package/scripts/hook-auto-capture.sh +100 -0
  189. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  190. package/scripts/hook-stop-self-score.sh +51 -0
  191. package/scripts/hook-stop-verify-deploy.sh +31 -0
  192. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  193. package/scripts/hook-verify-before-done.sh +20 -0
  194. package/scripts/hosted-config.js +156 -0
  195. package/scripts/hybrid-feedback-context.js +675 -0
  196. package/scripts/install-mcp.js +159 -0
  197. package/scripts/intent-router.js +392 -0
  198. package/scripts/internal-agent-bootstrap.js +490 -0
  199. package/scripts/jsonl-watcher.js +155 -0
  200. package/scripts/lesson-db.js +613 -0
  201. package/scripts/lesson-inference.js +310 -0
  202. package/scripts/lesson-retrieval.js +95 -0
  203. package/scripts/lesson-rotation.js +137 -0
  204. package/scripts/lesson-search.js +644 -0
  205. package/scripts/lesson-synthesis.js +196 -0
  206. package/scripts/license.js +50 -0
  207. package/scripts/local-model-profile.js +384 -0
  208. package/scripts/markdown-escape.js +12 -0
  209. package/scripts/marketing-experiment.js +671 -0
  210. package/scripts/mcp-config.js +149 -0
  211. package/scripts/mcp-policy.js +99 -0
  212. package/scripts/memalign-recall.js +111 -0
  213. package/scripts/memory-firewall.js +222 -0
  214. package/scripts/memory-migration.js +296 -0
  215. package/scripts/meta-policy.js +190 -0
  216. package/scripts/metered-billing.js +16 -0
  217. package/scripts/model-tier-router.js +301 -0
  218. package/scripts/money-watcher.js +71 -0
  219. package/scripts/multi-hop-recall.js +240 -0
  220. package/scripts/natural-language-harness.js +330 -0
  221. package/scripts/obsidian-export.js +713 -0
  222. package/scripts/operational-dashboard.js +103 -0
  223. package/scripts/operational-summary.js +93 -0
  224. package/scripts/optimize-context.js +17 -0
  225. package/scripts/org-dashboard.js +201 -0
  226. package/scripts/partner-orchestration.js +146 -0
  227. package/scripts/per-step-scoring.js +165 -0
  228. package/scripts/perplexity-marketing.js +466 -0
  229. package/scripts/pii-scanner.js +153 -0
  230. package/scripts/plan-gate.js +154 -0
  231. package/scripts/post-everywhere.js +308 -0
  232. package/scripts/post-to-x-retry.sh +22 -0
  233. package/scripts/post-to-x.js +369 -0
  234. package/scripts/pr-manager.js +236 -0
  235. package/scripts/predictive-insights.js +356 -0
  236. package/scripts/principle-extractor.js +162 -0
  237. package/scripts/pro-features.js +40 -0
  238. package/scripts/pro-local-dashboard.js +174 -0
  239. package/scripts/problem-detail.js +53 -0
  240. package/scripts/product-feedback.js +134 -0
  241. package/scripts/profile-router.js +245 -0
  242. package/scripts/prompt-dlp.js +221 -0
  243. package/scripts/prompt-guard.js +83 -0
  244. package/scripts/prove-adapters.js +863 -0
  245. package/scripts/prove-attribution.js +365 -0
  246. package/scripts/prove-automation.js +653 -0
  247. package/scripts/prove-autoresearch.js +304 -0
  248. package/scripts/prove-claim-verification.js +277 -0
  249. package/scripts/prove-cloudflare-sandbox.js +163 -0
  250. package/scripts/prove-data-pipeline.js +410 -0
  251. package/scripts/prove-data-quality.js +227 -0
  252. package/scripts/prove-evolution.js +352 -0
  253. package/scripts/prove-harnesses.js +287 -0
  254. package/scripts/prove-intelligence.js +259 -0
  255. package/scripts/prove-lancedb.js +371 -0
  256. package/scripts/prove-local-intelligence.js +342 -0
  257. package/scripts/prove-loop-closure.js +263 -0
  258. package/scripts/prove-predictive-insights.js +357 -0
  259. package/scripts/prove-runtime.js +350 -0
  260. package/scripts/prove-seo-gsd.js +234 -0
  261. package/scripts/prove-settings.js +279 -0
  262. package/scripts/prove-subway-upgrades.js +277 -0
  263. package/scripts/prove-tessl.js +229 -0
  264. package/scripts/prove-training-export.js +327 -0
  265. package/scripts/prove-workflow-contract.js +116 -0
  266. package/scripts/prove-xmemory.js +332 -0
  267. package/scripts/publish-decision.js +133 -0
  268. package/scripts/pulse.js +80 -0
  269. package/scripts/rate-limiter.js +125 -0
  270. package/scripts/reddit-dm-outreach.js +182 -0
  271. package/scripts/reddit-monitor-cron.sh +26 -0
  272. package/scripts/reflector-agent.js +221 -0
  273. package/scripts/reminder-engine.js +132 -0
  274. package/scripts/revenue-status.js +472 -0
  275. package/scripts/risk-scorer.js +459 -0
  276. package/scripts/rlaif-self-audit.js +129 -0
  277. package/scripts/rlhf_session_start.sh +32 -0
  278. package/scripts/rubric-engine.js +230 -0
  279. package/scripts/schedule-manager.js +251 -0
  280. package/scripts/secret-scanner.js +414 -0
  281. package/scripts/self-heal.js +147 -0
  282. package/scripts/self-healing-check.js +188 -0
  283. package/scripts/semantic-layer.js +98 -0
  284. package/scripts/seo-gsd.js +1153 -0
  285. package/scripts/settings-hierarchy.js +214 -0
  286. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  287. package/scripts/skill-exporter.js +262 -0
  288. package/scripts/skill-generator.js +446 -0
  289. package/scripts/skill-materializer.js +134 -0
  290. package/scripts/skill-packs.js +136 -0
  291. package/scripts/skill-proposer.js +99 -0
  292. package/scripts/skill-quality-tracker.js +282 -0
  293. package/scripts/slo-alert-engine.js +14 -0
  294. package/scripts/slow-loop.js +72 -0
  295. package/scripts/social-analytics/db/schema.sql +32 -0
  296. package/scripts/social-analytics/db/social-analytics.db +0 -0
  297. package/scripts/social-analytics/digest.js +256 -0
  298. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  299. package/scripts/social-analytics/instagram-thumbgate-post.js +107 -0
  300. package/scripts/social-analytics/load-env.js +46 -0
  301. package/scripts/social-analytics/mcp-server.js +289 -0
  302. package/scripts/social-analytics/normalizer.js +580 -0
  303. package/scripts/social-analytics/notify.js +162 -0
  304. package/scripts/social-analytics/poll-all.js +92 -0
  305. package/scripts/social-analytics/pollers/github.js +195 -0
  306. package/scripts/social-analytics/pollers/instagram.js +253 -0
  307. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  308. package/scripts/social-analytics/pollers/plausible.js +247 -0
  309. package/scripts/social-analytics/pollers/reddit.js +306 -0
  310. package/scripts/social-analytics/pollers/threads.js +233 -0
  311. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  312. package/scripts/social-analytics/pollers/x.js +227 -0
  313. package/scripts/social-analytics/pollers/youtube.js +304 -0
  314. package/scripts/social-analytics/pollers/zernio.js +183 -0
  315. package/scripts/social-analytics/publish-instagram-thumbgate.js +98 -0
  316. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  317. package/scripts/social-analytics/publishers/devto.js +122 -0
  318. package/scripts/social-analytics/publishers/instagram.js +317 -0
  319. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  320. package/scripts/social-analytics/publishers/reddit.js +390 -0
  321. package/scripts/social-analytics/publishers/threads.js +275 -0
  322. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  323. package/scripts/social-analytics/publishers/x.js +259 -0
  324. package/scripts/social-analytics/publishers/youtube.js +223 -0
  325. package/scripts/social-analytics/publishers/zernio.js +378 -0
  326. package/scripts/social-analytics/run-digest.js +34 -0
  327. package/scripts/social-analytics/store.js +257 -0
  328. package/scripts/social-analytics/utm.js +143 -0
  329. package/scripts/social-pipeline.js +2628 -0
  330. package/scripts/social-quality-gate.js +18 -0
  331. package/scripts/social-reply-monitor.js +445 -0
  332. package/scripts/status-dashboard.js +155 -0
  333. package/scripts/statusline-lesson.js +16 -0
  334. package/scripts/statusline-tower.js +8 -0
  335. package/scripts/statusline.sh +116 -0
  336. package/scripts/stripe-live-status.js +115 -0
  337. package/scripts/subagent-profiles.js +79 -0
  338. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  339. package/scripts/sync-github-about.js +52 -0
  340. package/scripts/sync-version.js +447 -0
  341. package/scripts/synthetic-dpo.js +234 -0
  342. package/scripts/telemetry-analytics.js +821 -0
  343. package/scripts/tessl-export.js +371 -0
  344. package/scripts/test-coverage.js +120 -0
  345. package/scripts/thompson-sampling.js +417 -0
  346. package/scripts/thumbgate-search.js +189 -0
  347. package/scripts/tool-kpi-tracker.js +12 -0
  348. package/scripts/tool-registry.js +811 -0
  349. package/scripts/train_from_feedback.py +933 -0
  350. package/scripts/user-profile.js +78 -0
  351. package/scripts/validate-feedback.js +581 -0
  352. package/scripts/validate-workflow-contract.js +287 -0
  353. package/scripts/vector-store.js +197 -0
  354. package/scripts/verification-loop.js +291 -0
  355. package/scripts/verify-obsidian-setup.sh +269 -0
  356. package/scripts/verify-run.js +269 -0
  357. package/scripts/webhook-delivery.js +62 -0
  358. package/scripts/weekly-auto-post.js +124 -0
  359. package/scripts/workflow-runs.js +154 -0
  360. package/scripts/workflow-sprint-intake.js +475 -0
  361. package/scripts/workspace-evolver.js +374 -0
  362. package/scripts/x-autonomous-marketing.js +139 -0
  363. package/scripts/xmemory-lite.js +405 -0
  364. package/skills/agent-memory/SKILL.md +97 -0
  365. package/skills/rlhf-feedback/SKILL.md +49 -0
  366. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  367. package/skills/solve-architecture-autonomy/tool.js +33 -0
  368. package/skills/thumbgate/SKILL.md +114 -0
  369. package/src/api/server.js +4206 -0
@@ -0,0 +1,671 @@
1
+ 'use strict';
2
+ /**
3
+ * Marketing Experiment Engine (AUTORESEARCH-02)
4
+ *
5
+ * Extends the experiment-tracker with marketing-specific variant generation,
6
+ * scoring, and selection. Implements the autoresearch loop for marketing:
7
+ * generate variants → score against metrics → keep top performers →
8
+ * feed winners back → generate next batch
9
+ *
10
+ * Channels: cold_email, ad_creative, landing_page, youtube_assets, sales_script
11
+ *
12
+ * Uses Thompson Sampling to balance exploration (new angles) vs exploitation
13
+ * (proven winners). The core engine is local-first; when a research query is
14
+ * provided it can ingest Hugging Face paper context into ContextFS first.
15
+ *
16
+ * Exports: createMarketingExperiment, recordMarketingResult, generateVariants,
17
+ * selectWinners, getChannelProgress, getWinningPatterns,
18
+ * MARKETING_CHANNELS, MARKETING_METRICS
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
24
+ const { loadModel, saveModel, updateModel, samplePosteriors, getReliability } = require('./thompson-sampling');
25
+ const { buildResearchBrief } = require('./hf-papers');
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Constants
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const MARKETING_CHANNELS = [
32
+ 'cold_email',
33
+ 'ad_creative',
34
+ 'landing_page',
35
+ 'youtube_assets',
36
+ 'sales_script',
37
+ ];
38
+
39
+ const MARKETING_METRICS = {
40
+ cold_email: ['open_rate', 'reply_rate', 'meeting_booked_rate'],
41
+ ad_creative: ['ctr', 'cpc', 'conversion_rate', 'roas'],
42
+ landing_page: ['bounce_rate', 'conversion_rate', 'time_on_page'],
43
+ youtube_assets: ['ctr', 'avg_view_duration', 'subscriber_conversion'],
44
+ sales_script: ['meeting_conversion', 'demo_to_close', 'objection_handle_rate'],
45
+ };
46
+
47
+ /** Top N% of variants to keep each cycle */
48
+ const DEFAULT_KEEP_RATE = 0.20;
49
+
50
+ /** Minimum variants per batch */
51
+ const MIN_BATCH_SIZE = 5;
52
+
53
+ /** Maximum variants per batch */
54
+ const MAX_BATCH_SIZE = 50;
55
+
56
+ /** Signal window defaults in hours per channel */
57
+ const SIGNAL_WINDOWS = {
58
+ cold_email: 72,
59
+ ad_creative: 48,
60
+ landing_page: 168,
61
+ youtube_assets: 168,
62
+ sales_script: 48,
63
+ };
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Paths
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function getMarketingPaths() {
70
+ const { FEEDBACK_DIR } = getFeedbackPaths();
71
+ const marketingDir = path.join(FEEDBACK_DIR, 'marketing');
72
+ return {
73
+ marketingDir,
74
+ experimentsPath: path.join(marketingDir, 'experiments.jsonl'),
75
+ variantsPath: path.join(marketingDir, 'variants.jsonl'),
76
+ winnersPath: path.join(marketingDir, 'winners.jsonl'),
77
+ progressPath: path.join(marketingDir, 'progress.json'),
78
+ modelPath: path.join(marketingDir, 'marketing_model.json'),
79
+ knowledgePath: path.join(marketingDir, 'knowledge-base.jsonl'),
80
+ };
81
+ }
82
+
83
+ function ensureDir(dirPath) {
84
+ if (!fs.existsSync(dirPath)) {
85
+ fs.mkdirSync(dirPath, { recursive: true });
86
+ }
87
+ }
88
+
89
+ function appendJSONL(filePath, record) {
90
+ ensureDir(path.dirname(filePath));
91
+ fs.appendFileSync(filePath, `${JSON.stringify(record)}\n`);
92
+ }
93
+
94
+ function readJSONLFile(filePath) {
95
+ if (!fs.existsSync(filePath)) return [];
96
+ const lines = fs.readFileSync(filePath, 'utf-8').trim().split('\n').filter(Boolean);
97
+ const records = [];
98
+ for (const line of lines) {
99
+ try { records.push(JSON.parse(line)); } catch { /* skip bad lines */ }
100
+ }
101
+ return records;
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Variant Generation
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Generate a batch of marketing variants for a given channel.
110
+ * Uses winning patterns to condition generation when available.
111
+ *
112
+ * @param {object} params
113
+ * @param {string} params.channel - Marketing channel (cold_email, ad_creative, etc.)
114
+ * @param {number} [params.batchSize=20] - Number of variants to generate
115
+ * @param {string} [params.targetAudience] - Target audience description
116
+ * @param {string} [params.product] - Product/service description
117
+ * @param {string[]} [params.constraints] - Brand/style constraints
118
+ * @param {object[]} [params.seedWinners] - Previous winners to iterate from
119
+ * @param {string} [params.researchQuery] - Optional external research query
120
+ * @param {number} [params.paperLimit] - Max papers to ingest for research context
121
+ * @returns {Promise<object>} batch record with variant templates
122
+ */
123
+ async function generateVariants(params) {
124
+ if (!params || !params.channel) {
125
+ throw new Error('generateVariants requires channel');
126
+ }
127
+ if (!MARKETING_CHANNELS.includes(params.channel)) {
128
+ throw new Error(`Invalid channel "${params.channel}". Must be one of: ${MARKETING_CHANNELS.join(', ')}`);
129
+ }
130
+
131
+ const batchSize = Math.min(
132
+ Math.max(Number(params.batchSize) || 20, MIN_BATCH_SIZE),
133
+ MAX_BATCH_SIZE,
134
+ );
135
+
136
+ const paths = getMarketingPaths();
137
+ const existingWinners = params.seedWinners || getWinningPatterns(params.channel, 5);
138
+ const research = params.researchQuery
139
+ ? await buildResearchBrief({
140
+ query: params.researchQuery,
141
+ limit: params.paperLimit,
142
+ fetchImpl: params.fetchImpl,
143
+ searchPapersImpl: params.searchPapersImpl,
144
+ template: 'gtm-research',
145
+ })
146
+ : null;
147
+
148
+ // Load Thompson model for explore/exploit balance
149
+ const model = loadModel(paths.modelPath);
150
+ const posteriors = samplePosteriors(model);
151
+ const channelScore = posteriors[params.channel] || 0.5;
152
+
153
+ // Higher score = more exploitation (iterate from winners)
154
+ // Lower score = more exploration (try new angles)
155
+ const exploitRatio = channelScore;
156
+ const exploitCount = Math.round(batchSize * exploitRatio);
157
+ const exploreCount = batchSize - exploitCount;
158
+
159
+ const variants = [];
160
+ const batchId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
161
+
162
+ // Exploitation variants: iterate from winners
163
+ for (let i = 0; i < exploitCount; i++) {
164
+ const seedIdx = existingWinners.length > 0 ? i % existingWinners.length : -1;
165
+ const seed = seedIdx >= 0 ? existingWinners[seedIdx] : null;
166
+ variants.push({
167
+ id: `var_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
168
+ batchId,
169
+ channel: params.channel,
170
+ strategy: 'exploit',
171
+ seedVariantId: seed ? seed.id : null,
172
+ seedPattern: seed ? seed.winningPattern : null,
173
+ targetAudience: params.targetAudience || null,
174
+ product: params.product || null,
175
+ constraints: params.constraints || [],
176
+ researchQuery: research ? research.query : null,
177
+ researchPackId: research ? research.packId : null,
178
+ researchPaperIds: research ? research.citations.map((citation) => citation.paperId).filter(Boolean) : [],
179
+ researchBrief: research ? research.brief : null,
180
+ metrics: Object.fromEntries(
181
+ (MARKETING_METRICS[params.channel] || []).map(m => [m, null]),
182
+ ),
183
+ status: 'pending',
184
+ createdAt: new Date().toISOString(),
185
+ });
186
+ }
187
+
188
+ // Exploration variants: novel angles
189
+ for (let i = 0; i < exploreCount; i++) {
190
+ variants.push({
191
+ id: `var_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
192
+ batchId,
193
+ channel: params.channel,
194
+ strategy: 'explore',
195
+ seedVariantId: null,
196
+ seedPattern: null,
197
+ targetAudience: params.targetAudience || null,
198
+ product: params.product || null,
199
+ constraints: params.constraints || [],
200
+ researchQuery: research ? research.query : null,
201
+ researchPackId: research ? research.packId : null,
202
+ researchPaperIds: research ? research.citations.map((citation) => citation.paperId).filter(Boolean) : [],
203
+ researchBrief: research ? research.brief : null,
204
+ metrics: Object.fromEntries(
205
+ (MARKETING_METRICS[params.channel] || []).map(m => [m, null]),
206
+ ),
207
+ status: 'pending',
208
+ createdAt: new Date().toISOString(),
209
+ });
210
+ }
211
+
212
+ // Persist variants
213
+ for (const v of variants) {
214
+ appendJSONL(paths.variantsPath, v);
215
+ }
216
+
217
+ const batch = {
218
+ batchId,
219
+ channel: params.channel,
220
+ totalVariants: variants.length,
221
+ exploitCount,
222
+ exploreCount,
223
+ exploitRatio: Number(exploitRatio.toFixed(3)),
224
+ signalWindowHours: SIGNAL_WINDOWS[params.channel] || 72,
225
+ targetAudience: params.targetAudience || null,
226
+ product: params.product || null,
227
+ researchQuery: research ? research.query : null,
228
+ researchPackId: research ? research.packId : null,
229
+ researchPaperIds: research ? research.citations.map((citation) => citation.paperId).filter(Boolean) : [],
230
+ researchBrief: research ? research.brief : null,
231
+ createdAt: new Date().toISOString(),
232
+ variants: variants.map(v => ({ id: v.id, strategy: v.strategy })),
233
+ };
234
+
235
+ return batch;
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Experiment Lifecycle
240
+ // ---------------------------------------------------------------------------
241
+
242
+ /**
243
+ * Create a marketing experiment (wraps a batch of variants).
244
+ *
245
+ * @param {object} params
246
+ * @param {string} params.channel - Marketing channel
247
+ * @param {string} params.hypothesis - What the experiment tests
248
+ * @param {number} [params.batchSize=20] - Variants per batch
249
+ * @param {string} [params.targetAudience] - Target audience
250
+ * @param {string} [params.product] - Product description
251
+ * @param {string[]} [params.constraints] - Brand constraints
252
+ * @param {string} [params.researchQuery] - Optional external research query
253
+ * @param {number} [params.paperLimit] - Max papers to ingest for research context
254
+ * @returns {Promise<object>} experiment record with batch
255
+ */
256
+ async function createMarketingExperiment(params) {
257
+ if (!params || !params.channel || !params.hypothesis) {
258
+ throw new Error('createMarketingExperiment requires channel and hypothesis');
259
+ }
260
+
261
+ const batch = await generateVariants(params);
262
+ const paths = getMarketingPaths();
263
+
264
+ const experiment = {
265
+ id: `mktexp_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
266
+ channel: params.channel,
267
+ hypothesis: params.hypothesis,
268
+ batchId: batch.batchId,
269
+ batchSize: batch.totalVariants,
270
+ exploitRatio: batch.exploitRatio,
271
+ targetAudience: params.targetAudience || null,
272
+ product: params.product || null,
273
+ constraints: params.constraints || [],
274
+ researchQuery: batch.researchQuery || null,
275
+ researchPackId: batch.researchPackId || null,
276
+ researchPaperIds: batch.researchPaperIds || [],
277
+ researchBrief: batch.researchBrief || null,
278
+ status: 'running',
279
+ createdAt: new Date().toISOString(),
280
+ completedAt: null,
281
+ signalWindowHours: batch.signalWindowHours,
282
+ results: null,
283
+ };
284
+
285
+ appendJSONL(paths.experimentsPath, experiment);
286
+ return { experiment, batch };
287
+ }
288
+
289
+ /**
290
+ * Record metrics for a specific variant.
291
+ *
292
+ * @param {object} params
293
+ * @param {string} params.variantId - Variant ID
294
+ * @param {object} params.metrics - Metric values (e.g., { open_rate: 0.32, reply_rate: 0.08 })
295
+ * @returns {object} updated variant
296
+ */
297
+ function recordVariantMetrics(params) {
298
+ if (!params || !params.variantId || !params.metrics) {
299
+ throw new Error('recordVariantMetrics requires variantId and metrics');
300
+ }
301
+
302
+ const paths = getMarketingPaths();
303
+ const variants = readJSONLFile(paths.variantsPath);
304
+ const variant = variants.find(v => v.id === params.variantId);
305
+
306
+ if (!variant) {
307
+ throw new Error(`Variant ${params.variantId} not found`);
308
+ }
309
+
310
+ const updated = {
311
+ ...variant,
312
+ metrics: { ...variant.metrics, ...params.metrics },
313
+ status: 'measured',
314
+ measuredAt: new Date().toISOString(),
315
+ };
316
+
317
+ appendJSONL(paths.variantsPath, updated);
318
+ return updated;
319
+ }
320
+
321
+ /**
322
+ * Select winners from a batch and update Thompson model.
323
+ *
324
+ * @param {object} params
325
+ * @param {string} params.batchId - Batch ID to evaluate
326
+ * @param {string} [params.primaryMetric] - Metric to rank by (default: first metric for channel)
327
+ * @param {number} [params.keepRate=0.20] - Top N% to keep
328
+ * @returns {object} selection results with winners and losers
329
+ */
330
+ function selectWinners(params) {
331
+ if (!params || !params.batchId) {
332
+ throw new Error('selectWinners requires batchId');
333
+ }
334
+
335
+ const paths = getMarketingPaths();
336
+ const allVariants = readJSONLFile(paths.variantsPath);
337
+
338
+ // Get latest state of each variant in this batch (last write wins)
339
+ const batchMap = new Map();
340
+ for (const v of allVariants) {
341
+ if (v.batchId === params.batchId) {
342
+ batchMap.set(v.id, v);
343
+ }
344
+ }
345
+ const batchVariants = Array.from(batchMap.values());
346
+
347
+ if (batchVariants.length === 0) {
348
+ throw new Error(`No variants found for batch ${params.batchId}`);
349
+ }
350
+
351
+ const channel = batchVariants[0].channel;
352
+ const channelMetrics = MARKETING_METRICS[channel] || [];
353
+ const primaryMetric = params.primaryMetric || channelMetrics[0];
354
+
355
+ if (!primaryMetric) {
356
+ throw new Error(`No metric defined for channel ${channel}`);
357
+ }
358
+
359
+ // Score variants by primary metric
360
+ const scored = batchVariants
361
+ .filter(v => v.metrics && v.metrics[primaryMetric] != null)
362
+ .map(v => ({
363
+ ...v,
364
+ primaryScore: Number(v.metrics[primaryMetric]) || 0,
365
+ }))
366
+ .sort((a, b) => b.primaryScore - a.primaryScore);
367
+
368
+ const keepRate = Number(params.keepRate) || DEFAULT_KEEP_RATE;
369
+ const keepCount = Math.max(1, Math.round(scored.length * keepRate));
370
+ const winners = scored.slice(0, keepCount);
371
+ const losers = scored.slice(keepCount);
372
+ const unmeasured = batchVariants.filter(
373
+ v => !v.metrics || v.metrics[primaryMetric] == null,
374
+ );
375
+
376
+ // Persist winners
377
+ for (const w of winners) {
378
+ const winnerRecord = {
379
+ ...w,
380
+ status: 'winner',
381
+ winningPattern: extractPattern(w),
382
+ selectedAt: new Date().toISOString(),
383
+ };
384
+ appendJSONL(paths.winnersPath, winnerRecord);
385
+ }
386
+
387
+ // Update Thompson model: winners = positive signal, losers = negative
388
+ const model = loadModel(paths.modelPath);
389
+ for (const w of winners) {
390
+ updateModel(model, {
391
+ signal: 'positive',
392
+ timestamp: new Date().toISOString(),
393
+ categories: [channel],
394
+ });
395
+ }
396
+ for (const l of losers) {
397
+ updateModel(model, {
398
+ signal: 'negative',
399
+ timestamp: new Date().toISOString(),
400
+ categories: [channel],
401
+ });
402
+ }
403
+
404
+ // Persist Thompson model updates
405
+ saveModel(model, paths.modelPath);
406
+
407
+ // Log to knowledge base
408
+ const knowledgeEntry = {
409
+ batchId: params.batchId,
410
+ channel,
411
+ primaryMetric,
412
+ totalScored: scored.length,
413
+ winnersCount: winners.length,
414
+ losersCount: losers.length,
415
+ unmeasuredCount: unmeasured.length,
416
+ keepRate,
417
+ topScore: winners.length > 0 ? winners[0].primaryScore : null,
418
+ avgWinnerScore: winners.length > 0
419
+ ? Number((winners.reduce((s, w) => s + w.primaryScore, 0) / winners.length).toFixed(4))
420
+ : null,
421
+ avgLoserScore: losers.length > 0
422
+ ? Number((losers.reduce((s, l) => s + l.primaryScore, 0) / losers.length).toFixed(4))
423
+ : null,
424
+ winningStrategies: winners.map(w => w.strategy),
425
+ researchQuery: batchVariants[0].researchQuery || null,
426
+ researchPackId: batchVariants[0].researchPackId || null,
427
+ researchPaperIds: [...new Set(batchVariants.flatMap((variant) => variant.researchPaperIds || []))],
428
+ timestamp: new Date().toISOString(),
429
+ };
430
+ appendJSONL(paths.knowledgePath, knowledgeEntry);
431
+
432
+ // Update progress
433
+ updateMarketingProgress();
434
+
435
+ return {
436
+ batchId: params.batchId,
437
+ channel,
438
+ primaryMetric,
439
+ keepRate,
440
+ winners: winners.map(w => ({
441
+ id: w.id,
442
+ strategy: w.strategy,
443
+ score: w.primaryScore,
444
+ pattern: extractPattern(w),
445
+ })),
446
+ losers: losers.map(l => ({
447
+ id: l.id,
448
+ strategy: l.strategy,
449
+ score: l.primaryScore,
450
+ })),
451
+ unmeasured: unmeasured.map(u => u.id),
452
+ reliability: getReliability(model)[channel] || null,
453
+ };
454
+ }
455
+
456
+ // ---------------------------------------------------------------------------
457
+ // Knowledge & Patterns
458
+ // ---------------------------------------------------------------------------
459
+
460
+ /**
461
+ * Extract a reusable pattern from a winning variant.
462
+ */
463
+ function extractPattern(variant) {
464
+ return {
465
+ channel: variant.channel,
466
+ strategy: variant.strategy,
467
+ seedPattern: variant.seedPattern,
468
+ metrics: variant.metrics,
469
+ constraints: variant.constraints,
470
+ };
471
+ }
472
+
473
+ /**
474
+ * Get winning patterns for a channel, ordered by recency.
475
+ *
476
+ * @param {string} channel - Marketing channel
477
+ * @param {number} [limit=5] - Max winners to return
478
+ * @returns {object[]}
479
+ */
480
+ function getWinningPatterns(channel, limit = 5) {
481
+ const paths = getMarketingPaths();
482
+ const winners = readJSONLFile(paths.winnersPath);
483
+ return winners
484
+ .filter(w => w.channel === channel)
485
+ .slice(-limit)
486
+ .reverse();
487
+ }
488
+
489
+ /**
490
+ * Get cross-channel knowledge entries.
491
+ *
492
+ * @param {object} [opts]
493
+ * @param {string} [opts.channel] - Filter by channel
494
+ * @param {number} [opts.limit=20] - Max entries
495
+ * @returns {object[]}
496
+ */
497
+ function getKnowledgeBase(opts = {}) {
498
+ const paths = getMarketingPaths();
499
+ let entries = readJSONLFile(paths.knowledgePath);
500
+ if (opts.channel) {
501
+ entries = entries.filter(e => e.channel === opts.channel);
502
+ }
503
+ return entries.slice(-(opts.limit || 20)).reverse();
504
+ }
505
+
506
+ // ---------------------------------------------------------------------------
507
+ // Progress
508
+ // ---------------------------------------------------------------------------
509
+
510
+ function updateMarketingProgress() {
511
+ const paths = getMarketingPaths();
512
+ const experiments = readJSONLFile(paths.experimentsPath);
513
+ const knowledge = readJSONLFile(paths.knowledgePath);
514
+ const winners = readJSONLFile(paths.winnersPath);
515
+
516
+ const channelStats = {};
517
+ for (const ch of MARKETING_CHANNELS) {
518
+ const chExps = experiments.filter(e => e.channel === ch);
519
+ const chWinners = winners.filter(w => w.channel === ch);
520
+ const chKnowledge = knowledge.filter(k => k.channel === ch);
521
+ const avgWinScore = chKnowledge.length > 0
522
+ ? chKnowledge.filter(k => k.avgWinnerScore != null)
523
+ .reduce((s, k) => s + k.avgWinnerScore, 0) / Math.max(1, chKnowledge.filter(k => k.avgWinnerScore != null).length)
524
+ : null;
525
+
526
+ channelStats[ch] = {
527
+ experiments: chExps.length,
528
+ winners: chWinners.length,
529
+ batches: chKnowledge.length,
530
+ avgWinnerScore: avgWinScore != null ? Number(avgWinScore.toFixed(4)) : null,
531
+ };
532
+ }
533
+
534
+ const progress = {
535
+ totalExperiments: experiments.length,
536
+ totalWinners: winners.length,
537
+ totalBatchesScored: knowledge.length,
538
+ channels: channelStats,
539
+ lastUpdated: new Date().toISOString(),
540
+ };
541
+
542
+ ensureDir(path.dirname(paths.progressPath));
543
+ fs.writeFileSync(paths.progressPath, JSON.stringify(progress, null, 2) + '\n');
544
+ return progress;
545
+ }
546
+
547
+ /**
548
+ * Get marketing experiment progress.
549
+ * @param {string} [channel] - Optional channel filter
550
+ * @returns {object}
551
+ */
552
+ function getChannelProgress(channel) {
553
+ const paths = getMarketingPaths();
554
+ if (fs.existsSync(paths.progressPath)) {
555
+ try {
556
+ const progress = JSON.parse(fs.readFileSync(paths.progressPath, 'utf-8'));
557
+ if (channel) {
558
+ return progress.channels[channel] || { experiments: 0, winners: 0, batches: 0 };
559
+ }
560
+ return progress;
561
+ } catch { /* fall through */ }
562
+ }
563
+ return updateMarketingProgress();
564
+ }
565
+
566
+ /**
567
+ * Record final experiment result (after selectWinners).
568
+ *
569
+ * @param {object} params
570
+ * @param {string} params.experimentId - Experiment ID
571
+ * @param {string} params.batchId - Batch that was scored
572
+ * @param {number} params.winnersCount - How many winners
573
+ * @param {number} params.totalScored - How many scored
574
+ * @param {number} [params.topScore] - Best score
575
+ * @returns {object}
576
+ */
577
+ function recordMarketingResult(params) {
578
+ if (!params || !params.experimentId) {
579
+ throw new Error('recordMarketingResult requires experimentId');
580
+ }
581
+
582
+ const paths = getMarketingPaths();
583
+ const result = {
584
+ id: params.experimentId,
585
+ status: 'completed',
586
+ completedAt: new Date().toISOString(),
587
+ batchId: params.batchId || null,
588
+ winnersCount: params.winnersCount || 0,
589
+ totalScored: params.totalScored || 0,
590
+ topScore: params.topScore || null,
591
+ keepRate: params.totalScored > 0
592
+ ? Number((params.winnersCount / params.totalScored).toFixed(3))
593
+ : 0,
594
+ };
595
+
596
+ appendJSONL(paths.experimentsPath, result);
597
+ updateMarketingProgress();
598
+ return result;
599
+ }
600
+
601
+ // ---------------------------------------------------------------------------
602
+ // CLI
603
+ // ---------------------------------------------------------------------------
604
+
605
+ if (require.main === module) {
606
+ const args = {};
607
+ process.argv.slice(2).forEach(arg => {
608
+ if (!arg.startsWith('--')) return;
609
+ const [key, ...rest] = arg.slice(2).split('=');
610
+ args[key] = rest.length > 0 ? rest.join('=') : true;
611
+ });
612
+
613
+ if (args.test) {
614
+ // Inline smoke test
615
+ console.log('Marketing Experiment Engine — smoke test');
616
+ (async () => {
617
+ const batch = await generateVariants({ channel: 'cold_email', batchSize: 10, product: 'ThumbGate' });
618
+ console.log(` Generated batch ${batch.batchId}: ${batch.totalVariants} variants (${batch.exploitCount} exploit, ${batch.exploreCount} explore)`);
619
+
620
+ // Simulate metrics
621
+ for (const v of batch.variants) {
622
+ recordVariantMetrics({ variantId: v.id, metrics: { open_rate: Math.random() * 0.5, reply_rate: Math.random() * 0.15 } });
623
+ }
624
+ console.log(' Recorded metrics for all variants');
625
+
626
+ const selection = selectWinners({ batchId: batch.batchId, primaryMetric: 'open_rate' });
627
+ console.log(` Selected ${selection.winners.length} winners, ${selection.losers.length} losers`);
628
+ console.log(` Top score: ${selection.winners[0]?.score?.toFixed(3) || 'n/a'}`);
629
+
630
+ const progress = getChannelProgress();
631
+ console.log(` Progress: ${JSON.stringify(progress, null, 2)}`);
632
+ console.log('✅ Marketing Experiment Engine smoke test passed');
633
+ })().catch((error) => {
634
+ console.error(error.message);
635
+ process.exit(1);
636
+ });
637
+ } else if (args.progress) {
638
+ console.log(JSON.stringify(getChannelProgress(args.channel || null), null, 2));
639
+ } else if (args.winners) {
640
+ const channel = args.channel || 'cold_email';
641
+ console.log(JSON.stringify(getWinningPatterns(channel, 10), null, 2));
642
+ } else if (args.knowledge) {
643
+ console.log(JSON.stringify(getKnowledgeBase({ channel: args.channel || null }), null, 2));
644
+ } else {
645
+ console.log(`Usage:
646
+ node scripts/marketing-experiment.js --test
647
+ node scripts/marketing-experiment.js --progress [--channel=cold_email]
648
+ node scripts/marketing-experiment.js --winners [--channel=cold_email]
649
+ node scripts/marketing-experiment.js --knowledge [--channel=cold_email]`);
650
+ }
651
+ }
652
+
653
+ // ---------------------------------------------------------------------------
654
+ // Exports
655
+ // ---------------------------------------------------------------------------
656
+
657
+ module.exports = {
658
+ createMarketingExperiment,
659
+ recordMarketingResult,
660
+ recordVariantMetrics,
661
+ generateVariants,
662
+ selectWinners,
663
+ getChannelProgress,
664
+ getWinningPatterns,
665
+ getKnowledgeBase,
666
+ getMarketingPaths,
667
+ updateMarketingProgress,
668
+ MARKETING_CHANNELS,
669
+ MARKETING_METRICS,
670
+ SIGNAL_WINDOWS,
671
+ };