marketing-mindset 1.8.0 → 1.9.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 +23 -0
- package/bin/cli.js +76 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -114,3 +114,26 @@ npx marketing-mindset mde --base 0.03 --per-arm 5000 --json
|
|
|
114
114
|
(0.05, 0.01, 0.1) are the same knobs as `sample`. When no lift is readable at that volume the command
|
|
115
115
|
says so instead of printing a number, and names the three ways out: more volume, a higher base rate, or a
|
|
116
116
|
cheaper measurable unit (for cold email the reply rate, floor ~1,500–2,000 sends per variant).
|
|
117
|
+
|
|
118
|
+
## `verdict` — was the difference real, and was the volume ever enough
|
|
119
|
+
|
|
120
|
+
Reading a finished test is a different job from planning one, and the two honest failure modes are opposite:
|
|
121
|
+
a difference that is inside the noise, and a difference that is real but was never readable at that volume.
|
|
122
|
+
`verdict` answers both from two counts.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx marketing-mindset verdict --a 120/4000 --b 168/4000
|
|
126
|
+
# control 120/4,000 = 3.00% · variant 168/4,000 = 4.20%
|
|
127
|
+
# difference: 1.2pp (40.0% relative) · interval 0.44pp .. 1.96pp
|
|
128
|
+
# separated from noise: true · volume clears the floor: true
|
|
129
|
+
# verdict: readable: the interval excludes zero and the volume clears the 4,994 the observed lift requires ...
|
|
130
|
+
|
|
131
|
+
npx marketing-mindset verdict --a 120/4000 --b 168/4000 --json
|
|
132
|
+
npx marketing-mindset verdict --a-conv 120 --a-n 4000 --b-conv 168 --b-n 4000
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each arm is `conversions/sample`. The command prints a Wilson interval per arm, a Newcombe interval for the
|
|
136
|
+
difference, and decides: not readable (the interval includes zero), a lead rather than a verdict (separated,
|
|
137
|
+
but below the per-arm volume the observed lift would need), or readable. `--alpha` takes the same three
|
|
138
|
+
values as `sample` (0.05, 0.01, 0.1) and the floor is computed with the same formula, so the answer stays
|
|
139
|
+
consistent with the plan that produced the test.
|
package/bin/cli.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
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
6
|
* marketing-mindset mde smallest relative lift a fixed per-arm volume can read
|
|
7
|
+
* marketing-mindset verdict read a finished test: interval, floor, honest verdict
|
|
7
8
|
* marketing-mindset sections list every section of SKILL.md
|
|
8
9
|
* marketing-mindset read <text> print one section by fuzzy title match
|
|
9
10
|
* marketing-mindset install copy the skill into your agent's skills directory
|
|
@@ -438,6 +439,77 @@ function runMde(opts) {
|
|
|
438
439
|
return 0;
|
|
439
440
|
}
|
|
440
441
|
|
|
442
|
+
function wilsonInterval(conv, n, alpha) {
|
|
443
|
+
const z = ZA[String(alpha)] || ZA["0.05"];
|
|
444
|
+
const p = conv / n;
|
|
445
|
+
const d = 1 + (z * z) / n;
|
|
446
|
+
const centre = (p + (z * z) / (2 * n)) / d;
|
|
447
|
+
const half = (z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n))) / d;
|
|
448
|
+
return { low: Math.max(0, centre - half), high: Math.min(1, centre + half) };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function parseArm(v) {
|
|
452
|
+
const s = String(v === undefined ? "" : v).trim();
|
|
453
|
+
const m = s.match(/^(\d+)\s*\/\s*(\d+)$/);
|
|
454
|
+
if (!m) { return null; }
|
|
455
|
+
return { conv: Number(m[1]), n: Number(m[2]) };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function r6(x) { return Math.round(x * 1e6) / 1e6; }
|
|
459
|
+
|
|
460
|
+
function runVerdict(opts) {
|
|
461
|
+
const alpha = Number(opts.alpha !== undefined ? opts.alpha : 0.05);
|
|
462
|
+
let aRaw = opts.a;
|
|
463
|
+
if (aRaw === undefined && opts["a-conv"] !== undefined && opts["a-n"] !== undefined) {
|
|
464
|
+
aRaw = opts["a-conv"] + "/" + opts["a-n"];
|
|
465
|
+
}
|
|
466
|
+
let bRaw = opts.b;
|
|
467
|
+
if (bRaw === undefined && opts["b-conv"] !== undefined && opts["b-n"] !== undefined) {
|
|
468
|
+
bRaw = opts["b-conv"] + "/" + opts["b-n"];
|
|
469
|
+
}
|
|
470
|
+
const problems = [];
|
|
471
|
+
const a = parseArm(aRaw), b = parseArm(bRaw);
|
|
472
|
+
if (!a) { problems.push("control arm: pass --a 120/4000 (conversions/sample)"); }
|
|
473
|
+
if (!b) { problems.push("variant arm: pass --b 168/4000 (conversions/sample)"); }
|
|
474
|
+
if (a && (a.n <= 0 || a.conv > a.n)) { problems.push("control arm must be conversions <= sample and a positive sample"); }
|
|
475
|
+
if (b && (b.n <= 0 || b.conv > b.n)) { problems.push("variant arm must be conversions <= sample and a positive sample"); }
|
|
476
|
+
if (!ZA[String(alpha)]) { problems.push("alpha must be 0.05, 0.01 or 0.1"); }
|
|
477
|
+
if (problems.length) { console.error("verdict: " + problems.join("; ")); return 1; }
|
|
478
|
+
|
|
479
|
+
const pa = a.conv / a.n, pb = b.conv / b.n;
|
|
480
|
+
const ia = wilsonInterval(a.conv, a.n, alpha), ib = wilsonInterval(b.conv, b.n, alpha);
|
|
481
|
+
const diff = pb - pa;
|
|
482
|
+
const lo = diff - Math.sqrt(Math.pow(pa - ia.low, 2) + Math.pow(ib.high - pb, 2));
|
|
483
|
+
const hi = diff + Math.sqrt(Math.pow(ia.high - pa, 2) + Math.pow(pb - ib.low, 2));
|
|
484
|
+
const decisive = lo > 0 || hi < 0;
|
|
485
|
+
const rel = pa > 0 ? diff / pa : null;
|
|
486
|
+
const floor = rel !== null && rel !== 0 ? sampleSize(pa, Math.abs(rel), 0.8, alpha) : null;
|
|
487
|
+
const underpowered = floor !== null && b.n < floor;
|
|
488
|
+
let verdict;
|
|
489
|
+
if (!decisive) {
|
|
490
|
+
verdict = "not readable: the " + ((1 - alpha) * 100) + "% interval for the difference includes zero, so the point estimate is noise dressed as a result. The honest moves are more volume, a higher base rate, or a bigger difference - not a rebuild.";
|
|
491
|
+
} else if (underpowered) {
|
|
492
|
+
verdict = "a lead, not a verdict: the interval excludes zero, but " + fmtNum(b.n) + " per arm is below the " + fmtNum(floor) + " the observed lift would need. Keep it running to the floor before anything is rebuilt on it.";
|
|
493
|
+
} else {
|
|
494
|
+
verdict = "readable: the interval excludes zero and the volume clears the " + fmtNum(floor) + " the observed lift requires - this is the number a decision can be taken on.";
|
|
495
|
+
}
|
|
496
|
+
const out = {
|
|
497
|
+
control: { conversions: a.conv, n: a.n, rate: r6(pa), wilson_low: r6(ia.low), wilson_high: r6(ia.high) },
|
|
498
|
+
variant: { conversions: b.conv, n: b.n, rate: r6(pb), wilson_low: r6(ib.low), wilson_high: r6(ib.high) },
|
|
499
|
+
absolute_lift_pp: r6(diff * 100),
|
|
500
|
+
relative_lift: rel === null ? null : r6(rel),
|
|
501
|
+
difference_interval: [r6(lo), r6(hi)],
|
|
502
|
+
decisive, floor_per_arm: floor, underpowered, alpha, verdict
|
|
503
|
+
};
|
|
504
|
+
if (opts.json) { console.log(JSON.stringify(out, null, 2)); } else {
|
|
505
|
+
console.log("control " + a.conv + "/" + fmtNum(a.n) + " = " + (pa * 100).toFixed(2) + "% · variant " + b.conv + "/" + fmtNum(b.n) + " = " + (pb * 100).toFixed(2) + "%");
|
|
506
|
+
console.log("difference: " + out.absolute_lift_pp + "pp" + (rel === null ? "" : " (" + (rel * 100).toFixed(1) + "% relative)") + " · interval " + (lo * 100).toFixed(2) + "pp .. " + (hi * 100).toFixed(2) + "pp");
|
|
507
|
+
console.log("separated from noise: " + decisive + " · volume clears the floor: " + !underpowered);
|
|
508
|
+
console.log("verdict: " + verdict);
|
|
509
|
+
}
|
|
510
|
+
return 0;
|
|
511
|
+
}
|
|
512
|
+
|
|
441
513
|
function fmtNum(n) {
|
|
442
514
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
443
515
|
}
|
|
@@ -568,6 +640,8 @@ if (cmd === "sample") {
|
|
|
568
640
|
? "keep running — below the required volume, a verdict now is noise"
|
|
569
641
|
: "read it — volume is sufficient; kill if conversions are at or under the floor"
|
|
570
642
|
}, null, 2));
|
|
643
|
+
} else if (cmd === "verdict") {
|
|
644
|
+
process.exit(runVerdict(flags(rest)));
|
|
571
645
|
} else if (cmd === "aeo") {
|
|
572
646
|
process.exit(runAeo(flags(rest)));
|
|
573
647
|
} else if (cmd === "brief") {
|
|
@@ -610,6 +684,8 @@ Usage:
|
|
|
610
684
|
--base 0.03 --lift 0.2 --cpl 4 [--daily 500]
|
|
611
685
|
npx marketing-mindset plan dated plan: weekly volume, gates, first honest read
|
|
612
686
|
--base 0.03 --lift 0.2 --daily 500 [--weeks 4] [--start 2026-09-21]
|
|
687
|
+
npx marketing-mindset verdict was the difference real, and was the volume ever enough?
|
|
688
|
+
--a 120/4000 --b 168/4000 [--alpha 0.05] [--json]
|
|
613
689
|
npx marketing-mindset mde what a volume you can afford can actually read
|
|
614
690
|
--base 0.03 --per-arm 5000 [--power 0.8] [--alpha 0.05] [--json]
|
|
615
691
|
npx marketing-mindset warmup day-by-day sending ramp with stop rules
|
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, the lift a volume can
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "A marketer's operating mindset as an installable agent skill and CLI: test-size floors, kill rules, honest verdicts on finished tests, the lift a volume can read, agent-readiness checklist, marketing-engineer job description. npx marketing-mindset sample|kill|verdict|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
|
+
"ab-test-verdict",
|
|
16
17
|
"minimum-detectable-effect",
|
|
17
18
|
"job-description",
|
|
18
19
|
"cold-email",
|