@idlekit/cli 0.4.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.
package/dist/main.js CHANGED
@@ -8397,7 +8397,7 @@ var init_validate = __esm(() => {
8397
8397
  OFFLINE_ACTORS = new Set(["player", "automation"]);
8398
8398
  });
8399
8399
 
8400
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_validateReport.mjs
8400
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_validateReport.mjs
8401
8401
  var _validateReport = (array) => {
8402
8402
  const isAncestor = (ancestor, descendant) => descendant === ancestor || descendant.startsWith(`${ancestor}.`) || descendant.startsWith(`${ancestor}[`);
8403
8403
  const reportable = (path) => {
@@ -8422,7 +8422,7 @@ var _validateReport = (array) => {
8422
8422
  };
8423
8423
  var init__validateReport = () => {};
8424
8424
 
8425
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_createStandardSchema.mjs
8425
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_createStandardSchema.mjs
8426
8426
  var _createStandardSchema = (fn) => Object.assign(fn, { "~standard": {
8427
8427
  version: 1,
8428
8428
  vendor: "typia",
@@ -18414,7 +18414,7 @@ var init_checks3 = __esm(() => {
18414
18414
  FAIL_ON = ["error", "warn", "inconclusive", "unsupported"];
18415
18415
  });
18416
18416
 
18417
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_accessExpressionAsString.mjs
18417
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_accessExpressionAsString.mjs
18418
18418
  var _accessExpressionAsString = (str) => variable(str) ? `.${str}` : `[${JSON.stringify(str)}]`, variable = (str) => reserved(str) === false && /^[a-zA-Z_$][a-zA-Z_$0-9]*$/g.test(str), reserved = (str) => RESERVED.has(str), RESERVED;
18419
18419
  var init__accessExpressionAsString = __esm(() => {
18420
18420
  RESERVED = /* @__PURE__ */ new Set([
@@ -18961,7 +18961,7 @@ var init_judge = __esm(() => {
18961
18961
  // ../core/dist/sim/analysis/flow.js
18962
18962
  var init_flow = () => {};
18963
18963
 
18964
- // ../../node_modules/.bun/typia@14.0.6+c0da6f6c08648afa/node_modules/typia/lib/internal/_isTypeUint32.mjs
18964
+ // ../../node_modules/.bun/typia@15.1.0+367f7514c87f1df1/node_modules/typia/lib/internal/_isTypeUint32.mjs
18965
18965
  var _isTypeUint32 = (value) => Math.floor(value) === value && MINIMUM <= value && value <= MAXIMUM, MINIMUM = 0, MAXIMUM;
18966
18966
  var init__isTypeUint32 = __esm(() => {
18967
18967
  MAXIMUM = 2 ** 32 - 1;
@@ -19440,6 +19440,363 @@ function compareScenarios(args) {
19440
19440
  };
19441
19441
  }
19442
19442
 
19443
+ // ../core/dist/ltv/projection.js
19444
+ function clamp012(v) {
19445
+ if (v < 0)
19446
+ return 0;
19447
+ if (v > 1)
19448
+ return 1;
19449
+ return v;
19450
+ }
19451
+ function deriveMonetizationConfig(scenario) {
19452
+ const m = scenario.monetization;
19453
+ const q = (m?.uncertainty?.quantiles ?? [0.5, 0.9]).filter((x) => typeof x === "number");
19454
+ const quantiles = q.length > 0 ? [...new Set(q)].sort((a, b) => a - b) : [0.5, 0.9];
19455
+ return {
19456
+ cohorts: {
19457
+ baseUsers: m?.cohorts?.baseUsers ?? 1
19458
+ },
19459
+ retention: {
19460
+ d1: m?.retention?.d1 ?? 0.42,
19461
+ d7: m?.retention?.d7 ?? 0.2,
19462
+ d30: m?.retention?.d30 ?? 0.09,
19463
+ d90: m?.retention?.d90 ?? 0.04,
19464
+ longTailDailyDecay: m?.retention?.longTailDailyDecay ?? 0.02
19465
+ },
19466
+ revenue: {
19467
+ payerConversion: m?.revenue?.payerConversion ?? 0.03,
19468
+ arppuDaily: m?.revenue?.arppuDaily ?? 0.7,
19469
+ adArpDau: m?.revenue?.adArpDau ?? 0.02,
19470
+ platformFeeRate: m?.revenue?.platformFeeRate ?? 0.3,
19471
+ grossMarginRate: m?.revenue?.grossMarginRate ?? 0.92,
19472
+ progressionRevenueLift: m?.revenue?.progressionRevenueLift ?? 0.4,
19473
+ progressionLogSpan: m?.revenue?.progressionLogSpan ?? 6
19474
+ },
19475
+ acquisition: {
19476
+ cpi: m?.acquisition?.cpi ?? 0
19477
+ },
19478
+ uncertainty: {
19479
+ enabled: m?.uncertainty?.enabled ?? false,
19480
+ draws: m?.uncertainty?.draws ?? 300,
19481
+ quantiles,
19482
+ seed: m?.uncertainty?.seed,
19483
+ sigma: {
19484
+ retention: m?.uncertainty?.sigma?.retention ?? 0.08,
19485
+ conversion: m?.uncertainty?.sigma?.conversion ?? 0.12,
19486
+ arppu: m?.uncertainty?.sigma?.arppu ?? 0.2,
19487
+ ad: m?.uncertainty?.sigma?.ad ?? 0.15
19488
+ },
19489
+ correlation: {
19490
+ retentionConversion: m?.uncertainty?.correlation?.retentionConversion ?? 0,
19491
+ retentionArppu: m?.uncertainty?.correlation?.retentionArppu ?? 0,
19492
+ retentionAd: m?.uncertainty?.correlation?.retentionAd ?? 0,
19493
+ conversionArppu: m?.uncertainty?.correlation?.conversionArppu ?? 0,
19494
+ conversionAd: m?.uncertainty?.correlation?.conversionAd ?? 0,
19495
+ arppuAd: m?.uncertainty?.correlation?.arppuAd ?? 0
19496
+ }
19497
+ }
19498
+ };
19499
+ }
19500
+ function retentionAtDay(ret, day) {
19501
+ if (day <= 0)
19502
+ return 1;
19503
+ if (day <= 1)
19504
+ return ret.d1;
19505
+ if (day <= 7)
19506
+ return ret.d1 + (ret.d7 - ret.d1) * (day - 1) / 6;
19507
+ if (day <= 30)
19508
+ return ret.d7 + (ret.d30 - ret.d7) * (day - 7) / 23;
19509
+ if (day <= 90)
19510
+ return ret.d30 + (ret.d90 - ret.d30) * (day - 30) / 60;
19511
+ const extra = day - 90;
19512
+ return clamp012(ret.d90 * Math.exp(-ret.longTailDailyDecay * extra));
19513
+ }
19514
+ function progressionFactor(startWorthLog10, endWorthLog10, span) {
19515
+ if (!Number.isFinite(startWorthLog10) || !Number.isFinite(endWorthLog10) || !Number.isFinite(span) || span <= 0) {
19516
+ return 0;
19517
+ }
19518
+ return clamp012((endWorthLog10 - startWorthLog10) / span);
19519
+ }
19520
+ function estimateLtvPerUser(args) {
19521
+ const { config, horizonDays } = args;
19522
+ const progression = clamp012(args.progression);
19523
+ const arppuDaily = config.revenue.arppuDaily * (1 + progression * config.revenue.progressionRevenueLift);
19524
+ const adArpDau = config.revenue.adArpDau * (1 + progression * config.revenue.progressionRevenueLift * 0.5);
19525
+ const payerConversion = config.revenue.payerConversion;
19526
+ const fullDays = Math.floor(Math.max(0, horizonDays));
19527
+ const remainder = Math.max(0, horizonDays - fullDays);
19528
+ let gross = 0;
19529
+ for (let day = 1;day <= fullDays; day++) {
19530
+ const active = retentionAtDay(config.retention, day);
19531
+ gross += active * (payerConversion * arppuDaily + adArpDau);
19532
+ }
19533
+ if (remainder > 0) {
19534
+ const active = retentionAtDay(config.retention, fullDays + 1);
19535
+ gross += remainder * active * (payerConversion * arppuDaily + adArpDau);
19536
+ }
19537
+ const net = gross * (1 - config.revenue.platformFeeRate) * config.revenue.grossMarginRate;
19538
+ const ltv = net - config.acquisition.cpi;
19539
+ return {
19540
+ cumulativeGrossRevenuePerUser: gross,
19541
+ cumulativeNetRevenuePerUser: net,
19542
+ cumulativeLtvPerUser: ltv
19543
+ };
19544
+ }
19545
+
19546
+ // ../core/dist/ltv/uncertainty.js
19547
+ function clamp013(v) {
19548
+ if (v < 0)
19549
+ return 0;
19550
+ if (v > 1)
19551
+ return 1;
19552
+ return v;
19553
+ }
19554
+ function clampCorrelation(v) {
19555
+ if (!Number.isFinite(v))
19556
+ return 0;
19557
+ if (v < -1)
19558
+ return -1;
19559
+ if (v > 1)
19560
+ return 1;
19561
+ return v;
19562
+ }
19563
+ function randNormal(rng) {
19564
+ const u1 = Math.max(0.000000000001, rng());
19565
+ const u2 = Math.max(0.000000000001, rng());
19566
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
19567
+ }
19568
+ function buildCorrelationMatrix(base) {
19569
+ const rc = clampCorrelation(base.retentionConversion);
19570
+ const ra = clampCorrelation(base.retentionArppu);
19571
+ const rd = clampCorrelation(base.retentionAd);
19572
+ const ca = clampCorrelation(base.conversionArppu);
19573
+ const cd = clampCorrelation(base.conversionAd);
19574
+ const ad = clampCorrelation(base.arppuAd);
19575
+ return [
19576
+ [1, rc, ra, rd],
19577
+ [rc, 1, ca, cd],
19578
+ [ra, ca, 1, ad],
19579
+ [rd, cd, ad, 1]
19580
+ ];
19581
+ }
19582
+ function withOffDiagonalScale(matrix, alpha) {
19583
+ return matrix.map((row, i) => row.map((v, j) => {
19584
+ if (i === j)
19585
+ return 1;
19586
+ return v * alpha;
19587
+ }));
19588
+ }
19589
+ function cholesky4(matrix) {
19590
+ const n = 4;
19591
+ const l = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
19592
+ for (let i = 0;i < n; i++) {
19593
+ for (let j = 0;j <= i; j++) {
19594
+ let sum = 0;
19595
+ for (let k = 0;k < j; k++) {
19596
+ sum += (l[i]?.[k] ?? 0) * (l[j]?.[k] ?? 0);
19597
+ }
19598
+ if (i === j) {
19599
+ const val = (matrix[i]?.[i] ?? 0) - sum;
19600
+ if (val <= 0.000000001)
19601
+ return null;
19602
+ l[i][j] = Math.sqrt(val);
19603
+ } else {
19604
+ const denom = l[j]?.[j] ?? 0;
19605
+ if (Math.abs(denom) < 0.000000001)
19606
+ return null;
19607
+ l[i][j] = ((matrix[i]?.[j] ?? 0) - sum) / denom;
19608
+ }
19609
+ }
19610
+ }
19611
+ return l;
19612
+ }
19613
+ function multiplyLowerTriangular(l, z) {
19614
+ const out = [0, 0, 0, 0];
19615
+ for (let i = 0;i < 4; i++) {
19616
+ let sum = 0;
19617
+ for (let j = 0;j <= i; j++) {
19618
+ sum += (l[i]?.[j] ?? 0) * (z[j] ?? 0);
19619
+ }
19620
+ out[i] = sum;
19621
+ }
19622
+ return out;
19623
+ }
19624
+ function sampleCorrelatedNormals(rng, corr) {
19625
+ const z = [randNormal(rng), randNormal(rng), randNormal(rng), randNormal(rng)];
19626
+ const matrix = buildCorrelationMatrix(corr);
19627
+ let l = cholesky4(matrix);
19628
+ let alpha = 0.99;
19629
+ while (!l && alpha > 0.000001) {
19630
+ l = cholesky4(withOffDiagonalScale(matrix, alpha));
19631
+ alpha *= 0.8;
19632
+ }
19633
+ if (!l) {
19634
+ return {
19635
+ retention: z[0] ?? 0,
19636
+ conversion: z[1] ?? 0,
19637
+ arppu: z[2] ?? 0,
19638
+ ad: z[3] ?? 0
19639
+ };
19640
+ }
19641
+ const c = multiplyLowerTriangular(l, z);
19642
+ return {
19643
+ retention: c[0] ?? 0,
19644
+ conversion: c[1] ?? 0,
19645
+ arppu: c[2] ?? 0,
19646
+ ad: c[3] ?? 0
19647
+ };
19648
+ }
19649
+ function applyLogNormalShock(mean, sigma, z) {
19650
+ if (sigma <= 0)
19651
+ return mean;
19652
+ return mean * Math.exp(sigma * z - 0.5 * sigma * sigma);
19653
+ }
19654
+ function makeUncertainConfig(base, rng) {
19655
+ const shock = sampleCorrelatedNormals(rng, base.uncertainty.correlation);
19656
+ const d1 = clamp013(applyLogNormalShock(base.retention.d1, base.uncertainty.sigma.retention, shock.retention));
19657
+ const d7 = Math.min(d1, clamp013(applyLogNormalShock(base.retention.d7, base.uncertainty.sigma.retention, shock.retention)));
19658
+ const d30 = Math.min(d7, clamp013(applyLogNormalShock(base.retention.d30, base.uncertainty.sigma.retention, shock.retention)));
19659
+ const d90 = Math.min(d30, clamp013(applyLogNormalShock(base.retention.d90, base.uncertainty.sigma.retention, shock.retention)));
19660
+ return {
19661
+ ...base,
19662
+ retention: {
19663
+ ...base.retention,
19664
+ d1,
19665
+ d7,
19666
+ d30,
19667
+ d90
19668
+ },
19669
+ revenue: {
19670
+ ...base.revenue,
19671
+ payerConversion: clamp013(applyLogNormalShock(base.revenue.payerConversion, base.uncertainty.sigma.conversion, shock.conversion)),
19672
+ arppuDaily: Math.max(0, applyLogNormalShock(base.revenue.arppuDaily, base.uncertainty.sigma.arppu, shock.arppu)),
19673
+ adArpDau: Math.max(0, applyLogNormalShock(base.revenue.adArpDau, base.uncertainty.sigma.ad, shock.ad))
19674
+ }
19675
+ };
19676
+ }
19677
+ function quantileFromSorted(sorted, q) {
19678
+ if (sorted.length === 0)
19679
+ return 0;
19680
+ const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
19681
+ return sorted[idx] ?? 0;
19682
+ }
19683
+ function estimateLtvDistribution(args) {
19684
+ const rng = mulberry32(args.seed);
19685
+ const grossSamples = [];
19686
+ const netSamples = [];
19687
+ const ltvSamples = [];
19688
+ const draws = Math.max(1, args.draws);
19689
+ for (let i = 0;i < draws; i++) {
19690
+ const sampled = makeUncertainConfig(args.config, rng);
19691
+ const point = estimateLtvPerUser({
19692
+ config: sampled,
19693
+ horizonDays: args.horizonDays,
19694
+ progression: args.progression
19695
+ });
19696
+ grossSamples.push(point.cumulativeGrossRevenuePerUser);
19697
+ netSamples.push(point.cumulativeNetRevenuePerUser);
19698
+ ltvSamples.push(point.cumulativeLtvPerUser);
19699
+ }
19700
+ const avg = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length);
19701
+ const sortedLtv = [...ltvSamples].sort((a, b) => a - b);
19702
+ const byQuantile = {};
19703
+ for (const q of args.quantiles) {
19704
+ byQuantile[`q${Math.round(q * 100)}`] = quantileFromSorted(sortedLtv, q);
19705
+ }
19706
+ return {
19707
+ mean: {
19708
+ cumulativeGrossRevenuePerUser: avg(grossSamples),
19709
+ cumulativeNetRevenuePerUser: avg(netSamples),
19710
+ cumulativeLtvPerUser: avg(ltvSamples)
19711
+ },
19712
+ quantiles: byQuantile
19713
+ };
19714
+ }
19715
+ var init_uncertainty = () => {};
19716
+
19717
+ // ../core/dist/ltv/public.js
19718
+ function assertMonetization(monetization) {
19719
+ const valid = validateScenarioV1({
19720
+ schemaVersion: 1,
19721
+ unit: { code: "LTV" },
19722
+ policy: { mode: "drop" },
19723
+ model: { id: "ltv-input", version: 1 },
19724
+ initial: { wallet: { unit: "LTV", amount: "0" } },
19725
+ clock: { stepSec: 1, durationSec: 1 },
19726
+ monetization
19727
+ });
19728
+ if (!valid.ok)
19729
+ throw new RangeError(valid.issues.map((issue) => issue.message).join("; "));
19730
+ }
19731
+ function assertQuantileValues(quantiles) {
19732
+ if (quantiles.length === 0)
19733
+ throw new RangeError("quantiles must be nonempty and in 0..1");
19734
+ for (const q of quantiles) {
19735
+ if (!Number.isFinite(q) || q < 0 || q > 1)
19736
+ throw new RangeError("quantiles must be nonempty and in 0..1");
19737
+ }
19738
+ }
19739
+ function assertConfig(config) {
19740
+ const { retention, ...other } = config;
19741
+ for (const day of ["d1", "d7", "d30", "d90"]) {
19742
+ if (!Number.isFinite(retention[day]) || retention[day] < 0 || retention[day] > 1) {
19743
+ throw new RangeError(`retention.${day} must be finite and in 0..1`);
19744
+ }
19745
+ }
19746
+ if (!Number.isFinite(retention.longTailDailyDecay) || retention.longTailDailyDecay < 0) {
19747
+ throw new RangeError("retention.longTailDailyDecay must be finite and >= 0");
19748
+ }
19749
+ const { quantiles, ...uncertainty } = other.uncertainty;
19750
+ assertQuantileValues(quantiles);
19751
+ assertMonetization({ ...other, uncertainty });
19752
+ }
19753
+ function deriveMonetizationConfig2(scenario) {
19754
+ assertMonetization(scenario.monetization ?? {});
19755
+ const config = deriveMonetizationConfig(scenario);
19756
+ assertConfig(config);
19757
+ return config;
19758
+ }
19759
+ function assertPoint(args) {
19760
+ assertConfig(args.config);
19761
+ if (!Number.isFinite(args.horizonDays) || args.horizonDays < 0 || args.horizonDays > 36500) {
19762
+ throw new RangeError("horizonDays must be finite and in 0..36500");
19763
+ }
19764
+ if (!Number.isFinite(args.progression))
19765
+ throw new RangeError("progression must be finite");
19766
+ }
19767
+ function finiteResult(result) {
19768
+ if (!Object.values(result).every((value) => typeof value === "number" && Number.isFinite(value))) {
19769
+ throw new RangeError("LTV calculation overflowed; reduce the input magnitudes");
19770
+ }
19771
+ return result;
19772
+ }
19773
+ function estimateLtvPerUser2(args) {
19774
+ assertPoint(args);
19775
+ return finiteResult(estimateLtvPerUser(args));
19776
+ }
19777
+ function estimateLtvDistribution2(args) {
19778
+ assertPoint(args);
19779
+ if (!Number.isSafeInteger(args.draws) || args.draws < 1 || args.draws > 1e5 || args.draws * Math.max(1, Math.ceil(args.horizonDays)) > 1e7) {
19780
+ throw new RangeError("draws must be 1..100000 and draws \xD7 ceil(horizonDays) at most 10000000");
19781
+ }
19782
+ if (!Number.isFinite(args.seed))
19783
+ throw new RangeError("seed must be finite");
19784
+ if (args.quantiles.length > 101)
19785
+ throw new RangeError("quantiles exceed the 101 distinct percent keys");
19786
+ assertQuantileValues(args.quantiles);
19787
+ if (new Set(args.quantiles.map((q) => Math.round(q * 100))).size !== args.quantiles.length) {
19788
+ throw new RangeError("quantiles must be nonempty, in 0..1 and have distinct rounded percent keys");
19789
+ }
19790
+ const result = estimateLtvDistribution(args);
19791
+ finiteResult(result.mean);
19792
+ finiteResult(result.quantiles);
19793
+ return result;
19794
+ }
19795
+ var init_public = __esm(() => {
19796
+ init_validate();
19797
+ init_uncertainty();
19798
+ });
19799
+
19443
19800
  // ../core/dist/index.js
19444
19801
  var init_dist2 = __esm(() => {
19445
19802
  init_types();
@@ -19505,6 +19862,7 @@ var init_dist2 = __esm(() => {
19505
19862
  init_judge();
19506
19863
  init_flow();
19507
19864
  init_jevAdvisory();
19865
+ init_public();
19508
19866
  });
19509
19867
 
19510
19868
  // src/runtime/bun.ts
@@ -19570,7 +19928,7 @@ var init_bun = __esm(() => {
19570
19928
  var require_package = __commonJS(function(exports, module) {
19571
19929
  module.exports = {
19572
19930
  name: "@idlekit/cli",
19573
- version: "0.4.0",
19931
+ version: "0.5.1",
19574
19932
  description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
19575
19933
  license: "MIT",
19576
19934
  type: "module",
@@ -19623,7 +19981,7 @@ var require_package = __commonJS(function(exports, module) {
19623
19981
  "@clack/prompts": "1.8.1",
19624
19982
  "@gunshi/plugin-completion": "0.37.3",
19625
19983
  "@gunshi/plugin-suggestion": "0.37.3",
19626
- "@idlekit/core": "0.4.0",
19984
+ "@idlekit/core": "0.5.0",
19627
19985
  ajv: "^8.20.0",
19628
19986
  gunshi: "0.37.3",
19629
19987
  "proper-lockfile": "4.1.2",
@@ -35201,8 +35559,10 @@ async function readScenarioFileWithMeta(path) {
35201
35559
  }
35202
35560
 
35203
35561
  // src/io/writeOutput.ts
35562
+ init_errors3();
35204
35563
  init_bun();
35205
35564
  import { resolve as resolve5 } from "path";
35565
+ import { realpath, stat } from "fs/promises";
35206
35566
  function toCsv(data) {
35207
35567
  if (!Array.isArray(data)) {
35208
35568
  return `value
@@ -35276,6 +35636,27 @@ async function writeOutput(args) {
35276
35636
  return;
35277
35637
  }
35278
35638
  const abs = resolve5(args.outPath);
35639
+ for (const input of args.protectedInputs ?? []) {
35640
+ const source = resolve5(input);
35641
+ let same = source === abs;
35642
+ if (!same) {
35643
+ try {
35644
+ const [outputReal, inputReal, outputStat, inputStat] = await Promise.all([
35645
+ realpath(abs),
35646
+ realpath(source),
35647
+ stat(abs, { bigint: true }),
35648
+ stat(source, { bigint: true })
35649
+ ]);
35650
+ same = outputReal === inputReal || outputStat.ino !== 0n && outputStat.dev === inputStat.dev && outputStat.ino === inputStat.ino;
35651
+ } catch (error) {
35652
+ const code = error.code;
35653
+ if (code !== "ENOENT" && code !== "ENOTDIR")
35654
+ throw error;
35655
+ }
35656
+ }
35657
+ if (same)
35658
+ throw usageError(`Output path must not overwrite an input file: ${input}`);
35659
+ }
35279
35660
  await writeTextFile(abs, body);
35280
35661
  console.log(`Wrote ${args.format} output to ${abs}`);
35281
35662
  }
@@ -36449,14 +36830,23 @@ init_outputMeta();
36449
36830
  import { extname as extname3, resolve as resolve8 } from "path";
36450
36831
 
36451
36832
  // src/lib/ltvCalibration.ts
36452
- function clamp012(v) {
36833
+ function assertFiniteNumbers(value, path) {
36834
+ if (typeof value === "number" && !Number.isFinite(value)) {
36835
+ throw new RangeError(`Calibration requires finite numbers; numeric overflow at ${path}`);
36836
+ }
36837
+ if (typeof value === "object" && value !== null) {
36838
+ for (const [key, child] of Object.entries(value))
36839
+ assertFiniteNumbers(child, `${path}.${key}`);
36840
+ }
36841
+ }
36842
+ function clamp014(v) {
36453
36843
  if (v < 0)
36454
36844
  return 0;
36455
36845
  if (v > 1)
36456
36846
  return 1;
36457
36847
  return v;
36458
36848
  }
36459
- function clampCorrelation(v) {
36849
+ function clampCorrelation2(v) {
36460
36850
  if (!Number.isFinite(v))
36461
36851
  return 0;
36462
36852
  if (v < -1)
@@ -36521,13 +36911,13 @@ function estimateCorrelation(x, y) {
36521
36911
  varianceY
36522
36912
  };
36523
36913
  }
36524
- const raw = clampCorrelation(cov / Math.sqrt(vx * vy));
36914
+ const raw = clampCorrelation2(cov / Math.sqrt(vx) / Math.sqrt(vy));
36525
36915
  const ci95 = 1.96 / Math.sqrt(Math.max(1, n - 3));
36526
- const sampleConfidence = clamp012(1 - ci95);
36916
+ const sampleConfidence = clamp014(1 - ci95);
36527
36917
  const varianceConfidenceX = varianceX / (varianceX + 0.01);
36528
36918
  const varianceConfidenceY = varianceY / (varianceY + 0.01);
36529
36919
  const varianceConfidence = Math.sqrt(varianceConfidenceX * varianceConfidenceY);
36530
- const confidence = clamp012(sampleConfidence * varianceConfidence);
36920
+ const confidence = clamp014(sampleConfidence * varianceConfidence);
36531
36921
  const shrinkage = confidence;
36532
36922
  const value = raw * shrinkage;
36533
36923
  return {
@@ -36601,7 +36991,7 @@ function calibrateMonetization(rows) {
36601
36991
  const userAdArpDau = [];
36602
36992
  for (const user of users.values()) {
36603
36993
  const activeDays = Math.max(1, user.days.size);
36604
- const retentionScore = clamp012(activeDays / 30);
36994
+ const retentionScore = clamp014(activeDays / 30);
36605
36995
  const conversionScore = user.iap > 0 ? 1 : 0;
36606
36996
  const arppuScore = user.iap > 0 ? user.iap / activeDays : 0;
36607
36997
  const adScore = user.ad / activeDays;
@@ -36642,20 +37032,20 @@ function calibrateMonetization(rows) {
36642
37032
  conversionAd: estimatedCorrelationPair.conversionAd.confidence,
36643
37033
  arppuAd: estimatedCorrelationPair.arppuAd.confidence
36644
37034
  };
36645
- return {
37035
+ const result = {
36646
37036
  monetization: {
36647
37037
  cohorts: {
36648
37038
  baseUsers: cohortSize
36649
37039
  },
36650
37040
  retention: {
36651
- d1: clamp012(d1),
36652
- d7: clamp012(Math.min(d1, d7)),
36653
- d30: clamp012(Math.min(d7, d30)),
36654
- d90: clamp012(Math.min(d30, d90)),
37041
+ d1: clamp014(d1),
37042
+ d7: clamp014(Math.min(d1, d7)),
37043
+ d30: clamp014(Math.min(d1, d7, d30)),
37044
+ d90: clamp014(Math.min(d1, d7, d30, d90)),
36655
37045
  longTailDailyDecay: 0.02
36656
37046
  },
36657
37047
  revenue: {
36658
- payerConversion: clamp012(payerConversion),
37048
+ payerConversion: clamp014(payerConversion),
36659
37049
  arppuDaily: Math.max(0, arppuDaily),
36660
37050
  adArpDau: Math.max(0, adArpDau),
36661
37051
  platformFeeRate: 0.3,
@@ -36710,288 +37100,142 @@ function calibrateMonetization(rows) {
36710
37100
  correlationDiagnostics: estimatedCorrelationPair
36711
37101
  }
36712
37102
  };
37103
+ assertFiniteNumbers(result, "calibration");
37104
+ return result;
36713
37105
  }
36714
37106
  // src/lib/ltvProjection.ts
36715
- function clamp013(v) {
36716
- if (v < 0)
36717
- return 0;
36718
- if (v > 1)
36719
- return 1;
36720
- return v;
36721
- }
36722
- function deriveMonetizationConfig(scenario) {
36723
- const m = scenario.monetization;
36724
- const q = (m?.uncertainty?.quantiles ?? [0.5, 0.9]).filter((x) => typeof x === "number");
36725
- const quantiles = q.length > 0 ? [...new Set(q)].sort((a, b) => a - b) : [0.5, 0.9];
36726
- return {
36727
- cohorts: {
36728
- baseUsers: m?.cohorts?.baseUsers ?? 1
36729
- },
36730
- retention: {
36731
- d1: m?.retention?.d1 ?? 0.42,
36732
- d7: m?.retention?.d7 ?? 0.2,
36733
- d30: m?.retention?.d30 ?? 0.09,
36734
- d90: m?.retention?.d90 ?? 0.04,
36735
- longTailDailyDecay: m?.retention?.longTailDailyDecay ?? 0.02
36736
- },
36737
- revenue: {
36738
- payerConversion: m?.revenue?.payerConversion ?? 0.03,
36739
- arppuDaily: m?.revenue?.arppuDaily ?? 0.7,
36740
- adArpDau: m?.revenue?.adArpDau ?? 0.02,
36741
- platformFeeRate: m?.revenue?.platformFeeRate ?? 0.3,
36742
- grossMarginRate: m?.revenue?.grossMarginRate ?? 0.92,
36743
- progressionRevenueLift: m?.revenue?.progressionRevenueLift ?? 0.4,
36744
- progressionLogSpan: m?.revenue?.progressionLogSpan ?? 6
36745
- },
36746
- acquisition: {
36747
- cpi: m?.acquisition?.cpi ?? 0
36748
- },
36749
- uncertainty: {
36750
- enabled: m?.uncertainty?.enabled ?? false,
36751
- draws: m?.uncertainty?.draws ?? 300,
36752
- quantiles,
36753
- seed: m?.uncertainty?.seed,
36754
- sigma: {
36755
- retention: m?.uncertainty?.sigma?.retention ?? 0.08,
36756
- conversion: m?.uncertainty?.sigma?.conversion ?? 0.12,
36757
- arppu: m?.uncertainty?.sigma?.arppu ?? 0.2,
36758
- ad: m?.uncertainty?.sigma?.ad ?? 0.15
36759
- },
36760
- correlation: {
36761
- retentionConversion: m?.uncertainty?.correlation?.retentionConversion ?? 0,
36762
- retentionArppu: m?.uncertainty?.correlation?.retentionArppu ?? 0,
36763
- retentionAd: m?.uncertainty?.correlation?.retentionAd ?? 0,
36764
- conversionArppu: m?.uncertainty?.correlation?.conversionArppu ?? 0,
36765
- conversionAd: m?.uncertainty?.correlation?.conversionAd ?? 0,
36766
- arppuAd: m?.uncertainty?.correlation?.arppuAd ?? 0
36767
- }
36768
- }
36769
- };
36770
- }
36771
- function retentionAtDay(ret, day) {
36772
- if (day <= 0)
36773
- return 1;
36774
- if (day <= 1)
36775
- return ret.d1;
36776
- if (day <= 7)
36777
- return ret.d1 + (ret.d7 - ret.d1) * (day - 1) / 6;
36778
- if (day <= 30)
36779
- return ret.d7 + (ret.d30 - ret.d7) * (day - 7) / 23;
36780
- if (day <= 90)
36781
- return ret.d30 + (ret.d90 - ret.d30) * (day - 30) / 60;
36782
- const extra = day - 90;
36783
- return clamp013(ret.d90 * Math.exp(-ret.longTailDailyDecay * extra));
36784
- }
36785
- function progressionFactor(startWorthLog10, endWorthLog10, span) {
36786
- if (!Number.isFinite(startWorthLog10) || !Number.isFinite(endWorthLog10) || !Number.isFinite(span) || span <= 0) {
36787
- return 0;
36788
- }
36789
- return clamp013((endWorthLog10 - startWorthLog10) / span);
36790
- }
36791
- function estimateLtvPerUser(args) {
36792
- const { config, horizonDays } = args;
36793
- const progression = clamp013(args.progression);
36794
- const arppuDaily = config.revenue.arppuDaily * (1 + progression * config.revenue.progressionRevenueLift);
36795
- const adArpDau = config.revenue.adArpDau * (1 + progression * config.revenue.progressionRevenueLift * 0.5);
36796
- const payerConversion = config.revenue.payerConversion;
36797
- const fullDays = Math.floor(Math.max(0, horizonDays));
36798
- const remainder = Math.max(0, horizonDays - fullDays);
36799
- let gross = 0;
36800
- for (let day = 1;day <= fullDays; day++) {
36801
- const active = retentionAtDay(config.retention, day);
36802
- gross += active * (payerConversion * arppuDaily + adArpDau);
36803
- }
36804
- if (remainder > 0) {
36805
- const active = retentionAtDay(config.retention, fullDays + 1);
36806
- gross += remainder * active * (payerConversion * arppuDaily + adArpDau);
36807
- }
36808
- const net = gross * (1 - config.revenue.platformFeeRate) * config.revenue.grossMarginRate;
36809
- const ltv = net - config.acquisition.cpi;
36810
- return {
36811
- cumulativeGrossRevenuePerUser: gross,
36812
- cumulativeNetRevenuePerUser: net,
36813
- cumulativeLtvPerUser: ltv
36814
- };
36815
- }
37107
+ init_dist2();
36816
37108
  // src/lib/ltvUncertainty.ts
36817
- function clamp014(v) {
36818
- if (v < 0)
36819
- return 0;
36820
- if (v > 1)
36821
- return 1;
36822
- return v;
36823
- }
36824
- function clampCorrelation2(v) {
36825
- if (!Number.isFinite(v))
36826
- return 0;
36827
- if (v < -1)
36828
- return -1;
36829
- if (v > 1)
36830
- return 1;
36831
- return v;
36832
- }
36833
- function mulberry323(seed) {
36834
- let a = seed >>> 0;
36835
- return () => {
36836
- a |= 0;
36837
- a = a + 1831565813 | 0;
36838
- let t = Math.imul(a ^ a >>> 15, 1 | a);
36839
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
36840
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
37109
+ init_dist2();
37110
+ // src/lib/cohortObservation.ts
37111
+ var RETENTION_DAYS = [1, 7, 30, 90];
37112
+ function observeCohort(rows, observedThroughDay) {
37113
+ if (!Number.isSafeInteger(observedThroughDay) || observedThroughDay < 0)
37114
+ throw new Error("observed-through-day must be a nonnegative safe integer");
37115
+ if (rows.length === 0)
37116
+ throw new Error("Observed cohort requires at least one install record");
37117
+ const users = new Map;
37118
+ for (const row of rows) {
37119
+ if (!row.userId.trim() || !Number.isSafeInteger(row.day) || row.day < 0 || row.day > observedThroughDay) {
37120
+ throw new Error("Observed cohort requires a user ID and integer days within observed-through-day");
37121
+ }
37122
+ if (![row.iapRevenue, row.adRevenue, ...row.acquisitionCost === undefined ? [] : [row.acquisitionCost]].every((v) => Number.isFinite(v) && v >= 0)) {
37123
+ throw new Error("Observed cohort revenue and acquisition costs must be finite and nonnegative");
37124
+ }
37125
+ if (row.active !== undefined && typeof row.active !== "boolean")
37126
+ throw new Error("Observed cohort active must be boolean");
37127
+ const user = users.get(row.userId) ?? { days: new Set, active: new Set };
37128
+ if (user.days.has(row.day))
37129
+ throw new Error("Observed cohort requires one aggregated row per user and day");
37130
+ user.days.add(row.day);
37131
+ if (row.active ?? true)
37132
+ user.active.add(row.day);
37133
+ users.set(row.userId, user);
37134
+ }
37135
+ for (const user of users.values()) {
37136
+ if (!user.days.has(0))
37137
+ throw new Error("Observed cohort requires a day 0 install record for every user, including inactive users");
37138
+ }
37139
+ const n = users.size;
37140
+ const observation = {
37141
+ observedThroughDay,
37142
+ users: n,
37143
+ retention: RETENTION_DAYS.map((day) => {
37144
+ if (day > observedThroughDay)
37145
+ return { day, status: "not-observed", users: n, activeUsers: null, rate: null, interval95: null };
37146
+ const activeUsers = [...users.values()].filter((user) => user.active.has(day)).length;
37147
+ const rate = activeUsers / n;
37148
+ const z2 = 1.959963984540054 ** 2;
37149
+ const denominator = 1 + z2 / n;
37150
+ const center = (rate + z2 / (2 * n)) / denominator;
37151
+ const radius = Math.sqrt(z2 * (rate * (1 - rate) / n + z2 / (4 * n * n))) / denominator;
37152
+ return { day, status: "observed", users: n, activeUsers, rate, interval95: { min: activeUsers === 0 ? 0 : Math.max(0, center - radius), max: activeUsers === n ? 1 : Math.min(1, center + radius) } };
37153
+ })
36841
37154
  };
36842
- }
36843
- function randNormal(rng) {
36844
- const u1 = Math.max(0.000000000001, rng());
36845
- const u2 = Math.max(0.000000000001, rng());
36846
- return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
36847
- }
36848
- function buildCorrelationMatrix(base) {
36849
- const rc = clampCorrelation2(base.retentionConversion);
36850
- const ra = clampCorrelation2(base.retentionArppu);
36851
- const rd = clampCorrelation2(base.retentionAd);
36852
- const ca = clampCorrelation2(base.conversionArppu);
36853
- const cd = clampCorrelation2(base.conversionAd);
36854
- const ad = clampCorrelation2(base.arppuAd);
36855
- return [
36856
- [1, rc, ra, rd],
36857
- [rc, 1, ca, cd],
36858
- [ra, ca, 1, ad],
36859
- [rd, cd, ad, 1]
36860
- ];
36861
- }
36862
- function withOffDiagonalScale(matrix, alpha) {
36863
- return matrix.map((row, i) => row.map((v, j) => {
36864
- if (i === j)
36865
- return 1;
36866
- return v * alpha;
36867
- }));
36868
- }
36869
- function cholesky4(matrix) {
36870
- const n = 4;
36871
- const l = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
36872
- for (let i = 0;i < n; i++) {
36873
- for (let j = 0;j <= i; j++) {
36874
- let sum = 0;
36875
- for (let k = 0;k < j; k++) {
36876
- sum += (l[i]?.[k] ?? 0) * (l[j]?.[k] ?? 0);
36877
- }
36878
- if (i === j) {
36879
- const val = (matrix[i]?.[i] ?? 0) - sum;
36880
- if (val <= 0.000000001)
36881
- return null;
36882
- l[i][j] = Math.sqrt(val);
36883
- } else {
36884
- const denom = l[j]?.[j] ?? 0;
36885
- if (Math.abs(denom) < 0.000000001)
36886
- return null;
36887
- l[i][j] = ((matrix[i]?.[j] ?? 0) - sum) / denom;
36888
- }
37155
+ assertCohortObservation(observation);
37156
+ return observation;
37157
+ }
37158
+ function assertCohortObservation(observation) {
37159
+ if (!Number.isSafeInteger(observation.users) || observation.users < 1 || !Number.isSafeInteger(observation.observedThroughDay) || observation.observedThroughDay < 0) {
37160
+ throw new Error("Invalid cohort population or observation cutoff");
37161
+ }
37162
+ if (observation.retention.length !== RETENTION_DAYS.length)
37163
+ throw new Error("Cohort retention must contain D1, D7, D30 and D90");
37164
+ for (const [index, point] of observation.retention.entries()) {
37165
+ if (point.day !== RETENTION_DAYS[index] || point.users !== observation.users)
37166
+ throw new Error("Cohort retention days and population must match the observation");
37167
+ const observed = point.day <= observation.observedThroughDay;
37168
+ if (point.status !== (observed ? "observed" : "not-observed"))
37169
+ throw new Error("Cohort retention status must match the observation cutoff");
37170
+ if (!observed) {
37171
+ if (point.activeUsers !== null || point.rate !== null || point.interval95 !== null)
37172
+ throw new Error("Future retention must remain unobserved");
37173
+ continue;
36889
37174
  }
36890
- }
36891
- return l;
36892
- }
36893
- function multiplyLowerTriangular(l, z) {
36894
- const out = [0, 0, 0, 0];
36895
- for (let i = 0;i < 4; i++) {
36896
- let sum = 0;
36897
- for (let j = 0;j <= i; j++) {
36898
- sum += (l[i]?.[j] ?? 0) * (z[j] ?? 0);
37175
+ if (point.activeUsers === null || !Number.isSafeInteger(point.activeUsers) || point.activeUsers < 0 || point.activeUsers > point.users || point.rate !== point.activeUsers / point.users) {
37176
+ throw new Error("Retained users and retention rate must match the installed population");
37177
+ }
37178
+ const interval = point.interval95;
37179
+ if (point.rate === null || !interval || !Number.isFinite(interval.min) || !Number.isFinite(interval.max) || interval.min < 0 || interval.max > 1 || interval.min > point.rate || interval.max < point.rate) {
37180
+ throw new Error("Retention confidence interval must be finite and contain the observed rate");
36899
37181
  }
36900
- out[i] = sum;
36901
37182
  }
36902
- return out;
36903
37183
  }
36904
- function sampleCorrelatedNormals(rng, corr) {
36905
- const z = [randNormal(rng), randNormal(rng), randNormal(rng), randNormal(rng)];
36906
- const matrix = buildCorrelationMatrix(corr);
36907
- let l = cholesky4(matrix);
36908
- let alpha = 0.99;
36909
- while (!l && alpha > 0.000001) {
36910
- l = cholesky4(withOffDiagonalScale(matrix, alpha));
36911
- alpha *= 0.8;
37184
+ function observedRetentionRates(observation) {
37185
+ return Object.fromEntries(observation.retention.filter((row) => row.rate !== null).map((row) => [`d${row.day}`, row.rate]));
37186
+ }
37187
+ function planRetentionPatch(observation, assumptions = {}) {
37188
+ const rates = observedRetentionRates(observation);
37189
+ if (Object.keys(rates).length === 0)
37190
+ return { status: "unavailable", retention: {}, warnings: ["No retention day has been fully observed."] };
37191
+ const merged = { ...assumptions, ...rates };
37192
+ const warnings = [];
37193
+ for (const day of RETENTION_DAYS) {
37194
+ if (merged[`d${day}`] === undefined)
37195
+ warnings.push(`Unobserved D${day} has no explicit retained assumption. An LTV default could conflict with the observations; supply this assumption in --scenario or observe this day before applying a patch.`);
36912
37196
  }
36913
- if (!l) {
36914
- return {
36915
- retention: z[0] ?? 0,
36916
- conversion: z[1] ?? 0,
36917
- arppu: z[2] ?? 0,
36918
- ad: z[3] ?? 0
36919
- };
37197
+ let previous;
37198
+ for (const day of RETENTION_DAYS) {
37199
+ const rate = merged[`d${day}`];
37200
+ if (rate === undefined)
37201
+ continue;
37202
+ if (previous && previous.rate < rate)
37203
+ 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.`);
37204
+ previous = { day, rate };
36920
37205
  }
36921
- const c = multiplyLowerTriangular(l, z);
36922
- return {
36923
- retention: c[0] ?? 0,
36924
- conversion: c[1] ?? 0,
36925
- arppu: c[2] ?? 0,
36926
- ad: c[3] ?? 0
36927
- };
36928
- }
36929
- function applyLogNormalShock(mean, sigma, z) {
36930
- if (sigma <= 0)
36931
- return mean;
36932
- return mean * Math.exp(sigma * z - 0.5 * sigma * sigma);
36933
- }
36934
- function makeUncertainConfig(base, rng) {
36935
- const shock = sampleCorrelatedNormals(rng, base.uncertainty.correlation);
36936
- const d1 = clamp014(applyLogNormalShock(base.retention.d1, base.uncertainty.sigma.retention, shock.retention));
36937
- const d7 = Math.min(d1, clamp014(applyLogNormalShock(base.retention.d7, base.uncertainty.sigma.retention, shock.retention)));
36938
- const d30 = Math.min(d7, clamp014(applyLogNormalShock(base.retention.d30, base.uncertainty.sigma.retention, shock.retention)));
36939
- const d90 = Math.min(d30, clamp014(applyLogNormalShock(base.retention.d90, base.uncertainty.sigma.retention, shock.retention)));
36940
- return {
36941
- ...base,
36942
- retention: {
36943
- ...base.retention,
36944
- d1,
36945
- d7,
36946
- d30,
36947
- d90
36948
- },
36949
- revenue: {
36950
- ...base.revenue,
36951
- payerConversion: clamp014(applyLogNormalShock(base.revenue.payerConversion, base.uncertainty.sigma.conversion, shock.conversion)),
36952
- arppuDaily: Math.max(0, applyLogNormalShock(base.revenue.arppuDaily, base.uncertainty.sigma.arppu, shock.arppu)),
36953
- adArpDau: Math.max(0, applyLogNormalShock(base.revenue.adArpDau, base.uncertainty.sigma.ad, shock.ad))
36954
- }
36955
- };
37206
+ return warnings.length ? { status: "requires-review", retention: {}, warnings } : { status: "ready", retention: rates, warnings };
36956
37207
  }
36957
- function quantile2(values, q) {
36958
- if (values.length === 0)
36959
- return 0;
36960
- const sorted = [...values].sort((a, b) => a - b);
36961
- const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
36962
- return sorted[idx] ?? 0;
37208
+ function compareRetention(observation, assumptions) {
37209
+ return observation.retention.map((row) => {
37210
+ const assumption = assumptions[`d${row.day}`] ?? null;
37211
+ return {
37212
+ day: row.day,
37213
+ assumption,
37214
+ observed: row.rate,
37215
+ differencePercentagePoints: row.rate === null || assumption === null ? null : (row.rate - assumption) * 100,
37216
+ status: row.rate === null ? "not-observed" : assumption === null ? "unconfigured" : "compared"
37217
+ };
37218
+ });
36963
37219
  }
36964
- function estimateLtvDistribution(args) {
36965
- const rng = mulberry323(args.seed);
36966
- const grossSamples = [];
36967
- const netSamples = [];
36968
- const ltvSamples = [];
36969
- const draws = Math.max(1, args.draws);
36970
- for (let i = 0;i < draws; i++) {
36971
- const sampled = makeUncertainConfig(args.config, rng);
36972
- const point = estimateLtvPerUser({
36973
- config: sampled,
36974
- horizonDays: args.horizonDays,
36975
- progression: args.progression
36976
- });
36977
- grossSamples.push(point.cumulativeGrossRevenuePerUser);
36978
- netSamples.push(point.cumulativeNetRevenuePerUser);
36979
- ltvSamples.push(point.cumulativeLtvPerUser);
36980
- }
36981
- const avg = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length);
36982
- const byQuantile = {};
36983
- for (const q of args.quantiles) {
36984
- byQuantile[`q${Math.round(q * 100)}`] = quantile2(ltvSamples, q);
37220
+ function renderCohortMarkdown(observation, comparison, patch) {
37221
+ const percent = (value) => value === null ? "unavailable" : `${(value * 100).toFixed(2)}%`;
37222
+ const lines = [
37223
+ "# Observed player retention",
37224
+ "",
37225
+ `${observation.users} installed users; complete activity export through day ${observation.observedThroughDay}. Exact-day retention counts every installed user, including those who never return.`,
37226
+ "",
37227
+ "| Day | Retained / installed | Observed rate | Wilson 95% interval | Assumption | Difference (percentage points) | Status |",
37228
+ "| --- | --- | --- | --- | --- | --- | --- |"
37229
+ ];
37230
+ for (const [index, row] of observation.retention.entries()) {
37231
+ const match = comparison?.[index];
37232
+ lines.push(`| D${row.day} | ${row.activeUsers === null ? "unavailable" : `${row.activeUsers} / ${row.users}`} | ${percent(row.rate)} | ${row.interval95 ? `${percent(row.interval95.min)} to ${percent(row.interval95.max)}` : "unavailable"} | ${percent(match?.assumption ?? null)} | ${match?.differencePercentagePoints == null ? "unavailable" : match.differencePercentagePoints.toFixed(2)} | ${match?.status ?? row.status} |`);
36985
37233
  }
36986
- return {
36987
- mean: {
36988
- cumulativeGrossRevenuePerUser: avg(grossSamples),
36989
- cumulativeNetRevenuePerUser: avg(netSamples),
36990
- cumulativeLtvPerUser: avg(ltvSamples)
36991
- },
36992
- quantiles: byQuantile
36993
- };
37234
+ lines.push("", `Retention patch: **${patch.status}**.`, "", ...patch.warnings.map((warning) => `- ${warning}`), "", "Positive differences mean observed retention exceeds the configured assumption. Only apply a ready patch. Unobserved days are omitted from scenarioPatch; merge its retention fields into the scenario and preserve the remaining assumptions.", "", "Use a separate held-out cohort to validate a fitted model. These observations and intervals describe this cohort; they do not establish fun, causal improvement, or future revenue. Mixed install ages, missing activity exports, and omitted nonreturning users invalidate this comparison.");
37235
+ return lines.join(`
37236
+ `);
36994
37237
  }
37238
+
36995
37239
  // src/commands/calibrate.ts
36996
37240
  init_bun();
36997
37241
  function parseCsvRows(raw) {
@@ -37064,21 +37308,46 @@ function parseNumeric(raw, row, field, opts) {
37064
37308
  }
37065
37309
  return n;
37066
37310
  }
37067
- function parseCsvTelemetry(raw) {
37311
+ function parseActive(value, strict) {
37312
+ if (value === undefined)
37313
+ return true;
37314
+ if (typeof value === "boolean")
37315
+ return value;
37316
+ if (typeof value === "number" && (value === 0 || value === 1))
37317
+ return value === 1;
37318
+ if (typeof value === "string") {
37319
+ if (/^(1|true|yes|y)$/i.test(value))
37320
+ return true;
37321
+ if (/^(0|false|no|n)$/i.test(value))
37322
+ return false;
37323
+ }
37324
+ if (strict)
37325
+ throw usageError("Observed cohort active must be true/false, yes/no, or 1/0");
37326
+ return typeof value === "string" ? /^(1|true|yes|y)$/i.test(value) : Boolean(value);
37327
+ }
37328
+ function parseCsvTelemetry(raw, strictCohort = false) {
37068
37329
  const rows2d = parseCsvRows(raw);
37069
37330
  if (rows2d.length < 2) {
37070
37331
  throw cliError("CLI_USAGE", "CSV telemetry requires a header and at least one data row");
37071
37332
  }
37072
37333
  const headers = (rows2d[0] ?? []).map(normalizeHeader);
37334
+ if (strictCohort && new Set(headers).size !== headers.length)
37335
+ throw usageError("Observed cohort CSV has duplicate headers");
37073
37336
  const rows = [];
37074
37337
  for (let i = 1;i < rows2d.length; i++) {
37075
37338
  const cols = rows2d[i] ?? [];
37339
+ if (strictCohort && cols.length !== headers.length)
37340
+ throw usageError(`Observed cohort CSV row ${i + 1} has the wrong number of columns`);
37076
37341
  const rec = {};
37077
37342
  headers.forEach((h, idx) => {
37078
37343
  rec[h] = cols[idx] ?? "";
37079
37344
  });
37080
- const userId = pick2(rec, ["user_id", "userid", "user", "id"]) ?? `anon-${i}`;
37081
- const dayRaw = pick2(rec, ["day", "day_index", "days_since_install"]) ?? "0";
37345
+ const userInput = pick2(rec, ["user_id", "userid", "user", "id"]);
37346
+ const dayInput = pick2(rec, ["day", "day_index", "days_since_install"]);
37347
+ if (strictCohort && (!userInput?.trim() || !dayInput?.trim()))
37348
+ throw usageError("Observed cohort requires explicit user_id and day fields");
37349
+ const userId = userInput ?? `anon-${i}`;
37350
+ const dayRaw = dayInput ?? "0";
37082
37351
  const iapRaw = pick2(rec, ["iap_revenue", "revenue", "iap", "purchase_revenue"]) ?? "0";
37083
37352
  const adRaw = pick2(rec, ["ad_revenue", "adrevenue", "ads"]) ?? "0";
37084
37353
  const activeRaw = pick2(rec, ["active", "is_active"]);
@@ -37087,7 +37356,7 @@ function parseCsvTelemetry(raw) {
37087
37356
  const iapRevenue = parseNumeric(iapRaw, i + 1, "revenue");
37088
37357
  const adRevenue = parseNumeric(adRaw, i + 1, "ad_revenue");
37089
37358
  const acquisitionCost = acqRaw !== undefined ? parseNumeric(acqRaw, i + 1, "acquisition_cost", { allowEmpty: true }) : undefined;
37090
- const active = activeRaw === undefined ? true : /^(1|true|yes|y)$/i.test(activeRaw);
37359
+ const active = parseActive(activeRaw, strictCohort);
37091
37360
  rows.push({
37092
37361
  userId,
37093
37362
  day,
@@ -37099,7 +37368,7 @@ function parseCsvTelemetry(raw) {
37099
37368
  }
37100
37369
  return rows;
37101
37370
  }
37102
- function parseJsonTelemetry(raw) {
37371
+ function parseJsonTelemetry(raw, strictCohort = false) {
37103
37372
  const parsed = JSON.parse(raw);
37104
37373
  if (!Array.isArray(parsed)) {
37105
37374
  throw cliError("CLI_USAGE", "JSON telemetry must be an array");
@@ -37110,12 +37379,21 @@ function parseJsonTelemetry(raw) {
37110
37379
  throw cliError("CLI_USAGE", `JSON telemetry row ${idx} must be an object`);
37111
37380
  }
37112
37381
  const r = row;
37113
- const userId = String(r.userId ?? r.user_id ?? r.user ?? r.id ?? `anon-${idx}`);
37114
- const day = Number(r.day ?? r.dayIndex ?? r.daysSinceInstall ?? 0);
37382
+ const userInput = r.userId ?? r.user_id ?? r.user ?? r.id;
37383
+ const dayInput = r.day ?? r.dayIndex ?? r.daysSinceInstall;
37384
+ if (strictCohort && (typeof userInput !== "string" || !userInput.trim() || typeof dayInput !== "number"))
37385
+ throw usageError("Observed cohort JSON requires explicit string userId and numeric day fields");
37386
+ if (strictCohort)
37387
+ for (const key of ["iapRevenue", "iap_revenue", "revenue", "adRevenue", "ad_revenue", "acquisitionCost", "acquisition_cost", "cpi"]) {
37388
+ if (r[key] !== undefined && typeof r[key] !== "number")
37389
+ throw usageError(`Observed cohort JSON ${key} must be numeric`);
37390
+ }
37391
+ const userId = String(userInput ?? `anon-${idx}`);
37392
+ const day = Number(dayInput ?? 0);
37115
37393
  const iapRevenue = Number(r.iapRevenue ?? r.iap_revenue ?? r.revenue ?? 0);
37116
37394
  const adRevenue = Number(r.adRevenue ?? r.ad_revenue ?? 0);
37117
37395
  const acquisitionCost = r.acquisitionCost !== undefined || r.acquisition_cost !== undefined || r.cpi !== undefined ? Number(r.acquisitionCost ?? r.acquisition_cost ?? r.cpi) : undefined;
37118
- const active = r.active === undefined ? true : Boolean(r.active);
37396
+ const active = parseActive(r.active, strictCohort);
37119
37397
  if (!Number.isFinite(day) || !Number.isFinite(iapRevenue) || !Number.isFinite(adRevenue)) {
37120
37398
  throw cliError("CLI_USAGE", `Invalid numeric value in json row ${idx}`);
37121
37399
  }
@@ -37137,26 +37415,65 @@ var calibrate_default = defineCommand({
37137
37415
  "input-format": option(_enum(["auto", "csv", "json"]).default("auto"), {
37138
37416
  description: "Telemetry file format"
37139
37417
  }),
37418
+ "observed-through-day": option(exports_coerce.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(), {
37419
+ description: "Complete activity cutoff for one install cohort; requires day 0 records for every installed user"
37420
+ }),
37421
+ scenario: option(string2().optional(), { description: "Compare observed retention with this scenario's explicit assumptions (requires --observed-through-day)" }),
37140
37422
  out: option(string2().optional(), { description: "Output path" }),
37141
37423
  format: option(_enum(["json", "md", "csv"]).default("json"), { description: "Output format" })
37142
37424
  },
37143
37425
  async handler({ flags, positional }) {
37144
37426
  const telemetryPath = positional[0];
37145
- if (!telemetryPath) {
37427
+ if (!telemetryPath || positional.length !== 1) {
37146
37428
  throw usageError("Usage: idk calibrate <telemetry.csv|json> [--input-format auto|csv|json]");
37147
37429
  }
37430
+ if (flags.scenario && flags["observed-through-day"] === undefined)
37431
+ throw usageError("--scenario requires --observed-through-day for a mature cohort comparison");
37148
37432
  const abs = resolve8(process.cwd(), telemetryPath);
37149
37433
  const raw = await readTextFile(abs);
37150
37434
  const format = flags["input-format"] === "auto" ? extname3(abs).toLowerCase() : flags["input-format"];
37151
- const rows = format === ".csv" || format === "csv" ? parseCsvTelemetry(raw) : parseJsonTelemetry(raw);
37152
- const calibrated = calibrateMonetization(rows);
37435
+ const rows = format === ".csv" || format === "csv" ? parseCsvTelemetry(raw, flags["observed-through-day"] !== undefined) : parseJsonTelemetry(raw, flags["observed-through-day"] !== undefined);
37436
+ let observation;
37437
+ if (flags["observed-through-day"] !== undefined) {
37438
+ try {
37439
+ observation = observeCohort(rows, flags["observed-through-day"]);
37440
+ } catch (error) {
37441
+ throw usageError(error instanceof Error ? error.message : String(error));
37442
+ }
37443
+ }
37444
+ let calibrated;
37445
+ try {
37446
+ calibrated = calibrateMonetization(rows);
37447
+ } catch (error) {
37448
+ if (error instanceof RangeError)
37449
+ throw usageError(error.message);
37450
+ throw error;
37451
+ }
37452
+ const retentionRates = observation ? observedRetentionRates(observation) : undefined;
37453
+ const scenario = flags.scenario ? await readScenarioFile(flags.scenario) : undefined;
37454
+ const rate = number2().min(0).max(1).optional();
37455
+ const parsedScenario = scenario === undefined ? undefined : object({
37456
+ schemaVersion: literal(1),
37457
+ monetization: object({ retention: object({ d1: rate, d7: rate, d30: rate, d90: rate }).passthrough().optional() }).passthrough().optional()
37458
+ }).passthrough().safeParse(scenario);
37459
+ if (parsedScenario && !parsedScenario.success)
37460
+ throw usageError(`Invalid scenario retention assumptions: ${parsedScenario.error.message}`);
37461
+ const comparison = observation && parsedScenario?.success ? compareRetention(observation, parsedScenario.data.monetization?.retention ?? {}) : undefined;
37462
+ const patch = observation ? planRetentionPatch(observation, parsedScenario?.success ? parsedScenario.data.monetization?.retention : undefined) : undefined;
37463
+ const monetization = observation ? {
37464
+ ...calibrated.monetization,
37465
+ retention: { longTailDailyDecay: calibrated.monetization?.retention?.longTailDailyDecay, ...patch?.retention }
37466
+ } : calibrated.monetization;
37153
37467
  const outputMeta = buildOutputMeta({
37154
37468
  command: "calibrate",
37155
- telemetry: rows
37469
+ telemetry: observation ? { rows, observedThroughDay: observation.observedThroughDay } : rows,
37470
+ scenario,
37471
+ scenarioPath: flags.scenario ? resolve8(flags.scenario) : undefined
37156
37472
  });
37157
37473
  await writeOutput({
37158
37474
  format: flags.format,
37159
37475
  outPath: flags.out,
37476
+ protectedInputs: [abs, ...flags.scenario ? [resolve8(flags.scenario)] : []],
37160
37477
  data: flags.format === "json" ? {
37161
37478
  ok: true,
37162
37479
  source: abs,
@@ -37164,12 +37481,27 @@ var calibrate_default = defineCommand({
37164
37481
  rows: rows.length,
37165
37482
  users: calibrated.diagnostics.users
37166
37483
  },
37167
- monetization: calibrated.monetization,
37168
- diagnostics: calibrated.diagnostics,
37169
- scenarioPatch: {
37484
+ monetization,
37485
+ diagnostics: observation ? { ...calibrated.diagnostics, retention: retentionRates, observedThroughDay: observation.observedThroughDay } : calibrated.diagnostics,
37486
+ ...observation ? { observation } : {},
37487
+ ...comparison ? { comparison } : {},
37488
+ ...patch ? { patchStatus: patch.status, patchWarnings: patch.warnings } : {},
37489
+ scenarioPatch: observation ? { monetization: { retention: patch.retention } } : {
37170
37490
  monetization: calibrated.monetization
37171
37491
  }
37172
- } : [
37492
+ } : observation ? flags.format === "md" ? renderCohortMarkdown(observation, comparison, patch) : observation.retention.map((row, index) => ({
37493
+ day: row.day,
37494
+ users: row.users,
37495
+ activeUsers: row.activeUsers,
37496
+ observedRate: row.rate,
37497
+ interval95Min: row.interval95?.min ?? null,
37498
+ interval95Max: row.interval95?.max ?? null,
37499
+ assumption: comparison?.[index]?.assumption ?? null,
37500
+ differencePercentagePoints: comparison?.[index]?.differencePercentagePoints ?? null,
37501
+ status: comparison?.[index]?.status ?? row.status,
37502
+ patchStatus: patch.status,
37503
+ patchWarnings: patch.warnings.join(" ")
37504
+ })) : [
37173
37505
  {
37174
37506
  source: abs,
37175
37507
  rows: rows.length,
@@ -37191,12 +37523,13 @@ var calibrate_default = defineCommand({
37191
37523
 
37192
37524
  // src/commands/balance.ts
37193
37525
  init_zod();
37526
+ import { resolve as resolve11 } from "path";
37194
37527
 
37195
37528
  // src/balance/workflow.ts
37196
37529
  init_dist2();
37197
37530
  init_zod();
37198
37531
  import { resolve as resolve10, dirname as dirname8 } from "path";
37199
- import { stat } from "fs/promises";
37532
+ import { stat as stat2 } from "fs/promises";
37200
37533
 
37201
37534
  // src/cliMeta.ts
37202
37535
  var import_package2 = __toESM(require_package(), 1);
@@ -37442,7 +37775,7 @@ async function runPacingChecks(input, evaluator) {
37442
37775
  // src/balance/sheet.ts
37443
37776
  init_bun();
37444
37777
  import { constants } from "fs";
37445
- import { lstat, mkdir, open as open2, realpath, rename, rm } from "fs/promises";
37778
+ import { lstat, mkdir, open as open2, realpath as realpath2, rename, rm } from "fs/promises";
37446
37779
  import { dirname as dirname7, isAbsolute as isAbsolute2, join, relative as relative2, resolve as resolve9, sep as sep2 } from "path";
37447
37780
  var SHEET_LIMITS = Object.freeze({
37448
37781
  maxCsvBytes: 2 * 1024 * 1024,
@@ -37816,7 +38149,7 @@ async function snapshotInputs(inputFiles, contextValue, limits) {
37816
38149
  let total = 0;
37817
38150
  for (const name of validInputNames(inputFiles, limits)) {
37818
38151
  const path = resolve9(inputFiles[name]);
37819
- const realPath = await realpath(path);
38152
+ const realPath = await realpath2(path);
37820
38153
  const bytes = await boundedRead(realPath, Math.min(limits.maxInputBytes, limits.maxTotalInputBytes - total));
37821
38154
  total += bytes.length;
37822
38155
  if (total > limits.maxTotalInputBytes)
@@ -37843,7 +38176,7 @@ async function snapshotInputs(inputFiles, contextValue, limits) {
37843
38176
  async function canonicalOutput(path) {
37844
38177
  path = resolve9(path);
37845
38178
  try {
37846
- return await realpath(path);
38179
+ return await realpath2(path);
37847
38180
  } catch (error) {
37848
38181
  if (error.code !== "ENOENT")
37849
38182
  throw error;
@@ -37880,7 +38213,7 @@ async function refreshSheetBundle(options) {
37880
38213
  validInputNames(options.inputFiles, limits);
37881
38214
  const output = await canonicalOutput(options.outputDir);
37882
38215
  for (const [name, path] of Object.entries(options.inputFiles)) {
37883
- if (isWithin(output, resolve9(path)) || isWithin(output, await realpath(path)))
38216
+ if (isWithin(output, resolve9(path)) || isWithin(output, await realpath2(path)))
37884
38217
  throw new Error(`Output/input collision: ${name}`);
37885
38218
  }
37886
38219
  await mkdir(output, { recursive: true });
@@ -37940,7 +38273,7 @@ async function refreshSheetBundle(options) {
37940
38273
  if (manifestBytes.length > 1024 * 1024)
37941
38274
  throw new Error("Bundle manifest exceeds byte limit");
37942
38275
  await mkdir(generations, { recursive: true });
37943
- if (await realpath(generations) !== generations)
38276
+ if (await realpath2(generations) !== generations)
37944
38277
  throw new Error("Bundle generations directory must not be a symlink");
37945
38278
  await mkdir(stage);
37946
38279
  for (const name of names)
@@ -38029,10 +38362,10 @@ async function readCurrentBundle(outputDir, inputFiles, options = {}) {
38029
38362
  if (!isRecord5(pointer) || pointer.version !== 1 || typeof pointer.generation !== "string" || !generationText.test(pointer.generation) || typeof pointer.manifestSha256 !== "string" || !sha256Text2.test(pointer.manifestSha256))
38030
38363
  throw new Error("Invalid bundle pointer");
38031
38364
  const generationPath = join(output, "generations", pointer.generation);
38032
- if (await realpath(generationPath) !== generationPath)
38365
+ if (await realpath2(generationPath) !== generationPath)
38033
38366
  throw new Error("Bundle generation must not be a symlink");
38034
38367
  const manifestPath = join(generationPath, "manifest.json");
38035
- if (await realpath(manifestPath) !== manifestPath)
38368
+ if (await realpath2(manifestPath) !== manifestPath)
38036
38369
  throw new Error("Bundle manifest must not be a symlink");
38037
38370
  const bytes = await boundedRead(manifestPath, 1024 * 1024);
38038
38371
  if (sha256Hex(bytes) !== pointer.manifestSha256)
@@ -38041,7 +38374,7 @@ async function readCurrentBundle(outputDir, inputFiles, options = {}) {
38041
38374
  let outputBytesRead = 0;
38042
38375
  for (const [name, artifact] of Object.entries(manifest.artifacts)) {
38043
38376
  try {
38044
- if (await realpath(artifact.path) !== artifact.path)
38377
+ if (await realpath2(artifact.path) !== artifact.path)
38045
38378
  throw new Error("Artifact is a symlink");
38046
38379
  const artifactBytes = await boundedRead(artifact.path, Math.min(limits.maxOutputBytes, limits.maxTotalOutputBytes - outputBytesRead));
38047
38380
  outputBytesRead += artifactBytes.length;
@@ -38141,7 +38474,7 @@ function normalizeRunDocument(document, strategy, horizon) {
38141
38474
  }
38142
38475
  async function refreshBalanceWorkflow(configPath, flags, check = false) {
38143
38476
  const absoluteConfig = resolve10(configPath);
38144
- const configInfo = await stat(absoluteConfig);
38477
+ const configInfo = await stat2(absoluteConfig);
38145
38478
  if (!configInfo.isFile() || configInfo.size > SHEET_LIMITS.maxInputBytes)
38146
38479
  throw new Error("Workflow config is not a bounded regular file");
38147
38480
  const rawConfig = await readTextFile(absoluteConfig);
@@ -38337,6 +38670,46 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38337
38670
  return { ...manifest, outcome };
38338
38671
  }
38339
38672
 
38673
+ // src/balance/report.ts
38674
+ function cell(value) {
38675
+ return String(value).replaceAll("|", "\\|").replace(/[\r\n]/g, " ");
38676
+ }
38677
+ function explain(result) {
38678
+ if (result.status === "breach" && result.value !== null) {
38679
+ if (result.min !== undefined && result.value < result.min)
38680
+ return `below the minimum by ${result.min - result.value} ${result.unit}`;
38681
+ if (result.max !== undefined && result.value > result.max)
38682
+ return `above the maximum by ${result.value - result.max} ${result.unit}`;
38683
+ }
38684
+ if (result.status === "pass")
38685
+ return "within the inclusive target bounds";
38686
+ return result.message ?? (result.status === "unreached" ? "not reached within the horizon" : "measurement unavailable");
38687
+ }
38688
+ function renderBalanceReport(report, generationPath) {
38689
+ const lines = [
38690
+ "# Balance targets",
38691
+ "",
38692
+ `Result: **${report.status}**. Simulated ${report.horizonSec} seconds using strategy ${cell(report.strategy)}; seeds ${report.seeds.join(", ")}.`,
38693
+ "A target is a design constraint on this model under these conditions. End net worth is modeled asset value; it can differ from spendable wallet money.",
38694
+ "",
38695
+ "| Variant | Seed | Metric | Measured | Target | Status | Explanation |",
38696
+ "| --- | --- | --- | --- | --- | --- | --- |"
38697
+ ];
38698
+ for (const entry of report.runs)
38699
+ for (const result of entry.results) {
38700
+ const target = [result.min === undefined ? undefined : `minimum ${result.min} ${result.unit}`, result.max === undefined ? undefined : `maximum ${result.max} ${result.unit}`].filter(Boolean).join(", ");
38701
+ const measured = result.value === null ? "unavailable" : `${result.value} ${result.unit}`;
38702
+ lines.push(`| ${cell(entry.run.variantId)} | ${entry.run.seed} | ${cell(result.metric)} | ${cell(measured)} | ${cell(target)} | ${result.status} | ${cell(explain(result))} |`);
38703
+ }
38704
+ lines.push("", report.ok ? "All declared targets passed." : "A target breached, was not reached, or could not be measured. The command exits 1; inspect the rows before changing the design.", "", `Saved generation: ${cell(generationPath)}`, "The generation contains exact values in results.json and reproduction inputs in provenance.json. Passing targets does not establish fun or player retention.");
38705
+ return lines.join(`
38706
+ `);
38707
+ }
38708
+ function renderBalanceFreshness(result) {
38709
+ return ["# Balance result freshness", "", `State: **${result.state}**. This checks saved inputs and artifacts; it does not check target success. Run balance without --check to evaluate the targets.`, ...result.staleReasons.map((reason) => `- ${cell(reason)}`)].join(`
38710
+ `);
38711
+ }
38712
+
38340
38713
  // src/commands/balance.ts
38341
38714
  init_errors3();
38342
38715
  init_bun();
@@ -38345,13 +38718,15 @@ var balance_default = defineCommand({
38345
38718
  description: "Refresh a typed CSV balance sheet into scenario, pacing checks, and result sheets",
38346
38719
  options: {
38347
38720
  ...pluginOptions(),
38348
- check: option(exports_coerce.boolean().default(false), { description: "Check result freshness without rerunning simulations" })
38721
+ check: option(exports_coerce.boolean().default(false), { description: "Check result freshness without rerunning simulations" }),
38722
+ format: option(_enum(["json", "md"]).default("json"), { description: "JSON manifest or human-readable target explanations" })
38349
38723
  },
38350
38724
  async handler({ flags, positional }) {
38351
38725
  if (positional.length !== 1)
38352
38726
  throw usageError("Usage: idk balance <workflow.json> [--check] [plugin options]");
38353
38727
  const result = await refreshBalanceWorkflow(positional[0], flags, flags.check);
38354
- await writeStdout(JSON.stringify(result, null, 2) + `
38728
+ const body = flags.format === "json" ? JSON.stringify(result, null, 2) : ("state" in result) ? renderBalanceFreshness(result) : renderBalanceReport(JSON.parse(await readTextFile(resolve11(result.generationPath, "results.json"))), result.generationPath);
38729
+ await writeStdout(body + `
38355
38730
  `);
38356
38731
  if (flags.check && "state" in result && result.state !== "current")
38357
38732
  process.exitCode = 1;
@@ -38363,7 +38738,7 @@ var balance_default = defineCommand({
38363
38738
  // src/commands/compare.ts
38364
38739
  init_dist2();
38365
38740
  init_zod();
38366
- import { resolve as resolve13 } from "path";
38741
+ import { resolve as resolve14 } from "path";
38367
38742
 
38368
38743
  // src/commands/_shared/compareEval.ts
38369
38744
  function formatEtaLabel(seconds, reached) {
@@ -38390,12 +38765,12 @@ init_errors3();
38390
38765
  init_outputMeta();
38391
38766
 
38392
38767
  // src/io/replayPolicy.ts
38393
- import { resolve as resolve12 } from "path";
38768
+ import { resolve as resolve13 } from "path";
38394
38769
 
38395
38770
  // src/io/replayArtifact.ts
38396
38771
  init_outputMeta();
38397
38772
  init_bun();
38398
- import { resolve as resolve11 } from "path";
38773
+ import { resolve as resolve12 } from "path";
38399
38774
  var REDACTED_KEYS = new Set([
38400
38775
  "generatedAt",
38401
38776
  "scenario",
@@ -38461,7 +38836,7 @@ function toReplayCommandLine(args) {
38461
38836
  return `bun run --cwd packages/cli dev -- ${rendered}`;
38462
38837
  }
38463
38838
  async function writeReplayArtifact(args) {
38464
- const abs = resolve11(args.outPath);
38839
+ const abs = resolve12(args.outPath);
38465
38840
  const runId = args.meta.runId;
38466
38841
  const seed = args.meta.seed;
38467
38842
  const scenarioHash = args.meta.scenarioHash;
@@ -38537,7 +38912,7 @@ var PATH_FLAG_KINDS = {
38537
38912
  function normalizePathLike(pathLike, cwd) {
38538
38913
  if (pathLike.trim().length === 0)
38539
38914
  return pathLike;
38540
- return resolve12(cwd, pathLike);
38915
+ return resolve13(cwd, pathLike);
38541
38916
  }
38542
38917
  function normalizeCsvPaths(raw, cwd) {
38543
38918
  return raw.split(",").map((x) => x.trim()).filter(Boolean).map((x) => normalizePathLike(x, cwd)).join(",");
@@ -38612,7 +38987,7 @@ function plain(value) {
38612
38987
  return "n/a";
38613
38988
  return String(value).replace(/[\r\n]+/g, " ");
38614
38989
  }
38615
- function cell(value) {
38990
+ function cell2(value) {
38616
38991
  return plain(value).replaceAll("|", "\\|");
38617
38992
  }
38618
38993
  function inlineCode(value) {
@@ -38640,9 +39015,9 @@ function renderCompareMarkdown(args) {
38640
39015
  const higher = ["endMoney", "endNetWorth", "visibleChangesPerMinute"].includes(metric);
38641
39016
  const lower = ["droppedRate", "etaToTargetWorth", "timeToMilestone", "maxNoRewardGapSec"].includes(metric);
38642
39017
  const seconds = ["etaToTargetWorth", "timeToMilestone", "maxNoRewardGapSec"].includes(metric);
38643
- const value = (item) => seconds && (typeof item === "number" || typeof item === "string" && item.trim().length > 0) && Number.isFinite(Number(item)) ? `${cell(item)}s` : cell(item);
39018
+ const value = (item) => seconds && (typeof item === "number" || typeof item === "string" && item.trim().length > 0) && Number.isFinite(Number(item)) ? `${cell2(item)}s` : cell2(item);
38644
39019
  const better = result.better === "a" || result.better === "b" ? result.better.toUpperCase() : result.better === "tie" ? "tie" : "undetermined";
38645
- lines.push(`| ${cell(metric)} | ${value(a)} | ${value(b)} | ${higher ? "higher" : lower ? "lower" : "n/a"} | ${better} |`);
39020
+ lines.push(`| ${cell2(metric)} | ${value(a)} | ${value(b)} | ${higher ? "higher" : lower ? "lower" : "n/a"} | ${better} |`);
38646
39021
  if (result.better === "a" || result.better === "b")
38647
39022
  preferences[result.better].push(metric);
38648
39023
  }
@@ -39319,8 +39694,8 @@ var compare_default = defineCommand({
39319
39694
  }
39320
39695
  } : singleResults[0];
39321
39696
  if (flags["artifact-out"]) {
39322
- const aAbs = resolve13(process.cwd(), aPath);
39323
- const bAbs = resolve13(process.cwd(), bPath);
39697
+ const aAbs = resolve14(process.cwd(), aPath);
39698
+ const bAbs = resolve14(process.cwd(), bPath);
39324
39699
  await writeCommandReplayArtifact({
39325
39700
  outPath: flags["artifact-out"],
39326
39701
  command: "compare",
@@ -39358,7 +39733,7 @@ init_outputMeta();
39358
39733
  init_errors3();
39359
39734
  init_bun();
39360
39735
  init_selfCli();
39361
- import { basename as basename4, dirname as dirname9, relative as relative3, resolve as resolve14 } from "path";
39736
+ import { basename as basename4, dirname as dirname9, relative as relative3, resolve as resolve15 } from "path";
39362
39737
  var START_MARKER = "# >>> idk completions >>>";
39363
39738
  var END_MARKER = "# <<< idk completions <<<";
39364
39739
  function userHome() {
@@ -39384,13 +39759,13 @@ function defaultRcPath(shell) {
39384
39759
  const home = userHome();
39385
39760
  switch (shell) {
39386
39761
  case "zsh":
39387
- return resolve14(home, ".zshrc");
39762
+ return resolve15(home, ".zshrc");
39388
39763
  case "bash":
39389
- return resolve14(home, ".bashrc");
39764
+ return resolve15(home, ".bashrc");
39390
39765
  case "fish":
39391
- return resolve14(home, ".config/fish/config.fish");
39766
+ return resolve15(home, ".config/fish/config.fish");
39392
39767
  case "powershell":
39393
- return resolve14(home, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1");
39768
+ return resolve15(home, "Documents/PowerShell/Microsoft.PowerShell_profile.ps1");
39394
39769
  }
39395
39770
  }
39396
39771
  function completionsSourceLine(shell, executable = "idk") {
@@ -39423,7 +39798,7 @@ function replaceManagedBlock(existing, block) {
39423
39798
  }
39424
39799
  async function installCompletions(args) {
39425
39800
  const shell = detectShell(args.shell);
39426
- const rcPath = resolve14(args.rcPath ?? defaultRcPath(shell));
39801
+ const rcPath = resolve15(args.rcPath ?? defaultRcPath(shell));
39427
39802
  const line = completionsSourceLine(shell, args.executable ?? "idk");
39428
39803
  const script = runSelfCli(["completions", shell]);
39429
39804
  if (script.exitCode !== 0) {
@@ -39467,7 +39842,7 @@ ${END_MARKER}
39467
39842
  }
39468
39843
  async function readCompletionSetup(args) {
39469
39844
  const shell = detectShell(args.shell);
39470
- const rcPath = resolve14(args.rcPath ?? defaultRcPath(shell));
39845
+ const rcPath = resolve15(args.rcPath ?? defaultRcPath(shell));
39471
39846
  const line = completionsSourceLine(shell, args.executable ?? "idk");
39472
39847
  const exists = await fileExists(rcPath);
39473
39848
  if (!exists) {
@@ -39478,7 +39853,7 @@ async function readCompletionSetup(args) {
39478
39853
  return { shell, rcPath, installed, line };
39479
39854
  }
39480
39855
  function commonDir(paths) {
39481
- const parts = paths.map((value) => dirname9(resolve14(value)).split("/").filter(Boolean));
39856
+ const parts = paths.map((value) => dirname9(resolve15(value)).split("/").filter(Boolean));
39482
39857
  if (parts.length === 0)
39483
39858
  return process.cwd();
39484
39859
  const first = parts[0] ?? [];
@@ -39499,13 +39874,13 @@ async function writePluginTrust(args) {
39499
39874
  if (args.plugins.length === 0) {
39500
39875
  throw usageError("setup plugin-trust requires at least one --plugin path.");
39501
39876
  }
39502
- const outPath = resolve14(args.outPath);
39877
+ const outPath = resolve15(args.outPath);
39503
39878
  if (await fileExists(outPath) && !args.force) {
39504
39879
  throw usageError(`Output file already exists: ${outPath}`, "Pass --force true to overwrite.");
39505
39880
  }
39506
39881
  const entries = {};
39507
39882
  for (const plugin of args.plugins) {
39508
- const path = resolve14(plugin);
39883
+ const path = resolve15(plugin);
39509
39884
  const digest = sha256Hex(await readTextFile(path));
39510
39885
  const key = args.relative === false ? path : relative3(dirname9(outPath), path);
39511
39886
  entries[key] = digest;
@@ -39517,18 +39892,18 @@ async function writePluginTrust(args) {
39517
39892
  `);
39518
39893
  return {
39519
39894
  outPath,
39520
- plugins: args.plugins.map((plugin) => resolve14(plugin)),
39895
+ plugins: args.plugins.map((plugin) => resolve15(plugin)),
39521
39896
  pluginCount: args.plugins.length,
39522
39897
  pluginEntries: entries,
39523
- pluginRootSuggestion: commonDir(args.plugins.map((plugin) => resolve14(plugin))),
39898
+ pluginRootSuggestion: commonDir(args.plugins.map((plugin) => resolve15(plugin))),
39524
39899
  updated: true
39525
39900
  };
39526
39901
  }
39527
39902
  function parsePluginList(input) {
39528
- return input.split(",").map((value) => value.trim()).filter(Boolean).map((value) => resolve14(value));
39903
+ return input.split(",").map((value) => value.trim()).filter(Boolean).map((value) => resolve15(value));
39529
39904
  }
39530
39905
  function suggestedTrustPath(baseDir = process.cwd()) {
39531
- return resolve14(baseDir, ".idk/plugin-trust.json");
39906
+ return resolve15(baseDir, ".idk/plugin-trust.json");
39532
39907
  }
39533
39908
  function describePluginTrustFlags(result) {
39534
39909
  return [
@@ -39540,7 +39915,7 @@ function describePluginTrustFlags(result) {
39540
39915
  }
39541
39916
  function inferredTuneWizardPath(scenarioPath) {
39542
39917
  const file = basename4(scenarioPath).replace(/\.json$/i, "");
39543
- return resolve14(process.cwd(), `${file}-tune.json`);
39918
+ return resolve15(process.cwd(), `${file}-tune.json`);
39544
39919
  }
39545
39920
 
39546
39921
  // src/commands/doctor.ts
@@ -39912,7 +40287,7 @@ var eta_default = defineCommand({
39912
40287
  // src/commands/experiment.ts
39913
40288
  init_dist2();
39914
40289
  init_zod();
39915
- import { resolve as resolve15 } from "path";
40290
+ import { resolve as resolve16 } from "path";
39916
40291
  init_errors3();
39917
40292
  init_outputMeta();
39918
40293
  init_bun();
@@ -39942,7 +40317,7 @@ var experiment_default = defineCommand({
39942
40317
  const spec = checked.data;
39943
40318
  if (spec.scenario.ref.length === 0)
39944
40319
  throw usageError("Usage: idk experiment <spec>");
39945
- const scenarioPath = resolve15(spec.scenario.ref);
40320
+ const scenarioPath = resolve16(spec.scenario.ref);
39946
40321
  const scenario = await readScenarioFile(scenarioPath);
39947
40322
  const loaded = await loadRegistriesFromFlags(flags);
39948
40323
  const valid = validateScenarioV1(scenario, loaded.modelRegistry);
@@ -39991,12 +40366,12 @@ var experiment_default = defineCommand({
39991
40366
  // src/commands/evaluate.ts
39992
40367
  init_dist2();
39993
40368
  init_zod();
39994
- import { resolve as resolve17 } from "path";
40369
+ import { resolve as resolve18 } from "path";
39995
40370
 
39996
40371
  // src/commands/ltv.ts
39997
40372
  init_dist2();
39998
40373
  init_zod();
39999
- import { resolve as resolve16 } from "path";
40374
+ import { resolve as resolve17 } from "path";
40000
40375
  init_errors3();
40001
40376
  init_outputMeta();
40002
40377
  var strategySchema2 = string2().min(1).optional();
@@ -40035,7 +40410,7 @@ function parseHorizons(raw) {
40035
40410
  }
40036
40411
  var DEFAULT_LTV_HORIZONS = "30m,2h,24h,7d,30d,90d";
40037
40412
  function ltvUncertaintyDraws(scenario, draws) {
40038
- const uncertainty = deriveMonetizationConfig(scenario).uncertainty;
40413
+ const uncertainty = deriveMonetizationConfig2(scenario).uncertainty;
40039
40414
  return uncertainty.enabled || draws !== undefined ? draws ?? uncertainty.draws : null;
40040
40415
  }
40041
40416
  function ltvStageInputs(scenario, horizons, draws) {
@@ -40097,7 +40472,7 @@ function runLtvAnalysis(args) {
40097
40472
  const horizons = parseHorizons(args.horizonsRaw);
40098
40473
  const stepSec = args.step ?? args.compiled.run.stepSec;
40099
40474
  const runFast = resolveFastMode(args.fast, args.compiled.run.fast);
40100
- const monetizationConfig = deriveMonetizationConfig(args.scenario);
40475
+ const monetizationConfig = deriveMonetizationConfig2(args.scenario);
40101
40476
  const uncertainEnabled = monetizationConfig.uncertainty.enabled || args.draws !== undefined;
40102
40477
  const draws = args.draws ?? monetizationConfig.uncertainty.draws;
40103
40478
  let state = args.compiled.initial;
@@ -40157,12 +40532,12 @@ function runLtvAnalysis(args) {
40157
40532
  const endWorthLog10 = args.compiled.ctx.E.absLog10(worth.amount);
40158
40533
  const progression = progressionFactor(startWorthLog10, endWorthLog10, monetizationConfig.revenue.progressionLogSpan);
40159
40534
  const horizonDays = h.seconds / 86400;
40160
- const point = estimateLtvPerUser({
40535
+ const point = estimateLtvPerUser2({
40161
40536
  config: monetizationConfig,
40162
40537
  horizonDays,
40163
40538
  progression
40164
40539
  });
40165
- const distribution = uncertainEnabled && draws > 1 ? estimateLtvDistribution({
40540
+ const distribution = uncertainEnabled && draws > 1 ? estimateLtvDistribution2({
40166
40541
  config: monetizationConfig,
40167
40542
  horizonDays,
40168
40543
  progression,
@@ -40365,7 +40740,7 @@ var ltv_default = defineCommand({
40365
40740
  };
40366
40741
  const output = flags.format === "json" ? jsonOutput : analysis.horizons;
40367
40742
  if (flags["artifact-out"]) {
40368
- const scenarioAbs = resolve16(process.cwd(), scenarioPath);
40743
+ const scenarioAbs = resolve17(process.cwd(), scenarioPath);
40369
40744
  await writeCommandReplayArtifact({
40370
40745
  outPath: flags["artifact-out"],
40371
40746
  command: "ltv",
@@ -40444,7 +40819,7 @@ async function loadBalanceChecks(flags) {
40444
40819
  if (!failOn) {
40445
40820
  throw usageError("--fail-on must list error, warn, inconclusive, or unsupported");
40446
40821
  }
40447
- const path = resolve17(process.cwd(), flags.checks);
40822
+ const path = resolve18(process.cwd(), flags.checks);
40448
40823
  let raw;
40449
40824
  try {
40450
40825
  raw = await readJsonFile(path);
@@ -40525,7 +40900,7 @@ var evaluate_default = defineCommand({
40525
40900
  throw scenarioInvalidError(valid.issues);
40526
40901
  }
40527
40902
  const scenario = valid.scenario;
40528
- const scenarioAbs = resolve17(process.cwd(), scenarioPath);
40903
+ const scenarioAbs = resolve18(process.cwd(), scenarioPath);
40529
40904
  const unseeded = prepareResolvedRun({
40530
40905
  scenario: valid.scenario,
40531
40906
  modelRegistry: loaded.modelRegistry,
@@ -40702,9 +41077,9 @@ var evaluate_default = defineCommand({
40702
41077
  stageScope
40703
41078
  });
40704
41079
  const files = {
40705
- simulate: resolve17(process.cwd(), flags["out-dir"] ?? ".", "simulate.json"),
40706
- experience: resolve17(process.cwd(), flags["out-dir"] ?? ".", "experience.json"),
40707
- ltv: resolve17(process.cwd(), flags["out-dir"] ?? ".", "ltv.json")
41080
+ simulate: resolve18(process.cwd(), flags["out-dir"] ?? ".", "simulate.json"),
41081
+ experience: resolve18(process.cwd(), flags["out-dir"] ?? ".", "experience.json"),
41082
+ ltv: resolve18(process.cwd(), flags["out-dir"] ?? ".", "ltv.json")
40708
41083
  };
40709
41084
  const output = {
40710
41085
  ok: balance?.exitCode !== 2,
@@ -40735,12 +41110,12 @@ var evaluate_default = defineCommand({
40735
41110
  ...balance ? { balanceChecks: balance.exitCode === 2 ? toBalanceCheckError(balance) : balance } : {}
40736
41111
  };
40737
41112
  if (flags["out-dir"]) {
40738
- const outDir = resolve17(process.cwd(), flags["out-dir"]);
41113
+ const outDir = resolve18(process.cwd(), flags["out-dir"]);
40739
41114
  await ensureDir(outDir);
40740
- files.simulate = resolve17(outDir, "simulate.json");
40741
- files.experience = resolve17(outDir, "experience.json");
40742
- files.ltv = resolve17(outDir, "ltv.json");
40743
- const summaryPath = resolve17(process.cwd(), flags["out-dir"], flags.format === "md" ? "summary.md" : "summary.json");
41115
+ files.simulate = resolve18(outDir, "simulate.json");
41116
+ files.experience = resolve18(outDir, "experience.json");
41117
+ files.ltv = resolve18(outDir, "ltv.json");
41118
+ const summaryPath = resolve18(process.cwd(), flags["out-dir"], flags.format === "md" ? "summary.md" : "summary.json");
40744
41119
  files.summary = summaryPath;
40745
41120
  await writeTextFile(files.simulate, `${JSON.stringify({ ...simulate, _meta: simulateMeta }, null, 2)}
40746
41121
  `);
@@ -41170,7 +41545,7 @@ var report_default = defineCommand({
41170
41545
  // src/commands/simulate.ts
41171
41546
  init_dist2();
41172
41547
  init_zod();
41173
- import { resolve as resolve18 } from "path";
41548
+ import { resolve as resolve19 } from "path";
41174
41549
 
41175
41550
  // src/commands/_shared/simulateView.ts
41176
41551
  function resolveEventLog(args) {
@@ -41324,11 +41699,11 @@ var simulate_default = defineCommand({
41324
41699
  const input = await readScenarioFile(scenarioPath);
41325
41700
  const loaded = await loadRegistriesFromFlags(flags);
41326
41701
  const scenario = assertValidScenario2(validateScenarioV1(input, loaded.modelRegistry));
41327
- const resumedJson = flags.resume ? await readJsonFile(resolve18(process.cwd(), flags.resume)).then((raw) => {
41702
+ const resumedJson = flags.resume ? await readJsonFile(resolve19(process.cwd(), flags.resume)).then((raw) => {
41328
41703
  try {
41329
41704
  return parseSimStateJSON(raw);
41330
41705
  } catch (error) {
41331
- throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve18(process.cwd(), flags.resume)}`, {
41706
+ throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve19(process.cwd(), flags.resume)}`, {
41332
41707
  detail: errorDetail(error),
41333
41708
  cause: error
41334
41709
  });
@@ -41336,7 +41711,7 @@ var simulate_default = defineCommand({
41336
41711
  }).catch((error) => {
41337
41712
  if (error instanceof Error && error.name === "CliError")
41338
41713
  throw error;
41339
- throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve18(process.cwd(), flags.resume)}`, {
41714
+ throw cliError("SIM_STATE_INVALID_JSON", `Invalid resume state json: ${resolve19(process.cwd(), flags.resume)}`, {
41340
41715
  detail: errorDetail(error),
41341
41716
  cause: error
41342
41717
  });
@@ -41453,7 +41828,7 @@ var simulate_default = defineCommand({
41453
41828
  const run = runScenario(effectiveScenario);
41454
41829
  const netWorth = effectiveScenario.model.netWorth?.(effectiveScenario.ctx, run.end) ?? run.end.wallet.money;
41455
41830
  const totalElapsedSec = priorElapsedSec + (offlineRun ? runElapsedSec(offlineRun) : 0) + runElapsedSec(run);
41456
- const stateOutPath = flags["state-out"] ? resolve18(process.cwd(), flags["state-out"]) : undefined;
41831
+ const stateOutPath = flags["state-out"] ? resolve19(process.cwd(), flags["state-out"]) : undefined;
41457
41832
  const seed = deterministicSeed;
41458
41833
  const offlineEndWorth = offlineRun && (runScenarioInput.model.netWorth?.(runScenarioInput.ctx, offlineRun.end) ?? offlineRun.end.wallet.money);
41459
41834
  if (stateOutPath) {
@@ -41517,7 +41892,7 @@ var simulate_default = defineCommand({
41517
41892
  eventLog: run.eventLog
41518
41893
  };
41519
41894
  if (flags["artifact-out"]) {
41520
- const scenarioAbs = resolve18(process.cwd(), scenarioPath);
41895
+ const scenarioAbs = resolve19(process.cwd(), scenarioPath);
41521
41896
  await writeCommandReplayArtifact({
41522
41897
  outPath: flags["artifact-out"],
41523
41898
  command: "simulate",
@@ -41552,7 +41927,7 @@ var simulate_default = defineCommand({
41552
41927
  // src/commands/tune.ts
41553
41928
  init_dist2();
41554
41929
  init_zod();
41555
- import { resolve as resolve20 } from "path";
41930
+ import { resolve as resolve21 } from "path";
41556
41931
  init_errors3();
41557
41932
  init_outputMeta();
41558
41933
 
@@ -41560,7 +41935,7 @@ init_outputMeta();
41560
41935
  init_dist2();
41561
41936
  init_errors3();
41562
41937
  init_bun();
41563
- import { basename as basename5, resolve as resolve19 } from "path";
41938
+ import { basename as basename5, resolve as resolve20 } from "path";
41564
41939
  var OBJECTIVE_OPTIONS = [
41565
41940
  {
41566
41941
  value: "endNetWorthLog10",
@@ -41634,7 +42009,7 @@ async function runTuneWizard(args) {
41634
42009
  }
41635
42010
  const scenario = valid.scenario;
41636
42011
  const strategyId = String(scenario.strategy?.id ?? "greedy");
41637
- const suggestedOut = resolve19(args.outPath?.trim() || inferredTuneWizardPath(args.scenarioPath));
42012
+ const suggestedOut = resolve20(args.outPath?.trim() || inferredTuneWizardPath(args.scenarioPath));
41638
42013
  args.prompt.intro(`Tune wizard for ${basename5(args.scenarioPath)}`);
41639
42014
  const objectiveId = await args.prompt.select("Objective", {
41640
42015
  options: OBJECTIVE_OPTIONS.map((entry) => ({ ...entry })),
@@ -41651,7 +42026,7 @@ async function runTuneWizard(args) {
41651
42026
  const tunePathRaw = await args.prompt.text("Tune spec output path", {
41652
42027
  default: suggestedOut
41653
42028
  });
41654
- const tunePath = resolve19(String(tunePathRaw));
42029
+ const tunePath = resolve20(String(tunePathRaw));
41655
42030
  if (await fileExists(tunePath) && !args.force) {
41656
42031
  throw usageError(`Output file already exists: ${tunePath}`, "Pass --force true to overwrite the generated tune spec.");
41657
42032
  }
@@ -41868,7 +42243,7 @@ var tune_default = defineCommand({
41868
42243
  throw usageError("Usage: idk tune <scenario> --tune <tunespec>");
41869
42244
  }
41870
42245
  const tuneSpecInput = wizardResult?.tuneSpec ?? await readScenarioFile(tunePath);
41871
- const baselinePath = flags["baseline-artifact"] ? resolve20(flags["baseline-artifact"]) : undefined;
42246
+ const baselinePath = flags["baseline-artifact"] ? resolve21(flags["baseline-artifact"]) : undefined;
41872
42247
  const baselineRaw = baselinePath ? await readJsonFile(baselinePath) : undefined;
41873
42248
  const seed = flags.seed ?? deriveDeterministicSeed({
41874
42249
  command: "tune",
@@ -41932,8 +42307,8 @@ var tune_default = defineCommand({
41932
42307
  };
41933
42308
  })();
41934
42309
  if (flags["artifact-out"]) {
41935
- const scenarioAbs = resolve20(process.cwd(), scenarioPath);
41936
- const tuneAbs = resolve20(process.cwd(), tunePath);
42310
+ const scenarioAbs = resolve21(process.cwd(), scenarioPath);
42311
+ const tuneAbs = resolve21(process.cwd(), tunePath);
41937
42312
  await writeCommandReplayArtifact({
41938
42313
  outPath: flags["artifact-out"],
41939
42314
  command: "tune",
@@ -41999,13 +42374,13 @@ var validate_default = defineCommand({
41999
42374
  // src/commands/initScenario.ts
42000
42375
  init_zod();
42001
42376
  init_errors3();
42002
- import { resolve as resolve22 } from "path";
42377
+ import { resolve as resolve23 } from "path";
42003
42378
 
42004
42379
  // src/lib/initWizard.ts
42005
42380
  init_errors3();
42006
42381
 
42007
42382
  // src/templates/scenario.ts
42008
- import { resolve as resolve21, parse as parse5 } from "path";
42383
+ import { resolve as resolve22, parse as parse5 } from "path";
42009
42384
  var TEMPLATE_TRACKS = ["intro", "design", "personal"];
42010
42385
  var TEMPLATE_PRESETS = ["session", "builder", "longrun"];
42011
42386
  var INTRO_PRESET_DEFAULT = {
@@ -42873,14 +43248,14 @@ function buildInitTemplatePlan(args) {
42873
43248
  const tuneSpec = createPersonalTuneSpec(preset, baseScenario);
42874
43249
  applyPersonalBundleIdentity({ base: baseScenario, compare: compareScenario, tune: tuneSpec }, { stem, displayName });
42875
43250
  return [
42876
- { kind: "scenario", path: resolve21(dir, `${stem}${ext}`), content: baseScenario },
42877
- { kind: "compare", path: resolve21(dir, `${stem}-compare-b${ext}`), content: compareScenario },
42878
- { kind: "tune", path: resolve21(dir, `${stem}-tune${ext}`), content: tuneSpec }
43251
+ { kind: "scenario", path: resolve22(dir, `${stem}${ext}`), content: baseScenario },
43252
+ { kind: "compare", path: resolve22(dir, `${stem}-compare-b${ext}`), content: compareScenario },
43253
+ { kind: "tune", path: resolve22(dir, `${stem}-tune${ext}`), content: tuneSpec }
42879
43254
  ];
42880
43255
  }
42881
43256
  const content = track === "intro" ? clone2(preset === "session" ? introSession : preset === "builder" ? introBuilder : introLongrun) : createDesignScenario(preset);
42882
43257
  applyScenarioOverrides(content, args.overrides);
42883
- return [{ kind: "scenario", path: resolve21(args.outPath), content }];
43258
+ return [{ kind: "scenario", path: resolve22(args.outPath), content }];
42884
43259
  }
42885
43260
  function serializeTemplate(template) {
42886
43261
  return `${JSON.stringify(template, null, 2)}
@@ -43137,7 +43512,7 @@ var initScenario_default = defineCommand({
43137
43512
  })
43138
43513
  },
43139
43514
  async handler({ flags, prompt, terminal, runtime }) {
43140
- const outPath = resolve22(process.cwd(), flags.out);
43515
+ const outPath = resolve23(process.cwd(), flags.out);
43141
43516
  const wizard = flags.wizard;
43142
43517
  const wizardResult = wizard ? await runInitWizard({
43143
43518
  prompt,
@@ -43207,11 +43582,11 @@ var init_default = defineGroup({
43207
43582
  init_dist2();
43208
43583
  init_zod();
43209
43584
  init_outputMeta();
43210
- import { resolve as resolve23 } from "path";
43585
+ import { resolve as resolve24 } from "path";
43211
43586
  init_bun();
43212
43587
  async function runKpiRegression(flags) {
43213
- const baselinePath = resolve23(flags.baseline);
43214
- const currentPath = resolve23(flags.current);
43588
+ const baselinePath = resolve24(flags.baseline);
43589
+ const currentPath = resolve24(flags.current);
43215
43590
  const baseline = await readJsonFile(baselinePath);
43216
43591
  const current = await readJsonFile(currentPath);
43217
43592
  const output = adaptKpiRegression({
@@ -43229,6 +43604,7 @@ async function runKpiRegression(flags) {
43229
43604
  await writeOutput({
43230
43605
  format: flags.format,
43231
43606
  outPath: flags.out,
43607
+ protectedInputs: [baselinePath, currentPath],
43232
43608
  data: output,
43233
43609
  meta: buildOutputMeta({ command: "kpi.regress" })
43234
43610
  });
@@ -43282,7 +43658,7 @@ init_zod();
43282
43658
  // src/commands/_shared/listOutput.ts
43283
43659
  init_outputMeta();
43284
43660
  init_bun();
43285
- import { resolve as resolve24 } from "path";
43661
+ import { resolve as resolve25 } from "path";
43286
43662
  async function writeListOutput(args) {
43287
43663
  const outputForRender = args.format === "json" ? {
43288
43664
  ...args.payload,
@@ -43293,7 +43669,7 @@ async function writeListOutput(args) {
43293
43669
  process.stdout.write(body);
43294
43670
  return;
43295
43671
  }
43296
- const path = resolve24(args.out);
43672
+ const path = resolve25(args.out);
43297
43673
  await writeTextFile(path, body);
43298
43674
  console.log(`Wrote ${args.format} output to ${path}`);
43299
43675
  }
@@ -43442,7 +43818,7 @@ var objectives_default2 = defineGroup({
43442
43818
  init_zod();
43443
43819
  init_errors3();
43444
43820
  init_outputMeta();
43445
- import { resolve as resolve25 } from "path";
43821
+ import { resolve as resolve26 } from "path";
43446
43822
  init_bun();
43447
43823
  init_selfCli();
43448
43824
  function deepEqual(a, b) {
@@ -43509,7 +43885,7 @@ var replayVerify_default = defineCommand({
43509
43885
  if (!artifactPath) {
43510
43886
  throw usageError("Usage: idk replay verify <artifact.json>");
43511
43887
  }
43512
- const artifactAbs = resolve25(process.cwd(), artifactPath);
43888
+ const artifactAbs = resolve26(process.cwd(), artifactPath);
43513
43889
  const raw = await readJsonFile(artifactAbs).catch((error) => {
43514
43890
  if (error instanceof Error && error.name === "CliError")
43515
43891
  throw error;
@@ -43810,4 +44186,4 @@ export {
43810
44186
  cli2 as cli
43811
44187
  };
43812
44188
 
43813
- //# debugId=4657B842FAC22F1D64756E2164756E21
44189
+ //# debugId=A3CAAFDF1955C7F064756E2164756E21