marketing-mindset 1.6.2 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/bin/cli.js +164 -2
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -82,3 +82,35 @@ npx -y marketing-mindset plan --base 0.03 --lift 0.2 --daily 500 --weeks 4
|
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
The floor from `sample`/`budget` laid out on a calendar: contacts per week, cumulative volume against the floor, the first date the test can honestly be read, the mechanics-only first week, and an honest verdict when the volume cannot reach the floor inside the plan. `--json` for agents. Work with me: https://axelfreeman.com/marketing-engineer.html
|
|
85
|
+
|
|
86
|
+
## `brief` — the plan as markdown you can send
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
npx marketing-mindset brief --base 0.03 --lift 0.2 --cpl 4 --daily 500 --start 2026-09-21
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
One page: the floor in contacts per arm and in total, what reaching it costs at your price per contact,
|
|
93
|
+
the dated read, a week-by-week table with the percentage of the floor covered, the three gates, and the
|
|
94
|
+
kill rule. `--json` returns the same object for an agent or a script.
|
|
95
|
+
|
|
96
|
+
Method, packages and the paid version of this work: https://axelfreeman.com/marketing-engineer.html
|
|
97
|
+
|
|
98
|
+
## `mde` — what a volume you can afford can actually read
|
|
99
|
+
|
|
100
|
+
The other direction of the same arithmetic: instead of the volume a lift requires, the lift a volume can
|
|
101
|
+
read. Useful when the channel caps the volume (a small list, a niche audience, one store's traffic) and the
|
|
102
|
+
honest question is not "how much do we need" but "what is the smallest win this test could ever show".
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx marketing-mindset mde --base 0.03 --per-arm 5000
|
|
106
|
+
# base rate 3.00% · per arm 5,000 · power 0.8 · alpha 0.05
|
|
107
|
+
# smallest readable relative lift: 33.4% (1pp absolute)
|
|
108
|
+
# verdict: at this volume only a difference of 33.4% or more is readable ...
|
|
109
|
+
|
|
110
|
+
npx marketing-mindset mde --base 0.03 --per-arm 5000 --json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`--per-arm` takes the volume you can actually buy per variant; `--power` (0.8 or 0.9) and `--alpha`
|
|
114
|
+
(0.05, 0.01, 0.1) are the same knobs as `sample`. When no lift is readable at that volume the command
|
|
115
|
+
says so instead of printing a number, and names the three ways out: more volume, a higher base rate, or a
|
|
116
|
+
cheaper measurable unit (for cold email the reply rate, floor ~1,500–2,000 sends per variant).
|
package/bin/cli.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* marketing-mindset help + the test-size floors
|
|
4
4
|
* marketing-mindset sample required per-arm sample size for a two-proportion test
|
|
5
5
|
* marketing-mindset kill the kill rule for a running test
|
|
6
|
+
* marketing-mindset mde smallest relative lift a fixed per-arm volume can read
|
|
6
7
|
* marketing-mindset sections list every section of SKILL.md
|
|
7
8
|
* marketing-mindset read <text> print one section by fuzzy title match
|
|
8
9
|
* marketing-mindset install copy the skill into your agent's skills directory
|
|
@@ -380,6 +381,161 @@ function runPlan(opts) {
|
|
|
380
381
|
return 0;
|
|
381
382
|
}
|
|
382
383
|
|
|
384
|
+
|
|
385
|
+
/* brief — the whole plan as one markdown page a client can keep: the floor, what reaching it costs,
|
|
386
|
+
* the dates, the gate for each week and the kill rule. --json returns the same object for an agent. */
|
|
387
|
+
function mdeLift(rate, perArm, power, alpha) {
|
|
388
|
+
if (sampleSize(rate, 5, power, alpha) > perArm) { return null; }
|
|
389
|
+
let lo = 1e-6, hi = 5;
|
|
390
|
+
for (let i = 0; i < 100; i++) {
|
|
391
|
+
const mid = (lo + hi) / 2;
|
|
392
|
+
if (sampleSize(rate, mid, power, alpha) <= perArm) { hi = mid; } else { lo = mid; }
|
|
393
|
+
}
|
|
394
|
+
return hi;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function runMde(opts) {
|
|
398
|
+
const rate = Number(opts.base !== undefined ? opts.base : 0.03);
|
|
399
|
+
const perArm = Number(opts["per-arm"] !== undefined ? opts["per-arm"] : (opts.n !== undefined ? opts.n : 5000));
|
|
400
|
+
const power = Number(opts.power !== undefined ? opts.power : 0.8);
|
|
401
|
+
const alpha = Number(opts.alpha !== undefined ? opts.alpha : 0.05);
|
|
402
|
+
const problems = [];
|
|
403
|
+
if (!(rate > 0 && rate < 1)) { problems.push("base must be between 0 and 1 (0.03 = 3%)"); }
|
|
404
|
+
if (!(perArm > 0)) { problems.push("per-arm must be a positive volume (contacts, sends or visitors)"); }
|
|
405
|
+
if (!ZB[String(power)]) { problems.push("power must be 0.8 or 0.9 - this CLI carries exact quantiles for those two"); }
|
|
406
|
+
if (!ZA[String(alpha)]) { problems.push("alpha must be 0.05, 0.01 or 0.1"); }
|
|
407
|
+
if (problems.length) { console.error("mde: " + problems.join("; ")); return 1; }
|
|
408
|
+
const lift = mdeLift(rate, perArm, power, alpha);
|
|
409
|
+
if (lift === null) {
|
|
410
|
+
const out = {
|
|
411
|
+
base_rate: rate, per_arm: perArm, power, alpha,
|
|
412
|
+
min_relative_lift: null, min_absolute_pp: null, readable: false,
|
|
413
|
+
verdict: "no lift is readable at this volume: even a five-fold difference needs more than " + fmtNum(perArm) + " per arm at a " + (rate * 100).toFixed(2) + "% base rate. Raise the volume, raise the base rate (tighter list or sharper promise), or measure a cheaper unit (for cold email the readable unit is the reply rate, floor ~1,500-2,000 sends per variant)."
|
|
414
|
+
};
|
|
415
|
+
if (opts.json) { console.log(JSON.stringify(out, null, 2)); } else {
|
|
416
|
+
console.log("base rate " + (rate * 100).toFixed(2) + "% · per arm " + fmtNum(perArm) + " · power " + power + " · alpha " + alpha);
|
|
417
|
+
console.log("smallest readable relative lift: none");
|
|
418
|
+
console.log("verdict: " + out.verdict);
|
|
419
|
+
}
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
const pct = Math.round(lift * 1000) / 10;
|
|
423
|
+
const pp = Math.round(rate * lift * 100 * 100) / 100;
|
|
424
|
+
const verdict = pct >= 20
|
|
425
|
+
? "at this volume only a difference of " + pct + "% or more is readable - a smaller win will not separate from noise, so the honest plan is a bigger volume before the first read"
|
|
426
|
+
: "at this volume a difference of " + pct + "% or more is readable - a plan that promises less than that is promising a number this test cannot produce";
|
|
427
|
+
const out = {
|
|
428
|
+
base_rate: rate, per_arm: perArm, power, alpha,
|
|
429
|
+
min_relative_lift: Math.round(lift * 1e9) / 1e9,
|
|
430
|
+
min_absolute_pp: pp, readable: true, verdict
|
|
431
|
+
};
|
|
432
|
+
if (opts.json) { console.log(JSON.stringify(out, null, 2)); } else {
|
|
433
|
+
console.log("base rate " + (rate * 100).toFixed(2) + "% · per arm " + fmtNum(perArm) + " · power " + power + " · alpha " + alpha);
|
|
434
|
+
console.log("smallest readable relative lift: " + pct + "% (" + pp + "pp absolute)");
|
|
435
|
+
console.log("contacts needed at that lift: " + fmtNum(sampleSize(rate, lift, power, alpha)) + " per arm");
|
|
436
|
+
console.log("verdict: " + verdict);
|
|
437
|
+
}
|
|
438
|
+
return 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function fmtNum(n) {
|
|
442
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
443
|
+
}
|
|
444
|
+
function addDays(iso, n) {
|
|
445
|
+
const d = new Date(iso + "T00:00:00Z");
|
|
446
|
+
d.setUTCDate(d.getUTCDate() + n);
|
|
447
|
+
return d.toISOString().slice(0, 10);
|
|
448
|
+
}
|
|
449
|
+
function runBrief(opts) {
|
|
450
|
+
const base = Number(opts.base === undefined ? 0 : opts.base);
|
|
451
|
+
const lift = Number(opts.lift === undefined ? 0 : opts.lift);
|
|
452
|
+
if (!isFinite(base) || base <= 0 || base >= 1) {
|
|
453
|
+
console.error("brief: --base must be a rate between 0 and 1 (got: " + JSON.stringify(opts.base) + ")");
|
|
454
|
+
return 1;
|
|
455
|
+
}
|
|
456
|
+
if (!isFinite(lift) || lift <= 0) {
|
|
457
|
+
console.error("brief: --lift must be a positive relative lift, e.g. 0.2 for +20%");
|
|
458
|
+
return 1;
|
|
459
|
+
}
|
|
460
|
+
const cpl = Number(opts.cpl === undefined ? 0 : opts.cpl);
|
|
461
|
+
if (!isFinite(cpl) || cpl <= 0) {
|
|
462
|
+
console.error("brief: --cpl must be a positive cost per contact (got: " + JSON.stringify(opts.cpl) + ")");
|
|
463
|
+
return 1;
|
|
464
|
+
}
|
|
465
|
+
const daily = Number(opts.daily === undefined ? 0 : opts.daily);
|
|
466
|
+
if (opts.daily !== undefined && (!isFinite(daily) || daily <= 0)) {
|
|
467
|
+
console.error("brief: --daily must be a positive number of contacts per day");
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
470
|
+
const weeks = opts.weeks === undefined ? 4 : Number(opts.weeks);
|
|
471
|
+
if (!isFinite(weeks) || weeks < 1 || weeks > 52 || Math.floor(weeks) !== weeks) {
|
|
472
|
+
console.error("brief: --weeks must be a whole number between 1 and 52");
|
|
473
|
+
return 1;
|
|
474
|
+
}
|
|
475
|
+
const power = opts.power === undefined ? 0.8 : opts.power;
|
|
476
|
+
const alpha = opts.alpha === undefined ? 0.05 : opts.alpha;
|
|
477
|
+
const start = typeof opts.start === "string" && /^\d{4}-\d{2}-\d{2}$/.test(opts.start)
|
|
478
|
+
? opts.start : new Date().toISOString().slice(0, 10);
|
|
479
|
+
|
|
480
|
+
const perArm = sampleSize(base, lift, power, alpha);
|
|
481
|
+
const need = perArm * 2;
|
|
482
|
+
const cost = Math.round(need * cpl * 100) / 100;
|
|
483
|
+
const days = daily > 0 ? Math.ceil(need / daily) : null;
|
|
484
|
+
const readDate = days ? addDays(start, days - 1) : null;
|
|
485
|
+
const rows = [];
|
|
486
|
+
for (let w = 1; w <= weeks; w++) {
|
|
487
|
+
const planned = daily > 0 ? daily * 7 : 0;
|
|
488
|
+
const cumulative = planned * w;
|
|
489
|
+
rows.push({
|
|
490
|
+
week: w, start: addDays(start, (w - 1) * 7), end: addDays(start, w * 7 - 1),
|
|
491
|
+
planned_contacts: planned, cumulative,
|
|
492
|
+
against_floor_pct: Math.round(Math.min(100, (cumulative / need) * 100) * 10) / 10
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const gates = [
|
|
496
|
+
"gate 1 (week 1): the tracked event fires on a real lead - if it does not, stop and fix the event, the channel stays closed",
|
|
497
|
+
"gate 2 (50% of floor): counts per arm look like the plan; if one arm is 30% behind, the split is broken, not the offer",
|
|
498
|
+
"gate 3 (read date): read the result at the declared floor or later, never earlier - below the floor the difference is noise"
|
|
499
|
+
];
|
|
500
|
+
const killRule = "kill if conversions are at or under the floor on the read date; double only when the target rate is hit at or above the floor";
|
|
501
|
+
const result = {
|
|
502
|
+
base_rate: base, target_rate: Math.round(base * (1 + lift) * 10000) / 10000, relative_lift: lift,
|
|
503
|
+
power, alpha, cost_per_contact: cpl, per_arm: perArm, contacts_needed: need,
|
|
504
|
+
cost_to_read_the_test: cost, daily_contacts: daily > 0 ? daily : null,
|
|
505
|
+
days_to_read: days, first_honest_read: readDate, start_date: start, weeks: rows,
|
|
506
|
+
gates, kill_rule: killRule,
|
|
507
|
+
offer: "https://axelfreeman.com/marketing-engineer.html",
|
|
508
|
+
method: "two-proportion floor; same formula as `marketing-mindset sample`"
|
|
509
|
+
};
|
|
510
|
+
if (opts.json) {
|
|
511
|
+
console.log(JSON.stringify(result, null, 2));
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
const pct = (x) => (x * 100).toFixed(2);
|
|
515
|
+
console.log("# Campaign brief - the honest test plan\n");
|
|
516
|
+
console.log("Base rate " + pct(base) + "% -> target " + pct(base * (1 + lift)) + "% (relative lift +"
|
|
517
|
+
+ (lift * 100).toFixed(0) + "%), power " + power + ", alpha " + alpha);
|
|
518
|
+
console.log("Floor: " + fmtNum(perArm) + " contacts per arm | " + fmtNum(need) + " in total");
|
|
519
|
+
console.log("At $" + cpl + " per contact: $" + fmtNum(cost) + " to read the test");
|
|
520
|
+
if (days) {
|
|
521
|
+
console.log("At " + fmtNum(daily) + " contacts/day: " + days + " days -> first honest read " + readDate);
|
|
522
|
+
} else {
|
|
523
|
+
console.log("Daily volume not given: add --daily to date the read");
|
|
524
|
+
}
|
|
525
|
+
console.log("\n## Weeks");
|
|
526
|
+
console.log("| Week | Dates | Contacts | Cumulative | % of floor |");
|
|
527
|
+
console.log("| --- | --- | --- | --- | --- |");
|
|
528
|
+
rows.forEach((r) => console.log("| " + r.week + " | " + r.start + " .. " + r.end + " | " + fmtNum(r.planned_contacts)
|
|
529
|
+
+ " | " + fmtNum(r.cumulative) + " | " + r.against_floor_pct.toFixed(1) + "% |"));
|
|
530
|
+
console.log("\n## Gates");
|
|
531
|
+
gates.forEach((g) => console.log("- " + g));
|
|
532
|
+
console.log("\n## Kill rule");
|
|
533
|
+
console.log("- " + killRule);
|
|
534
|
+
console.log("\nMethod and packages: " + result.offer);
|
|
535
|
+
console.log("Tools: https://axelfreeman.github.io/marketing-mindset/tools/email-test-planner.html");
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
383
539
|
const cmd = (process.argv[2] || "").toLowerCase();
|
|
384
540
|
const rest = process.argv.slice(3);
|
|
385
541
|
|
|
@@ -414,8 +570,12 @@ if (cmd === "sample") {
|
|
|
414
570
|
}, null, 2));
|
|
415
571
|
} else if (cmd === "aeo") {
|
|
416
572
|
process.exit(runAeo(flags(rest)));
|
|
573
|
+
} else if (cmd === "brief") {
|
|
574
|
+
process.exit(runBrief(flags(rest)));
|
|
417
575
|
} else if (cmd === "plan") {
|
|
418
576
|
process.exit(runPlan(flags(rest)));
|
|
577
|
+
} else if (cmd === "mde") {
|
|
578
|
+
process.exit(runMde(flags(rest)));
|
|
419
579
|
} else if (cmd === "budget") {
|
|
420
580
|
process.exit(runBudget(flags(rest)));
|
|
421
581
|
} else if (cmd === "warmup") {
|
|
@@ -444,12 +604,14 @@ Usage:
|
|
|
444
604
|
--base 0.03 --lift 0.2 --power 0.8 --alpha 0.05
|
|
445
605
|
npx marketing-mindset kill kill rule for a running test
|
|
446
606
|
--base 0.03 --n 1500 --lift 0.2
|
|
607
|
+
npx marketing-mindset brief the whole plan as markdown you can send to a client
|
|
608
|
+
--base 0.03 --lift 0.2 --cpl 4 --daily 500 [--weeks 4] [--start 2026-09-21] [--json]
|
|
447
609
|
npx marketing-mindset budget what an honest test costs before you can read it
|
|
448
610
|
--base 0.03 --lift 0.2 --cpl 4 [--daily 500]
|
|
449
611
|
npx marketing-mindset plan dated plan: weekly volume, gates, first honest read
|
|
450
612
|
--base 0.03 --lift 0.2 --daily 500 [--weeks 4] [--start 2026-09-21]
|
|
451
|
-
npx marketing-mindset
|
|
452
|
-
--base 0.03 --
|
|
613
|
+
npx marketing-mindset mde what a volume you can afford can actually read
|
|
614
|
+
--base 0.03 --per-arm 5000 [--power 0.8] [--alpha 0.05] [--json]
|
|
453
615
|
npx marketing-mindset warmup day-by-day sending ramp with stop rules
|
|
454
616
|
--start 2026-09-21 --target 200 --days 21 --mailboxes 4
|
|
455
617
|
npx marketing-mindset jd marketing engineer job description
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marketing-mindset",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A marketer's operating mindset as an installable agent skill and CLI: test-size floors, kill rules, agent-readiness checklist, marketing-engineer job description. npx marketing-mindset sample|kill|budget|warmup|aeo|jd|sections|read|install.",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "A marketer's operating mindset as an installable agent skill and CLI: test-size floors, kill rules, the lift a volume can actually read, agent-readiness checklist, marketing-engineer job description. npx marketing-mindset sample|kill|mde|budget|brief|plan|warmup|aeo|jd|sections|read|install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skill",
|
|
7
7
|
"skill",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"llm",
|
|
14
14
|
"prompt",
|
|
15
15
|
"sample-size",
|
|
16
|
+
"minimum-detectable-effect",
|
|
16
17
|
"job-description",
|
|
17
18
|
"cold-email",
|
|
18
19
|
"deliverability",
|