@idlekit/cli 0.5.0 → 0.5.1

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.
@@ -19020,6 +19020,363 @@ var init_jevAdvisory = __esm(() => {
19020
19020
  init__createStandardSchema();
19021
19021
  });
19022
19022
 
19023
+ // ../core/dist/ltv/projection.js
19024
+ function clamp012(v) {
19025
+ if (v < 0)
19026
+ return 0;
19027
+ if (v > 1)
19028
+ return 1;
19029
+ return v;
19030
+ }
19031
+ function deriveMonetizationConfig(scenario) {
19032
+ const m = scenario.monetization;
19033
+ const q = (m?.uncertainty?.quantiles ?? [0.5, 0.9]).filter((x) => typeof x === "number");
19034
+ const quantiles = q.length > 0 ? [...new Set(q)].sort((a, b) => a - b) : [0.5, 0.9];
19035
+ return {
19036
+ cohorts: {
19037
+ baseUsers: m?.cohorts?.baseUsers ?? 1
19038
+ },
19039
+ retention: {
19040
+ d1: m?.retention?.d1 ?? 0.42,
19041
+ d7: m?.retention?.d7 ?? 0.2,
19042
+ d30: m?.retention?.d30 ?? 0.09,
19043
+ d90: m?.retention?.d90 ?? 0.04,
19044
+ longTailDailyDecay: m?.retention?.longTailDailyDecay ?? 0.02
19045
+ },
19046
+ revenue: {
19047
+ payerConversion: m?.revenue?.payerConversion ?? 0.03,
19048
+ arppuDaily: m?.revenue?.arppuDaily ?? 0.7,
19049
+ adArpDau: m?.revenue?.adArpDau ?? 0.02,
19050
+ platformFeeRate: m?.revenue?.platformFeeRate ?? 0.3,
19051
+ grossMarginRate: m?.revenue?.grossMarginRate ?? 0.92,
19052
+ progressionRevenueLift: m?.revenue?.progressionRevenueLift ?? 0.4,
19053
+ progressionLogSpan: m?.revenue?.progressionLogSpan ?? 6
19054
+ },
19055
+ acquisition: {
19056
+ cpi: m?.acquisition?.cpi ?? 0
19057
+ },
19058
+ uncertainty: {
19059
+ enabled: m?.uncertainty?.enabled ?? false,
19060
+ draws: m?.uncertainty?.draws ?? 300,
19061
+ quantiles,
19062
+ seed: m?.uncertainty?.seed,
19063
+ sigma: {
19064
+ retention: m?.uncertainty?.sigma?.retention ?? 0.08,
19065
+ conversion: m?.uncertainty?.sigma?.conversion ?? 0.12,
19066
+ arppu: m?.uncertainty?.sigma?.arppu ?? 0.2,
19067
+ ad: m?.uncertainty?.sigma?.ad ?? 0.15
19068
+ },
19069
+ correlation: {
19070
+ retentionConversion: m?.uncertainty?.correlation?.retentionConversion ?? 0,
19071
+ retentionArppu: m?.uncertainty?.correlation?.retentionArppu ?? 0,
19072
+ retentionAd: m?.uncertainty?.correlation?.retentionAd ?? 0,
19073
+ conversionArppu: m?.uncertainty?.correlation?.conversionArppu ?? 0,
19074
+ conversionAd: m?.uncertainty?.correlation?.conversionAd ?? 0,
19075
+ arppuAd: m?.uncertainty?.correlation?.arppuAd ?? 0
19076
+ }
19077
+ }
19078
+ };
19079
+ }
19080
+ function retentionAtDay(ret, day) {
19081
+ if (day <= 0)
19082
+ return 1;
19083
+ if (day <= 1)
19084
+ return ret.d1;
19085
+ if (day <= 7)
19086
+ return ret.d1 + (ret.d7 - ret.d1) * (day - 1) / 6;
19087
+ if (day <= 30)
19088
+ return ret.d7 + (ret.d30 - ret.d7) * (day - 7) / 23;
19089
+ if (day <= 90)
19090
+ return ret.d30 + (ret.d90 - ret.d30) * (day - 30) / 60;
19091
+ const extra = day - 90;
19092
+ return clamp012(ret.d90 * Math.exp(-ret.longTailDailyDecay * extra));
19093
+ }
19094
+ function progressionFactor(startWorthLog10, endWorthLog10, span) {
19095
+ if (!Number.isFinite(startWorthLog10) || !Number.isFinite(endWorthLog10) || !Number.isFinite(span) || span <= 0) {
19096
+ return 0;
19097
+ }
19098
+ return clamp012((endWorthLog10 - startWorthLog10) / span);
19099
+ }
19100
+ function estimateLtvPerUser(args) {
19101
+ const { config, horizonDays } = args;
19102
+ const progression = clamp012(args.progression);
19103
+ const arppuDaily = config.revenue.arppuDaily * (1 + progression * config.revenue.progressionRevenueLift);
19104
+ const adArpDau = config.revenue.adArpDau * (1 + progression * config.revenue.progressionRevenueLift * 0.5);
19105
+ const payerConversion = config.revenue.payerConversion;
19106
+ const fullDays = Math.floor(Math.max(0, horizonDays));
19107
+ const remainder = Math.max(0, horizonDays - fullDays);
19108
+ let gross = 0;
19109
+ for (let day = 1;day <= fullDays; day++) {
19110
+ const active = retentionAtDay(config.retention, day);
19111
+ gross += active * (payerConversion * arppuDaily + adArpDau);
19112
+ }
19113
+ if (remainder > 0) {
19114
+ const active = retentionAtDay(config.retention, fullDays + 1);
19115
+ gross += remainder * active * (payerConversion * arppuDaily + adArpDau);
19116
+ }
19117
+ const net = gross * (1 - config.revenue.platformFeeRate) * config.revenue.grossMarginRate;
19118
+ const ltv = net - config.acquisition.cpi;
19119
+ return {
19120
+ cumulativeGrossRevenuePerUser: gross,
19121
+ cumulativeNetRevenuePerUser: net,
19122
+ cumulativeLtvPerUser: ltv
19123
+ };
19124
+ }
19125
+
19126
+ // ../core/dist/ltv/uncertainty.js
19127
+ function clamp013(v) {
19128
+ if (v < 0)
19129
+ return 0;
19130
+ if (v > 1)
19131
+ return 1;
19132
+ return v;
19133
+ }
19134
+ function clampCorrelation(v) {
19135
+ if (!Number.isFinite(v))
19136
+ return 0;
19137
+ if (v < -1)
19138
+ return -1;
19139
+ if (v > 1)
19140
+ return 1;
19141
+ return v;
19142
+ }
19143
+ function randNormal(rng) {
19144
+ const u1 = Math.max(0.000000000001, rng());
19145
+ const u2 = Math.max(0.000000000001, rng());
19146
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
19147
+ }
19148
+ function buildCorrelationMatrix(base) {
19149
+ const rc = clampCorrelation(base.retentionConversion);
19150
+ const ra = clampCorrelation(base.retentionArppu);
19151
+ const rd = clampCorrelation(base.retentionAd);
19152
+ const ca = clampCorrelation(base.conversionArppu);
19153
+ const cd = clampCorrelation(base.conversionAd);
19154
+ const ad = clampCorrelation(base.arppuAd);
19155
+ return [
19156
+ [1, rc, ra, rd],
19157
+ [rc, 1, ca, cd],
19158
+ [ra, ca, 1, ad],
19159
+ [rd, cd, ad, 1]
19160
+ ];
19161
+ }
19162
+ function withOffDiagonalScale(matrix, alpha) {
19163
+ return matrix.map((row, i) => row.map((v, j) => {
19164
+ if (i === j)
19165
+ return 1;
19166
+ return v * alpha;
19167
+ }));
19168
+ }
19169
+ function cholesky4(matrix) {
19170
+ const n = 4;
19171
+ const l = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
19172
+ for (let i = 0;i < n; i++) {
19173
+ for (let j = 0;j <= i; j++) {
19174
+ let sum = 0;
19175
+ for (let k = 0;k < j; k++) {
19176
+ sum += (l[i]?.[k] ?? 0) * (l[j]?.[k] ?? 0);
19177
+ }
19178
+ if (i === j) {
19179
+ const val = (matrix[i]?.[i] ?? 0) - sum;
19180
+ if (val <= 0.000000001)
19181
+ return null;
19182
+ l[i][j] = Math.sqrt(val);
19183
+ } else {
19184
+ const denom = l[j]?.[j] ?? 0;
19185
+ if (Math.abs(denom) < 0.000000001)
19186
+ return null;
19187
+ l[i][j] = ((matrix[i]?.[j] ?? 0) - sum) / denom;
19188
+ }
19189
+ }
19190
+ }
19191
+ return l;
19192
+ }
19193
+ function multiplyLowerTriangular(l, z) {
19194
+ const out = [0, 0, 0, 0];
19195
+ for (let i = 0;i < 4; i++) {
19196
+ let sum = 0;
19197
+ for (let j = 0;j <= i; j++) {
19198
+ sum += (l[i]?.[j] ?? 0) * (z[j] ?? 0);
19199
+ }
19200
+ out[i] = sum;
19201
+ }
19202
+ return out;
19203
+ }
19204
+ function sampleCorrelatedNormals(rng, corr) {
19205
+ const z = [randNormal(rng), randNormal(rng), randNormal(rng), randNormal(rng)];
19206
+ const matrix = buildCorrelationMatrix(corr);
19207
+ let l = cholesky4(matrix);
19208
+ let alpha = 0.99;
19209
+ while (!l && alpha > 0.000001) {
19210
+ l = cholesky4(withOffDiagonalScale(matrix, alpha));
19211
+ alpha *= 0.8;
19212
+ }
19213
+ if (!l) {
19214
+ return {
19215
+ retention: z[0] ?? 0,
19216
+ conversion: z[1] ?? 0,
19217
+ arppu: z[2] ?? 0,
19218
+ ad: z[3] ?? 0
19219
+ };
19220
+ }
19221
+ const c = multiplyLowerTriangular(l, z);
19222
+ return {
19223
+ retention: c[0] ?? 0,
19224
+ conversion: c[1] ?? 0,
19225
+ arppu: c[2] ?? 0,
19226
+ ad: c[3] ?? 0
19227
+ };
19228
+ }
19229
+ function applyLogNormalShock(mean, sigma, z) {
19230
+ if (sigma <= 0)
19231
+ return mean;
19232
+ return mean * Math.exp(sigma * z - 0.5 * sigma * sigma);
19233
+ }
19234
+ function makeUncertainConfig(base, rng) {
19235
+ const shock = sampleCorrelatedNormals(rng, base.uncertainty.correlation);
19236
+ const d1 = clamp013(applyLogNormalShock(base.retention.d1, base.uncertainty.sigma.retention, shock.retention));
19237
+ const d7 = Math.min(d1, clamp013(applyLogNormalShock(base.retention.d7, base.uncertainty.sigma.retention, shock.retention)));
19238
+ const d30 = Math.min(d7, clamp013(applyLogNormalShock(base.retention.d30, base.uncertainty.sigma.retention, shock.retention)));
19239
+ const d90 = Math.min(d30, clamp013(applyLogNormalShock(base.retention.d90, base.uncertainty.sigma.retention, shock.retention)));
19240
+ return {
19241
+ ...base,
19242
+ retention: {
19243
+ ...base.retention,
19244
+ d1,
19245
+ d7,
19246
+ d30,
19247
+ d90
19248
+ },
19249
+ revenue: {
19250
+ ...base.revenue,
19251
+ payerConversion: clamp013(applyLogNormalShock(base.revenue.payerConversion, base.uncertainty.sigma.conversion, shock.conversion)),
19252
+ arppuDaily: Math.max(0, applyLogNormalShock(base.revenue.arppuDaily, base.uncertainty.sigma.arppu, shock.arppu)),
19253
+ adArpDau: Math.max(0, applyLogNormalShock(base.revenue.adArpDau, base.uncertainty.sigma.ad, shock.ad))
19254
+ }
19255
+ };
19256
+ }
19257
+ function quantileFromSorted(sorted, q) {
19258
+ if (sorted.length === 0)
19259
+ return 0;
19260
+ const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
19261
+ return sorted[idx] ?? 0;
19262
+ }
19263
+ function estimateLtvDistribution(args) {
19264
+ const rng = mulberry32(args.seed);
19265
+ const grossSamples = [];
19266
+ const netSamples = [];
19267
+ const ltvSamples = [];
19268
+ const draws = Math.max(1, args.draws);
19269
+ for (let i = 0;i < draws; i++) {
19270
+ const sampled = makeUncertainConfig(args.config, rng);
19271
+ const point = estimateLtvPerUser({
19272
+ config: sampled,
19273
+ horizonDays: args.horizonDays,
19274
+ progression: args.progression
19275
+ });
19276
+ grossSamples.push(point.cumulativeGrossRevenuePerUser);
19277
+ netSamples.push(point.cumulativeNetRevenuePerUser);
19278
+ ltvSamples.push(point.cumulativeLtvPerUser);
19279
+ }
19280
+ const avg = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length);
19281
+ const sortedLtv = [...ltvSamples].sort((a, b) => a - b);
19282
+ const byQuantile = {};
19283
+ for (const q of args.quantiles) {
19284
+ byQuantile[`q${Math.round(q * 100)}`] = quantileFromSorted(sortedLtv, q);
19285
+ }
19286
+ return {
19287
+ mean: {
19288
+ cumulativeGrossRevenuePerUser: avg(grossSamples),
19289
+ cumulativeNetRevenuePerUser: avg(netSamples),
19290
+ cumulativeLtvPerUser: avg(ltvSamples)
19291
+ },
19292
+ quantiles: byQuantile
19293
+ };
19294
+ }
19295
+ var init_uncertainty = () => {};
19296
+
19297
+ // ../core/dist/ltv/public.js
19298
+ function assertMonetization(monetization) {
19299
+ const valid = validateScenarioV1({
19300
+ schemaVersion: 1,
19301
+ unit: { code: "LTV" },
19302
+ policy: { mode: "drop" },
19303
+ model: { id: "ltv-input", version: 1 },
19304
+ initial: { wallet: { unit: "LTV", amount: "0" } },
19305
+ clock: { stepSec: 1, durationSec: 1 },
19306
+ monetization
19307
+ });
19308
+ if (!valid.ok)
19309
+ throw new RangeError(valid.issues.map((issue) => issue.message).join("; "));
19310
+ }
19311
+ function assertQuantileValues(quantiles) {
19312
+ if (quantiles.length === 0)
19313
+ throw new RangeError("quantiles must be nonempty and in 0..1");
19314
+ for (const q of quantiles) {
19315
+ if (!Number.isFinite(q) || q < 0 || q > 1)
19316
+ throw new RangeError("quantiles must be nonempty and in 0..1");
19317
+ }
19318
+ }
19319
+ function assertConfig(config) {
19320
+ const { retention, ...other } = config;
19321
+ for (const day of ["d1", "d7", "d30", "d90"]) {
19322
+ if (!Number.isFinite(retention[day]) || retention[day] < 0 || retention[day] > 1) {
19323
+ throw new RangeError(`retention.${day} must be finite and in 0..1`);
19324
+ }
19325
+ }
19326
+ if (!Number.isFinite(retention.longTailDailyDecay) || retention.longTailDailyDecay < 0) {
19327
+ throw new RangeError("retention.longTailDailyDecay must be finite and >= 0");
19328
+ }
19329
+ const { quantiles, ...uncertainty } = other.uncertainty;
19330
+ assertQuantileValues(quantiles);
19331
+ assertMonetization({ ...other, uncertainty });
19332
+ }
19333
+ function deriveMonetizationConfig2(scenario) {
19334
+ assertMonetization(scenario.monetization ?? {});
19335
+ const config = deriveMonetizationConfig(scenario);
19336
+ assertConfig(config);
19337
+ return config;
19338
+ }
19339
+ function assertPoint(args) {
19340
+ assertConfig(args.config);
19341
+ if (!Number.isFinite(args.horizonDays) || args.horizonDays < 0 || args.horizonDays > 36500) {
19342
+ throw new RangeError("horizonDays must be finite and in 0..36500");
19343
+ }
19344
+ if (!Number.isFinite(args.progression))
19345
+ throw new RangeError("progression must be finite");
19346
+ }
19347
+ function finiteResult(result) {
19348
+ if (!Object.values(result).every((value) => typeof value === "number" && Number.isFinite(value))) {
19349
+ throw new RangeError("LTV calculation overflowed; reduce the input magnitudes");
19350
+ }
19351
+ return result;
19352
+ }
19353
+ function estimateLtvPerUser2(args) {
19354
+ assertPoint(args);
19355
+ return finiteResult(estimateLtvPerUser(args));
19356
+ }
19357
+ function estimateLtvDistribution2(args) {
19358
+ assertPoint(args);
19359
+ if (!Number.isSafeInteger(args.draws) || args.draws < 1 || args.draws > 1e5 || args.draws * Math.max(1, Math.ceil(args.horizonDays)) > 1e7) {
19360
+ throw new RangeError("draws must be 1..100000 and draws \xD7 ceil(horizonDays) at most 10000000");
19361
+ }
19362
+ if (!Number.isFinite(args.seed))
19363
+ throw new RangeError("seed must be finite");
19364
+ if (args.quantiles.length > 101)
19365
+ throw new RangeError("quantiles exceed the 101 distinct percent keys");
19366
+ assertQuantileValues(args.quantiles);
19367
+ if (new Set(args.quantiles.map((q) => Math.round(q * 100))).size !== args.quantiles.length) {
19368
+ throw new RangeError("quantiles must be nonempty, in 0..1 and have distinct rounded percent keys");
19369
+ }
19370
+ const result = estimateLtvDistribution(args);
19371
+ finiteResult(result.mean);
19372
+ finiteResult(result.quantiles);
19373
+ return result;
19374
+ }
19375
+ var init_public = __esm(() => {
19376
+ init_validate();
19377
+ init_uncertainty();
19378
+ });
19379
+
19023
19380
  // ../core/dist/index.js
19024
19381
  var init_dist2 = __esm(() => {
19025
19382
  init_types();
@@ -19085,6 +19442,7 @@ var init_dist2 = __esm(() => {
19085
19442
  init_judge();
19086
19443
  init_flow();
19087
19444
  init_jevAdvisory();
19445
+ init_public();
19088
19446
  });
19089
19447
 
19090
19448
  // src/errors.ts
@@ -19291,7 +19649,7 @@ var init_bun = __esm(() => {
19291
19649
  var require_package = __commonJS(function(exports, module) {
19292
19650
  module.exports = {
19293
19651
  name: "@idlekit/cli",
19294
- version: "0.5.0",
19652
+ version: "0.5.1",
19295
19653
  description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
19296
19654
  license: "MIT",
19297
19655
  type: "module",
@@ -19344,7 +19702,7 @@ var require_package = __commonJS(function(exports, module) {
19344
19702
  "@clack/prompts": "1.8.1",
19345
19703
  "@gunshi/plugin-completion": "0.37.3",
19346
19704
  "@gunshi/plugin-suggestion": "0.37.3",
19347
- "@idlekit/core": "0.4.1",
19705
+ "@idlekit/core": "0.5.0",
19348
19706
  ajv: "^8.20.0",
19349
19707
  gunshi: "0.37.3",
19350
19708
  "proper-lockfile": "4.1.2",
@@ -21163,4 +21521,4 @@ export {
21163
21521
  workerEntryMarker
21164
21522
  };
21165
21523
 
21166
- //# debugId=06867CE34AA5ED6764756E2164756E21
21524
+ //# debugId=36317EC36EA8D6E664756E2164756E21