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