@idlekit/cli 0.5.0 → 0.6.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 +25 -0
- package/dist/main.js +1768 -1425
- package/dist/main.js.map +26 -18
- package/dist/runtime/workers/entry.js +655 -25
- package/dist/runtime/workers/entry.js.map +15 -8
- package/package.json +2 -2
|
@@ -11316,6 +11316,63 @@ function logicalDrawId(drawIndex) {
|
|
|
11316
11316
|
return String(drawIndex);
|
|
11317
11317
|
}
|
|
11318
11318
|
|
|
11319
|
+
// ../core/dist/sim/sessionOffline.js
|
|
11320
|
+
function validateCreditPolicy(policy) {
|
|
11321
|
+
if (!policy || typeof policy !== "object" || Array.isArray(policy) || "actions" in policy) {
|
|
11322
|
+
throw new RangeError("prepareOffline policy must be a credit policy; set actions on scenario.run.offline");
|
|
11323
|
+
}
|
|
11324
|
+
if (policy.maxSec !== undefined && (typeof policy.maxSec !== "number" || !Number.isFinite(policy.maxSec) || policy.maxSec < 0)) {
|
|
11325
|
+
throw new RangeError("prepareOffline policy.maxSec must be finite and nonnegative");
|
|
11326
|
+
}
|
|
11327
|
+
if (policy.overflowPolicy !== undefined && policy.overflowPolicy !== "clamp" && policy.overflowPolicy !== "reject") {
|
|
11328
|
+
throw new RangeError("prepareOffline policy.overflowPolicy must be clamp or reject");
|
|
11329
|
+
}
|
|
11330
|
+
const decay = policy.decay;
|
|
11331
|
+
if (decay !== undefined) {
|
|
11332
|
+
if (!decay || typeof decay !== "object" || Array.isArray(decay) || decay.kind !== "none" && decay.kind !== "linear") {
|
|
11333
|
+
throw new RangeError("prepareOffline policy.decay must select none or linear");
|
|
11334
|
+
}
|
|
11335
|
+
if (decay.floorRatio !== undefined && (typeof decay.floorRatio !== "number" || !Number.isFinite(decay.floorRatio) || decay.floorRatio < 0 || decay.floorRatio > 1)) {
|
|
11336
|
+
throw new RangeError("prepareOffline policy.decay.floorRatio must be finite and in 0..1");
|
|
11337
|
+
}
|
|
11338
|
+
}
|
|
11339
|
+
}
|
|
11340
|
+
function prepareOfflineSegment(args) {
|
|
11341
|
+
const { scenario, prepare } = args;
|
|
11342
|
+
if (!prepare)
|
|
11343
|
+
return { model: scenario.model, policy: scenario.run.offline, stepSec: scenario.run.stepSec };
|
|
11344
|
+
const prepared = prepare({
|
|
11345
|
+
ctx: scenario.ctx,
|
|
11346
|
+
model: scenario.model,
|
|
11347
|
+
state: deepClonePreservingPrototype(args.state),
|
|
11348
|
+
seconds: args.seconds,
|
|
11349
|
+
wallStartT: args.wallStartT,
|
|
11350
|
+
wallEndT: args.wallEndT,
|
|
11351
|
+
...scenario.run.offline ? { policy: deepClonePreservingPrototype(scenario.run.offline) } : {}
|
|
11352
|
+
});
|
|
11353
|
+
if (!prepared || typeof prepared !== "object" || Array.isArray(prepared) || "then" in prepared) {
|
|
11354
|
+
throw new RangeError("prepareOffline must synchronously return a preparation object");
|
|
11355
|
+
}
|
|
11356
|
+
if (prepared.stepSec !== undefined && (typeof prepared.stepSec !== "number" || !Number.isFinite(prepared.stepSec) || prepared.stepSec <= 0)) {
|
|
11357
|
+
throw new RangeError("prepareOffline stepSec must be finite and positive");
|
|
11358
|
+
}
|
|
11359
|
+
if (prepared.policy !== undefined)
|
|
11360
|
+
validateCreditPolicy(prepared.policy);
|
|
11361
|
+
const model = prepared.model === undefined ? scenario.model : prepared.model;
|
|
11362
|
+
if (!model || typeof model !== "object" || typeof model.income !== "function" || typeof model.actions !== "function") {
|
|
11363
|
+
throw new RangeError("prepareOffline model must provide income and actions");
|
|
11364
|
+
}
|
|
11365
|
+
return {
|
|
11366
|
+
model,
|
|
11367
|
+
stepSec: prepared.stepSec ?? scenario.run.stepSec,
|
|
11368
|
+
policy: prepared.policy === undefined ? scenario.run.offline : {
|
|
11369
|
+
...deepClonePreservingPrototype(prepared.policy),
|
|
11370
|
+
actions: scenario.run.offline?.actions
|
|
11371
|
+
}
|
|
11372
|
+
};
|
|
11373
|
+
}
|
|
11374
|
+
var init_sessionOffline = () => {};
|
|
11375
|
+
|
|
11319
11376
|
// ../core/dist/sim/session.js
|
|
11320
11377
|
function buildBlocks(pattern) {
|
|
11321
11378
|
const blocks = [];
|
|
@@ -11411,6 +11468,9 @@ function withClocks(scenario, wallT, wallEndT, rewardT, activeT) {
|
|
|
11411
11468
|
};
|
|
11412
11469
|
}
|
|
11413
11470
|
function simulateSessionPattern(args) {
|
|
11471
|
+
if (args.prepareOffline !== undefined && typeof args.prepareOffline !== "function") {
|
|
11472
|
+
throw new TypeError("prepareOffline must be a function");
|
|
11473
|
+
}
|
|
11414
11474
|
const sc = args.seed === undefined ? args.scenario : { ...args.scenario, ctx: { ...args.scenario.ctx, seed: args.seed } };
|
|
11415
11475
|
const start = sc.initial;
|
|
11416
11476
|
const horizonSec = args.pattern.days * 86400;
|
|
@@ -11497,7 +11557,19 @@ function simulateSessionPattern(args) {
|
|
|
11497
11557
|
return;
|
|
11498
11558
|
const requested = wallEnd - wallT;
|
|
11499
11559
|
const wallStart = wallT;
|
|
11500
|
-
|
|
11560
|
+
let segment = segmentScenario(wallStart, wallEnd);
|
|
11561
|
+
const prepared = prepareOfflineSegment({
|
|
11562
|
+
scenario: segment,
|
|
11563
|
+
state,
|
|
11564
|
+
seconds: requested,
|
|
11565
|
+
wallStartT: wallStart,
|
|
11566
|
+
wallEndT: wallEnd,
|
|
11567
|
+
prepare: args.prepareOffline
|
|
11568
|
+
});
|
|
11569
|
+
if (prepared.model !== segment.model) {
|
|
11570
|
+
const { clocks: _clocks, ...ctx } = segment.ctx;
|
|
11571
|
+
segment = withClocks({ ...segment, ctx, model: prepared.model }, wallStart, wallEnd, state.t, activeSec);
|
|
11572
|
+
}
|
|
11501
11573
|
const offlineRun = applyOfflineSeconds({
|
|
11502
11574
|
scenario: segment,
|
|
11503
11575
|
seconds: requested,
|
|
@@ -11509,15 +11581,16 @@ function simulateSessionPattern(args) {
|
|
|
11509
11581
|
enabled: sc.run.eventLog?.enabled ?? true,
|
|
11510
11582
|
maxEvents: sc.run.eventLog?.maxEvents
|
|
11511
11583
|
},
|
|
11512
|
-
|
|
11584
|
+
stepSec: prepared.stepSec,
|
|
11585
|
+
policy: prepared.policy,
|
|
11513
11586
|
...stopFn ? { until: stopFn } : {}
|
|
11514
11587
|
}
|
|
11515
11588
|
});
|
|
11516
11589
|
const credited = offlineRun.offline.simulatedSec;
|
|
11517
11590
|
const reason = classify();
|
|
11518
|
-
const absence = reason ? offlineAbsenceForCredit(credited, requested,
|
|
11591
|
+
const absence = reason ? offlineAbsenceForCredit(credited, requested, prepared.policy) : requested;
|
|
11519
11592
|
const gapEnd = wallStart + absence;
|
|
11520
|
-
const effective = absence === requested ? offlineRun.offline.effectiveSec : resolveOfflineSeconds(absence,
|
|
11593
|
+
const effective = absence === requested ? offlineRun.offline.effectiveSec : resolveOfflineSeconds(absence, prepared.policy).effectiveSec;
|
|
11521
11594
|
const lost = Math.max(0, absence - effective);
|
|
11522
11595
|
offlineElapsedSec += absence;
|
|
11523
11596
|
offlineCreditedSec += credited;
|
|
@@ -11688,6 +11761,7 @@ var init_session = __esm(() => {
|
|
|
11688
11761
|
init_goalRead();
|
|
11689
11762
|
init_observation();
|
|
11690
11763
|
init_offline();
|
|
11764
|
+
init_sessionOffline();
|
|
11691
11765
|
init_simulator();
|
|
11692
11766
|
});
|
|
11693
11767
|
|
|
@@ -12683,11 +12757,19 @@ var init_params2 = __esm(() => {
|
|
|
12683
12757
|
}));
|
|
12684
12758
|
});
|
|
12685
12759
|
|
|
12686
|
-
// ../core/dist/sim/strategy/opt/
|
|
12687
|
-
function
|
|
12688
|
-
|
|
12689
|
-
|
|
12760
|
+
// ../core/dist/sim/strategy/opt/finiteAmountLog10.js
|
|
12761
|
+
function finiteAmountLog10(E, amount) {
|
|
12762
|
+
if (!E.isFinite(amount))
|
|
12763
|
+
throw new RangeError("Tuning requires finite monetary amounts in the selected engine");
|
|
12764
|
+
const value = E.absLog10(amount);
|
|
12765
|
+
if (value === Number.NEGATIVE_INFINITY)
|
|
12766
|
+
return -300;
|
|
12767
|
+
if (!Number.isFinite(value))
|
|
12768
|
+
throw new RangeError("Tuning requires a finite monetary logarithm");
|
|
12769
|
+
return value;
|
|
12690
12770
|
}
|
|
12771
|
+
|
|
12772
|
+
// ../core/dist/sim/strategy/opt/objectives/builtins.js
|
|
12691
12773
|
var builtinObjectiveFactories;
|
|
12692
12774
|
var init_builtins2 = __esm(() => {
|
|
12693
12775
|
init_parseMoney2();
|
|
@@ -12701,7 +12783,7 @@ var init_builtins2 = __esm(() => {
|
|
|
12701
12783
|
id: "endMoneyLog10",
|
|
12702
12784
|
score: ({ scenario, run }) => {
|
|
12703
12785
|
const { E } = scenario.ctx;
|
|
12704
|
-
return
|
|
12786
|
+
return finiteAmountLog10(E, run.end.wallet.money.amount);
|
|
12705
12787
|
}
|
|
12706
12788
|
})
|
|
12707
12789
|
},
|
|
@@ -12714,7 +12796,7 @@ var init_builtins2 = __esm(() => {
|
|
|
12714
12796
|
score: ({ scenario, run }) => {
|
|
12715
12797
|
const { E } = scenario.ctx;
|
|
12716
12798
|
const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
|
|
12717
|
-
return
|
|
12799
|
+
return finiteAmountLog10(E, worth.amount);
|
|
12718
12800
|
}
|
|
12719
12801
|
})
|
|
12720
12802
|
},
|
|
@@ -12729,7 +12811,7 @@ var init_builtins2 = __esm(() => {
|
|
|
12729
12811
|
const sec = Math.max(1, runElapsedSec(run));
|
|
12730
12812
|
const hours = sec / 3600;
|
|
12731
12813
|
const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
|
|
12732
|
-
return
|
|
12814
|
+
return finiteAmountLog10(E, worth.amount) - Math.log10(Math.max(0.000000000001, hours));
|
|
12733
12815
|
}
|
|
12734
12816
|
})
|
|
12735
12817
|
},
|
|
@@ -12743,7 +12825,7 @@ var init_builtins2 = __esm(() => {
|
|
|
12743
12825
|
const { E } = scenario.ctx;
|
|
12744
12826
|
const sec = Math.max(1, runElapsedSec(run));
|
|
12745
12827
|
const hours = sec / 3600;
|
|
12746
|
-
return
|
|
12828
|
+
return finiteAmountLog10(E, run.end.prestige.points) - Math.log10(Math.max(0.000000000001, hours));
|
|
12747
12829
|
}
|
|
12748
12830
|
})
|
|
12749
12831
|
},
|
|
@@ -12758,7 +12840,7 @@ var init_builtins2 = __esm(() => {
|
|
|
12758
12840
|
const sec = Math.max(1, runElapsedSec(run));
|
|
12759
12841
|
const startWorth = scenario.model.netWorth?.(scenario.ctx, run.start) ?? run.start.wallet.money;
|
|
12760
12842
|
const endWorth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
|
|
12761
|
-
const deltaLog =
|
|
12843
|
+
const deltaLog = finiteAmountLog10(E, endWorth.amount) - finiteAmountLog10(E, startWorth.amount);
|
|
12762
12844
|
return deltaLog / (sec / 3600);
|
|
12763
12845
|
}
|
|
12764
12846
|
})
|
|
@@ -12784,6 +12866,9 @@ var init_builtins2 = __esm(() => {
|
|
|
12784
12866
|
suffix: { kind: "alphaInfinite", minLen: 2 }
|
|
12785
12867
|
}).amount;
|
|
12786
12868
|
const endWorth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
|
|
12869
|
+
if (!E.isFinite(threshold) || !E.isFinite(endWorth.amount)) {
|
|
12870
|
+
throw new RangeError("ETA objective requires finite target and net worth in the selected engine");
|
|
12871
|
+
}
|
|
12787
12872
|
const reached = E.cmp(endWorth.amount, threshold) >= 0;
|
|
12788
12873
|
if (!reached)
|
|
12789
12874
|
return -(params.unreachedPenaltySec ?? 1e9);
|
|
@@ -12813,7 +12898,7 @@ var init_builtins2 = __esm(() => {
|
|
|
12813
12898
|
const sec = Math.max(1, runElapsedSec(run));
|
|
12814
12899
|
const hours = sec / 3600;
|
|
12815
12900
|
const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
|
|
12816
|
-
const worthLog =
|
|
12901
|
+
const worthLog = finiteAmountLog10(E, worth.amount);
|
|
12817
12902
|
const stats = run.stats;
|
|
12818
12903
|
if (!stats || stats.money.droppedRate === null || stats.actions.status === "missing") {
|
|
12819
12904
|
throw new Error("pacingBalancedLog10 needs observed money and action counters, but this run has none (run.observation.enabled is false)");
|
|
@@ -13012,8 +13097,45 @@ function cloneCandidateInitial(state) {
|
|
|
13012
13097
|
}
|
|
13013
13098
|
var init_freshRun = () => {};
|
|
13014
13099
|
|
|
13100
|
+
// ../core/dist/sim/strategy/opt/finiteMean.js
|
|
13101
|
+
function finiteMean(values) {
|
|
13102
|
+
if (values.length === 0)
|
|
13103
|
+
throw new RangeError("A mean requires at least one value");
|
|
13104
|
+
const bits = new DataView(new ArrayBuffer(8));
|
|
13105
|
+
let total = 0n;
|
|
13106
|
+
for (const value of values) {
|
|
13107
|
+
if (!Number.isFinite(value))
|
|
13108
|
+
throw new RangeError("A mean requires finite values");
|
|
13109
|
+
bits.setFloat64(0, value, false);
|
|
13110
|
+
const high = bits.getUint32(0, false);
|
|
13111
|
+
const low = bits.getUint32(4, false);
|
|
13112
|
+
const exponent = high >>> 20 & 2047;
|
|
13113
|
+
const fraction = BigInt(high & 1048575) << 32n | BigInt(low);
|
|
13114
|
+
const significand = exponent === 0 ? fraction : 1n << 52n | fraction;
|
|
13115
|
+
const units = significand << BigInt(Math.max(0, exponent - 1));
|
|
13116
|
+
total += high >>> 31 ? -units : units;
|
|
13117
|
+
}
|
|
13118
|
+
if (total === 0n)
|
|
13119
|
+
return 0;
|
|
13120
|
+
const negative = total < 0n;
|
|
13121
|
+
const magnitude = negative ? -total : total;
|
|
13122
|
+
const count = BigInt(values.length);
|
|
13123
|
+
const integerMean = magnitude / count;
|
|
13124
|
+
const shift = Math.max(0, integerMean.toString(2).length - 53);
|
|
13125
|
+
const denominator = count << BigInt(shift);
|
|
13126
|
+
let significand = magnitude / denominator;
|
|
13127
|
+
const twiceRemainder = 2n * (magnitude % denominator);
|
|
13128
|
+
if (twiceRemainder > denominator || twiceRemainder === denominator && (significand & 1n) !== 0n) {
|
|
13129
|
+
significand++;
|
|
13130
|
+
}
|
|
13131
|
+
const mean = Number(significand) * 2 ** (shift - 1074);
|
|
13132
|
+
return negative ? -mean : mean;
|
|
13133
|
+
}
|
|
13134
|
+
|
|
13015
13135
|
// ../core/dist/sim/strategy/opt/runner.js
|
|
13016
13136
|
function runCandidateAndScore(args) {
|
|
13137
|
+
if (args.seeds.length === 0)
|
|
13138
|
+
throw new RangeError("Tuning requires at least one seed");
|
|
13017
13139
|
const stratFactory = args.strategyRegistry.get(args.strategyId);
|
|
13018
13140
|
if (!stratFactory)
|
|
13019
13141
|
throw new Error(`Unknown strategy: ${args.strategyId}`);
|
|
@@ -13060,24 +13182,28 @@ function runCandidateAndScore(args) {
|
|
|
13060
13182
|
const sc = openScenario();
|
|
13061
13183
|
const run = runScenario(sc);
|
|
13062
13184
|
assertHorizonReached(run, "runCandidateAndScore");
|
|
13185
|
+
const worth = sc.model.netWorth?.(sc.ctx, run.end) ?? run.end.wallet.money;
|
|
13186
|
+
const endMoneyLog10 = finiteAmountLog10(sc.ctx.E, run.end.wallet.money.amount);
|
|
13187
|
+
const endNetWorthLog10 = finiteAmountLog10(sc.ctx.E, worth.amount);
|
|
13063
13188
|
const s = objective.score({ scenario: sc, run, evaluation: {
|
|
13064
13189
|
open: openScenario,
|
|
13065
13190
|
registries: { models: args.modelRegistry, strategies: args.strategyRegistry },
|
|
13066
13191
|
isolation: { model: args.model, statefulModel: args.statefulModel, strategy: { id: args.strategyId, params: candidateParams } }
|
|
13067
13192
|
} });
|
|
13193
|
+
if (!Number.isFinite(s))
|
|
13194
|
+
throw new RangeError(`Objective ${args.objectiveId} must return a finite score (seed ${seed})`);
|
|
13068
13195
|
seedScores.push(s);
|
|
13069
|
-
const worth = sc.model.netWorth?.(sc.ctx, run.end) ?? run.end.wallet.money;
|
|
13070
13196
|
seedResults.push({
|
|
13071
13197
|
seed,
|
|
13072
13198
|
score: s,
|
|
13073
13199
|
durationSec: Math.max(0, runElapsedSec(run)),
|
|
13074
|
-
endMoneyLog10
|
|
13075
|
-
endNetWorthLog10
|
|
13200
|
+
endMoneyLog10,
|
|
13201
|
+
endNetWorthLog10,
|
|
13076
13202
|
droppedRate: run.stats?.money.droppedRate ?? null,
|
|
13077
13203
|
actionsApplied: run.stats && run.stats.actions.status === "observed" ? run.stats.actions.applied : null
|
|
13078
13204
|
});
|
|
13079
13205
|
}
|
|
13080
|
-
const score =
|
|
13206
|
+
const score = finiteMean(seedScores);
|
|
13081
13207
|
return { score, seedScores, seedResults };
|
|
13082
13208
|
}
|
|
13083
13209
|
var init_runner = __esm(() => {
|
|
@@ -14064,7 +14190,7 @@ function compensatedSum(values, scale) {
|
|
|
14064
14190
|
}
|
|
14065
14191
|
return sum + compensation;
|
|
14066
14192
|
}
|
|
14067
|
-
function
|
|
14193
|
+
function finiteMean2(values) {
|
|
14068
14194
|
let smallest = Number.POSITIVE_INFINITY;
|
|
14069
14195
|
let largest = Number.NEGATIVE_INFINITY;
|
|
14070
14196
|
for (const value of values) {
|
|
@@ -14214,7 +14340,7 @@ function analyzeDistribution(request) {
|
|
|
14214
14340
|
}
|
|
14215
14341
|
const eligible = observed.length + rightCensored;
|
|
14216
14342
|
const completionRate = eligible > 0 ? observed.length / eligible : null;
|
|
14217
|
-
const arithmetic = observed.length > 0 ?
|
|
14343
|
+
const arithmetic = observed.length > 0 ? finiteMean2(observed) : null;
|
|
14218
14344
|
const identical = observed.every((value) => value === observed[0]);
|
|
14219
14345
|
const rawVariance = arithmetic === null ? null : finiteVariance(observed, arithmetic);
|
|
14220
14346
|
const variance = rawVariance === 0 && !identical ? null : rawVariance;
|
|
@@ -19020,6 +19146,508 @@ var init_jevAdvisory = __esm(() => {
|
|
|
19020
19146
|
init__createStandardSchema();
|
|
19021
19147
|
});
|
|
19022
19148
|
|
|
19149
|
+
// ../core/dist/ltv/projection.js
|
|
19150
|
+
function clamp012(v) {
|
|
19151
|
+
if (v < 0)
|
|
19152
|
+
return 0;
|
|
19153
|
+
if (v > 1)
|
|
19154
|
+
return 1;
|
|
19155
|
+
return v;
|
|
19156
|
+
}
|
|
19157
|
+
function deriveMonetizationConfig(scenario) {
|
|
19158
|
+
const m = scenario.monetization;
|
|
19159
|
+
const q = (m?.uncertainty?.quantiles ?? [0.5, 0.9]).filter((x) => typeof x === "number");
|
|
19160
|
+
const quantiles = q.length > 0 ? [...new Set(q)].sort((a, b) => a - b) : [0.5, 0.9];
|
|
19161
|
+
return {
|
|
19162
|
+
cohorts: {
|
|
19163
|
+
baseUsers: m?.cohorts?.baseUsers ?? 1
|
|
19164
|
+
},
|
|
19165
|
+
retention: {
|
|
19166
|
+
d1: m?.retention?.d1 ?? 0.42,
|
|
19167
|
+
d7: m?.retention?.d7 ?? 0.2,
|
|
19168
|
+
d30: m?.retention?.d30 ?? 0.09,
|
|
19169
|
+
d90: m?.retention?.d90 ?? 0.04,
|
|
19170
|
+
longTailDailyDecay: m?.retention?.longTailDailyDecay ?? 0.02
|
|
19171
|
+
},
|
|
19172
|
+
revenue: {
|
|
19173
|
+
payerConversion: m?.revenue?.payerConversion ?? 0.03,
|
|
19174
|
+
arppuDaily: m?.revenue?.arppuDaily ?? 0.7,
|
|
19175
|
+
adArpDau: m?.revenue?.adArpDau ?? 0.02,
|
|
19176
|
+
platformFeeRate: m?.revenue?.platformFeeRate ?? 0.3,
|
|
19177
|
+
grossMarginRate: m?.revenue?.grossMarginRate ?? 0.92,
|
|
19178
|
+
progressionRevenueLift: m?.revenue?.progressionRevenueLift ?? 0.4,
|
|
19179
|
+
progressionLogSpan: m?.revenue?.progressionLogSpan ?? 6
|
|
19180
|
+
},
|
|
19181
|
+
acquisition: {
|
|
19182
|
+
cpi: m?.acquisition?.cpi ?? 0
|
|
19183
|
+
},
|
|
19184
|
+
uncertainty: {
|
|
19185
|
+
enabled: m?.uncertainty?.enabled ?? false,
|
|
19186
|
+
draws: m?.uncertainty?.draws ?? 300,
|
|
19187
|
+
quantiles,
|
|
19188
|
+
seed: m?.uncertainty?.seed,
|
|
19189
|
+
sigma: {
|
|
19190
|
+
retention: m?.uncertainty?.sigma?.retention ?? 0.08,
|
|
19191
|
+
conversion: m?.uncertainty?.sigma?.conversion ?? 0.12,
|
|
19192
|
+
arppu: m?.uncertainty?.sigma?.arppu ?? 0.2,
|
|
19193
|
+
ad: m?.uncertainty?.sigma?.ad ?? 0.15
|
|
19194
|
+
},
|
|
19195
|
+
correlation: {
|
|
19196
|
+
retentionConversion: m?.uncertainty?.correlation?.retentionConversion ?? 0,
|
|
19197
|
+
retentionArppu: m?.uncertainty?.correlation?.retentionArppu ?? 0,
|
|
19198
|
+
retentionAd: m?.uncertainty?.correlation?.retentionAd ?? 0,
|
|
19199
|
+
conversionArppu: m?.uncertainty?.correlation?.conversionArppu ?? 0,
|
|
19200
|
+
conversionAd: m?.uncertainty?.correlation?.conversionAd ?? 0,
|
|
19201
|
+
arppuAd: m?.uncertainty?.correlation?.arppuAd ?? 0
|
|
19202
|
+
}
|
|
19203
|
+
}
|
|
19204
|
+
};
|
|
19205
|
+
}
|
|
19206
|
+
function retentionAtDay(ret, day) {
|
|
19207
|
+
if (day <= 0)
|
|
19208
|
+
return 1;
|
|
19209
|
+
if (day <= 1)
|
|
19210
|
+
return ret.d1;
|
|
19211
|
+
if (day <= 7)
|
|
19212
|
+
return ret.d1 + (ret.d7 - ret.d1) * (day - 1) / 6;
|
|
19213
|
+
if (day <= 30)
|
|
19214
|
+
return ret.d7 + (ret.d30 - ret.d7) * (day - 7) / 23;
|
|
19215
|
+
if (day <= 90)
|
|
19216
|
+
return ret.d30 + (ret.d90 - ret.d30) * (day - 30) / 60;
|
|
19217
|
+
const extra = day - 90;
|
|
19218
|
+
return clamp012(ret.d90 * Math.exp(-ret.longTailDailyDecay * extra));
|
|
19219
|
+
}
|
|
19220
|
+
function progressionFactor(startWorthLog10, endWorthLog10, span) {
|
|
19221
|
+
if (!Number.isFinite(startWorthLog10) || !Number.isFinite(endWorthLog10) || !Number.isFinite(span) || span <= 0) {
|
|
19222
|
+
return 0;
|
|
19223
|
+
}
|
|
19224
|
+
return clamp012((endWorthLog10 - startWorthLog10) / span);
|
|
19225
|
+
}
|
|
19226
|
+
function estimateLtvPerUser(args) {
|
|
19227
|
+
const { config, horizonDays } = args;
|
|
19228
|
+
const progression = clamp012(args.progression);
|
|
19229
|
+
const arppuDaily = config.revenue.arppuDaily * (1 + progression * config.revenue.progressionRevenueLift);
|
|
19230
|
+
const adArpDau = config.revenue.adArpDau * (1 + progression * config.revenue.progressionRevenueLift * 0.5);
|
|
19231
|
+
const payerConversion = config.revenue.payerConversion;
|
|
19232
|
+
const fullDays = Math.floor(Math.max(0, horizonDays));
|
|
19233
|
+
const remainder = Math.max(0, horizonDays - fullDays);
|
|
19234
|
+
let gross = 0;
|
|
19235
|
+
for (let day = 1;day <= fullDays; day++) {
|
|
19236
|
+
const active = retentionAtDay(config.retention, day);
|
|
19237
|
+
gross += active * (payerConversion * arppuDaily + adArpDau);
|
|
19238
|
+
}
|
|
19239
|
+
if (remainder > 0) {
|
|
19240
|
+
const active = retentionAtDay(config.retention, fullDays + 1);
|
|
19241
|
+
gross += remainder * active * (payerConversion * arppuDaily + adArpDau);
|
|
19242
|
+
}
|
|
19243
|
+
const net = gross * (1 - config.revenue.platformFeeRate) * config.revenue.grossMarginRate;
|
|
19244
|
+
const ltv = net - config.acquisition.cpi;
|
|
19245
|
+
return {
|
|
19246
|
+
cumulativeGrossRevenuePerUser: gross,
|
|
19247
|
+
cumulativeNetRevenuePerUser: net,
|
|
19248
|
+
cumulativeLtvPerUser: ltv
|
|
19249
|
+
};
|
|
19250
|
+
}
|
|
19251
|
+
|
|
19252
|
+
// ../core/dist/ltv/uncertainty.js
|
|
19253
|
+
function clamp013(v) {
|
|
19254
|
+
if (v < 0)
|
|
19255
|
+
return 0;
|
|
19256
|
+
if (v > 1)
|
|
19257
|
+
return 1;
|
|
19258
|
+
return v;
|
|
19259
|
+
}
|
|
19260
|
+
function clampCorrelation(v) {
|
|
19261
|
+
if (!Number.isFinite(v))
|
|
19262
|
+
return 0;
|
|
19263
|
+
if (v < -1)
|
|
19264
|
+
return -1;
|
|
19265
|
+
if (v > 1)
|
|
19266
|
+
return 1;
|
|
19267
|
+
return v;
|
|
19268
|
+
}
|
|
19269
|
+
function randNormal(rng) {
|
|
19270
|
+
const u1 = Math.max(0.000000000001, rng());
|
|
19271
|
+
const u2 = Math.max(0.000000000001, rng());
|
|
19272
|
+
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
19273
|
+
}
|
|
19274
|
+
function buildCorrelationMatrix(base) {
|
|
19275
|
+
const rc = clampCorrelation(base.retentionConversion);
|
|
19276
|
+
const ra = clampCorrelation(base.retentionArppu);
|
|
19277
|
+
const rd = clampCorrelation(base.retentionAd);
|
|
19278
|
+
const ca = clampCorrelation(base.conversionArppu);
|
|
19279
|
+
const cd = clampCorrelation(base.conversionAd);
|
|
19280
|
+
const ad = clampCorrelation(base.arppuAd);
|
|
19281
|
+
return [
|
|
19282
|
+
[1, rc, ra, rd],
|
|
19283
|
+
[rc, 1, ca, cd],
|
|
19284
|
+
[ra, ca, 1, ad],
|
|
19285
|
+
[rd, cd, ad, 1]
|
|
19286
|
+
];
|
|
19287
|
+
}
|
|
19288
|
+
function withOffDiagonalScale(matrix, alpha) {
|
|
19289
|
+
return matrix.map((row, i) => row.map((v, j) => {
|
|
19290
|
+
if (i === j)
|
|
19291
|
+
return 1;
|
|
19292
|
+
return v * alpha;
|
|
19293
|
+
}));
|
|
19294
|
+
}
|
|
19295
|
+
function cholesky4(matrix) {
|
|
19296
|
+
const n = 4;
|
|
19297
|
+
const l = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
|
|
19298
|
+
for (let i = 0;i < n; i++) {
|
|
19299
|
+
for (let j = 0;j <= i; j++) {
|
|
19300
|
+
let sum = 0;
|
|
19301
|
+
for (let k = 0;k < j; k++) {
|
|
19302
|
+
sum += (l[i]?.[k] ?? 0) * (l[j]?.[k] ?? 0);
|
|
19303
|
+
}
|
|
19304
|
+
if (i === j) {
|
|
19305
|
+
const val = (matrix[i]?.[i] ?? 0) - sum;
|
|
19306
|
+
if (val <= 0.000000001)
|
|
19307
|
+
return null;
|
|
19308
|
+
l[i][j] = Math.sqrt(val);
|
|
19309
|
+
} else {
|
|
19310
|
+
const denom = l[j]?.[j] ?? 0;
|
|
19311
|
+
if (Math.abs(denom) < 0.000000001)
|
|
19312
|
+
return null;
|
|
19313
|
+
l[i][j] = ((matrix[i]?.[j] ?? 0) - sum) / denom;
|
|
19314
|
+
}
|
|
19315
|
+
}
|
|
19316
|
+
}
|
|
19317
|
+
return l;
|
|
19318
|
+
}
|
|
19319
|
+
function multiplyLowerTriangular(l, z) {
|
|
19320
|
+
const out = [0, 0, 0, 0];
|
|
19321
|
+
for (let i = 0;i < 4; i++) {
|
|
19322
|
+
let sum = 0;
|
|
19323
|
+
for (let j = 0;j <= i; j++) {
|
|
19324
|
+
sum += (l[i]?.[j] ?? 0) * (z[j] ?? 0);
|
|
19325
|
+
}
|
|
19326
|
+
out[i] = sum;
|
|
19327
|
+
}
|
|
19328
|
+
return out;
|
|
19329
|
+
}
|
|
19330
|
+
function sampleCorrelatedNormals(rng, corr) {
|
|
19331
|
+
const z = [randNormal(rng), randNormal(rng), randNormal(rng), randNormal(rng)];
|
|
19332
|
+
const matrix = buildCorrelationMatrix(corr);
|
|
19333
|
+
let l = cholesky4(matrix);
|
|
19334
|
+
let alpha = 0.99;
|
|
19335
|
+
while (!l && alpha > 0.000001) {
|
|
19336
|
+
l = cholesky4(withOffDiagonalScale(matrix, alpha));
|
|
19337
|
+
alpha *= 0.8;
|
|
19338
|
+
}
|
|
19339
|
+
if (!l) {
|
|
19340
|
+
return {
|
|
19341
|
+
retention: z[0] ?? 0,
|
|
19342
|
+
conversion: z[1] ?? 0,
|
|
19343
|
+
arppu: z[2] ?? 0,
|
|
19344
|
+
ad: z[3] ?? 0
|
|
19345
|
+
};
|
|
19346
|
+
}
|
|
19347
|
+
const c = multiplyLowerTriangular(l, z);
|
|
19348
|
+
return {
|
|
19349
|
+
retention: c[0] ?? 0,
|
|
19350
|
+
conversion: c[1] ?? 0,
|
|
19351
|
+
arppu: c[2] ?? 0,
|
|
19352
|
+
ad: c[3] ?? 0
|
|
19353
|
+
};
|
|
19354
|
+
}
|
|
19355
|
+
function applyLogNormalShock(mean, sigma, z) {
|
|
19356
|
+
if (sigma <= 0)
|
|
19357
|
+
return mean;
|
|
19358
|
+
return mean * Math.exp(sigma * z - 0.5 * sigma * sigma);
|
|
19359
|
+
}
|
|
19360
|
+
function makeUncertainConfig(base, rng) {
|
|
19361
|
+
const shock = sampleCorrelatedNormals(rng, base.uncertainty.correlation);
|
|
19362
|
+
const d1 = clamp013(applyLogNormalShock(base.retention.d1, base.uncertainty.sigma.retention, shock.retention));
|
|
19363
|
+
const d7 = Math.min(d1, clamp013(applyLogNormalShock(base.retention.d7, base.uncertainty.sigma.retention, shock.retention)));
|
|
19364
|
+
const d30 = Math.min(d7, clamp013(applyLogNormalShock(base.retention.d30, base.uncertainty.sigma.retention, shock.retention)));
|
|
19365
|
+
const d90 = Math.min(d30, clamp013(applyLogNormalShock(base.retention.d90, base.uncertainty.sigma.retention, shock.retention)));
|
|
19366
|
+
return {
|
|
19367
|
+
...base,
|
|
19368
|
+
retention: {
|
|
19369
|
+
...base.retention,
|
|
19370
|
+
d1,
|
|
19371
|
+
d7,
|
|
19372
|
+
d30,
|
|
19373
|
+
d90
|
|
19374
|
+
},
|
|
19375
|
+
revenue: {
|
|
19376
|
+
...base.revenue,
|
|
19377
|
+
payerConversion: clamp013(applyLogNormalShock(base.revenue.payerConversion, base.uncertainty.sigma.conversion, shock.conversion)),
|
|
19378
|
+
arppuDaily: Math.max(0, applyLogNormalShock(base.revenue.arppuDaily, base.uncertainty.sigma.arppu, shock.arppu)),
|
|
19379
|
+
adArpDau: Math.max(0, applyLogNormalShock(base.revenue.adArpDau, base.uncertainty.sigma.ad, shock.ad))
|
|
19380
|
+
}
|
|
19381
|
+
};
|
|
19382
|
+
}
|
|
19383
|
+
function quantileFromSorted(sorted, q) {
|
|
19384
|
+
if (sorted.length === 0)
|
|
19385
|
+
return 0;
|
|
19386
|
+
const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
|
|
19387
|
+
return sorted[idx] ?? 0;
|
|
19388
|
+
}
|
|
19389
|
+
function estimateLtvDistribution(args) {
|
|
19390
|
+
const rng = mulberry32(args.seed);
|
|
19391
|
+
const grossSamples = [];
|
|
19392
|
+
const netSamples = [];
|
|
19393
|
+
const ltvSamples = [];
|
|
19394
|
+
const draws = Math.max(1, args.draws);
|
|
19395
|
+
for (let i = 0;i < draws; i++) {
|
|
19396
|
+
const sampled = makeUncertainConfig(args.config, rng);
|
|
19397
|
+
const point = estimateLtvPerUser({
|
|
19398
|
+
config: sampled,
|
|
19399
|
+
horizonDays: args.horizonDays,
|
|
19400
|
+
progression: args.progression
|
|
19401
|
+
});
|
|
19402
|
+
grossSamples.push(point.cumulativeGrossRevenuePerUser);
|
|
19403
|
+
netSamples.push(point.cumulativeNetRevenuePerUser);
|
|
19404
|
+
ltvSamples.push(point.cumulativeLtvPerUser);
|
|
19405
|
+
}
|
|
19406
|
+
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length);
|
|
19407
|
+
const sortedLtv = [...ltvSamples].sort((a, b) => a - b);
|
|
19408
|
+
const byQuantile = {};
|
|
19409
|
+
for (const q of args.quantiles) {
|
|
19410
|
+
byQuantile[`q${Math.round(q * 100)}`] = quantileFromSorted(sortedLtv, q);
|
|
19411
|
+
}
|
|
19412
|
+
return {
|
|
19413
|
+
mean: {
|
|
19414
|
+
cumulativeGrossRevenuePerUser: avg(grossSamples),
|
|
19415
|
+
cumulativeNetRevenuePerUser: avg(netSamples),
|
|
19416
|
+
cumulativeLtvPerUser: avg(ltvSamples)
|
|
19417
|
+
},
|
|
19418
|
+
quantiles: byQuantile
|
|
19419
|
+
};
|
|
19420
|
+
}
|
|
19421
|
+
var init_uncertainty = () => {};
|
|
19422
|
+
|
|
19423
|
+
// ../core/dist/ltv/public.js
|
|
19424
|
+
function assertMonetization(monetization) {
|
|
19425
|
+
const valid = validateScenarioV1({
|
|
19426
|
+
schemaVersion: 1,
|
|
19427
|
+
unit: { code: "LTV" },
|
|
19428
|
+
policy: { mode: "drop" },
|
|
19429
|
+
model: { id: "ltv-input", version: 1 },
|
|
19430
|
+
initial: { wallet: { unit: "LTV", amount: "0" } },
|
|
19431
|
+
clock: { stepSec: 1, durationSec: 1 },
|
|
19432
|
+
monetization
|
|
19433
|
+
});
|
|
19434
|
+
if (!valid.ok)
|
|
19435
|
+
throw new RangeError(valid.issues.map((issue) => issue.message).join("; "));
|
|
19436
|
+
}
|
|
19437
|
+
function assertQuantileValues(quantiles) {
|
|
19438
|
+
if (quantiles.length === 0)
|
|
19439
|
+
throw new RangeError("quantiles must be nonempty and in 0..1");
|
|
19440
|
+
for (const q of quantiles) {
|
|
19441
|
+
if (!Number.isFinite(q) || q < 0 || q > 1)
|
|
19442
|
+
throw new RangeError("quantiles must be nonempty and in 0..1");
|
|
19443
|
+
}
|
|
19444
|
+
}
|
|
19445
|
+
function assertConfig(config) {
|
|
19446
|
+
const { retention, ...other } = config;
|
|
19447
|
+
for (const day of ["d1", "d7", "d30", "d90"]) {
|
|
19448
|
+
if (!Number.isFinite(retention[day]) || retention[day] < 0 || retention[day] > 1) {
|
|
19449
|
+
throw new RangeError(`retention.${day} must be finite and in 0..1`);
|
|
19450
|
+
}
|
|
19451
|
+
}
|
|
19452
|
+
if (!Number.isFinite(retention.longTailDailyDecay) || retention.longTailDailyDecay < 0) {
|
|
19453
|
+
throw new RangeError("retention.longTailDailyDecay must be finite and >= 0");
|
|
19454
|
+
}
|
|
19455
|
+
const { quantiles, ...uncertainty } = other.uncertainty;
|
|
19456
|
+
assertQuantileValues(quantiles);
|
|
19457
|
+
assertMonetization({ ...other, uncertainty });
|
|
19458
|
+
}
|
|
19459
|
+
function deriveMonetizationConfig2(scenario) {
|
|
19460
|
+
assertMonetization(scenario.monetization ?? {});
|
|
19461
|
+
const config = deriveMonetizationConfig(scenario);
|
|
19462
|
+
assertConfig(config);
|
|
19463
|
+
return config;
|
|
19464
|
+
}
|
|
19465
|
+
function assertPoint(args) {
|
|
19466
|
+
assertConfig(args.config);
|
|
19467
|
+
if (!Number.isFinite(args.horizonDays) || args.horizonDays < 0 || args.horizonDays > 36500) {
|
|
19468
|
+
throw new RangeError("horizonDays must be finite and in 0..36500");
|
|
19469
|
+
}
|
|
19470
|
+
if (!Number.isFinite(args.progression))
|
|
19471
|
+
throw new RangeError("progression must be finite");
|
|
19472
|
+
}
|
|
19473
|
+
function finiteResult(result) {
|
|
19474
|
+
if (!Object.values(result).every((value) => typeof value === "number" && Number.isFinite(value))) {
|
|
19475
|
+
throw new RangeError("LTV calculation overflowed; reduce the input magnitudes");
|
|
19476
|
+
}
|
|
19477
|
+
return result;
|
|
19478
|
+
}
|
|
19479
|
+
function estimateLtvPerUser2(args) {
|
|
19480
|
+
assertPoint(args);
|
|
19481
|
+
return finiteResult(estimateLtvPerUser(args));
|
|
19482
|
+
}
|
|
19483
|
+
function estimateLtvDistribution2(args) {
|
|
19484
|
+
assertPoint(args);
|
|
19485
|
+
if (!Number.isSafeInteger(args.draws) || args.draws < 1 || args.draws > 1e5 || args.draws * Math.max(1, Math.ceil(args.horizonDays)) > 1e7) {
|
|
19486
|
+
throw new RangeError("draws must be 1..100000 and draws \xD7 ceil(horizonDays) at most 10000000");
|
|
19487
|
+
}
|
|
19488
|
+
if (!Number.isFinite(args.seed))
|
|
19489
|
+
throw new RangeError("seed must be finite");
|
|
19490
|
+
if (args.quantiles.length > 101)
|
|
19491
|
+
throw new RangeError("quantiles exceed the 101 distinct percent keys");
|
|
19492
|
+
assertQuantileValues(args.quantiles);
|
|
19493
|
+
if (new Set(args.quantiles.map((q) => Math.round(q * 100))).size !== args.quantiles.length) {
|
|
19494
|
+
throw new RangeError("quantiles must be nonempty, in 0..1 and have distinct rounded percent keys");
|
|
19495
|
+
}
|
|
19496
|
+
const result = estimateLtvDistribution(args);
|
|
19497
|
+
finiteResult(result.mean);
|
|
19498
|
+
finiteResult(result.quantiles);
|
|
19499
|
+
return result;
|
|
19500
|
+
}
|
|
19501
|
+
var init_public = __esm(() => {
|
|
19502
|
+
init_validate();
|
|
19503
|
+
init_uncertainty();
|
|
19504
|
+
});
|
|
19505
|
+
|
|
19506
|
+
// ../core/dist/ltv/cohort.js
|
|
19507
|
+
function wilsonInterval(activeUsers, users) {
|
|
19508
|
+
const rate = activeUsers / users;
|
|
19509
|
+
const z2 = 1.959963984540054 ** 2;
|
|
19510
|
+
const denominator = 1 + z2 / users;
|
|
19511
|
+
const center = (rate + z2 / (2 * users)) / denominator;
|
|
19512
|
+
const radius = Math.sqrt(z2 * (rate * (1 - rate) / users + z2 / (4 * users * users))) / denominator;
|
|
19513
|
+
return { min: activeUsers === 0 ? 0 : Math.max(0, center - radius), max: activeUsers === users ? 1 : Math.min(1, center + radius) };
|
|
19514
|
+
}
|
|
19515
|
+
function observeCohort(rows, observedThroughDay) {
|
|
19516
|
+
if (!Number.isSafeInteger(observedThroughDay) || observedThroughDay < 0)
|
|
19517
|
+
throw new Error("observed-through-day must be a nonnegative safe integer");
|
|
19518
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
19519
|
+
throw new Error("Observed cohort requires at least one install record");
|
|
19520
|
+
const users = new Map;
|
|
19521
|
+
for (const row of rows) {
|
|
19522
|
+
if (!row || typeof row.userId !== "string" || !row.userId.trim() || !Number.isSafeInteger(row.day) || row.day < 0 || row.day > observedThroughDay) {
|
|
19523
|
+
throw new Error("Observed cohort requires a user ID and integer days within observed-through-day");
|
|
19524
|
+
}
|
|
19525
|
+
if (![row.iapRevenue, row.adRevenue, row.acquisitionCost].every((v) => v === undefined || typeof v === "number" && Number.isFinite(v) && v >= 0)) {
|
|
19526
|
+
throw new Error("Observed cohort revenue and acquisition costs must be finite and nonnegative");
|
|
19527
|
+
}
|
|
19528
|
+
if (row.active !== undefined && typeof row.active !== "boolean")
|
|
19529
|
+
throw new Error("Observed cohort active must be boolean");
|
|
19530
|
+
const user = users.get(row.userId) ?? { days: new Set, active: new Set };
|
|
19531
|
+
if (user.days.has(row.day))
|
|
19532
|
+
throw new Error("Observed cohort requires one aggregated row per user and day");
|
|
19533
|
+
user.days.add(row.day);
|
|
19534
|
+
if (row.active ?? true)
|
|
19535
|
+
user.active.add(row.day);
|
|
19536
|
+
users.set(row.userId, user);
|
|
19537
|
+
}
|
|
19538
|
+
for (const user of users.values()) {
|
|
19539
|
+
if (!user.days.has(0))
|
|
19540
|
+
throw new Error("Observed cohort requires a day 0 install record for every user, including inactive users");
|
|
19541
|
+
}
|
|
19542
|
+
const n = users.size;
|
|
19543
|
+
const observation = {
|
|
19544
|
+
observedThroughDay,
|
|
19545
|
+
users: n,
|
|
19546
|
+
retention: RETENTION_DAYS.map((day) => {
|
|
19547
|
+
if (day > observedThroughDay)
|
|
19548
|
+
return { day, status: "not-observed", users: n, activeUsers: null, rate: null, interval95: null };
|
|
19549
|
+
const activeUsers = [...users.values()].filter((user) => user.active.has(day)).length;
|
|
19550
|
+
const rate = activeUsers / n;
|
|
19551
|
+
return { day, status: "observed", users: n, activeUsers, rate, interval95: wilsonInterval(activeUsers, n) };
|
|
19552
|
+
})
|
|
19553
|
+
};
|
|
19554
|
+
assertCohortObservation(observation);
|
|
19555
|
+
return observation;
|
|
19556
|
+
}
|
|
19557
|
+
function object2(value) {
|
|
19558
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19559
|
+
}
|
|
19560
|
+
function assertCohortObservation(observation) {
|
|
19561
|
+
if (!object2(observation) || typeof observation.users !== "number" || !Number.isSafeInteger(observation.users) || observation.users < 1 || typeof observation.observedThroughDay !== "number" || !Number.isSafeInteger(observation.observedThroughDay) || observation.observedThroughDay < 0) {
|
|
19562
|
+
throw new Error("Invalid cohort population or observation cutoff");
|
|
19563
|
+
}
|
|
19564
|
+
if (!Array.isArray(observation.retention) || observation.retention.length !== RETENTION_DAYS.length)
|
|
19565
|
+
throw new Error("Cohort retention must contain D1, D7, D30 and D90");
|
|
19566
|
+
for (const [index, value] of observation.retention.entries()) {
|
|
19567
|
+
const point = value;
|
|
19568
|
+
if (!object2(point) || typeof point.day !== "number" || point.day !== RETENTION_DAYS[index] || point.users !== observation.users)
|
|
19569
|
+
throw new Error("Cohort retention days and population must match the observation");
|
|
19570
|
+
const observed = point.day <= observation.observedThroughDay;
|
|
19571
|
+
if (point.status !== (observed ? "observed" : "not-observed"))
|
|
19572
|
+
throw new Error("Cohort retention status must match the observation cutoff");
|
|
19573
|
+
if (!observed) {
|
|
19574
|
+
if (point.activeUsers !== null || point.rate !== null || point.interval95 !== null)
|
|
19575
|
+
throw new Error("Future retention must remain unobserved");
|
|
19576
|
+
continue;
|
|
19577
|
+
}
|
|
19578
|
+
if (typeof point.activeUsers !== "number" || !Number.isSafeInteger(point.activeUsers) || point.activeUsers < 0 || point.activeUsers > observation.users || point.rate !== point.activeUsers / observation.users) {
|
|
19579
|
+
throw new Error("Retained users and retention rate must match the installed population");
|
|
19580
|
+
}
|
|
19581
|
+
const interval = point.interval95;
|
|
19582
|
+
if (typeof point.rate !== "number" || !object2(interval) || typeof interval.min !== "number" || typeof interval.max !== "number" || !Number.isFinite(interval.min) || !Number.isFinite(interval.max) || interval.min < 0 || interval.max > 1 || interval.min > point.rate || interval.max < point.rate) {
|
|
19583
|
+
throw new Error("Retention confidence interval must be finite and contain the observed rate");
|
|
19584
|
+
}
|
|
19585
|
+
const expected = wilsonInterval(point.activeUsers, observation.users);
|
|
19586
|
+
if (interval.min !== expected.min || interval.max !== expected.max) {
|
|
19587
|
+
throw new Error("Retention confidence interval must match the Wilson 95% bounds for the observed population");
|
|
19588
|
+
}
|
|
19589
|
+
}
|
|
19590
|
+
}
|
|
19591
|
+
function observedRetentionRates(observation) {
|
|
19592
|
+
assertCohortObservation(observation);
|
|
19593
|
+
const rates = {};
|
|
19594
|
+
for (const row of observation.retention) {
|
|
19595
|
+
if (row.rate !== null)
|
|
19596
|
+
rates[`d${row.day}`] = row.rate;
|
|
19597
|
+
}
|
|
19598
|
+
return rates;
|
|
19599
|
+
}
|
|
19600
|
+
function assertRetentionAssumptions(assumptions) {
|
|
19601
|
+
if (!object2(assumptions))
|
|
19602
|
+
throw new RangeError("Retention assumptions must be an object");
|
|
19603
|
+
for (const day of RETENTION_DAYS) {
|
|
19604
|
+
const rate = assumptions[`d${day}`];
|
|
19605
|
+
if (rate !== undefined && (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1)) {
|
|
19606
|
+
throw new RangeError(`Retention assumption d${day} must be finite and in 0..1`);
|
|
19607
|
+
}
|
|
19608
|
+
}
|
|
19609
|
+
}
|
|
19610
|
+
function planRetentionPatch(observation, assumptions = {}) {
|
|
19611
|
+
assertRetentionAssumptions(assumptions);
|
|
19612
|
+
const rates = observedRetentionRates(observation);
|
|
19613
|
+
if (Object.keys(rates).length === 0)
|
|
19614
|
+
return { status: "unavailable", retention: {}, warnings: ["No retention day has been fully observed."] };
|
|
19615
|
+
const merged = { ...assumptions, ...rates };
|
|
19616
|
+
const warnings = [];
|
|
19617
|
+
for (const day of RETENTION_DAYS) {
|
|
19618
|
+
if (merged[`d${day}`] === undefined)
|
|
19619
|
+
warnings.push(`Unobserved D${day} has no explicit retained assumption. An LTV default could conflict with the observations; supply monetization.retention.d${day} in the scenario or observe this day before applying a patch.`);
|
|
19620
|
+
}
|
|
19621
|
+
let previous;
|
|
19622
|
+
for (const day of RETENTION_DAYS) {
|
|
19623
|
+
const rate = merged[`d${day}`];
|
|
19624
|
+
if (rate === undefined)
|
|
19625
|
+
continue;
|
|
19626
|
+
if (previous && previous.rate < rate)
|
|
19627
|
+
warnings.push(`D${previous.day}=${previous.rate} is below D${day}=${rate}. LTV requires d1 >= d7 >= d30 >= d90; review observations and retained assumptions before applying a patch.`);
|
|
19628
|
+
previous = { day, rate };
|
|
19629
|
+
}
|
|
19630
|
+
return warnings.length ? { status: "requires-review", retention: {}, warnings } : { status: "ready", retention: rates, warnings };
|
|
19631
|
+
}
|
|
19632
|
+
function compareRetention(observation, assumptions) {
|
|
19633
|
+
assertCohortObservation(observation);
|
|
19634
|
+
assertRetentionAssumptions(assumptions);
|
|
19635
|
+
return observation.retention.map((row) => {
|
|
19636
|
+
const assumption = assumptions[`d${row.day}`] ?? null;
|
|
19637
|
+
return {
|
|
19638
|
+
day: row.day,
|
|
19639
|
+
assumption,
|
|
19640
|
+
observed: row.rate,
|
|
19641
|
+
differencePercentagePoints: row.rate === null || assumption === null ? null : (row.rate - assumption) * 100,
|
|
19642
|
+
status: row.rate === null ? "not-observed" : assumption === null ? "unconfigured" : "compared"
|
|
19643
|
+
};
|
|
19644
|
+
});
|
|
19645
|
+
}
|
|
19646
|
+
var RETENTION_DAYS;
|
|
19647
|
+
var init_cohort = __esm(() => {
|
|
19648
|
+
RETENTION_DAYS = Object.freeze([1, 7, 30, 90]);
|
|
19649
|
+
});
|
|
19650
|
+
|
|
19023
19651
|
// ../core/dist/index.js
|
|
19024
19652
|
var init_dist2 = __esm(() => {
|
|
19025
19653
|
init_types();
|
|
@@ -19085,6 +19713,8 @@ var init_dist2 = __esm(() => {
|
|
|
19085
19713
|
init_judge();
|
|
19086
19714
|
init_flow();
|
|
19087
19715
|
init_jevAdvisory();
|
|
19716
|
+
init_public();
|
|
19717
|
+
init_cohort();
|
|
19088
19718
|
});
|
|
19089
19719
|
|
|
19090
19720
|
// src/errors.ts
|
|
@@ -19291,7 +19921,7 @@ var init_bun = __esm(() => {
|
|
|
19291
19921
|
var require_package = __commonJS(function(exports, module) {
|
|
19292
19922
|
module.exports = {
|
|
19293
19923
|
name: "@idlekit/cli",
|
|
19294
|
-
version: "0.
|
|
19924
|
+
version: "0.6.0",
|
|
19295
19925
|
description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
|
|
19296
19926
|
license: "MIT",
|
|
19297
19927
|
type: "module",
|
|
@@ -19344,7 +19974,7 @@ var require_package = __commonJS(function(exports, module) {
|
|
|
19344
19974
|
"@clack/prompts": "1.8.1",
|
|
19345
19975
|
"@gunshi/plugin-completion": "0.37.3",
|
|
19346
19976
|
"@gunshi/plugin-suggestion": "0.37.3",
|
|
19347
|
-
"@idlekit/core": "0.
|
|
19977
|
+
"@idlekit/core": "0.6.0",
|
|
19348
19978
|
ajv: "^8.20.0",
|
|
19349
19979
|
gunshi: "0.37.3",
|
|
19350
19980
|
"proper-lockfile": "4.1.2",
|
|
@@ -20208,8 +20838,8 @@ function ownedCount(state) {
|
|
|
20208
20838
|
}
|
|
20209
20839
|
function finiteLinearAmount(E, value, label) {
|
|
20210
20840
|
if (!E.isFinite(value)) {
|
|
20211
|
-
throw cliError("SCENARIO_INVALID", `linear ${label} exceeds the selected engine's finite range. Use --engine breakInfinity with simulate/evaluate/experience/ltv for a large economy, or reduce the inputs.`, {
|
|
20212
|
-
hint: "
|
|
20841
|
+
throw cliError("SCENARIO_INVALID", `linear ${label} exceeds the selected engine's finite range. Use --engine breakInfinity with simulate/evaluate/experience/ltv/compare/tune/balance for a large economy, or reduce the inputs.`, {
|
|
20842
|
+
hint: "The report command uses the number engine; reduce its inputs."
|
|
20213
20843
|
});
|
|
20214
20844
|
}
|
|
20215
20845
|
return value;
|
|
@@ -21163,4 +21793,4 @@ export {
|
|
|
21163
21793
|
workerEntryMarker
|
|
21164
21794
|
};
|
|
21165
21795
|
|
|
21166
|
-
//# debugId=
|
|
21796
|
+
//# debugId=8F910D642493528C64756E2164756E21
|