marketing-mindset 1.10.0 → 1.12.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 +44 -0
- package/bin/cli.js +171 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -156,3 +156,47 @@ npx marketing-mindset queue --daily 500 --days 90 --start 2026-09-21
|
|
|
156
156
|
`read_dates`, `idle_days`, `used_pct` and the three gates. The floor uses the same formula as `sample`, so a
|
|
157
157
|
queue planned here and a test read with `verdict` stay consistent. Two things it refuses to hide: the read-out
|
|
158
158
|
is one look (peeking daily is not covered by alpha), and two live tests in the same arm split the volume.
|
|
159
|
+
|
|
160
|
+
## `cost` — what one readable verdict costs to finish
|
|
161
|
+
|
|
162
|
+
A test budget is not spent on ideas, it is spent on reaching a floor. `cost` prices one readable verdict: the
|
|
163
|
+
contacts a strict two-arm test needs, multiplied by what a contact costs you, plus the days it takes to send them.
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npx marketing-mindset cost --cost 0.02 --daily 500 --budget 5000
|
|
167
|
+
# floor: 13,914 contacts per arm (27,828 in both arms) at a 3.00% base rate, +20% relative lift
|
|
168
|
+
# time: 56 days at 500 contacts a day
|
|
169
|
+
# cost of one readable test: 556.56 (278.28 per arm) at 0.02 per contact
|
|
170
|
+
# cost of one conversion: 0.67 at the base rate, 0.56 at the lifted rate
|
|
171
|
+
# budget: 8 fully funded test(s) a month at a budget of 5000
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--json` returns the same object: `floor_per_arm`, `total_contacts`, `days_per_test`, `cost_per_arm`,
|
|
175
|
+
`cost_per_test`, `cost_per_conversion_base`, `cost_per_conversion_target`, `tests_per_budget`,
|
|
176
|
+
`months_to_fund_one` and the three gates. Money is expressed in whatever unit you pass to `--cost`; no currency is
|
|
177
|
+
assumed. The floor uses the same formula as `sample`, so a budget planned here, a queue planned with `queue` and a
|
|
178
|
+
test read with `verdict` stay consistent. Two things it refuses to hide: stopping before the floor buys a sample
|
|
179
|
+
rather than a result, and money buys tests while volume limits how many can run at once.
|
|
180
|
+
|
|
181
|
+
## `unit` — which step of your funnel can fund a verdict
|
|
182
|
+
|
|
183
|
+
A funnel is a list of counted events per month. `unit` answers the only question that decides whether a
|
|
184
|
+
channel can be judged: which of those steps happens often enough to reach a test floor, and how many
|
|
185
|
+
months it needs.
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx marketing-mindset unit --steps visits:12000,signups:900,activations:270,paid:40
|
|
189
|
+
```
|
|
190
|
+
```
|
|
191
|
+
unit visits -> signups: rate 7.50%, floor 5,280 per arm, 6,000 events per arm a month -> 1 month(s) to read
|
|
192
|
+
unit signups -> activations: rate 30.00%, floor 963 per arm, 450 events per arm a month -> 3 month(s) to read
|
|
193
|
+
unit activations -> paid: rate 14.81%, floor 2,438 per arm, 135 events per arm a month -> 19 month(s) to read
|
|
194
|
+
readable at this volume: visits -> signups in 1 month(s)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`--json` returns the same object: one entry per consecutive pair with `rate`, `floor_per_arm`,
|
|
198
|
+
`arm_events_per_month` and `months_to_read`, plus `readable_unit`, `unreadable_units` and three gates.
|
|
199
|
+
The floor comes from the same formula as `sample`, and the arms split the earlier step's volume in half,
|
|
200
|
+
so a unit chosen here, a queue planned with `queue` and a result read with `verdict` stay consistent.
|
|
201
|
+
Two things it refuses to hide: the earliest step reads soonest and owns the least, and money always reads
|
|
202
|
+
one step behind whatever unit you chose.
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* marketing-mindset mde smallest relative lift a fixed per-arm volume can read
|
|
7
7
|
* marketing-mindset verdict read a finished test: interval, floor, honest verdict
|
|
8
8
|
* marketing-mindset queue volume into a queue of honest tests with read-out dates
|
|
9
|
+
* marketing-mindset cost the cost of one readable verdict, and funded tests per budget
|
|
10
|
+
* marketing-mindset unit which funnel step can fund a verdict, and in how many months
|
|
9
11
|
* marketing-mindset sections list every section of SKILL.md
|
|
10
12
|
* marketing-mindset read <text> print one section by fuzzy title match
|
|
11
13
|
* marketing-mindset install copy the skill into your agent's skills directory
|
|
@@ -567,6 +569,167 @@ function runQueue(opts) {
|
|
|
567
569
|
return 0;
|
|
568
570
|
}
|
|
569
571
|
|
|
572
|
+
function cRound2(x) { return Math.round(x * 100) / 100; }
|
|
573
|
+
|
|
574
|
+
function runCost(opts) {
|
|
575
|
+
const base = Number(opts.base !== undefined ? opts.base : 0.03);
|
|
576
|
+
const lift = Number(opts.lift !== undefined ? opts.lift : 0.2);
|
|
577
|
+
const cost = Number(opts.cost);
|
|
578
|
+
const daily = Number(opts.daily);
|
|
579
|
+
const budget = opts.budget !== undefined ? Number(opts.budget) : 0;
|
|
580
|
+
const power = Number(opts.power !== undefined ? opts.power : 0.8);
|
|
581
|
+
const alpha = Number(opts.alpha !== undefined ? opts.alpha : 0.05);
|
|
582
|
+
const problems = [];
|
|
583
|
+
if (!(base > 0) || !(base < 1)) { problems.push("base rate: a number between 0 and 1 (0.03 = 3%)"); }
|
|
584
|
+
if (!(lift > 0) || !(lift < 5)) { problems.push("lift worth detecting: a fraction, 0.2 = +20% relative"); }
|
|
585
|
+
if (!(cost > 0)) { problems.push("cost per contact: pass --cost 0.02 in your own currency"); }
|
|
586
|
+
if (!(daily > 0)) { problems.push("contacts a day: pass --daily 500"); }
|
|
587
|
+
if (!(budget >= 0)) { problems.push("monthly budget: pass --budget 5000, or leave it out"); }
|
|
588
|
+
if (!ZB[String(power)]) { problems.push("power must be 0.8 or 0.9"); }
|
|
589
|
+
if (!ZA[String(alpha)]) { problems.push("alpha must be 0.05, 0.01 or 0.1"); }
|
|
590
|
+
if (problems.length) { console.error("cost: " + problems.join("; ")); return 1; }
|
|
591
|
+
|
|
592
|
+
const per_arm = sampleSize(base, lift, power, alpha);
|
|
593
|
+
const total_contacts = per_arm * 2;
|
|
594
|
+
const days_per_test = Math.ceil(total_contacts / daily);
|
|
595
|
+
const cost_per_arm = cRound2(per_arm * cost);
|
|
596
|
+
const cost_per_test = cRound2(total_contacts * cost);
|
|
597
|
+
const cost_per_conversion = cRound2(cost / base);
|
|
598
|
+
const cost_per_conversion_target = cRound2(cost / (base * (1 + lift)));
|
|
599
|
+
const tests_per_budget = budget > 0 ? Math.floor(budget / cost_per_test) : 0;
|
|
600
|
+
const months_to_fund_one = (budget > 0 && tests_per_budget === 0) ? Math.ceil(cost_per_test / budget) : 0;
|
|
601
|
+
const gates = [
|
|
602
|
+
"a verdict costs the whole floor: stopping before " + fmtNum(total_contacts) + " contacts produces a sample, not a result",
|
|
603
|
+
"cost per contact is not cost per result: at a " + (base * 100).toFixed(2) + "% base rate one conversion costs about " + cost_per_conversion + " in contacts",
|
|
604
|
+
tests_per_budget === 0
|
|
605
|
+
? "budget: one fully funded test needs " + cost_per_test + ", which this budget reaches in " + months_to_fund_one + " month(s)"
|
|
606
|
+
: "budget: " + tests_per_budget + " fully funded test(s) a month - volume, not money, sets how many can run at once"
|
|
607
|
+
];
|
|
608
|
+
const out = {
|
|
609
|
+
base_rate: base, lift: lift, power: power, alpha: alpha, cost_per_contact: cost, daily: daily, budget: budget,
|
|
610
|
+
floor_per_arm: per_arm, total_contacts: total_contacts, days_per_test: days_per_test,
|
|
611
|
+
cost_per_arm: cost_per_arm, cost_per_test: cost_per_test,
|
|
612
|
+
cost_per_conversion_base: cost_per_conversion, cost_per_conversion_target: cost_per_conversion_target,
|
|
613
|
+
tests_per_budget: tests_per_budget, months_to_fund_one: months_to_fund_one, gates: gates
|
|
614
|
+
};
|
|
615
|
+
if (opts.json) { console.log(JSON.stringify(out, null, 2)); } else {
|
|
616
|
+
console.log("floor: " + fmtNum(per_arm) + " contacts per arm (" + fmtNum(total_contacts) + " in both arms) at a " + (base * 100).toFixed(2) + "% base rate, +" + (lift * 100).toFixed(0) + "% relative lift");
|
|
617
|
+
console.log("time: " + days_per_test + " days at " + fmtNum(daily) + " contacts a day");
|
|
618
|
+
console.log("cost of one readable test: " + cost_per_test + " (" + cost_per_arm + " per arm) at " + cost + " per contact");
|
|
619
|
+
console.log("cost of one conversion: " + cost_per_conversion + " at the base rate, " + cost_per_conversion_target + " at the lifted rate");
|
|
620
|
+
if (budget > 0) {
|
|
621
|
+
console.log("budget: " + tests_per_budget + " fully funded test(s) a month at a budget of " + budget);
|
|
622
|
+
if (tests_per_budget === 0) { console.log("budget: one test needs " + months_to_fund_one + " month(s) of this budget"); }
|
|
623
|
+
} else { console.log("budget: not given - pass --budget to see how many funded tests it carries"); }
|
|
624
|
+
gates.forEach(function (g) { console.log("gate: " + g); });
|
|
625
|
+
}
|
|
626
|
+
return 0;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
var UN_ZA = { "0.05": 1.9599639845, "0.01": 2.5758293036, "0.1": 1.6448536270 };
|
|
630
|
+
var UN_ZB = { "0.8": 0.8416212336, "0.9": 1.2815515655 };
|
|
631
|
+
function unRound6(x) { return Math.round(x * 1000000) / 1000000; }
|
|
632
|
+
function unSampleSize(rate, lift, power, alpha) {
|
|
633
|
+
const p1 = Math.min(Math.max(rate, 0.000001), 0.999999);
|
|
634
|
+
const p2 = Math.min(p1 * (1 + lift), 0.999999);
|
|
635
|
+
const pbar = (p1 + p2) / 2;
|
|
636
|
+
const za = UN_ZA[String(alpha)] || UN_ZA["0.05"];
|
|
637
|
+
const zb = UN_ZB[String(power)] || UN_ZB["0.8"];
|
|
638
|
+
const num = Math.pow(za * Math.sqrt(2 * pbar * (1 - pbar)) + zb * Math.sqrt(p1 * (1 - p1) + p2 * (1 - p2)), 2);
|
|
639
|
+
return Math.ceil(num / Math.pow(p2 - p1, 2));
|
|
640
|
+
}
|
|
641
|
+
function planUnits(input) {
|
|
642
|
+
const steps = input.steps || [];
|
|
643
|
+
const lift = Number(input.lift !== undefined ? input.lift : 0.2);
|
|
644
|
+
const power = Number(input.power !== undefined ? input.power : 0.8);
|
|
645
|
+
const alpha = Number(input.alpha !== undefined ? input.alpha : 0.05);
|
|
646
|
+
const horizon = Number(input.horizon !== undefined ? input.horizon : 6);
|
|
647
|
+
const problems = [];
|
|
648
|
+
if (steps.length < 2) { problems.push("steps: pass at least two steps, e.g. visits:12000,signups:900"); }
|
|
649
|
+
steps.forEach(function (s) {
|
|
650
|
+
if (!s || !s.name) { problems.push("steps: every step needs a name"); }
|
|
651
|
+
if (!(Number(s.count) > 0)) { problems.push("steps: count for '" + ((s && s.name) || "?") + "' must be a positive number of events per month"); }
|
|
652
|
+
});
|
|
653
|
+
if (!(lift > 0) || !(lift < 5)) { problems.push("lift worth detecting: a fraction, 0.2 = +20% relative"); }
|
|
654
|
+
if (!UN_ZB[String(power)]) { problems.push("power must be 0.8 or 0.9"); }
|
|
655
|
+
if (!UN_ZA[String(alpha)]) { problems.push("alpha must be 0.05, 0.01 or 0.1"); }
|
|
656
|
+
if (!(horizon > 0)) { problems.push("horizon: months you are willing to wait, a positive number"); }
|
|
657
|
+
if (problems.length) { return { problems: problems }; }
|
|
658
|
+
|
|
659
|
+
const units = [];
|
|
660
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
661
|
+
const a = steps[i], b = steps[i + 1];
|
|
662
|
+
const rate = Number(b.count) / Number(a.count);
|
|
663
|
+
const per_arm = unSampleSize(rate, lift, power, alpha);
|
|
664
|
+
const arm_events_per_month = Number(a.count) / 2;
|
|
665
|
+
const months = Math.ceil(per_arm / arm_events_per_month);
|
|
666
|
+
units.push({
|
|
667
|
+
unit: a.name + " -> " + b.name, from: a.name, to: b.name, rate: unRound6(rate),
|
|
668
|
+
floor_per_arm: per_arm, months_to_read: months,
|
|
669
|
+
arm_events_per_month: arm_events_per_month
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
let chosen = null;
|
|
673
|
+
units.forEach(function (u) { if (!chosen && u.months_to_read <= horizon) { chosen = u; } });
|
|
674
|
+
const unreadable = units.filter(function (u) { return u.months_to_read > horizon; }).map(function (u) { return u.unit; });
|
|
675
|
+
const last = units[units.length - 1];
|
|
676
|
+
const gates = [
|
|
677
|
+
"the earliest step is the cheapest to read and the least able to own the outcome: " + units[0].unit +
|
|
678
|
+
" reads in " + units[0].months_to_read + " month(s), which is why it is usually the number already in the dashboard",
|
|
679
|
+
"the money step has no next step at this volume: " + last.unit + " is the last pair in this funnel, so money always reads one step behind the unit you chose",
|
|
680
|
+
chosen
|
|
681
|
+
? "readable at this volume: " + chosen.unit + " in " + chosen.months_to_read + " month(s) - fund that one test and set the read-out date before it starts"
|
|
682
|
+
: "nothing in this funnel is readable within " + horizon + " month(s): pick a more frequent unit or write down that the month produces a progress report"
|
|
683
|
+
];
|
|
684
|
+
return {
|
|
685
|
+
steps: steps.length, lift: lift, power: power, alpha: alpha, horizon_months: horizon,
|
|
686
|
+
units: units,
|
|
687
|
+
readable_unit: chosen ? chosen.unit : "",
|
|
688
|
+
readable_months: chosen ? chosen.months_to_read : 0,
|
|
689
|
+
unreadable_units: unreadable,
|
|
690
|
+
gates: gates
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function runUnit(opts) {
|
|
695
|
+
const spec = opts.steps;
|
|
696
|
+
const lift = Number(opts.lift !== undefined ? opts.lift : 0.2);
|
|
697
|
+
const power = Number(opts.power !== undefined ? opts.power : 0.8);
|
|
698
|
+
const alpha = Number(opts.alpha !== undefined ? opts.alpha : 0.05);
|
|
699
|
+
const horizon = Number(opts.horizon !== undefined ? opts.horizon : 6);
|
|
700
|
+
const problems = [];
|
|
701
|
+
let steps = [];
|
|
702
|
+
if (typeof spec !== "string" || spec.length === 0) {
|
|
703
|
+
problems.push("steps: pass --steps visits:12000,signups:900,activations:270,paid:40");
|
|
704
|
+
} else {
|
|
705
|
+
spec.split(",").forEach(function (part) {
|
|
706
|
+
const bits = part.split(":");
|
|
707
|
+
if (bits.length !== 2 || !bits[0] || !(Number(bits[1]) > 0)) {
|
|
708
|
+
problems.push("steps: '" + part.trim() + "' must look like name:events_per_month");
|
|
709
|
+
} else {
|
|
710
|
+
steps.push({ name: bits[0].trim(), count: Number(bits[1]) });
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
if (problems.length) { console.error("unit: " + problems.join("; ")); return 1; }
|
|
715
|
+
const out = planUnits({ steps: steps, lift: lift, power: power, alpha: alpha, horizon: horizon });
|
|
716
|
+
if (out.problems) { console.error("unit: " + out.problems.join("; ")); return 1; }
|
|
717
|
+
if (opts.json) { console.log(JSON.stringify(out, null, 2)); } else {
|
|
718
|
+
out.units.forEach(function (u) {
|
|
719
|
+
console.log("unit " + u.unit + ": rate " + (u.rate * 100).toFixed(2) + "%, floor " + fmtNum(u.floor_per_arm) +
|
|
720
|
+
" per arm, " + fmtNum(u.arm_events_per_month) + " events per arm a month -> " + u.months_to_read + " month(s) to read");
|
|
721
|
+
});
|
|
722
|
+
if (out.readable_unit) {
|
|
723
|
+
console.log("readable at this volume: " + out.readable_unit + " in " + out.readable_months + " month(s)");
|
|
724
|
+
} else {
|
|
725
|
+
console.log("readable at this volume: nothing within " + horizon + " month(s)");
|
|
726
|
+
}
|
|
727
|
+
if (out.unreadable_units.length) { console.log("out of reach: " + out.unreadable_units.join(", ")); }
|
|
728
|
+
out.gates.forEach(function (g) { console.log("gate: " + g); });
|
|
729
|
+
}
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
570
733
|
function fmtNum(n) {
|
|
571
734
|
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
572
735
|
}
|
|
@@ -701,6 +864,10 @@ if (cmd === "sample") {
|
|
|
701
864
|
process.exit(runVerdict(flags(rest)));
|
|
702
865
|
} else if (cmd === "queue") {
|
|
703
866
|
process.exit(runQueue(flags(rest)));
|
|
867
|
+
} else if (cmd === "unit") {
|
|
868
|
+
process.exit(runUnit(flags(rest)));
|
|
869
|
+
} else if (cmd === "cost") {
|
|
870
|
+
process.exit(runCost(flags(rest)));
|
|
704
871
|
} else if (cmd === "aeo") {
|
|
705
872
|
process.exit(runAeo(flags(rest)));
|
|
706
873
|
} else if (cmd === "brief") {
|
|
@@ -745,6 +912,10 @@ Usage:
|
|
|
745
912
|
--base 0.03 --lift 0.2 --daily 500 [--weeks 4] [--start 2026-09-21]
|
|
746
913
|
npx marketing-mindset verdict was the difference real, and was the volume ever enough?
|
|
747
914
|
npx marketing-mindset queue how many honest tests your volume can finish, and when each reads
|
|
915
|
+
npx marketing-mindset cost what one readable verdict costs to finish, and how many a budget funds
|
|
916
|
+
npx marketing-mindset unit which step of your funnel can fund a verdict, and how many months it needs
|
|
917
|
+
--steps visits:12000,signups:900,activations:270,paid:40 [--lift 0.2] [--power 0.8] [--alpha 0.05] [--horizon 6]
|
|
918
|
+
--cost 0.02 --daily 500 [--budget 5000] [--base 0.03] [--lift 0.2] [--power 0.8] [--alpha 0.05]
|
|
748
919
|
--daily 500 --days 90 --start 2026-09-21 [--base 0.03] [--lift 0.2] [--power 0.8] [--alpha 0.05]
|
|
749
920
|
--a 120/4000 --b 168/4000 [--alpha 0.05] [--json]
|
|
750
921
|
npx marketing-mindset mde what a volume you can afford can actually read
|
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, honest verdicts on finished tests, the lift a volume can read, a queue of readable tests with read-out dates,
|
|
3
|
+
"version": "1.12.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, which funnel step can fund a verdict, the lift a volume can read, a queue of readable tests with read-out dates, the cost of one readable verdict. npx marketing-mindset sample|kill|verdict|unit|queue|cost|mde|budget|brief|plan|warmup|aeo|jd|sections|read|install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skill",
|
|
7
7
|
"skill",
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"prompt",
|
|
15
15
|
"sample-size",
|
|
16
16
|
"test-queue",
|
|
17
|
+
"cost-per-verdict",
|
|
18
|
+
"funnel-unit",
|
|
17
19
|
"ab-test-verdict",
|
|
18
20
|
"minimum-detectable-effect",
|
|
19
21
|
"job-description",
|