@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.
@@ -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
- const segment = segmentScenario(wallStart, wallEnd);
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
- policy: sc.run.offline,
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, sc.run.offline) : 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, sc.run.offline).effectiveSec;
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/objectives/builtins.js
12687
- function safeAbsLog10(E, x) {
12688
- const v = E.absLog10(x);
12689
- return Number.isFinite(v) ? v : -300;
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 safeAbsLog10(E, run.end.wallet.money.amount);
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 safeAbsLog10(E, worth.amount);
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 safeAbsLog10(E, worth.amount) - Math.log10(Math.max(0.000000000001, hours));
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 safeAbsLog10(E, run.end.prestige.points) - Math.log10(Math.max(0.000000000001, hours));
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 = safeAbsLog10(E, endWorth.amount) - safeAbsLog10(E, startWorth.amount);
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 = safeAbsLog10(E, worth.amount);
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: sc.ctx.E.absLog10(run.end.wallet.money.amount),
13075
- endNetWorthLog10: sc.ctx.E.absLog10(worth.amount),
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 = seedScores.reduce((a, b) => a + b, 0) / Math.max(1, seedScores.length);
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 finiteMean(values) {
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 ? finiteMean(observed) : null;
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.5.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.4.1",
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: "Other commands, including report/compare/tune, use the number engine; reduce their inputs."
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=06867CE34AA5ED6764756E2164756E21
21796
+ //# debugId=8F910D642493528C64756E2164756E21