@idlekit/cli 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js CHANGED
@@ -11605,6 +11605,63 @@ function logicalDrawId(drawIndex) {
11605
11605
  return String(drawIndex);
11606
11606
  }
11607
11607
 
11608
+ // ../core/dist/sim/sessionOffline.js
11609
+ function validateCreditPolicy(policy) {
11610
+ if (!policy || typeof policy !== "object" || Array.isArray(policy) || "actions" in policy) {
11611
+ throw new RangeError("prepareOffline policy must be a credit policy; set actions on scenario.run.offline");
11612
+ }
11613
+ if (policy.maxSec !== undefined && (typeof policy.maxSec !== "number" || !Number.isFinite(policy.maxSec) || policy.maxSec < 0)) {
11614
+ throw new RangeError("prepareOffline policy.maxSec must be finite and nonnegative");
11615
+ }
11616
+ if (policy.overflowPolicy !== undefined && policy.overflowPolicy !== "clamp" && policy.overflowPolicy !== "reject") {
11617
+ throw new RangeError("prepareOffline policy.overflowPolicy must be clamp or reject");
11618
+ }
11619
+ const decay = policy.decay;
11620
+ if (decay !== undefined) {
11621
+ if (!decay || typeof decay !== "object" || Array.isArray(decay) || decay.kind !== "none" && decay.kind !== "linear") {
11622
+ throw new RangeError("prepareOffline policy.decay must select none or linear");
11623
+ }
11624
+ if (decay.floorRatio !== undefined && (typeof decay.floorRatio !== "number" || !Number.isFinite(decay.floorRatio) || decay.floorRatio < 0 || decay.floorRatio > 1)) {
11625
+ throw new RangeError("prepareOffline policy.decay.floorRatio must be finite and in 0..1");
11626
+ }
11627
+ }
11628
+ }
11629
+ function prepareOfflineSegment(args) {
11630
+ const { scenario, prepare } = args;
11631
+ if (!prepare)
11632
+ return { model: scenario.model, policy: scenario.run.offline, stepSec: scenario.run.stepSec };
11633
+ const prepared = prepare({
11634
+ ctx: scenario.ctx,
11635
+ model: scenario.model,
11636
+ state: deepClonePreservingPrototype(args.state),
11637
+ seconds: args.seconds,
11638
+ wallStartT: args.wallStartT,
11639
+ wallEndT: args.wallEndT,
11640
+ ...scenario.run.offline ? { policy: deepClonePreservingPrototype(scenario.run.offline) } : {}
11641
+ });
11642
+ if (!prepared || typeof prepared !== "object" || Array.isArray(prepared) || "then" in prepared) {
11643
+ throw new RangeError("prepareOffline must synchronously return a preparation object");
11644
+ }
11645
+ if (prepared.stepSec !== undefined && (typeof prepared.stepSec !== "number" || !Number.isFinite(prepared.stepSec) || prepared.stepSec <= 0)) {
11646
+ throw new RangeError("prepareOffline stepSec must be finite and positive");
11647
+ }
11648
+ if (prepared.policy !== undefined)
11649
+ validateCreditPolicy(prepared.policy);
11650
+ const model = prepared.model === undefined ? scenario.model : prepared.model;
11651
+ if (!model || typeof model !== "object" || typeof model.income !== "function" || typeof model.actions !== "function") {
11652
+ throw new RangeError("prepareOffline model must provide income and actions");
11653
+ }
11654
+ return {
11655
+ model,
11656
+ stepSec: prepared.stepSec ?? scenario.run.stepSec,
11657
+ policy: prepared.policy === undefined ? scenario.run.offline : {
11658
+ ...deepClonePreservingPrototype(prepared.policy),
11659
+ actions: scenario.run.offline?.actions
11660
+ }
11661
+ };
11662
+ }
11663
+ var init_sessionOffline = () => {};
11664
+
11608
11665
  // ../core/dist/sim/session.js
11609
11666
  function buildBlocks(pattern) {
11610
11667
  const blocks = [];
@@ -11700,6 +11757,9 @@ function withClocks(scenario, wallT, wallEndT, rewardT, activeT) {
11700
11757
  };
11701
11758
  }
11702
11759
  function simulateSessionPattern(args) {
11760
+ if (args.prepareOffline !== undefined && typeof args.prepareOffline !== "function") {
11761
+ throw new TypeError("prepareOffline must be a function");
11762
+ }
11703
11763
  const sc = args.seed === undefined ? args.scenario : { ...args.scenario, ctx: { ...args.scenario.ctx, seed: args.seed } };
11704
11764
  const start = sc.initial;
11705
11765
  const horizonSec = args.pattern.days * 86400;
@@ -11786,7 +11846,19 @@ function simulateSessionPattern(args) {
11786
11846
  return;
11787
11847
  const requested = wallEnd - wallT;
11788
11848
  const wallStart = wallT;
11789
- const segment = segmentScenario(wallStart, wallEnd);
11849
+ let segment = segmentScenario(wallStart, wallEnd);
11850
+ const prepared = prepareOfflineSegment({
11851
+ scenario: segment,
11852
+ state,
11853
+ seconds: requested,
11854
+ wallStartT: wallStart,
11855
+ wallEndT: wallEnd,
11856
+ prepare: args.prepareOffline
11857
+ });
11858
+ if (prepared.model !== segment.model) {
11859
+ const { clocks: _clocks, ...ctx } = segment.ctx;
11860
+ segment = withClocks({ ...segment, ctx, model: prepared.model }, wallStart, wallEnd, state.t, activeSec);
11861
+ }
11790
11862
  const offlineRun = applyOfflineSeconds({
11791
11863
  scenario: segment,
11792
11864
  seconds: requested,
@@ -11798,15 +11870,16 @@ function simulateSessionPattern(args) {
11798
11870
  enabled: sc.run.eventLog?.enabled ?? true,
11799
11871
  maxEvents: sc.run.eventLog?.maxEvents
11800
11872
  },
11801
- policy: sc.run.offline,
11873
+ stepSec: prepared.stepSec,
11874
+ policy: prepared.policy,
11802
11875
  ...stopFn ? { until: stopFn } : {}
11803
11876
  }
11804
11877
  });
11805
11878
  const credited = offlineRun.offline.simulatedSec;
11806
11879
  const reason = classify();
11807
- const absence = reason ? offlineAbsenceForCredit(credited, requested, sc.run.offline) : requested;
11880
+ const absence = reason ? offlineAbsenceForCredit(credited, requested, prepared.policy) : requested;
11808
11881
  const gapEnd = wallStart + absence;
11809
- const effective = absence === requested ? offlineRun.offline.effectiveSec : resolveOfflineSeconds(absence, sc.run.offline).effectiveSec;
11882
+ const effective = absence === requested ? offlineRun.offline.effectiveSec : resolveOfflineSeconds(absence, prepared.policy).effectiveSec;
11810
11883
  const lost = Math.max(0, absence - effective);
11811
11884
  offlineElapsedSec += absence;
11812
11885
  offlineCreditedSec += credited;
@@ -11977,6 +12050,7 @@ var init_session = __esm(() => {
11977
12050
  init_goalRead();
11978
12051
  init_observation();
11979
12052
  init_offline();
12053
+ init_sessionOffline();
11980
12054
  init_simulator();
11981
12055
  });
11982
12056
 
@@ -12972,11 +13046,19 @@ var init_params2 = __esm(() => {
12972
13046
  }));
12973
13047
  });
12974
13048
 
12975
- // ../core/dist/sim/strategy/opt/objectives/builtins.js
12976
- function safeAbsLog10(E, x) {
12977
- const v = E.absLog10(x);
12978
- return Number.isFinite(v) ? v : -300;
13049
+ // ../core/dist/sim/strategy/opt/finiteAmountLog10.js
13050
+ function finiteAmountLog10(E, amount) {
13051
+ if (!E.isFinite(amount))
13052
+ throw new RangeError("Tuning requires finite monetary amounts in the selected engine");
13053
+ const value = E.absLog10(amount);
13054
+ if (value === Number.NEGATIVE_INFINITY)
13055
+ return -300;
13056
+ if (!Number.isFinite(value))
13057
+ throw new RangeError("Tuning requires a finite monetary logarithm");
13058
+ return value;
12979
13059
  }
13060
+
13061
+ // ../core/dist/sim/strategy/opt/objectives/builtins.js
12980
13062
  var builtinObjectiveFactories;
12981
13063
  var init_builtins2 = __esm(() => {
12982
13064
  init_parseMoney2();
@@ -12990,7 +13072,7 @@ var init_builtins2 = __esm(() => {
12990
13072
  id: "endMoneyLog10",
12991
13073
  score: ({ scenario, run }) => {
12992
13074
  const { E } = scenario.ctx;
12993
- return safeAbsLog10(E, run.end.wallet.money.amount);
13075
+ return finiteAmountLog10(E, run.end.wallet.money.amount);
12994
13076
  }
12995
13077
  })
12996
13078
  },
@@ -13003,7 +13085,7 @@ var init_builtins2 = __esm(() => {
13003
13085
  score: ({ scenario, run }) => {
13004
13086
  const { E } = scenario.ctx;
13005
13087
  const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
13006
- return safeAbsLog10(E, worth.amount);
13088
+ return finiteAmountLog10(E, worth.amount);
13007
13089
  }
13008
13090
  })
13009
13091
  },
@@ -13018,7 +13100,7 @@ var init_builtins2 = __esm(() => {
13018
13100
  const sec = Math.max(1, runElapsedSec(run));
13019
13101
  const hours = sec / 3600;
13020
13102
  const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
13021
- return safeAbsLog10(E, worth.amount) - Math.log10(Math.max(0.000000000001, hours));
13103
+ return finiteAmountLog10(E, worth.amount) - Math.log10(Math.max(0.000000000001, hours));
13022
13104
  }
13023
13105
  })
13024
13106
  },
@@ -13032,7 +13114,7 @@ var init_builtins2 = __esm(() => {
13032
13114
  const { E } = scenario.ctx;
13033
13115
  const sec = Math.max(1, runElapsedSec(run));
13034
13116
  const hours = sec / 3600;
13035
- return safeAbsLog10(E, run.end.prestige.points) - Math.log10(Math.max(0.000000000001, hours));
13117
+ return finiteAmountLog10(E, run.end.prestige.points) - Math.log10(Math.max(0.000000000001, hours));
13036
13118
  }
13037
13119
  })
13038
13120
  },
@@ -13047,7 +13129,7 @@ var init_builtins2 = __esm(() => {
13047
13129
  const sec = Math.max(1, runElapsedSec(run));
13048
13130
  const startWorth = scenario.model.netWorth?.(scenario.ctx, run.start) ?? run.start.wallet.money;
13049
13131
  const endWorth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
13050
- const deltaLog = safeAbsLog10(E, endWorth.amount) - safeAbsLog10(E, startWorth.amount);
13132
+ const deltaLog = finiteAmountLog10(E, endWorth.amount) - finiteAmountLog10(E, startWorth.amount);
13051
13133
  return deltaLog / (sec / 3600);
13052
13134
  }
13053
13135
  })
@@ -13073,6 +13155,9 @@ var init_builtins2 = __esm(() => {
13073
13155
  suffix: { kind: "alphaInfinite", minLen: 2 }
13074
13156
  }).amount;
13075
13157
  const endWorth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
13158
+ if (!E.isFinite(threshold) || !E.isFinite(endWorth.amount)) {
13159
+ throw new RangeError("ETA objective requires finite target and net worth in the selected engine");
13160
+ }
13076
13161
  const reached = E.cmp(endWorth.amount, threshold) >= 0;
13077
13162
  if (!reached)
13078
13163
  return -(params.unreachedPenaltySec ?? 1e9);
@@ -13102,7 +13187,7 @@ var init_builtins2 = __esm(() => {
13102
13187
  const sec = Math.max(1, runElapsedSec(run));
13103
13188
  const hours = sec / 3600;
13104
13189
  const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
13105
- const worthLog = safeAbsLog10(E, worth.amount);
13190
+ const worthLog = finiteAmountLog10(E, worth.amount);
13106
13191
  const stats = run.stats;
13107
13192
  if (!stats || stats.money.droppedRate === null || stats.actions.status === "missing") {
13108
13193
  throw new Error("pacingBalancedLog10 needs observed money and action counters, but this run has none (run.observation.enabled is false)");
@@ -13301,8 +13386,45 @@ function cloneCandidateInitial(state) {
13301
13386
  }
13302
13387
  var init_freshRun = () => {};
13303
13388
 
13389
+ // ../core/dist/sim/strategy/opt/finiteMean.js
13390
+ function finiteMean(values) {
13391
+ if (values.length === 0)
13392
+ throw new RangeError("A mean requires at least one value");
13393
+ const bits = new DataView(new ArrayBuffer(8));
13394
+ let total = 0n;
13395
+ for (const value of values) {
13396
+ if (!Number.isFinite(value))
13397
+ throw new RangeError("A mean requires finite values");
13398
+ bits.setFloat64(0, value, false);
13399
+ const high = bits.getUint32(0, false);
13400
+ const low = bits.getUint32(4, false);
13401
+ const exponent = high >>> 20 & 2047;
13402
+ const fraction = BigInt(high & 1048575) << 32n | BigInt(low);
13403
+ const significand = exponent === 0 ? fraction : 1n << 52n | fraction;
13404
+ const units = significand << BigInt(Math.max(0, exponent - 1));
13405
+ total += high >>> 31 ? -units : units;
13406
+ }
13407
+ if (total === 0n)
13408
+ return 0;
13409
+ const negative = total < 0n;
13410
+ const magnitude = negative ? -total : total;
13411
+ const count = BigInt(values.length);
13412
+ const integerMean = magnitude / count;
13413
+ const shift = Math.max(0, integerMean.toString(2).length - 53);
13414
+ const denominator = count << BigInt(shift);
13415
+ let significand = magnitude / denominator;
13416
+ const twiceRemainder = 2n * (magnitude % denominator);
13417
+ if (twiceRemainder > denominator || twiceRemainder === denominator && (significand & 1n) !== 0n) {
13418
+ significand++;
13419
+ }
13420
+ const mean = Number(significand) * 2 ** (shift - 1074);
13421
+ return negative ? -mean : mean;
13422
+ }
13423
+
13304
13424
  // ../core/dist/sim/strategy/opt/runner.js
13305
13425
  function runCandidateAndScore(args) {
13426
+ if (args.seeds.length === 0)
13427
+ throw new RangeError("Tuning requires at least one seed");
13306
13428
  const stratFactory = args.strategyRegistry.get(args.strategyId);
13307
13429
  if (!stratFactory)
13308
13430
  throw new Error(`Unknown strategy: ${args.strategyId}`);
@@ -13349,24 +13471,28 @@ function runCandidateAndScore(args) {
13349
13471
  const sc = openScenario();
13350
13472
  const run = runScenario(sc);
13351
13473
  assertHorizonReached(run, "runCandidateAndScore");
13474
+ const worth = sc.model.netWorth?.(sc.ctx, run.end) ?? run.end.wallet.money;
13475
+ const endMoneyLog10 = finiteAmountLog10(sc.ctx.E, run.end.wallet.money.amount);
13476
+ const endNetWorthLog10 = finiteAmountLog10(sc.ctx.E, worth.amount);
13352
13477
  const s = objective.score({ scenario: sc, run, evaluation: {
13353
13478
  open: openScenario,
13354
13479
  registries: { models: args.modelRegistry, strategies: args.strategyRegistry },
13355
13480
  isolation: { model: args.model, statefulModel: args.statefulModel, strategy: { id: args.strategyId, params: candidateParams } }
13356
13481
  } });
13482
+ if (!Number.isFinite(s))
13483
+ throw new RangeError(`Objective ${args.objectiveId} must return a finite score (seed ${seed})`);
13357
13484
  seedScores.push(s);
13358
- const worth = sc.model.netWorth?.(sc.ctx, run.end) ?? run.end.wallet.money;
13359
13485
  seedResults.push({
13360
13486
  seed,
13361
13487
  score: s,
13362
13488
  durationSec: Math.max(0, runElapsedSec(run)),
13363
- endMoneyLog10: sc.ctx.E.absLog10(run.end.wallet.money.amount),
13364
- endNetWorthLog10: sc.ctx.E.absLog10(worth.amount),
13489
+ endMoneyLog10,
13490
+ endNetWorthLog10,
13365
13491
  droppedRate: run.stats?.money.droppedRate ?? null,
13366
13492
  actionsApplied: run.stats && run.stats.actions.status === "observed" ? run.stats.actions.applied : null
13367
13493
  });
13368
13494
  }
13369
- const score = seedScores.reduce((a, b) => a + b, 0) / Math.max(1, seedScores.length);
13495
+ const score = finiteMean(seedScores);
13370
13496
  return { score, seedScores, seedResults };
13371
13497
  }
13372
13498
  var init_runner = __esm(() => {
@@ -14353,7 +14479,7 @@ function compensatedSum(values, scale) {
14353
14479
  }
14354
14480
  return sum + compensation;
14355
14481
  }
14356
- function finiteMean(values) {
14482
+ function finiteMean2(values) {
14357
14483
  let smallest = Number.POSITIVE_INFINITY;
14358
14484
  let largest = Number.NEGATIVE_INFINITY;
14359
14485
  for (const value of values) {
@@ -14503,7 +14629,7 @@ function analyzeDistribution(request) {
14503
14629
  }
14504
14630
  const eligible = observed.length + rightCensored;
14505
14631
  const completionRate = eligible > 0 ? observed.length / eligible : null;
14506
- const arithmetic = observed.length > 0 ? finiteMean(observed) : null;
14632
+ const arithmetic = observed.length > 0 ? finiteMean2(observed) : null;
14507
14633
  const identical = observed.every((value) => value === observed[0]);
14508
14634
  const rawVariance = arithmetic === null ? null : finiteVariance(observed, arithmetic);
14509
14635
  const variance = rawVariance === 0 && !identical ? null : rawVariance;
@@ -19353,32 +19479,46 @@ function buildTimeline(args) {
19353
19479
  }
19354
19480
 
19355
19481
  // ../core/dist/report/compare.js
19356
- function safeNum(input) {
19357
- if (typeof input === "number")
19358
- return input;
19359
- if (typeof input === "string") {
19360
- const n = Number(input);
19361
- if (Number.isFinite(n))
19362
- return n;
19363
- const m = /^\s*([+-]?\d+(?:\.\d+)?)e([+-]?\d+)\s*$/i.exec(input);
19364
- if (m) {
19365
- const mantissa = Number(m[1]);
19366
- const exp = Number(m[2]);
19367
- if (mantissa === 0)
19368
- return 0;
19369
- if (Number.isFinite(mantissa) && Number.isFinite(exp)) {
19370
- return Math.log10(Math.abs(mantissa)) + exp;
19371
- }
19372
- }
19373
- }
19374
- return Number.NaN;
19482
+ function decimalParts(input) {
19483
+ const match = /^([+-]?)(?:(\d+)(?:\.(\d+))?|\.(\d+))(?:e([+-]?\d+))?([a-z]{2,})?$/i.exec(input.trim());
19484
+ if (!match)
19485
+ throw new RangeError("Static monetary comparison requires decimal amounts");
19486
+ let suffixExponent = 0n;
19487
+ if (match[6]) {
19488
+ let index = 0n;
19489
+ for (const ch of match[6].toLowerCase())
19490
+ index = index * 26n + BigInt(ch.charCodeAt(0) - 96);
19491
+ suffixExponent = (index - 26n) * 3n;
19492
+ }
19493
+ const fraction = match[3] ?? match[4] ?? "";
19494
+ const digits = `${match[2] ?? ""}${fraction}`.replace(/^0+/, "");
19495
+ let end = digits.length;
19496
+ while (end > 0 && digits[end - 1] === "0")
19497
+ end--;
19498
+ return {
19499
+ sign: digits.length === 0 ? 0 : match[1] === "-" ? -1 : 1,
19500
+ digits: digits.slice(0, end),
19501
+ magnitude: BigInt(match[5] ?? "0") + suffixExponent - BigInt(fraction.length) + BigInt(digits.length)
19502
+ };
19503
+ }
19504
+ function compareStaticMoney(left, right) {
19505
+ const a = decimalParts(left), b = decimalParts(right);
19506
+ if (a.sign !== b.sign)
19507
+ return a.sign < b.sign ? -1 : 1;
19508
+ if (a.sign === 0)
19509
+ return 0;
19510
+ if (a.magnitude !== b.magnitude)
19511
+ return (a.magnitude < b.magnitude ? -1 : 1) * a.sign;
19512
+ const length = Math.max(a.digits.length, b.digits.length);
19513
+ const ad = a.digits.padEnd(length, "0"), bd = b.digits.padEnd(length, "0");
19514
+ return (ad < bd ? -1 : ad > bd ? 1 : 0) * a.sign;
19375
19515
  }
19376
19516
  function score(s, metric) {
19377
19517
  switch (metric) {
19378
19518
  case "endMoney":
19379
- return safeNum(s.initial.wallet.amount);
19519
+ return s.initial.wallet.amount;
19380
19520
  case "endNetWorth":
19381
- return safeNum(s.initial.maxMoneyEver ?? s.initial.wallet.amount);
19521
+ return s.initial.maxMoneyEver ?? s.initial.wallet.amount;
19382
19522
  case "droppedRate":
19383
19523
  return -(s.policy.maxLogGap ?? 0);
19384
19524
  case "etaToTargetWorth":
@@ -19417,6 +19557,13 @@ function compareScenarios(args) {
19417
19557
  const useMeasured = measuredA !== undefined && measuredB !== undefined;
19418
19558
  const aScore = useMeasured ? measuredA : score(args.a, args.metric);
19419
19559
  const bScore = useMeasured ? measuredB : score(args.b, args.metric);
19560
+ if (!useMeasured && (args.metric === "endMoney" || args.metric === "endNetWorth")) {
19561
+ const order = compareStaticMoney(String(aScore), String(bScore));
19562
+ return {
19563
+ better: order === 0 ? "tie" : order > 0 ? "a" : "b",
19564
+ detail: { aScore, bScore, metric: args.metric, source: "static" }
19565
+ };
19566
+ }
19420
19567
  const higherBetter = args.metric === "endMoney" || args.metric === "endNetWorth" || args.metric === "visibleChangesPerMinute";
19421
19568
  if (aScore === bScore) {
19422
19569
  return {
@@ -19440,420 +19587,924 @@ function compareScenarios(args) {
19440
19587
  };
19441
19588
  }
19442
19589
 
19443
- // ../core/dist/index.js
19444
- var init_dist2 = __esm(() => {
19445
- init_types();
19446
- init_breakInfinity2();
19447
- init_breakEternity2();
19448
- init_types5();
19449
- init_types6();
19450
- init_emitter();
19451
- init_tickMoney2();
19452
- init_suffixer();
19453
- init_formatMoney2();
19454
- init_parseMoney2();
19455
- init_visibilityTracker2();
19456
- init_moneyState();
19457
- init_simState();
19458
- init_types7();
19459
- init_validate();
19460
- init_concreteValidator();
19461
- init_scenarioContract();
19462
- init_compile2();
19463
- init_simulator();
19464
- init_advance();
19465
- init_step();
19466
- init_stepTypes();
19467
- init_offline();
19468
- init_session();
19469
- init_monteCarlo();
19470
- init_executionBackend();
19471
- init_integrationMatrix();
19472
- init_runFactory();
19473
- init_constraints();
19474
- init_observation();
19475
- init_types8();
19476
- init_contracts();
19477
- init_stability();
19478
- init_scripted();
19479
- init_greedy();
19480
- init_planner();
19481
- init_params();
19482
- init_builtins();
19483
- init_tuneSpec();
19484
- init_validate2();
19485
- init_objective();
19486
- init_params2();
19487
- init_builtins2();
19488
- init_tuner();
19489
- init_randomSearch();
19490
- init_runner();
19491
- init_eta();
19492
- init_prestigeCycle();
19493
- init_resetCycle();
19494
- init_resetOptimization();
19495
- init_distribution();
19496
- init_experiment();
19497
- init_sensitivity();
19498
- init_growthFit();
19499
- init_contracts2();
19500
- init_metrics();
19501
- init_registry();
19502
- init_analyzers();
19503
- init_inspect();
19504
- init_checks3();
19505
- init_judge();
19506
- init_flow();
19507
- init_jevAdvisory();
19508
- });
19509
-
19510
- // src/runtime/bun.ts
19511
- var {$ } = globalThis.Bun;
19512
- import { dirname } from "path";
19513
- async function readTextFile(path) {
19514
- return Bun.file(path).text();
19590
+ // ../core/dist/ltv/projection.js
19591
+ function clamp012(v) {
19592
+ if (v < 0)
19593
+ return 0;
19594
+ if (v > 1)
19595
+ return 1;
19596
+ return v;
19515
19597
  }
19516
- async function readJsonFile(path) {
19517
- return Bun.file(path).json();
19598
+ function deriveMonetizationConfig(scenario) {
19599
+ const m = scenario.monetization;
19600
+ const q = (m?.uncertainty?.quantiles ?? [0.5, 0.9]).filter((x) => typeof x === "number");
19601
+ const quantiles = q.length > 0 ? [...new Set(q)].sort((a, b) => a - b) : [0.5, 0.9];
19602
+ return {
19603
+ cohorts: {
19604
+ baseUsers: m?.cohorts?.baseUsers ?? 1
19605
+ },
19606
+ retention: {
19607
+ d1: m?.retention?.d1 ?? 0.42,
19608
+ d7: m?.retention?.d7 ?? 0.2,
19609
+ d30: m?.retention?.d30 ?? 0.09,
19610
+ d90: m?.retention?.d90 ?? 0.04,
19611
+ longTailDailyDecay: m?.retention?.longTailDailyDecay ?? 0.02
19612
+ },
19613
+ revenue: {
19614
+ payerConversion: m?.revenue?.payerConversion ?? 0.03,
19615
+ arppuDaily: m?.revenue?.arppuDaily ?? 0.7,
19616
+ adArpDau: m?.revenue?.adArpDau ?? 0.02,
19617
+ platformFeeRate: m?.revenue?.platformFeeRate ?? 0.3,
19618
+ grossMarginRate: m?.revenue?.grossMarginRate ?? 0.92,
19619
+ progressionRevenueLift: m?.revenue?.progressionRevenueLift ?? 0.4,
19620
+ progressionLogSpan: m?.revenue?.progressionLogSpan ?? 6
19621
+ },
19622
+ acquisition: {
19623
+ cpi: m?.acquisition?.cpi ?? 0
19624
+ },
19625
+ uncertainty: {
19626
+ enabled: m?.uncertainty?.enabled ?? false,
19627
+ draws: m?.uncertainty?.draws ?? 300,
19628
+ quantiles,
19629
+ seed: m?.uncertainty?.seed,
19630
+ sigma: {
19631
+ retention: m?.uncertainty?.sigma?.retention ?? 0.08,
19632
+ conversion: m?.uncertainty?.sigma?.conversion ?? 0.12,
19633
+ arppu: m?.uncertainty?.sigma?.arppu ?? 0.2,
19634
+ ad: m?.uncertainty?.sigma?.ad ?? 0.15
19635
+ },
19636
+ correlation: {
19637
+ retentionConversion: m?.uncertainty?.correlation?.retentionConversion ?? 0,
19638
+ retentionArppu: m?.uncertainty?.correlation?.retentionArppu ?? 0,
19639
+ retentionAd: m?.uncertainty?.correlation?.retentionAd ?? 0,
19640
+ conversionArppu: m?.uncertainty?.correlation?.conversionArppu ?? 0,
19641
+ conversionAd: m?.uncertainty?.correlation?.conversionAd ?? 0,
19642
+ arppuAd: m?.uncertainty?.correlation?.arppuAd ?? 0
19643
+ }
19644
+ }
19645
+ };
19518
19646
  }
19519
- async function writeTextFile(path, body) {
19520
- try {
19521
- await ensureDir(dirname(path));
19522
- await Bun.write(path, body);
19523
- } catch (error) {
19524
- throw cliError("OUTPUT_WRITE_FAILED", `Unable to write output file: ${path}`, {
19525
- detail: errorDetail(error),
19526
- hint: "Choose a writable output path and check that its parent is a directory.",
19527
- cause: error
19528
- });
19647
+ function retentionAtDay(ret, day) {
19648
+ if (day <= 0)
19649
+ return 1;
19650
+ if (day <= 1)
19651
+ return ret.d1;
19652
+ if (day <= 7)
19653
+ return ret.d1 + (ret.d7 - ret.d1) * (day - 1) / 6;
19654
+ if (day <= 30)
19655
+ return ret.d7 + (ret.d30 - ret.d7) * (day - 7) / 23;
19656
+ if (day <= 90)
19657
+ return ret.d30 + (ret.d90 - ret.d30) * (day - 30) / 60;
19658
+ const extra = day - 90;
19659
+ return clamp012(ret.d90 * Math.exp(-ret.longTailDailyDecay * extra));
19660
+ }
19661
+ function progressionFactor(startWorthLog10, endWorthLog10, span) {
19662
+ if (!Number.isFinite(startWorthLog10) || !Number.isFinite(endWorthLog10) || !Number.isFinite(span) || span <= 0) {
19663
+ return 0;
19529
19664
  }
19665
+ return clamp012((endWorthLog10 - startWorthLog10) / span);
19530
19666
  }
19531
- function writeStdout(body) {
19532
- return new Promise((resolve, reject) => {
19533
- process.stdout.write(body, (error) => error ? reject(error) : resolve());
19534
- });
19667
+ function estimateLtvPerUser(args) {
19668
+ const { config, horizonDays } = args;
19669
+ const progression = clamp012(args.progression);
19670
+ const arppuDaily = config.revenue.arppuDaily * (1 + progression * config.revenue.progressionRevenueLift);
19671
+ const adArpDau = config.revenue.adArpDau * (1 + progression * config.revenue.progressionRevenueLift * 0.5);
19672
+ const payerConversion = config.revenue.payerConversion;
19673
+ const fullDays = Math.floor(Math.max(0, horizonDays));
19674
+ const remainder = Math.max(0, horizonDays - fullDays);
19675
+ let gross = 0;
19676
+ for (let day = 1;day <= fullDays; day++) {
19677
+ const active = retentionAtDay(config.retention, day);
19678
+ gross += active * (payerConversion * arppuDaily + adArpDau);
19679
+ }
19680
+ if (remainder > 0) {
19681
+ const active = retentionAtDay(config.retention, fullDays + 1);
19682
+ gross += remainder * active * (payerConversion * arppuDaily + adArpDau);
19683
+ }
19684
+ const net = gross * (1 - config.revenue.platformFeeRate) * config.revenue.grossMarginRate;
19685
+ const ltv = net - config.acquisition.cpi;
19686
+ return {
19687
+ cumulativeGrossRevenuePerUser: gross,
19688
+ cumulativeNetRevenuePerUser: net,
19689
+ cumulativeLtvPerUser: ltv
19690
+ };
19535
19691
  }
19536
- async function ensureDir(path) {
19537
- await $`mkdir -p ${path}`.quiet();
19692
+
19693
+ // ../core/dist/ltv/uncertainty.js
19694
+ function clamp013(v) {
19695
+ if (v < 0)
19696
+ return 0;
19697
+ if (v > 1)
19698
+ return 1;
19699
+ return v;
19538
19700
  }
19539
- async function fileExists(path) {
19540
- return Bun.file(path).exists();
19701
+ function clampCorrelation(v) {
19702
+ if (!Number.isFinite(v))
19703
+ return 0;
19704
+ if (v < -1)
19705
+ return -1;
19706
+ if (v > 1)
19707
+ return 1;
19708
+ return v;
19541
19709
  }
19542
- function sha256Hex(value) {
19543
- const hasher = new Bun.CryptoHasher("sha256");
19544
- hasher.update(value);
19545
- return hasher.digest("hex");
19710
+ function randNormal(rng) {
19711
+ const u1 = Math.max(0.000000000001, rng());
19712
+ const u2 = Math.max(0.000000000001, rng());
19713
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
19546
19714
  }
19547
- function runText(args, opts) {
19548
- const proc = Bun.spawnSync(args, {
19549
- cwd: opts?.cwd,
19550
- env: opts?.env,
19551
- stdout: "pipe",
19552
- stderr: "pipe",
19553
- maxBuffer: MAX_CAPTURE_BYTES
19554
- });
19555
- if (proc.exitCode !== 0) {
19556
- const stderr = proc.stderr.toString().trim();
19557
- const stdout = proc.stdout.toString().trim();
19558
- const detail = stderr || stdout || `command failed with exit code ${proc.exitCode}`;
19559
- throw new Error(detail);
19715
+ function buildCorrelationMatrix(base) {
19716
+ const rc = clampCorrelation(base.retentionConversion);
19717
+ const ra = clampCorrelation(base.retentionArppu);
19718
+ const rd = clampCorrelation(base.retentionAd);
19719
+ const ca = clampCorrelation(base.conversionArppu);
19720
+ const cd = clampCorrelation(base.conversionAd);
19721
+ const ad = clampCorrelation(base.arppuAd);
19722
+ return [
19723
+ [1, rc, ra, rd],
19724
+ [rc, 1, ca, cd],
19725
+ [ra, ca, 1, ad],
19726
+ [rd, cd, ad, 1]
19727
+ ];
19728
+ }
19729
+ function withOffDiagonalScale(matrix, alpha) {
19730
+ return matrix.map((row, i) => row.map((v, j) => {
19731
+ if (i === j)
19732
+ return 1;
19733
+ return v * alpha;
19734
+ }));
19735
+ }
19736
+ function cholesky4(matrix) {
19737
+ const n = 4;
19738
+ const l = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
19739
+ for (let i = 0;i < n; i++) {
19740
+ for (let j = 0;j <= i; j++) {
19741
+ let sum = 0;
19742
+ for (let k = 0;k < j; k++) {
19743
+ sum += (l[i]?.[k] ?? 0) * (l[j]?.[k] ?? 0);
19744
+ }
19745
+ if (i === j) {
19746
+ const val = (matrix[i]?.[i] ?? 0) - sum;
19747
+ if (val <= 0.000000001)
19748
+ return null;
19749
+ l[i][j] = Math.sqrt(val);
19750
+ } else {
19751
+ const denom = l[j]?.[j] ?? 0;
19752
+ if (Math.abs(denom) < 0.000000001)
19753
+ return null;
19754
+ l[i][j] = ((matrix[i]?.[j] ?? 0) - sum) / denom;
19755
+ }
19756
+ }
19560
19757
  }
19561
- return proc.stdout.toString();
19758
+ return l;
19562
19759
  }
19563
- var MAX_CAPTURE_BYTES;
19564
- var init_bun = __esm(() => {
19565
- init_errors3();
19566
- MAX_CAPTURE_BYTES = 8 * 1024 * 1024;
19567
- });
19568
-
19569
- // package.json
19570
- var require_package = __commonJS(function(exports, module) {
19571
- module.exports = {
19572
- name: "@idlekit/cli",
19573
- version: "0.5.0",
19574
- description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
19575
- license: "MIT",
19576
- type: "module",
19577
- files: [
19578
- "dist",
19579
- "README.md",
19580
- "LICENSE"
19581
- ],
19582
- bin: {
19583
- idk: "dist/main.js"
19584
- },
19585
- repository: {
19586
- type: "git",
19587
- url: "git+https://github.com/imjlk/idlekit.git",
19588
- directory: "packages/cli"
19589
- },
19590
- homepage: "https://github.com/imjlk/idlekit/tree/main/packages/cli#readme",
19591
- bugs: {
19592
- url: "https://github.com/imjlk/idlekit/issues"
19593
- },
19594
- keywords: [
19595
- "bun",
19596
- "idle-game",
19597
- "incremental-game",
19598
- "cli",
19599
- "economy-simulator",
19600
- "game-balancing",
19601
- "simulation"
19602
- ],
19603
- publishConfig: {
19604
- access: "public"
19605
- },
19606
- engines: {
19607
- bun: ">=1.4.2"
19608
- },
19609
- scripts: {
19610
- dev: "bun src/main.ts",
19611
- "dev:watch": "bun --watch src/main.ts",
19612
- "generate:check": "bun scripts/command-check.ts",
19613
- "doctor:completions": "bun scripts/command-check.ts",
19614
- "review:smoke": "bun scripts/review-smoke.ts",
19615
- build: "bun scripts/cli-bundle.ts --outdir dist --sourcemap",
19616
- "build:bin": "bun scripts/cli-bundle.ts --entry src/bin.ts --outdir dist/bin --targets native",
19617
- typecheck: "ttsc -p tsconfig.json --noEmit",
19618
- test: "bun scripts/cli-bundle.ts --outdir .test-bundle && IDLEKIT_CLI_ENTRY=.test-bundle/main.js bun test --timeout 90000 src",
19619
- prepack: "bun run build && cd ../.. && bun tools/package-manifest.ts prepare packages/cli",
19620
- postpack: "cd ../.. && bun tools/package-manifest.ts restore packages/cli"
19621
- },
19622
- dependencies: {
19623
- "@clack/prompts": "1.8.1",
19624
- "@gunshi/plugin-completion": "0.37.3",
19625
- "@gunshi/plugin-suggestion": "0.37.3",
19626
- "@idlekit/core": "0.4.1",
19627
- ajv: "^8.20.0",
19628
- gunshi: "0.37.3",
19629
- "proper-lockfile": "4.1.2",
19630
- yaml: "^2.9.1",
19631
- zod: "^4.6.5"
19760
+ function multiplyLowerTriangular(l, z) {
19761
+ const out = [0, 0, 0, 0];
19762
+ for (let i = 0;i < 4; i++) {
19763
+ let sum = 0;
19764
+ for (let j = 0;j <= i; j++) {
19765
+ sum += (l[i]?.[j] ?? 0) * (z[j] ?? 0);
19766
+ }
19767
+ out[i] = sum;
19768
+ }
19769
+ return out;
19770
+ }
19771
+ function sampleCorrelatedNormals(rng, corr) {
19772
+ const z = [randNormal(rng), randNormal(rng), randNormal(rng), randNormal(rng)];
19773
+ const matrix = buildCorrelationMatrix(corr);
19774
+ let l = cholesky4(matrix);
19775
+ let alpha = 0.99;
19776
+ while (!l && alpha > 0.000001) {
19777
+ l = cholesky4(withOffDiagonalScale(matrix, alpha));
19778
+ alpha *= 0.8;
19779
+ }
19780
+ if (!l) {
19781
+ return {
19782
+ retention: z[0] ?? 0,
19783
+ conversion: z[1] ?? 0,
19784
+ arppu: z[2] ?? 0,
19785
+ ad: z[3] ?? 0
19786
+ };
19787
+ }
19788
+ const c = multiplyLowerTriangular(l, z);
19789
+ return {
19790
+ retention: c[0] ?? 0,
19791
+ conversion: c[1] ?? 0,
19792
+ arppu: c[2] ?? 0,
19793
+ ad: c[3] ?? 0
19794
+ };
19795
+ }
19796
+ function applyLogNormalShock(mean, sigma, z) {
19797
+ if (sigma <= 0)
19798
+ return mean;
19799
+ return mean * Math.exp(sigma * z - 0.5 * sigma * sigma);
19800
+ }
19801
+ function makeUncertainConfig(base, rng) {
19802
+ const shock = sampleCorrelatedNormals(rng, base.uncertainty.correlation);
19803
+ const d1 = clamp013(applyLogNormalShock(base.retention.d1, base.uncertainty.sigma.retention, shock.retention));
19804
+ const d7 = Math.min(d1, clamp013(applyLogNormalShock(base.retention.d7, base.uncertainty.sigma.retention, shock.retention)));
19805
+ const d30 = Math.min(d7, clamp013(applyLogNormalShock(base.retention.d30, base.uncertainty.sigma.retention, shock.retention)));
19806
+ const d90 = Math.min(d30, clamp013(applyLogNormalShock(base.retention.d90, base.uncertainty.sigma.retention, shock.retention)));
19807
+ return {
19808
+ ...base,
19809
+ retention: {
19810
+ ...base.retention,
19811
+ d1,
19812
+ d7,
19813
+ d30,
19814
+ d90
19632
19815
  },
19633
- devDependencies: {
19634
- "@types/proper-lockfile": "4.1.4"
19816
+ revenue: {
19817
+ ...base.revenue,
19818
+ payerConversion: clamp013(applyLogNormalShock(base.revenue.payerConversion, base.uncertainty.sigma.conversion, shock.conversion)),
19819
+ arppuDaily: Math.max(0, applyLogNormalShock(base.revenue.arppuDaily, base.uncertainty.sigma.arppu, shock.arppu)),
19820
+ adArpDau: Math.max(0, applyLogNormalShock(base.revenue.adArpDau, base.uncertainty.sigma.ad, shock.ad))
19635
19821
  }
19636
19822
  };
19637
- });
19638
-
19639
- // src/runtime/selfCli.ts
19640
- import { existsSync } from "fs";
19641
- import { dirname as dirname3, resolve as resolve2 } from "path";
19642
- function currentEntry() {
19643
- return Bun.main;
19644
19823
  }
19645
- function isJavaScriptEntry(entry) {
19646
- return entry.endsWith(".js") || entry.endsWith(".mjs");
19824
+ function quantileFromSorted(sorted, q) {
19825
+ if (sorted.length === 0)
19826
+ return 0;
19827
+ const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
19828
+ return sorted[idx] ?? 0;
19647
19829
  }
19648
- function isNativeEntry(entry) {
19649
- return entry.includes("$bunfs") || entry.includes("~BUN");
19830
+ function estimateLtvDistribution(args) {
19831
+ const rng = mulberry32(args.seed);
19832
+ const grossSamples = [];
19833
+ const netSamples = [];
19834
+ const ltvSamples = [];
19835
+ const draws = Math.max(1, args.draws);
19836
+ for (let i = 0;i < draws; i++) {
19837
+ const sampled = makeUncertainConfig(args.config, rng);
19838
+ const point = estimateLtvPerUser({
19839
+ config: sampled,
19840
+ horizonDays: args.horizonDays,
19841
+ progression: args.progression
19842
+ });
19843
+ grossSamples.push(point.cumulativeGrossRevenuePerUser);
19844
+ netSamples.push(point.cumulativeNetRevenuePerUser);
19845
+ ltvSamples.push(point.cumulativeLtvPerUser);
19846
+ }
19847
+ const avg = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length);
19848
+ const sortedLtv = [...ltvSamples].sort((a, b) => a - b);
19849
+ const byQuantile = {};
19850
+ for (const q of args.quantiles) {
19851
+ byQuantile[`q${Math.round(q * 100)}`] = quantileFromSorted(sortedLtv, q);
19852
+ }
19853
+ return {
19854
+ mean: {
19855
+ cumulativeGrossRevenuePerUser: avg(grossSamples),
19856
+ cumulativeNetRevenuePerUser: avg(netSamples),
19857
+ cumulativeLtvPerUser: avg(ltvSamples)
19858
+ },
19859
+ quantiles: byQuantile
19860
+ };
19650
19861
  }
19651
- function isBundledCliProcess(entry = currentEntry()) {
19652
- return isJavaScriptEntry(entry) || isNativeEntry(entry);
19862
+ var init_uncertainty = () => {};
19863
+
19864
+ // ../core/dist/ltv/public.js
19865
+ function assertMonetization(monetization) {
19866
+ const valid = validateScenarioV1({
19867
+ schemaVersion: 1,
19868
+ unit: { code: "LTV" },
19869
+ policy: { mode: "drop" },
19870
+ model: { id: "ltv-input", version: 1 },
19871
+ initial: { wallet: { unit: "LTV", amount: "0" } },
19872
+ clock: { stepSec: 1, durationSec: 1 },
19873
+ monetization
19874
+ });
19875
+ if (!valid.ok)
19876
+ throw new RangeError(valid.issues.map((issue) => issue.message).join("; "));
19653
19877
  }
19654
- function cliPackageRoot(entry = currentEntry()) {
19655
- if (isNativeEntry(entry))
19656
- return process.cwd();
19657
- if (isBundledCliProcess(entry))
19658
- return resolve2(dirname3(entry), "..");
19659
- return resolve2(import.meta.dir, "../..");
19878
+ function assertQuantileValues(quantiles) {
19879
+ if (quantiles.length === 0)
19880
+ throw new RangeError("quantiles must be nonempty and in 0..1");
19881
+ for (const q of quantiles) {
19882
+ if (!Number.isFinite(q) || q < 0 || q > 1)
19883
+ throw new RangeError("quantiles must be nonempty and in 0..1");
19884
+ }
19660
19885
  }
19661
- function bundledCliConfig(entry) {
19662
- const config = resolve2(dirname3(entry), "../../../tools/bundled-cli-bunfig.toml");
19663
- if (!existsSync(config))
19664
- return;
19886
+ function assertConfig(config) {
19887
+ const { retention, ...other } = config;
19888
+ for (const day of ["d1", "d7", "d30", "d90"]) {
19889
+ if (!Number.isFinite(retention[day]) || retention[day] < 0 || retention[day] > 1) {
19890
+ throw new RangeError(`retention.${day} must be finite and in 0..1`);
19891
+ }
19892
+ }
19893
+ if (!Number.isFinite(retention.longTailDailyDecay) || retention.longTailDailyDecay < 0) {
19894
+ throw new RangeError("retention.longTailDailyDecay must be finite and >= 0");
19895
+ }
19896
+ const { quantiles, ...uncertainty } = other.uncertainty;
19897
+ assertQuantileValues(quantiles);
19898
+ assertMonetization({ ...other, uncertainty });
19899
+ }
19900
+ function deriveMonetizationConfig2(scenario) {
19901
+ assertMonetization(scenario.monetization ?? {});
19902
+ const config = deriveMonetizationConfig(scenario);
19903
+ assertConfig(config);
19665
19904
  return config;
19666
19905
  }
19667
- function selfCliCommand(args, entry = currentEntry()) {
19668
- if (isNativeEntry(entry))
19669
- return [process.execPath, ...args];
19670
- const bun = process.argv[0] ?? "bun";
19671
- if (isJavaScriptEntry(entry)) {
19672
- const config = bundledCliConfig(entry);
19673
- if (config)
19674
- return [bun, `--config=${config}`, entry, ...args];
19675
- return [bun, entry, ...args];
19906
+ function assertPoint(args) {
19907
+ assertConfig(args.config);
19908
+ if (!Number.isFinite(args.horizonDays) || args.horizonDays < 0 || args.horizonDays > 36500) {
19909
+ throw new RangeError("horizonDays must be finite and in 0..36500");
19676
19910
  }
19677
- const bunfigApplies = resolve2(process.cwd()) === CLI_ROOT;
19678
- if (entry.endsWith(".ts") && !bunfigApplies) {
19679
- return [bun, "--preload", "@ttsc/unplugin/bun-register", entry, ...args];
19911
+ if (!Number.isFinite(args.progression))
19912
+ throw new RangeError("progression must be finite");
19913
+ }
19914
+ function finiteResult(result) {
19915
+ if (!Object.values(result).every((value) => typeof value === "number" && Number.isFinite(value))) {
19916
+ throw new RangeError("LTV calculation overflowed; reduce the input magnitudes");
19680
19917
  }
19681
- return [bun, entry, ...args];
19918
+ return result;
19682
19919
  }
19683
- function runSelfCli(args) {
19684
- const proc = Bun.spawnSync(selfCliCommand(args), {
19685
- cwd: process.cwd(),
19686
- stdout: "pipe",
19687
- stderr: "pipe",
19688
- env: process.env,
19689
- maxBuffer: MAX_CAPTURE_BYTES2
19690
- });
19691
- return {
19692
- exitCode: proc.exitCode,
19693
- stdout: proc.stdout.toString(),
19694
- stderr: proc.stderr.toString()
19695
- };
19920
+ function estimateLtvPerUser2(args) {
19921
+ assertPoint(args);
19922
+ return finiteResult(estimateLtvPerUser(args));
19923
+ }
19924
+ function estimateLtvDistribution2(args) {
19925
+ assertPoint(args);
19926
+ if (!Number.isSafeInteger(args.draws) || args.draws < 1 || args.draws > 1e5 || args.draws * Math.max(1, Math.ceil(args.horizonDays)) > 1e7) {
19927
+ throw new RangeError("draws must be 1..100000 and draws \xD7 ceil(horizonDays) at most 10000000");
19928
+ }
19929
+ if (!Number.isFinite(args.seed))
19930
+ throw new RangeError("seed must be finite");
19931
+ if (args.quantiles.length > 101)
19932
+ throw new RangeError("quantiles exceed the 101 distinct percent keys");
19933
+ assertQuantileValues(args.quantiles);
19934
+ if (new Set(args.quantiles.map((q) => Math.round(q * 100))).size !== args.quantiles.length) {
19935
+ throw new RangeError("quantiles must be nonempty, in 0..1 and have distinct rounded percent keys");
19936
+ }
19937
+ const result = estimateLtvDistribution(args);
19938
+ finiteResult(result.mean);
19939
+ finiteResult(result.quantiles);
19940
+ return result;
19696
19941
  }
19697
- var CLI_ROOT, MAX_CAPTURE_BYTES2;
19698
- var init_selfCli = __esm(() => {
19699
- CLI_ROOT = resolve2(import.meta.dir, "../..");
19700
- MAX_CAPTURE_BYTES2 = 8 * 1024 * 1024;
19942
+ var init_public = __esm(() => {
19943
+ init_validate();
19944
+ init_uncertainty();
19701
19945
  });
19702
19946
 
19703
- // src/io/outputMeta.ts
19704
- import { dirname as dirname4, resolve as resolve3 } from "path";
19705
- function repoRoot() {
19706
- if (isBundledCliProcess())
19707
- return resolve3(dirname4(Bun.main), "../..");
19708
- return resolve3(import.meta.dir, "../../..");
19947
+ // ../core/dist/ltv/cohort.js
19948
+ function wilsonInterval(activeUsers, users) {
19949
+ const rate = activeUsers / users;
19950
+ const z2 = 1.959963984540054 ** 2;
19951
+ const denominator = 1 + z2 / users;
19952
+ const center = (rate + z2 / (2 * users)) / denominator;
19953
+ const radius = Math.sqrt(z2 * (rate * (1 - rate) / users + z2 / (4 * users * users))) / denominator;
19954
+ return { min: activeUsers === 0 ? 0 : Math.max(0, center - radius), max: activeUsers === users ? 1 : Math.min(1, center + radius) };
19709
19955
  }
19710
- function stableStringify(value) {
19711
- if (value === null || typeof value !== "object") {
19712
- return JSON.stringify(value);
19956
+ function observeCohort(rows, observedThroughDay) {
19957
+ if (!Number.isSafeInteger(observedThroughDay) || observedThroughDay < 0)
19958
+ throw new Error("observed-through-day must be a nonnegative safe integer");
19959
+ if (!Array.isArray(rows) || rows.length === 0)
19960
+ throw new Error("Observed cohort requires at least one install record");
19961
+ const users = new Map;
19962
+ for (const row of rows) {
19963
+ if (!row || typeof row.userId !== "string" || !row.userId.trim() || !Number.isSafeInteger(row.day) || row.day < 0 || row.day > observedThroughDay) {
19964
+ throw new Error("Observed cohort requires a user ID and integer days within observed-through-day");
19965
+ }
19966
+ if (![row.iapRevenue, row.adRevenue, row.acquisitionCost].every((v) => v === undefined || typeof v === "number" && Number.isFinite(v) && v >= 0)) {
19967
+ throw new Error("Observed cohort revenue and acquisition costs must be finite and nonnegative");
19968
+ }
19969
+ if (row.active !== undefined && typeof row.active !== "boolean")
19970
+ throw new Error("Observed cohort active must be boolean");
19971
+ const user = users.get(row.userId) ?? { days: new Set, active: new Set };
19972
+ if (user.days.has(row.day))
19973
+ throw new Error("Observed cohort requires one aggregated row per user and day");
19974
+ user.days.add(row.day);
19975
+ if (row.active ?? true)
19976
+ user.active.add(row.day);
19977
+ users.set(row.userId, user);
19713
19978
  }
19714
- if (Array.isArray(value)) {
19715
- return `[${value.map((x) => stableStringify(x)).join(",")}]`;
19979
+ for (const user of users.values()) {
19980
+ if (!user.days.has(0))
19981
+ throw new Error("Observed cohort requires a day 0 install record for every user, including inactive users");
19716
19982
  }
19717
- const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
19718
- return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
19983
+ const n = users.size;
19984
+ const observation = {
19985
+ observedThroughDay,
19986
+ users: n,
19987
+ retention: RETENTION_DAYS.map((day) => {
19988
+ if (day > observedThroughDay)
19989
+ return { day, status: "not-observed", users: n, activeUsers: null, rate: null, interval95: null };
19990
+ const activeUsers = [...users.values()].filter((user) => user.active.has(day)).length;
19991
+ const rate = activeUsers / n;
19992
+ return { day, status: "observed", users: n, activeUsers, rate, interval95: wilsonInterval(activeUsers, n) };
19993
+ })
19994
+ };
19995
+ assertCohortObservation(observation);
19996
+ return observation;
19719
19997
  }
19720
- function sha256(value) {
19721
- return sha256Hex(value);
19998
+ function object2(value) {
19999
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19722
20000
  }
19723
- function resolveGitSha() {
19724
- if (hasResolvedGitSha)
19725
- return cachedGitSha ?? UNKNOWN_GIT_SHA;
19726
- hasResolvedGitSha = true;
19727
- const envSha = process.env.GITHUB_SHA?.trim();
19728
- if (envSha) {
19729
- cachedGitSha = envSha;
19730
- return cachedGitSha;
20001
+ function assertCohortObservation(observation) {
20002
+ if (!object2(observation) || typeof observation.users !== "number" || !Number.isSafeInteger(observation.users) || observation.users < 1 || typeof observation.observedThroughDay !== "number" || !Number.isSafeInteger(observation.observedThroughDay) || observation.observedThroughDay < 0) {
20003
+ throw new Error("Invalid cohort population or observation cutoff");
19731
20004
  }
19732
- try {
19733
- cachedGitSha = runText(["git", "rev-parse", "--short", "HEAD"], {
19734
- cwd: REPO_ROOT,
19735
- env: process.env
19736
- }).trim();
19737
- } catch {
19738
- cachedGitSha = UNKNOWN_GIT_SHA;
20005
+ if (!Array.isArray(observation.retention) || observation.retention.length !== RETENTION_DAYS.length)
20006
+ throw new Error("Cohort retention must contain D1, D7, D30 and D90");
20007
+ for (const [index, value] of observation.retention.entries()) {
20008
+ const point = value;
20009
+ if (!object2(point) || typeof point.day !== "number" || point.day !== RETENTION_DAYS[index] || point.users !== observation.users)
20010
+ throw new Error("Cohort retention days and population must match the observation");
20011
+ const observed = point.day <= observation.observedThroughDay;
20012
+ if (point.status !== (observed ? "observed" : "not-observed"))
20013
+ throw new Error("Cohort retention status must match the observation cutoff");
20014
+ if (!observed) {
20015
+ if (point.activeUsers !== null || point.rate !== null || point.interval95 !== null)
20016
+ throw new Error("Future retention must remain unobserved");
20017
+ continue;
20018
+ }
20019
+ if (typeof point.activeUsers !== "number" || !Number.isSafeInteger(point.activeUsers) || point.activeUsers < 0 || point.activeUsers > observation.users || point.rate !== point.activeUsers / observation.users) {
20020
+ throw new Error("Retained users and retention rate must match the installed population");
20021
+ }
20022
+ const interval = point.interval95;
20023
+ if (typeof point.rate !== "number" || !object2(interval) || typeof interval.min !== "number" || typeof interval.max !== "number" || !Number.isFinite(interval.min) || !Number.isFinite(interval.max) || interval.min < 0 || interval.max > 1 || interval.min > point.rate || interval.max < point.rate) {
20024
+ throw new Error("Retention confidence interval must be finite and contain the observed rate");
20025
+ }
20026
+ const expected = wilsonInterval(point.activeUsers, observation.users);
20027
+ if (interval.min !== expected.min || interval.max !== expected.max) {
20028
+ throw new Error("Retention confidence interval must match the Wilson 95% bounds for the observed population");
20029
+ }
19739
20030
  }
19740
- return cachedGitSha ?? UNKNOWN_GIT_SHA;
19741
- }
19742
- function hashContent(value) {
19743
- return sha256(stableStringify(value));
19744
20031
  }
19745
- function deriveDeterministicSeed(value) {
19746
- const digest = hashContent(value);
19747
- return Number.parseInt(digest.slice(0, 8), 16) >>> 0;
20032
+ function observedRetentionRates(observation) {
20033
+ assertCohortObservation(observation);
20034
+ const rates = {};
20035
+ for (const row of observation.retention) {
20036
+ if (row.rate !== null)
20037
+ rates[`d${row.day}`] = row.rate;
20038
+ }
20039
+ return rates;
19748
20040
  }
19749
- function deriveDeterministicRunId(args) {
19750
- const suffix = hashContent({
19751
- command: args.command,
19752
- seed: args.seed,
19753
- scope: args.scope ?? null
19754
- }).slice(0, 8);
19755
- return `${args.command}-${args.seed.toString(36)}-${suffix}`;
20041
+ function assertRetentionAssumptions(assumptions) {
20042
+ if (!object2(assumptions))
20043
+ throw new RangeError("Retention assumptions must be an object");
20044
+ for (const day of RETENTION_DAYS) {
20045
+ const rate = assumptions[`d${day}`];
20046
+ if (rate !== undefined && (typeof rate !== "number" || !Number.isFinite(rate) || rate < 0 || rate > 1)) {
20047
+ throw new RangeError(`Retention assumption d${day} must be finite and in 0..1`);
20048
+ }
20049
+ }
19756
20050
  }
19757
- function outputSchemaRefForCommand(command) {
19758
- return OUTPUT_SCHEMA_REF[command];
20051
+ function planRetentionPatch(observation, assumptions = {}) {
20052
+ assertRetentionAssumptions(assumptions);
20053
+ const rates = observedRetentionRates(observation);
20054
+ if (Object.keys(rates).length === 0)
20055
+ return { status: "unavailable", retention: {}, warnings: ["No retention day has been fully observed."] };
20056
+ const merged = { ...assumptions, ...rates };
20057
+ const warnings = [];
20058
+ for (const day of RETENTION_DAYS) {
20059
+ if (merged[`d${day}`] === undefined)
20060
+ warnings.push(`Unobserved D${day} has no explicit retained assumption. An LTV default could conflict with the observations; supply monetization.retention.d${day} in the scenario or observe this day before applying a patch.`);
20061
+ }
20062
+ let previous;
20063
+ for (const day of RETENTION_DAYS) {
20064
+ const rate = merged[`d${day}`];
20065
+ if (rate === undefined)
20066
+ continue;
20067
+ if (previous && previous.rate < rate)
20068
+ 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.`);
20069
+ previous = { day, rate };
20070
+ }
20071
+ return warnings.length ? { status: "requires-review", retention: {}, warnings } : { status: "ready", retention: rates, warnings };
19759
20072
  }
19760
- function buildOutputMeta(args) {
19761
- const scenarioHash = (() => {
19762
- if (args.scenario !== undefined)
19763
- return hashContent(args.scenario);
19764
- if (args.scenarios) {
19765
- const out = {};
19766
- for (const [k, v] of Object.entries(args.scenarios)) {
19767
- out[k] = hashContent(v);
19768
- }
19769
- return out;
19770
- }
19771
- return;
19772
- })();
19773
- return {
19774
- command: args.command,
19775
- generatedAt: new Date().toISOString(),
19776
- contractVersion: OUTPUT_CONTRACT_VERSION,
19777
- schemaRef: args.schemaRef ?? outputSchemaRefForCommand(args.command),
19778
- cliVersion,
19779
- gitSha: resolveGitSha(),
19780
- runId: args.runId,
19781
- seed: args.seed,
19782
- scenarioPath: args.scenarioPath,
19783
- scenarioHash,
19784
- tuneSpecHash: args.tuneSpec !== undefined ? hashContent(args.tuneSpec) : undefined,
19785
- telemetryHash: args.telemetry !== undefined ? hashContent(args.telemetry) : undefined,
19786
- pluginDigest: args.pluginDigest ?? {},
19787
- ...args.effectiveRunHash !== undefined ? { effectiveRunHash: args.effectiveRunHash } : {},
19788
- ...args.effectiveEngine !== undefined ? { effectiveEngine: args.effectiveEngine } : {},
19789
- ...args.stageScope !== undefined ? { stageScope: args.stageScope } : {}
19790
- };
20073
+ function compareRetention(observation, assumptions) {
20074
+ assertCohortObservation(observation);
20075
+ assertRetentionAssumptions(assumptions);
20076
+ return observation.retention.map((row) => {
20077
+ const assumption = assumptions[`d${row.day}`] ?? null;
20078
+ return {
20079
+ day: row.day,
20080
+ assumption,
20081
+ observed: row.rate,
20082
+ differencePercentagePoints: row.rate === null || assumption === null ? null : (row.rate - assumption) * 100,
20083
+ status: row.rate === null ? "not-observed" : assumption === null ? "unconfigured" : "compared"
20084
+ };
20085
+ });
19791
20086
  }
19792
- var import_package, REPO_ROOT, OUTPUT_CONTRACT_VERSION = "1.4.0", UNKNOWN_GIT_SHA = "unknown", OUTPUT_SCHEMA_REF, cachedGitSha, hasResolvedGitSha = false, cliVersion;
19793
- var init_outputMeta = __esm(() => {
19794
- init_bun();
19795
- init_selfCli();
19796
- import_package = __toESM(require_package(), 1);
19797
- REPO_ROOT = repoRoot();
19798
- OUTPUT_SCHEMA_REF = {
19799
- simulate: "docs/schemas/simulate.output.schema.json",
19800
- eta: "docs/schemas/eta.output.schema.json",
19801
- compare: "docs/schemas/compare.output.schema.json",
19802
- doctor: "docs/schemas/doctor.output.schema.json",
19803
- evaluate: "docs/schemas/evaluate.output.schema.json",
19804
- "judge.jev": "docs/schemas/jev.judge.output.schema.json",
19805
- "execution.batch": "docs/worker-execution.md",
19806
- experience: "docs/schemas/experience.output.schema.json",
19807
- "setup.completions": "docs/schemas/setup.output.schema.json",
19808
- "setup.plugin-trust": "docs/schemas/setup.output.schema.json",
19809
- tune: "docs/schemas/tune.output.schema.json",
19810
- ltv: "docs/schemas/ltv.output.schema.json",
19811
- calibrate: "docs/schemas/calibrate.output.schema.json",
19812
- "kpi.regress": "docs/schemas/kpi.regress.output.schema.json",
19813
- "replay.verify": "docs/schemas/replay.verify.output.schema.json"
19814
- };
19815
- cliVersion = import_package.default.version ?? "0.0.0";
20087
+ var RETENTION_DAYS;
20088
+ var init_cohort = __esm(() => {
20089
+ RETENTION_DAYS = Object.freeze([1, 7, 30, 90]);
19816
20090
  });
19817
20091
 
19818
- // ../../node_modules/.bun/yaml@2.9.1/node_modules/yaml/dist/nodes/identity.js
19819
- var require_identity = __commonJS(function(exports) {
19820
- var ALIAS = Symbol.for("yaml.alias");
19821
- var DOC = Symbol.for("yaml.document");
19822
- var MAP = Symbol.for("yaml.map");
19823
- var PAIR = Symbol.for("yaml.pair");
19824
- var SCALAR = Symbol.for("yaml.scalar");
19825
- var SEQ = Symbol.for("yaml.seq");
19826
- var NODE_TYPE = Symbol.for("yaml.node.type");
19827
- var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
19828
- var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
19829
- var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
19830
- var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
19831
- var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
19832
- var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
19833
- function isCollection(node) {
19834
- if (node && typeof node === "object")
19835
- switch (node[NODE_TYPE]) {
19836
- case MAP:
19837
- case SEQ:
19838
- return true;
19839
- }
19840
- return false;
19841
- }
19842
- function isNode(node) {
19843
- if (node && typeof node === "object")
19844
- switch (node[NODE_TYPE]) {
19845
- case ALIAS:
19846
- case MAP:
19847
- case SCALAR:
19848
- case SEQ:
19849
- return true;
19850
- }
19851
- return false;
19852
- }
19853
- var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
19854
- exports.ALIAS = ALIAS;
19855
- exports.DOC = DOC;
19856
- exports.MAP = MAP;
20092
+ // ../core/dist/index.js
20093
+ var init_dist2 = __esm(() => {
20094
+ init_types();
20095
+ init_breakInfinity2();
20096
+ init_breakEternity2();
20097
+ init_types5();
20098
+ init_types6();
20099
+ init_emitter();
20100
+ init_tickMoney2();
20101
+ init_suffixer();
20102
+ init_formatMoney2();
20103
+ init_parseMoney2();
20104
+ init_visibilityTracker2();
20105
+ init_moneyState();
20106
+ init_simState();
20107
+ init_types7();
20108
+ init_validate();
20109
+ init_concreteValidator();
20110
+ init_scenarioContract();
20111
+ init_compile2();
20112
+ init_simulator();
20113
+ init_advance();
20114
+ init_step();
20115
+ init_stepTypes();
20116
+ init_offline();
20117
+ init_session();
20118
+ init_monteCarlo();
20119
+ init_executionBackend();
20120
+ init_integrationMatrix();
20121
+ init_runFactory();
20122
+ init_constraints();
20123
+ init_observation();
20124
+ init_types8();
20125
+ init_contracts();
20126
+ init_stability();
20127
+ init_scripted();
20128
+ init_greedy();
20129
+ init_planner();
20130
+ init_params();
20131
+ init_builtins();
20132
+ init_tuneSpec();
20133
+ init_validate2();
20134
+ init_objective();
20135
+ init_params2();
20136
+ init_builtins2();
20137
+ init_tuner();
20138
+ init_randomSearch();
20139
+ init_runner();
20140
+ init_eta();
20141
+ init_prestigeCycle();
20142
+ init_resetCycle();
20143
+ init_resetOptimization();
20144
+ init_distribution();
20145
+ init_experiment();
20146
+ init_sensitivity();
20147
+ init_growthFit();
20148
+ init_contracts2();
20149
+ init_metrics();
20150
+ init_registry();
20151
+ init_analyzers();
20152
+ init_inspect();
20153
+ init_checks3();
20154
+ init_judge();
20155
+ init_flow();
20156
+ init_jevAdvisory();
20157
+ init_public();
20158
+ init_cohort();
20159
+ });
20160
+
20161
+ // src/runtime/bun.ts
20162
+ var {$ } = globalThis.Bun;
20163
+ import { dirname } from "path";
20164
+ async function readTextFile(path) {
20165
+ return Bun.file(path).text();
20166
+ }
20167
+ async function readJsonFile(path) {
20168
+ return Bun.file(path).json();
20169
+ }
20170
+ async function writeTextFile(path, body) {
20171
+ try {
20172
+ await ensureDir(dirname(path));
20173
+ await Bun.write(path, body);
20174
+ } catch (error) {
20175
+ throw cliError("OUTPUT_WRITE_FAILED", `Unable to write output file: ${path}`, {
20176
+ detail: errorDetail(error),
20177
+ hint: "Choose a writable output path and check that its parent is a directory.",
20178
+ cause: error
20179
+ });
20180
+ }
20181
+ }
20182
+ function writeStdout(body) {
20183
+ return new Promise((resolve, reject) => {
20184
+ process.stdout.write(body, (error) => error ? reject(error) : resolve());
20185
+ });
20186
+ }
20187
+ async function ensureDir(path) {
20188
+ await $`mkdir -p ${path}`.quiet();
20189
+ }
20190
+ async function fileExists(path) {
20191
+ return Bun.file(path).exists();
20192
+ }
20193
+ function sha256Hex(value) {
20194
+ const hasher = new Bun.CryptoHasher("sha256");
20195
+ hasher.update(value);
20196
+ return hasher.digest("hex");
20197
+ }
20198
+ function runText(args, opts) {
20199
+ const proc = Bun.spawnSync(args, {
20200
+ cwd: opts?.cwd,
20201
+ env: opts?.env,
20202
+ stdout: "pipe",
20203
+ stderr: "pipe",
20204
+ maxBuffer: MAX_CAPTURE_BYTES
20205
+ });
20206
+ if (proc.exitCode !== 0) {
20207
+ const stderr = proc.stderr.toString().trim();
20208
+ const stdout = proc.stdout.toString().trim();
20209
+ const detail = stderr || stdout || `command failed with exit code ${proc.exitCode}`;
20210
+ throw new Error(detail);
20211
+ }
20212
+ return proc.stdout.toString();
20213
+ }
20214
+ var MAX_CAPTURE_BYTES;
20215
+ var init_bun = __esm(() => {
20216
+ init_errors3();
20217
+ MAX_CAPTURE_BYTES = 8 * 1024 * 1024;
20218
+ });
20219
+
20220
+ // package.json
20221
+ var require_package = __commonJS(function(exports, module) {
20222
+ module.exports = {
20223
+ name: "@idlekit/cli",
20224
+ version: "0.6.0",
20225
+ description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
20226
+ license: "MIT",
20227
+ type: "module",
20228
+ files: [
20229
+ "dist",
20230
+ "README.md",
20231
+ "LICENSE"
20232
+ ],
20233
+ bin: {
20234
+ idk: "dist/main.js"
20235
+ },
20236
+ repository: {
20237
+ type: "git",
20238
+ url: "git+https://github.com/imjlk/idlekit.git",
20239
+ directory: "packages/cli"
20240
+ },
20241
+ homepage: "https://github.com/imjlk/idlekit/tree/main/packages/cli#readme",
20242
+ bugs: {
20243
+ url: "https://github.com/imjlk/idlekit/issues"
20244
+ },
20245
+ keywords: [
20246
+ "bun",
20247
+ "idle-game",
20248
+ "incremental-game",
20249
+ "cli",
20250
+ "economy-simulator",
20251
+ "game-balancing",
20252
+ "simulation"
20253
+ ],
20254
+ publishConfig: {
20255
+ access: "public"
20256
+ },
20257
+ engines: {
20258
+ bun: ">=1.4.2"
20259
+ },
20260
+ scripts: {
20261
+ dev: "bun src/main.ts",
20262
+ "dev:watch": "bun --watch src/main.ts",
20263
+ "generate:check": "bun scripts/command-check.ts",
20264
+ "doctor:completions": "bun scripts/command-check.ts",
20265
+ "review:smoke": "bun scripts/review-smoke.ts",
20266
+ build: "bun scripts/cli-bundle.ts --outdir dist --sourcemap",
20267
+ "build:bin": "bun scripts/cli-bundle.ts --entry src/bin.ts --outdir dist/bin --targets native",
20268
+ typecheck: "ttsc -p tsconfig.json --noEmit",
20269
+ test: "bun scripts/cli-bundle.ts --outdir .test-bundle && IDLEKIT_CLI_ENTRY=.test-bundle/main.js bun test --timeout 90000 src",
20270
+ prepack: "bun run build && cd ../.. && bun tools/package-manifest.ts prepare packages/cli",
20271
+ postpack: "cd ../.. && bun tools/package-manifest.ts restore packages/cli"
20272
+ },
20273
+ dependencies: {
20274
+ "@clack/prompts": "1.8.1",
20275
+ "@gunshi/plugin-completion": "0.37.3",
20276
+ "@gunshi/plugin-suggestion": "0.37.3",
20277
+ "@idlekit/core": "0.6.0",
20278
+ ajv: "^8.20.0",
20279
+ gunshi: "0.37.3",
20280
+ "proper-lockfile": "4.1.2",
20281
+ yaml: "^2.9.1",
20282
+ zod: "^4.6.5"
20283
+ },
20284
+ devDependencies: {
20285
+ "@types/proper-lockfile": "4.1.4"
20286
+ }
20287
+ };
20288
+ });
20289
+
20290
+ // src/runtime/selfCli.ts
20291
+ import { existsSync } from "fs";
20292
+ import { dirname as dirname3, resolve as resolve2 } from "path";
20293
+ function currentEntry() {
20294
+ return Bun.main;
20295
+ }
20296
+ function isJavaScriptEntry(entry) {
20297
+ return entry.endsWith(".js") || entry.endsWith(".mjs");
20298
+ }
20299
+ function isNativeEntry(entry) {
20300
+ return entry.includes("$bunfs") || entry.includes("~BUN");
20301
+ }
20302
+ function isBundledCliProcess(entry = currentEntry()) {
20303
+ return isJavaScriptEntry(entry) || isNativeEntry(entry);
20304
+ }
20305
+ function cliPackageRoot(entry = currentEntry()) {
20306
+ if (isNativeEntry(entry))
20307
+ return process.cwd();
20308
+ if (isBundledCliProcess(entry))
20309
+ return resolve2(dirname3(entry), "..");
20310
+ return resolve2(import.meta.dir, "../..");
20311
+ }
20312
+ function bundledCliConfig(entry) {
20313
+ const config = resolve2(dirname3(entry), "../../../tools/bundled-cli-bunfig.toml");
20314
+ if (!existsSync(config))
20315
+ return;
20316
+ return config;
20317
+ }
20318
+ function selfCliCommand(args, entry = currentEntry()) {
20319
+ if (isNativeEntry(entry))
20320
+ return [process.execPath, ...args];
20321
+ const bun = process.argv[0] ?? "bun";
20322
+ if (isJavaScriptEntry(entry)) {
20323
+ const config = bundledCliConfig(entry);
20324
+ if (config)
20325
+ return [bun, `--config=${config}`, entry, ...args];
20326
+ return [bun, entry, ...args];
20327
+ }
20328
+ const bunfigApplies = resolve2(process.cwd()) === CLI_ROOT;
20329
+ if (entry.endsWith(".ts") && !bunfigApplies) {
20330
+ return [bun, "--preload", "@ttsc/unplugin/bun-register", entry, ...args];
20331
+ }
20332
+ return [bun, entry, ...args];
20333
+ }
20334
+ function runSelfCli(args) {
20335
+ const proc = Bun.spawnSync(selfCliCommand(args), {
20336
+ cwd: process.cwd(),
20337
+ stdout: "pipe",
20338
+ stderr: "pipe",
20339
+ env: process.env,
20340
+ maxBuffer: MAX_CAPTURE_BYTES2
20341
+ });
20342
+ return {
20343
+ exitCode: proc.exitCode,
20344
+ stdout: proc.stdout.toString(),
20345
+ stderr: proc.stderr.toString()
20346
+ };
20347
+ }
20348
+ var CLI_ROOT, MAX_CAPTURE_BYTES2;
20349
+ var init_selfCli = __esm(() => {
20350
+ CLI_ROOT = resolve2(import.meta.dir, "../..");
20351
+ MAX_CAPTURE_BYTES2 = 8 * 1024 * 1024;
20352
+ });
20353
+
20354
+ // src/io/outputMeta.ts
20355
+ import { dirname as dirname4, resolve as resolve3 } from "path";
20356
+ function repoRoot() {
20357
+ if (isBundledCliProcess())
20358
+ return resolve3(dirname4(Bun.main), "../..");
20359
+ return resolve3(import.meta.dir, "../../..");
20360
+ }
20361
+ function stableStringify(value) {
20362
+ if (value === null || typeof value !== "object") {
20363
+ return JSON.stringify(value);
20364
+ }
20365
+ if (Array.isArray(value)) {
20366
+ return `[${value.map((x) => stableStringify(x)).join(",")}]`;
20367
+ }
20368
+ const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
20369
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
20370
+ }
20371
+ function sha256(value) {
20372
+ return sha256Hex(value);
20373
+ }
20374
+ function resolveGitSha() {
20375
+ if (hasResolvedGitSha)
20376
+ return cachedGitSha ?? UNKNOWN_GIT_SHA;
20377
+ hasResolvedGitSha = true;
20378
+ const envSha = process.env.GITHUB_SHA?.trim();
20379
+ if (envSha) {
20380
+ cachedGitSha = envSha;
20381
+ return cachedGitSha;
20382
+ }
20383
+ try {
20384
+ cachedGitSha = runText(["git", "rev-parse", "--short", "HEAD"], {
20385
+ cwd: REPO_ROOT,
20386
+ env: process.env
20387
+ }).trim();
20388
+ } catch {
20389
+ cachedGitSha = UNKNOWN_GIT_SHA;
20390
+ }
20391
+ return cachedGitSha ?? UNKNOWN_GIT_SHA;
20392
+ }
20393
+ function hashContent(value) {
20394
+ return sha256(stableStringify(value));
20395
+ }
20396
+ function deriveDeterministicSeed(value) {
20397
+ const digest = hashContent(value);
20398
+ return Number.parseInt(digest.slice(0, 8), 16) >>> 0;
20399
+ }
20400
+ function deriveDeterministicRunId(args) {
20401
+ const suffix = hashContent({
20402
+ command: args.command,
20403
+ seed: args.seed,
20404
+ scope: args.scope ?? null
20405
+ }).slice(0, 8);
20406
+ return `${args.command}-${args.seed.toString(36)}-${suffix}`;
20407
+ }
20408
+ function outputSchemaRefForCommand(command) {
20409
+ return OUTPUT_SCHEMA_REF[command];
20410
+ }
20411
+ function buildOutputMeta(args) {
20412
+ const scenarioHash = (() => {
20413
+ if (args.scenario !== undefined)
20414
+ return hashContent(args.scenario);
20415
+ if (args.scenarios) {
20416
+ const out = {};
20417
+ for (const [k, v] of Object.entries(args.scenarios)) {
20418
+ out[k] = hashContent(v);
20419
+ }
20420
+ return out;
20421
+ }
20422
+ return;
20423
+ })();
20424
+ return {
20425
+ command: args.command,
20426
+ generatedAt: new Date().toISOString(),
20427
+ contractVersion: OUTPUT_CONTRACT_VERSION,
20428
+ schemaRef: args.schemaRef ?? outputSchemaRefForCommand(args.command),
20429
+ cliVersion,
20430
+ gitSha: resolveGitSha(),
20431
+ runId: args.runId,
20432
+ seed: args.seed,
20433
+ scenarioPath: args.scenarioPath,
20434
+ scenarioHash,
20435
+ tuneSpecHash: args.tuneSpec !== undefined ? hashContent(args.tuneSpec) : undefined,
20436
+ telemetryHash: args.telemetry !== undefined ? hashContent(args.telemetry) : undefined,
20437
+ pluginDigest: args.pluginDigest ?? {},
20438
+ ...args.effectiveRunHash !== undefined ? { effectiveRunHash: args.effectiveRunHash } : {},
20439
+ ...args.effectiveEngine !== undefined ? { effectiveEngine: args.effectiveEngine } : {},
20440
+ ...args.stageScope !== undefined ? { stageScope: args.stageScope } : {}
20441
+ };
20442
+ }
20443
+ var import_package, REPO_ROOT, OUTPUT_CONTRACT_VERSION = "1.4.0", UNKNOWN_GIT_SHA = "unknown", OUTPUT_SCHEMA_REF, cachedGitSha, hasResolvedGitSha = false, cliVersion;
20444
+ var init_outputMeta = __esm(() => {
20445
+ init_bun();
20446
+ init_selfCli();
20447
+ import_package = __toESM(require_package(), 1);
20448
+ REPO_ROOT = repoRoot();
20449
+ OUTPUT_SCHEMA_REF = {
20450
+ simulate: "docs/schemas/simulate.output.schema.json",
20451
+ eta: "docs/schemas/eta.output.schema.json",
20452
+ compare: "docs/schemas/compare.output.schema.json",
20453
+ doctor: "docs/schemas/doctor.output.schema.json",
20454
+ evaluate: "docs/schemas/evaluate.output.schema.json",
20455
+ "judge.jev": "docs/schemas/jev.judge.output.schema.json",
20456
+ "execution.batch": "docs/worker-execution.md",
20457
+ experience: "docs/schemas/experience.output.schema.json",
20458
+ "setup.completions": "docs/schemas/setup.output.schema.json",
20459
+ "setup.plugin-trust": "docs/schemas/setup.output.schema.json",
20460
+ tune: "docs/schemas/tune.output.schema.json",
20461
+ ltv: "docs/schemas/ltv.output.schema.json",
20462
+ calibrate: "docs/schemas/calibrate.output.schema.json",
20463
+ "kpi.regress": "docs/schemas/kpi.regress.output.schema.json",
20464
+ "replay.verify": "docs/schemas/replay.verify.output.schema.json"
20465
+ };
20466
+ cliVersion = import_package.default.version ?? "0.0.0";
20467
+ });
20468
+
20469
+ // ../../node_modules/.bun/yaml@2.9.1/node_modules/yaml/dist/nodes/identity.js
20470
+ var require_identity = __commonJS(function(exports) {
20471
+ var ALIAS = Symbol.for("yaml.alias");
20472
+ var DOC = Symbol.for("yaml.document");
20473
+ var MAP = Symbol.for("yaml.map");
20474
+ var PAIR = Symbol.for("yaml.pair");
20475
+ var SCALAR = Symbol.for("yaml.scalar");
20476
+ var SEQ = Symbol.for("yaml.seq");
20477
+ var NODE_TYPE = Symbol.for("yaml.node.type");
20478
+ var isAlias = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === ALIAS;
20479
+ var isDocument = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === DOC;
20480
+ var isMap = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === MAP;
20481
+ var isPair = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === PAIR;
20482
+ var isScalar = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SCALAR;
20483
+ var isSeq = (node) => !!node && typeof node === "object" && node[NODE_TYPE] === SEQ;
20484
+ function isCollection(node) {
20485
+ if (node && typeof node === "object")
20486
+ switch (node[NODE_TYPE]) {
20487
+ case MAP:
20488
+ case SEQ:
20489
+ return true;
20490
+ }
20491
+ return false;
20492
+ }
20493
+ function isNode(node) {
20494
+ if (node && typeof node === "object")
20495
+ switch (node[NODE_TYPE]) {
20496
+ case ALIAS:
20497
+ case MAP:
20498
+ case SCALAR:
20499
+ case SEQ:
20500
+ return true;
20501
+ }
20502
+ return false;
20503
+ }
20504
+ var hasAnchor = (node) => (isScalar(node) || isCollection(node)) && !!node.anchor;
20505
+ exports.ALIAS = ALIAS;
20506
+ exports.DOC = DOC;
20507
+ exports.MAP = MAP;
19857
20508
  exports.NODE_TYPE = NODE_TYPE;
19858
20509
  exports.PAIR = PAIR;
19859
20510
  exports.SCALAR = SCALAR;
@@ -34562,8 +35213,8 @@ function ownedCount(state) {
34562
35213
  }
34563
35214
  function finiteLinearAmount(E, value, label) {
34564
35215
  if (!E.isFinite(value)) {
34565
- throw cliError("SCENARIO_INVALID", `linear ${label} exceeds the selected engine's finite range. Use --engine breakInfinity with simulate/evaluate/experience/ltv for a large economy, or reduce the inputs.`, {
34566
- hint: "Other commands, including report/compare/tune, use the number engine; reduce their inputs."
35216
+ throw cliError("SCENARIO_INVALID", `linear ${label} exceeds the selected engine's finite range. Use --engine breakInfinity with simulate/evaluate/experience/ltv/compare/tune/balance for a large economy, or reduce the inputs.`, {
35217
+ hint: "The report command uses the number engine; reduce its inputs."
34567
35218
  });
34568
35219
  }
34569
35220
  return value;
@@ -36087,767 +36738,400 @@ var inspect_default = defineCommand({
36087
36738
  if (skipping) {
36088
36739
  try {
36089
36740
  resolveStrategySelection({ scenario, strategyRegistry: loaded.strategyRegistry, overrideId: flags.strategy });
36090
- } catch {
36091
- strategyUnchecked = true;
36092
- }
36093
- }
36094
- if (strategyUnchecked && strategyId !== undefined) {
36095
- const report = buildScenarioInspect({
36096
- ...base,
36097
- decimals: strategyDecimals(flags.strategy?.trim() ? [] : [scenario.strategy?.params]),
36098
- engine: {
36099
- effectiveId: engine.effectiveId,
36100
- source: engine.source,
36101
- accepted: true,
36102
- ...engine.scenarioEngine ? { scenarioEngine: engine.scenarioEngine } : {}
36103
- },
36104
- stages: [],
36105
- planner: null,
36106
- plugins: { ...base.plugins, unresolvedStrategy: strategyId }
36107
- });
36108
- await emit2(flags, report, scenarioPath, scenario, pluginDigest);
36109
- return;
36110
- }
36111
- const prepared = planResolvedRun(planArgs);
36112
- const simulate = prepared.planFor("simulate");
36113
- const experience = prepared.planFor("experience");
36114
- const ltv = prepared.planFor("ltv");
36115
- const plans = [simulate, experience, ltv];
36116
- const report = buildScenarioInspect({
36117
- ...base,
36118
- decimals: strategyDecimals(plans.map((plan) => plan.strategy.params)),
36119
- engine: {
36120
- effectiveId: prepared.engine.effectiveId,
36121
- source: prepared.engine.source,
36122
- accepted: true,
36123
- ...prepared.engine.scenarioEngine ? { scenarioEngine: prepared.engine.scenarioEngine } : {}
36124
- },
36125
- stages: plans.map((plan) => stageInput(plan, scenario)),
36126
- planner: skipping ? null : plannerParams(simulate, pluginStrategyIds)
36127
- });
36128
- await emit2(flags, report, scenarioPath, scenario, pluginDigest);
36129
- }
36130
- });
36131
- async function emit2(flags, report, scenarioPath, scenario, pluginDigest) {
36132
- if (flags.format === "md") {
36133
- const body = renderInspectMarkdown(report);
36134
- if (!flags.out) {
36135
- console.log(body);
36136
- return;
36137
- }
36138
- }
36139
- await writeOutput({
36140
- format: flags.format === "md" ? "md" : "json",
36141
- outPath: flags.out,
36142
- data: flags.format === "md" ? renderInspectMarkdown(report) : report,
36143
- meta: flags.format === "json" ? buildOutputMeta({ command: "inspect", scenarioPath, scenario, pluginDigest }) : undefined
36144
- });
36145
- }
36146
-
36147
- // src/commands/judge.ts
36148
- init_zod();
36149
- init_errors3();
36150
- init_outputMeta();
36151
- init_bun();
36152
- import { closeSync as closeSync2, lstatSync as lstatSync2, openSync as openSync2, readlinkSync as readlinkSync2, realpathSync as realpathSync3, statSync, unlinkSync as unlinkSync2 } from "fs";
36153
- import { basename as basename3, dirname as dirname6, resolve as resolve7 } from "path";
36154
-
36155
- // src/lib/evaluation/input.ts
36156
- init_dist2();
36157
- function readJudgeInputs(artifactRaw, rubricRaw) {
36158
- const artifact = validateAnalysisArtifact(artifactRaw);
36159
- const rubric = parseJudgeRubric(rubricRaw);
36160
- const issues = [];
36161
- if (!artifact.success) {
36162
- issues.push(...artifact.errors.map((error) => `artifact ${error.path} ${error.expected}`));
36163
- }
36164
- if (!rubric.ok)
36165
- issues.push(...rubric.issues);
36166
- if (!artifact.success || !rubric.ok)
36167
- return { ok: false, issues };
36168
- return { ok: true, artifact: artifact.data, rubric: rubric.rubric };
36169
- }
36170
-
36171
- // src/lib/evaluation/rules.ts
36172
- init_dist2();
36173
- function evaluateRules(input) {
36174
- const judged = judgeArtifact(input.artifact, input.rubric);
36175
- if (!judged.ok) {
36176
- throw new Error(judged.issues.join("; "));
36177
- }
36178
- return judged.report;
36179
- }
36180
-
36181
- // src/commands/judge.ts
36182
- function canonical(path, depth = 0) {
36183
- const absolute = resolve7(path);
36184
- try {
36185
- return realpathSync3(absolute);
36186
- } catch {
36187
- try {
36188
- if (depth < 32 && lstatSync2(absolute).isSymbolicLink()) {
36189
- return canonical(resolve7(dirname6(absolute), readlinkSync2(absolute)), depth + 1);
36190
- }
36191
- } catch {}
36192
- const parent = dirname6(absolute);
36193
- return parent === absolute ? absolute : resolve7(canonical(parent, depth), basename3(absolute));
36194
- }
36195
- }
36196
- function existingDirectory(path) {
36197
- let directory = dirname6(path);
36198
- for (;; ) {
36199
- try {
36200
- const stat = statSync(directory);
36201
- return stat.isDirectory() ? { path: directory, dev: stat.dev, ino: stat.ino } : null;
36202
- } catch {
36203
- const parent = dirname6(directory);
36204
- if (parent === directory)
36205
- return null;
36206
- directory = parent;
36207
- }
36208
- }
36209
- }
36210
- function ignoresCase(directory) {
36211
- const name = `.idlekit-case-${crypto.randomUUID()}`;
36212
- const path = resolve7(directory, name);
36213
- let fd;
36214
- try {
36215
- fd = openSync2(path, "wx");
36216
- const original = statSync(path);
36217
- const folded = statSync(resolve7(directory, name.toUpperCase()));
36218
- return original.dev === folded.dev && original.ino === folded.ino;
36219
- } catch {
36220
- return fd === undefined && process.platform === "win32";
36221
- } finally {
36222
- if (fd !== undefined) {
36223
- try {
36224
- closeSync2(fd);
36225
- } finally {
36226
- unlinkSync2(path);
36227
- }
36228
- }
36229
- }
36230
- }
36231
- function sameFile(left, right) {
36232
- const aPath = canonical(left);
36233
- const bPath = canonical(right);
36234
- if (aPath === bPath)
36235
- return true;
36236
- try {
36237
- const a = statSync(left);
36238
- const b = statSync(right);
36239
- return a.dev === b.dev && a.ino === b.ino;
36240
- } catch {
36241
- if (aPath.toLowerCase() !== bPath.toLowerCase())
36242
- return false;
36243
- const a = existingDirectory(aPath);
36244
- const b = existingDirectory(bPath);
36245
- return a !== null && b !== null && a.dev === b.dev && a.ino === b.ino && ignoresCase(a.path);
36246
- }
36247
- }
36248
- function ancestorFile(parent, child) {
36249
- let directory = dirname6(canonical(child));
36250
- for (;; ) {
36251
- if (sameFile(parent, directory))
36252
- return true;
36253
- const next = dirname6(directory);
36254
- if (next === directory)
36255
- return false;
36256
- directory = next;
36257
- }
36258
- }
36259
- function requireFileTarget(path, flag) {
36260
- const target = canonical(path);
36261
- let current = target;
36262
- for (;; ) {
36263
- let stat;
36264
- try {
36265
- stat = statSync(current);
36266
- } catch (error) {
36267
- if (error.code !== "ENOENT") {
36268
- throw usageError(`--${flag} requires a regular file target with directory parents.`);
36269
- }
36270
- }
36271
- if (stat) {
36272
- const valid = current === target ? stat.isFile() : stat.isDirectory();
36273
- if (!valid)
36274
- throw usageError(`--${flag} requires a regular file target with directory parents.`);
36275
- return;
36276
- }
36277
- const parent = dirname6(current);
36278
- if (parent === current)
36279
- throw usageError(`--${flag} has no usable parent directory.`);
36280
- current = parent;
36281
- }
36282
- }
36283
- function renderJudgeMarkdown(report) {
36284
- const lines = [
36285
- "# Rules judge",
36286
- "",
36287
- "This is not doctor. The numerical verdict is not rewritten.",
36288
- "",
36289
- `- Provider: ${report.provider}`,
36290
- `- Rubric: ${report.rubricVersion}`,
36291
- `- Digest: ${report.advisoryDigest}`,
36292
- "",
36293
- "## Numerical",
36294
- ""
36295
- ];
36296
- for (const result of report.numerical.results) {
36297
- lines.push(`- ${result.metricId}: ${result.verdict} ${result.evidence.value ?? "none"} ${result.evidence.unit}`);
36298
- }
36299
- lines.push("", "## Estimates", "");
36300
- if (report.estimatedObservations.length === 0)
36301
- lines.push("None.");
36302
- for (const estimate of report.estimatedObservations) {
36303
- lines.push(`- ${estimate.metricId}: ${estimate.value ?? "none"}`);
36304
- }
36305
- lines.push("", "## Semantic", "");
36306
- for (const diagnostic of report.diagnostics) {
36307
- lines.push(`- ${diagnostic.status} ${diagnostic.recommendationId}: ${diagnostic.text.en}`);
36308
- }
36309
- return lines.join(`
36310
- `);
36311
- }
36312
- function judgeCsvRows(report) {
36313
- return report.numerical.results.map((result, index) => {
36314
- const diagnostic = report.diagnostics[index];
36315
- return {
36316
- id: result.id,
36317
- metricId: result.metricId,
36318
- verdict: result.verdict,
36319
- severity: result.severity,
36320
- evidenceStatus: result.evidence.status,
36321
- value: result.evidence.value,
36322
- unit: result.evidence.unit,
36323
- operator: result.evidence.operator,
36324
- threshold: result.evidence.threshold,
36325
- upperThreshold: result.evidence.upperThreshold,
36326
- diagnosticStatus: diagnostic?.status ?? null,
36327
- recommendationId: diagnostic?.recommendationId ?? null
36328
- };
36329
- });
36330
- }
36331
- async function runJudge(flags, analysisPath, hooks) {
36332
- const requestedModel = (flags.model ?? "jev-latest").trim();
36333
- if (flags.provider === "jev" && requestedModel.length === 0) {
36334
- throw usageError("--model requires a nonempty model name.");
36335
- }
36336
- if (flags.provider === "jev" && flags.format === "csv") {
36337
- throw usageError("--provider jev writes json or md. csv cannot hold the rules report and the advisory together.");
36338
- }
36339
- if (flags.provider === "jev" && flags.cache) {
36340
- if (flags.out && sameFile(flags.cache, flags.out)) {
36341
- throw usageError("--cache and --out name the same file. Give the Jev cache its own path.");
36342
- }
36343
- if (flags.out && (ancestorFile(flags.cache, flags.out) || ancestorFile(flags.out, flags.cache))) {
36344
- throw usageError("--cache and --out cannot be ancestor paths. Give each file a separate target.");
36741
+ } catch {
36742
+ strategyUnchecked = true;
36743
+ }
36345
36744
  }
36346
- if (sameFile(flags.cache, analysisPath) || sameFile(flags.cache, flags.rubric)) {
36347
- throw usageError("--cache names an input file. Give the Jev cache its own path.");
36745
+ if (strategyUnchecked && strategyId !== undefined) {
36746
+ const report = buildScenarioInspect({
36747
+ ...base,
36748
+ decimals: strategyDecimals(flags.strategy?.trim() ? [] : [scenario.strategy?.params]),
36749
+ engine: {
36750
+ effectiveId: engine.effectiveId,
36751
+ source: engine.source,
36752
+ accepted: true,
36753
+ ...engine.scenarioEngine ? { scenarioEngine: engine.scenarioEngine } : {}
36754
+ },
36755
+ stages: [],
36756
+ planner: null,
36757
+ plugins: { ...base.plugins, unresolvedStrategy: strategyId }
36758
+ });
36759
+ await emit2(flags, report, scenarioPath, scenario, pluginDigest);
36760
+ return;
36348
36761
  }
36349
- }
36350
- if (flags.provider === "jev") {
36351
- if (flags.cache)
36352
- requireFileTarget(flags.cache, "cache");
36353
- if (flags.out)
36354
- requireFileTarget(flags.out, "out");
36355
- }
36356
- let artifactRaw;
36357
- let rubricRaw;
36358
- try {
36359
- artifactRaw = await readJsonFile(analysisPath);
36360
- rubricRaw = await readJsonFile(flags.rubric);
36361
- } catch (error) {
36362
- throw usageError(`Invalid judge input: ${error instanceof Error ? error.message : String(error)}`);
36363
- }
36364
- const parsed = readJudgeInputs(artifactRaw, rubricRaw);
36365
- if (!parsed.ok) {
36366
- throw usageError(`Invalid judge input: ${parsed.issues.join("; ")}`);
36367
- }
36368
- let report;
36369
- try {
36370
- report = evaluateRules(parsed);
36371
- } catch (error) {
36372
- const message = error instanceof Error ? error.message : String(error);
36373
- throw usageError(`Invalid judge input: ${message}`);
36374
- }
36375
- if (flags.provider === "rules") {
36376
- await writeOutput({
36377
- format: flags.format,
36378
- outPath: flags.out,
36379
- data: flags.format === "md" ? renderJudgeMarkdown(report) : flags.format === "csv" ? judgeCsvRows(report) : report
36762
+ const prepared = planResolvedRun(planArgs);
36763
+ const simulate = prepared.planFor("simulate");
36764
+ const experience = prepared.planFor("experience");
36765
+ const ltv = prepared.planFor("ltv");
36766
+ const plans = [simulate, experience, ltv];
36767
+ const report = buildScenarioInspect({
36768
+ ...base,
36769
+ decimals: strategyDecimals(plans.map((plan) => plan.strategy.params)),
36770
+ engine: {
36771
+ effectiveId: prepared.engine.effectiveId,
36772
+ source: prepared.engine.source,
36773
+ accepted: true,
36774
+ ...prepared.engine.scenarioEngine ? { scenarioEngine: prepared.engine.scenarioEngine } : {}
36775
+ },
36776
+ stages: plans.map((plan) => stageInput(plan, scenario)),
36777
+ planner: skipping ? null : plannerParams(simulate, pluginStrategyIds)
36380
36778
  });
36381
- return report;
36382
- }
36383
- await Promise.resolve().then(() => init_jev());
36384
- const key = (hooks?.env ?? process.env).TYPESAFE_API_KEY?.trim() ?? "";
36385
- if (!isSafeJevReport(report, key)) {
36386
- throw usageError("Unsafe Jev numerical report metadata. Remove secrets and local paths from the judge inputs.");
36779
+ await emit2(flags, report, scenarioPath, scenario, pluginDigest);
36387
36780
  }
36388
- const advisory = await adviseWithJev({
36389
- artifact: parsed.artifact,
36390
- rubric: parsed.rubric,
36391
- numerical: report,
36392
- requestedModel,
36393
- dryRun: flags["dry-run"] === true,
36394
- cachePath: flags.cache,
36395
- transport: hooks?.transport,
36396
- env: hooks?.env,
36397
- nowMs: hooks?.nowMs,
36398
- sleep: hooks?.sleep
36399
- });
36400
- const bundle = { numericalReport: report, advisory };
36781
+ });
36782
+ async function emit2(flags, report, scenarioPath, scenario, pluginDigest) {
36401
36783
  if (flags.format === "md") {
36402
- const markdown = [
36403
- renderJudgeMarkdown(report),
36404
- `- Jev: ${advisory.status} ${advisory.reason}`,
36405
- `- Endpoint: ${advisory.endpoint}`,
36406
- `- Planned requests: ${advisory.plannedRequests}`,
36407
- "",
36408
- "## Jev answers",
36409
- "",
36410
- ...advisory.answers.map((answer) => {
36411
- const value = answer.noul ?? answer.choice ?? answer.score ?? "none";
36412
- const confidence = answer.confidence === null ? "" : `; confidence=${answer.confidence}`;
36413
- return `- ${answer.id}: ${answer.type}=${value}${confidence}; ${answer.disposition}`;
36414
- }),
36415
- ...advisory.answers.length === 0 ? ["None."] : []
36416
- ].join(`
36417
- `);
36418
- if (flags.out) {
36419
- const outPath = resolve7(flags.out);
36420
- await writeTextFile(outPath, `${markdown}
36421
- `);
36422
- console.log(`Wrote md output to ${outPath}`);
36423
- } else {
36424
- await writeStdout(`${markdown}
36425
- `);
36784
+ const body = renderInspectMarkdown(report);
36785
+ if (!flags.out) {
36786
+ console.log(body);
36787
+ return;
36426
36788
  }
36427
- return bundle;
36428
36789
  }
36429
36790
  await writeOutput({
36430
- format: flags.format,
36791
+ format: flags.format === "md" ? "md" : "json",
36431
36792
  outPath: flags.out,
36432
- data: bundle,
36433
- meta: buildOutputMeta({
36434
- command: "judge",
36435
- pluginDigest: {},
36436
- schemaRef: outputSchemaRefForCommand("judge.jev")
36437
- })
36793
+ data: flags.format === "md" ? renderInspectMarkdown(report) : report,
36794
+ meta: flags.format === "json" ? buildOutputMeta({ command: "inspect", scenarioPath, scenario, pluginDigest }) : undefined
36438
36795
  });
36439
- return bundle;
36440
36796
  }
36441
- var judge_default = defineCommand({
36442
- name: "judge",
36443
- description: "Advise on an analysis artifact with a versioned rules rubric",
36444
- options: {
36445
- rubric: option(string2(), { description: "Versioned rubric JSON path" }),
36446
- provider: option(_enum(["rules", "jev"]).default("rules"), {
36447
- description: "Advisory provider. rules is local. jev is an opt-in network provider."
36448
- }),
36449
- "dry-run": option(exports_coerce.boolean().default(false), {
36450
- description: "Show the Jev request plan without calling the network"
36451
- }),
36452
- model: option(string2().trim().min(1).default("jev-latest"), {
36453
- description: "Requested Jev model. jev-latest is an alias, not a pinned id."
36454
- }),
36455
- cache: option(string2().optional(), { description: "Optional Jev advisory cache path" }),
36456
- out: option(string2().optional(), { description: "Output path" }),
36457
- format: option(_enum(["json", "md", "csv"]).default("json"), { description: "Output format" })
36458
- },
36459
- async handler({ flags, positional }) {
36460
- const analysisPath = positional[0];
36461
- if (!analysisPath) {
36462
- throw usageError("Usage: idk judge <analysis.json> --rubric <rubric.json> --provider rules");
36463
- }
36464
- await runJudge(flags, analysisPath);
36465
- }
36466
- });
36467
36797
 
36468
- // src/commands/calibrate.ts
36798
+ // src/commands/judge.ts
36469
36799
  init_zod();
36470
36800
  init_errors3();
36471
36801
  init_outputMeta();
36472
- import { extname as extname3, resolve as resolve8 } from "path";
36802
+ init_bun();
36803
+ import { closeSync as closeSync2, lstatSync as lstatSync2, openSync as openSync2, readlinkSync as readlinkSync2, realpathSync as realpathSync3, statSync, unlinkSync as unlinkSync2 } from "fs";
36804
+ import { basename as basename3, dirname as dirname6, resolve as resolve7 } from "path";
36473
36805
 
36474
- // src/lib/ltvCalibration.ts
36475
- function assertFiniteNumbers(value, path) {
36476
- if (typeof value === "number" && !Number.isFinite(value)) {
36477
- throw new RangeError(`Calibration requires finite numbers; numeric overflow at ${path}`);
36478
- }
36479
- if (typeof value === "object" && value !== null) {
36480
- for (const [key, child] of Object.entries(value))
36481
- assertFiniteNumbers(child, `${path}.${key}`);
36482
- }
36483
- }
36484
- function clamp012(v) {
36485
- if (v < 0)
36486
- return 0;
36487
- if (v > 1)
36488
- return 1;
36489
- return v;
36490
- }
36491
- function clampCorrelation(v) {
36492
- if (!Number.isFinite(v))
36493
- return 0;
36494
- if (v < -1)
36495
- return -1;
36496
- if (v > 1)
36497
- return 1;
36498
- return v;
36499
- }
36500
- function mean(values) {
36501
- if (values.length === 0)
36502
- return 0;
36503
- return values.reduce((a, b) => a + b, 0) / values.length;
36504
- }
36505
- function variance(values) {
36506
- if (values.length === 0)
36507
- return 0;
36508
- const m = mean(values);
36509
- let acc = 0;
36510
- for (const v of values) {
36511
- const d = v - m;
36512
- acc += d * d;
36806
+ // src/lib/evaluation/input.ts
36807
+ init_dist2();
36808
+ function readJudgeInputs(artifactRaw, rubricRaw) {
36809
+ const artifact = validateAnalysisArtifact(artifactRaw);
36810
+ const rubric = parseJudgeRubric(rubricRaw);
36811
+ const issues = [];
36812
+ if (!artifact.success) {
36813
+ issues.push(...artifact.errors.map((error) => `artifact ${error.path} ${error.expected}`));
36513
36814
  }
36514
- return acc / values.length;
36815
+ if (!rubric.ok)
36816
+ issues.push(...rubric.issues);
36817
+ if (!artifact.success || !rubric.ok)
36818
+ return { ok: false, issues };
36819
+ return { ok: true, artifact: artifact.data, rubric: rubric.rubric };
36515
36820
  }
36516
- function estimateCorrelation(x, y) {
36517
- const n = Math.min(x.length, y.length);
36518
- if (n < 3) {
36519
- return {
36520
- raw: 0,
36521
- value: 0,
36522
- confidence: 0,
36523
- shrinkage: 0,
36524
- sampleSize: n,
36525
- varianceX: 0,
36526
- varianceY: 0
36527
- };
36528
- }
36529
- const sx = x.slice(0, n);
36530
- const sy = y.slice(0, n);
36531
- const mx = mean(sx);
36532
- const my = mean(sy);
36533
- let cov = 0;
36534
- let vx = 0;
36535
- let vy = 0;
36536
- for (let i = 0;i < n; i++) {
36537
- const dx = (sx[i] ?? 0) - mx;
36538
- const dy = (sy[i] ?? 0) - my;
36539
- cov += dx * dy;
36540
- vx += dx * dx;
36541
- vy += dy * dy;
36542
- }
36543
- const varianceX = vx / n;
36544
- const varianceY = vy / n;
36545
- if (vx <= 0.000000000001 || vy <= 0.000000000001) {
36546
- return {
36547
- raw: 0,
36548
- value: 0,
36549
- confidence: 0,
36550
- shrinkage: 0,
36551
- sampleSize: n,
36552
- varianceX,
36553
- varianceY
36554
- };
36821
+
36822
+ // src/lib/evaluation/rules.ts
36823
+ init_dist2();
36824
+ function evaluateRules(input) {
36825
+ const judged = judgeArtifact(input.artifact, input.rubric);
36826
+ if (!judged.ok) {
36827
+ throw new Error(judged.issues.join("; "));
36555
36828
  }
36556
- const raw = clampCorrelation(cov / Math.sqrt(vx) / Math.sqrt(vy));
36557
- const ci95 = 1.96 / Math.sqrt(Math.max(1, n - 3));
36558
- const sampleConfidence = clamp012(1 - ci95);
36559
- const varianceConfidenceX = varianceX / (varianceX + 0.01);
36560
- const varianceConfidenceY = varianceY / (varianceY + 0.01);
36561
- const varianceConfidence = Math.sqrt(varianceConfidenceX * varianceConfidenceY);
36562
- const confidence = clamp012(sampleConfidence * varianceConfidence);
36563
- const shrinkage = confidence;
36564
- const value = raw * shrinkage;
36565
- return {
36566
- raw,
36567
- value,
36568
- confidence,
36569
- shrinkage,
36570
- sampleSize: n,
36571
- varianceX,
36572
- varianceY
36573
- };
36829
+ return judged.report;
36574
36830
  }
36575
- function calibrateMonetization(rows) {
36576
- if (rows.length === 0) {
36577
- throw new Error("calibration rows cannot be empty");
36831
+
36832
+ // src/commands/judge.ts
36833
+ function canonical(path, depth = 0) {
36834
+ const absolute = resolve7(path);
36835
+ try {
36836
+ return realpathSync3(absolute);
36837
+ } catch {
36838
+ try {
36839
+ if (depth < 32 && lstatSync2(absolute).isSymbolicLink()) {
36840
+ return canonical(resolve7(dirname6(absolute), readlinkSync2(absolute)), depth + 1);
36841
+ }
36842
+ } catch {}
36843
+ const parent = dirname6(absolute);
36844
+ return parent === absolute ? absolute : resolve7(canonical(parent, depth), basename3(absolute));
36578
36845
  }
36579
- const users = new Map;
36580
- for (const row of rows) {
36581
- const day = Math.max(0, Math.floor(row.day));
36582
- const u = users.get(row.userId) ?? { days: new Set, iap: 0, ad: 0, acq: undefined };
36583
- if (row.active ?? true)
36584
- u.days.add(day);
36585
- u.iap += Math.max(0, row.iapRevenue);
36586
- u.ad += Math.max(0, row.adRevenue);
36587
- if (row.acquisitionCost !== undefined && Number.isFinite(row.acquisitionCost)) {
36588
- u.acq = (u.acq ?? 0) + Math.max(0, row.acquisitionCost);
36846
+ }
36847
+ function existingDirectory(path) {
36848
+ let directory = dirname6(path);
36849
+ for (;; ) {
36850
+ try {
36851
+ const stat = statSync(directory);
36852
+ return stat.isDirectory() ? { path: directory, dev: stat.dev, ino: stat.ino } : null;
36853
+ } catch {
36854
+ const parent = dirname6(directory);
36855
+ if (parent === directory)
36856
+ return null;
36857
+ directory = parent;
36589
36858
  }
36590
- users.set(row.userId, u);
36591
36859
  }
36592
- const cohortSize = Math.max(1, users.size);
36593
- const dayActiveCount = (targetDay) => {
36594
- let count = 0;
36595
- for (const user of users.values())
36596
- if (user.days.has(targetDay))
36597
- count += 1;
36598
- return count;
36599
- };
36600
- const d1 = dayActiveCount(1) / cohortSize;
36601
- const d7 = dayActiveCount(7) / cohortSize;
36602
- const d30 = dayActiveCount(30) / cohortSize;
36603
- const d90 = dayActiveCount(90) / cohortSize;
36604
- let totalIap = 0;
36605
- let totalAd = 0;
36606
- let payerUsers = 0;
36607
- let activeUserDays = 0;
36608
- let payerDays = 0;
36609
- let totalAcq = 0;
36610
- let acqUsers = 0;
36611
- for (const user of users.values()) {
36612
- totalIap += user.iap;
36613
- totalAd += user.ad;
36614
- if (user.iap > 0)
36615
- payerUsers += 1;
36616
- activeUserDays += user.days.size;
36617
- for (const day of user.days) {
36618
- if (day >= 0 && user.iap > 0)
36619
- payerDays += 1;
36620
- }
36621
- if (user.acq !== undefined) {
36622
- totalAcq += user.acq;
36623
- acqUsers += 1;
36860
+ }
36861
+ function ignoresCase(directory) {
36862
+ const name = `.idlekit-case-${crypto.randomUUID()}`;
36863
+ const path = resolve7(directory, name);
36864
+ let fd;
36865
+ try {
36866
+ fd = openSync2(path, "wx");
36867
+ const original = statSync(path);
36868
+ const folded = statSync(resolve7(directory, name.toUpperCase()));
36869
+ return original.dev === folded.dev && original.ino === folded.ino;
36870
+ } catch {
36871
+ return fd === undefined && process.platform === "win32";
36872
+ } finally {
36873
+ if (fd !== undefined) {
36874
+ try {
36875
+ closeSync2(fd);
36876
+ } finally {
36877
+ unlinkSync2(path);
36878
+ }
36624
36879
  }
36625
36880
  }
36626
- const payerConversion = payerUsers / cohortSize;
36627
- const arppuDaily = totalIap / Math.max(1, payerDays);
36628
- const adArpDau = totalAd / Math.max(1, activeUserDays);
36629
- const cpi = totalAcq / Math.max(1, acqUsers);
36630
- const userRetention = [];
36631
- const userConversion = [];
36632
- const userArppu = [];
36633
- const userAdArpDau = [];
36634
- for (const user of users.values()) {
36635
- const activeDays = Math.max(1, user.days.size);
36636
- const retentionScore = clamp012(activeDays / 30);
36637
- const conversionScore = user.iap > 0 ? 1 : 0;
36638
- const arppuScore = user.iap > 0 ? user.iap / activeDays : 0;
36639
- const adScore = user.ad / activeDays;
36640
- userRetention.push(retentionScore);
36641
- userConversion.push(conversionScore);
36642
- userArppu.push(arppuScore);
36643
- userAdArpDau.push(adScore);
36881
+ }
36882
+ function sameFile(left, right) {
36883
+ const aPath = canonical(left);
36884
+ const bPath = canonical(right);
36885
+ if (aPath === bPath)
36886
+ return true;
36887
+ try {
36888
+ const a = statSync(left);
36889
+ const b = statSync(right);
36890
+ return a.dev === b.dev && a.ino === b.ino;
36891
+ } catch {
36892
+ if (aPath.toLowerCase() !== bPath.toLowerCase())
36893
+ return false;
36894
+ const a = existingDirectory(aPath);
36895
+ const b = existingDirectory(bPath);
36896
+ return a !== null && b !== null && a.dev === b.dev && a.ino === b.ino && ignoresCase(a.path);
36644
36897
  }
36645
- const estimatedCorrelationPair = {
36646
- retentionConversion: estimateCorrelation(userRetention, userConversion),
36647
- retentionArppu: estimateCorrelation(userRetention, userArppu),
36648
- retentionAd: estimateCorrelation(userRetention, userAdArpDau),
36649
- conversionArppu: estimateCorrelation(userConversion, userArppu),
36650
- conversionAd: estimateCorrelation(userConversion, userAdArpDau),
36651
- arppuAd: estimateCorrelation(userArppu, userAdArpDau)
36652
- };
36653
- const estimatedCorrelation = {
36654
- retentionConversion: estimatedCorrelationPair.retentionConversion.value,
36655
- retentionArppu: estimatedCorrelationPair.retentionArppu.value,
36656
- retentionAd: estimatedCorrelationPair.retentionAd.value,
36657
- conversionArppu: estimatedCorrelationPair.conversionArppu.value,
36658
- conversionAd: estimatedCorrelationPair.conversionAd.value,
36659
- arppuAd: estimatedCorrelationPair.arppuAd.value
36660
- };
36661
- const estimatedCorrelationRaw = {
36662
- retentionConversion: estimatedCorrelationPair.retentionConversion.raw,
36663
- retentionArppu: estimatedCorrelationPair.retentionArppu.raw,
36664
- retentionAd: estimatedCorrelationPair.retentionAd.raw,
36665
- conversionArppu: estimatedCorrelationPair.conversionArppu.raw,
36666
- conversionAd: estimatedCorrelationPair.conversionAd.raw,
36667
- arppuAd: estimatedCorrelationPair.arppuAd.raw
36668
- };
36669
- const correlationConfidence = {
36670
- retentionConversion: estimatedCorrelationPair.retentionConversion.confidence,
36671
- retentionArppu: estimatedCorrelationPair.retentionArppu.confidence,
36672
- retentionAd: estimatedCorrelationPair.retentionAd.confidence,
36673
- conversionArppu: estimatedCorrelationPair.conversionArppu.confidence,
36674
- conversionAd: estimatedCorrelationPair.conversionAd.confidence,
36675
- arppuAd: estimatedCorrelationPair.arppuAd.confidence
36676
- };
36677
- const result = {
36678
- monetization: {
36679
- cohorts: {
36680
- baseUsers: cohortSize
36681
- },
36682
- retention: {
36683
- d1: clamp012(d1),
36684
- d7: clamp012(Math.min(d1, d7)),
36685
- d30: clamp012(Math.min(d1, d7, d30)),
36686
- d90: clamp012(Math.min(d1, d7, d30, d90)),
36687
- longTailDailyDecay: 0.02
36688
- },
36689
- revenue: {
36690
- payerConversion: clamp012(payerConversion),
36691
- arppuDaily: Math.max(0, arppuDaily),
36692
- adArpDau: Math.max(0, adArpDau),
36693
- platformFeeRate: 0.3,
36694
- grossMarginRate: 0.92
36695
- },
36696
- acquisition: {
36697
- cpi: Math.max(0, cpi)
36698
- },
36699
- uncertainty: {
36700
- enabled: true,
36701
- draws: 300,
36702
- quantiles: [0.5, 0.9],
36703
- sigma: {
36704
- retention: 0.08,
36705
- conversion: 0.12,
36706
- arppu: 0.2,
36707
- ad: 0.15
36708
- },
36709
- correlation: {
36710
- retentionConversion: estimatedCorrelation.retentionConversion,
36711
- retentionArppu: estimatedCorrelation.retentionArppu,
36712
- retentionAd: estimatedCorrelation.retentionAd,
36713
- conversionArppu: estimatedCorrelation.conversionArppu,
36714
- conversionAd: estimatedCorrelation.conversionAd,
36715
- arppuAd: estimatedCorrelation.arppuAd
36716
- }
36717
- }
36718
- },
36719
- diagnostics: {
36720
- users: cohortSize,
36721
- totalRows: rows.length,
36722
- totalIap,
36723
- totalAd,
36724
- activeUserDays,
36725
- payerUsers,
36726
- payerDays,
36727
- retention: {
36728
- d1,
36729
- d7,
36730
- d30,
36731
- d90
36732
- },
36733
- userFeatureVariance: {
36734
- retention: variance(userRetention),
36735
- conversion: variance(userConversion),
36736
- arppu: variance(userArppu),
36737
- adArpDau: variance(userAdArpDau)
36738
- },
36739
- estimatedCorrelation,
36740
- estimatedCorrelationRaw,
36741
- correlationConfidence,
36742
- correlationDiagnostics: estimatedCorrelationPair
36743
- }
36744
- };
36745
- assertFiniteNumbers(result, "calibration");
36746
- return result;
36747
36898
  }
36748
- // src/lib/ltvProjection.ts
36749
- function clamp013(v) {
36750
- if (v < 0)
36751
- return 0;
36752
- if (v > 1)
36753
- return 1;
36754
- return v;
36899
+ function ancestorFile(parent, child) {
36900
+ let directory = dirname6(canonical(child));
36901
+ for (;; ) {
36902
+ if (sameFile(parent, directory))
36903
+ return true;
36904
+ const next = dirname6(directory);
36905
+ if (next === directory)
36906
+ return false;
36907
+ directory = next;
36908
+ }
36755
36909
  }
36756
- function deriveMonetizationConfig(scenario) {
36757
- const m = scenario.monetization;
36758
- const q = (m?.uncertainty?.quantiles ?? [0.5, 0.9]).filter((x) => typeof x === "number");
36759
- const quantiles = q.length > 0 ? [...new Set(q)].sort((a, b) => a - b) : [0.5, 0.9];
36760
- return {
36761
- cohorts: {
36762
- baseUsers: m?.cohorts?.baseUsers ?? 1
36763
- },
36764
- retention: {
36765
- d1: m?.retention?.d1 ?? 0.42,
36766
- d7: m?.retention?.d7 ?? 0.2,
36767
- d30: m?.retention?.d30 ?? 0.09,
36768
- d90: m?.retention?.d90 ?? 0.04,
36769
- longTailDailyDecay: m?.retention?.longTailDailyDecay ?? 0.02
36770
- },
36771
- revenue: {
36772
- payerConversion: m?.revenue?.payerConversion ?? 0.03,
36773
- arppuDaily: m?.revenue?.arppuDaily ?? 0.7,
36774
- adArpDau: m?.revenue?.adArpDau ?? 0.02,
36775
- platformFeeRate: m?.revenue?.platformFeeRate ?? 0.3,
36776
- grossMarginRate: m?.revenue?.grossMarginRate ?? 0.92,
36777
- progressionRevenueLift: m?.revenue?.progressionRevenueLift ?? 0.4,
36778
- progressionLogSpan: m?.revenue?.progressionLogSpan ?? 6
36779
- },
36780
- acquisition: {
36781
- cpi: m?.acquisition?.cpi ?? 0
36782
- },
36783
- uncertainty: {
36784
- enabled: m?.uncertainty?.enabled ?? false,
36785
- draws: m?.uncertainty?.draws ?? 300,
36786
- quantiles,
36787
- seed: m?.uncertainty?.seed,
36788
- sigma: {
36789
- retention: m?.uncertainty?.sigma?.retention ?? 0.08,
36790
- conversion: m?.uncertainty?.sigma?.conversion ?? 0.12,
36791
- arppu: m?.uncertainty?.sigma?.arppu ?? 0.2,
36792
- ad: m?.uncertainty?.sigma?.ad ?? 0.15
36793
- },
36794
- correlation: {
36795
- retentionConversion: m?.uncertainty?.correlation?.retentionConversion ?? 0,
36796
- retentionArppu: m?.uncertainty?.correlation?.retentionArppu ?? 0,
36797
- retentionAd: m?.uncertainty?.correlation?.retentionAd ?? 0,
36798
- conversionArppu: m?.uncertainty?.correlation?.conversionArppu ?? 0,
36799
- conversionAd: m?.uncertainty?.correlation?.conversionAd ?? 0,
36800
- arppuAd: m?.uncertainty?.correlation?.arppuAd ?? 0
36910
+ function requireFileTarget(path, flag) {
36911
+ const target = canonical(path);
36912
+ let current = target;
36913
+ for (;; ) {
36914
+ let stat;
36915
+ try {
36916
+ stat = statSync(current);
36917
+ } catch (error) {
36918
+ if (error.code !== "ENOENT") {
36919
+ throw usageError(`--${flag} requires a regular file target with directory parents.`);
36801
36920
  }
36802
36921
  }
36803
- };
36922
+ if (stat) {
36923
+ const valid = current === target ? stat.isFile() : stat.isDirectory();
36924
+ if (!valid)
36925
+ throw usageError(`--${flag} requires a regular file target with directory parents.`);
36926
+ return;
36927
+ }
36928
+ const parent = dirname6(current);
36929
+ if (parent === current)
36930
+ throw usageError(`--${flag} has no usable parent directory.`);
36931
+ current = parent;
36932
+ }
36804
36933
  }
36805
- function retentionAtDay(ret, day) {
36806
- if (day <= 0)
36807
- return 1;
36808
- if (day <= 1)
36809
- return ret.d1;
36810
- if (day <= 7)
36811
- return ret.d1 + (ret.d7 - ret.d1) * (day - 1) / 6;
36812
- if (day <= 30)
36813
- return ret.d7 + (ret.d30 - ret.d7) * (day - 7) / 23;
36814
- if (day <= 90)
36815
- return ret.d30 + (ret.d90 - ret.d30) * (day - 30) / 60;
36816
- const extra = day - 90;
36817
- return clamp013(ret.d90 * Math.exp(-ret.longTailDailyDecay * extra));
36934
+ function renderJudgeMarkdown(report) {
36935
+ const lines = [
36936
+ "# Rules judge",
36937
+ "",
36938
+ "This is not doctor. The numerical verdict is not rewritten.",
36939
+ "",
36940
+ `- Provider: ${report.provider}`,
36941
+ `- Rubric: ${report.rubricVersion}`,
36942
+ `- Digest: ${report.advisoryDigest}`,
36943
+ "",
36944
+ "## Numerical",
36945
+ ""
36946
+ ];
36947
+ for (const result of report.numerical.results) {
36948
+ lines.push(`- ${result.metricId}: ${result.verdict} ${result.evidence.value ?? "none"} ${result.evidence.unit}`);
36949
+ }
36950
+ lines.push("", "## Estimates", "");
36951
+ if (report.estimatedObservations.length === 0)
36952
+ lines.push("None.");
36953
+ for (const estimate of report.estimatedObservations) {
36954
+ lines.push(`- ${estimate.metricId}: ${estimate.value ?? "none"}`);
36955
+ }
36956
+ lines.push("", "## Semantic", "");
36957
+ for (const diagnostic of report.diagnostics) {
36958
+ lines.push(`- ${diagnostic.status} ${diagnostic.recommendationId}: ${diagnostic.text.en}`);
36959
+ }
36960
+ return lines.join(`
36961
+ `);
36818
36962
  }
36819
- function progressionFactor(startWorthLog10, endWorthLog10, span) {
36820
- if (!Number.isFinite(startWorthLog10) || !Number.isFinite(endWorthLog10) || !Number.isFinite(span) || span <= 0) {
36821
- return 0;
36963
+ function judgeCsvRows(report) {
36964
+ return report.numerical.results.map((result, index) => {
36965
+ const diagnostic = report.diagnostics[index];
36966
+ return {
36967
+ id: result.id,
36968
+ metricId: result.metricId,
36969
+ verdict: result.verdict,
36970
+ severity: result.severity,
36971
+ evidenceStatus: result.evidence.status,
36972
+ value: result.evidence.value,
36973
+ unit: result.evidence.unit,
36974
+ operator: result.evidence.operator,
36975
+ threshold: result.evidence.threshold,
36976
+ upperThreshold: result.evidence.upperThreshold,
36977
+ diagnosticStatus: diagnostic?.status ?? null,
36978
+ recommendationId: diagnostic?.recommendationId ?? null
36979
+ };
36980
+ });
36981
+ }
36982
+ async function runJudge(flags, analysisPath, hooks) {
36983
+ const requestedModel = (flags.model ?? "jev-latest").trim();
36984
+ if (flags.provider === "jev" && requestedModel.length === 0) {
36985
+ throw usageError("--model requires a nonempty model name.");
36986
+ }
36987
+ if (flags.provider === "jev" && flags.format === "csv") {
36988
+ throw usageError("--provider jev writes json or md. csv cannot hold the rules report and the advisory together.");
36989
+ }
36990
+ if (flags.provider === "jev" && flags.cache) {
36991
+ if (flags.out && sameFile(flags.cache, flags.out)) {
36992
+ throw usageError("--cache and --out name the same file. Give the Jev cache its own path.");
36993
+ }
36994
+ if (flags.out && (ancestorFile(flags.cache, flags.out) || ancestorFile(flags.out, flags.cache))) {
36995
+ throw usageError("--cache and --out cannot be ancestor paths. Give each file a separate target.");
36996
+ }
36997
+ if (sameFile(flags.cache, analysisPath) || sameFile(flags.cache, flags.rubric)) {
36998
+ throw usageError("--cache names an input file. Give the Jev cache its own path.");
36999
+ }
37000
+ }
37001
+ if (flags.provider === "jev") {
37002
+ if (flags.cache)
37003
+ requireFileTarget(flags.cache, "cache");
37004
+ if (flags.out)
37005
+ requireFileTarget(flags.out, "out");
37006
+ }
37007
+ let artifactRaw;
37008
+ let rubricRaw;
37009
+ try {
37010
+ artifactRaw = await readJsonFile(analysisPath);
37011
+ rubricRaw = await readJsonFile(flags.rubric);
37012
+ } catch (error) {
37013
+ throw usageError(`Invalid judge input: ${error instanceof Error ? error.message : String(error)}`);
37014
+ }
37015
+ const parsed = readJudgeInputs(artifactRaw, rubricRaw);
37016
+ if (!parsed.ok) {
37017
+ throw usageError(`Invalid judge input: ${parsed.issues.join("; ")}`);
37018
+ }
37019
+ let report;
37020
+ try {
37021
+ report = evaluateRules(parsed);
37022
+ } catch (error) {
37023
+ const message = error instanceof Error ? error.message : String(error);
37024
+ throw usageError(`Invalid judge input: ${message}`);
37025
+ }
37026
+ if (flags.provider === "rules") {
37027
+ await writeOutput({
37028
+ format: flags.format,
37029
+ outPath: flags.out,
37030
+ data: flags.format === "md" ? renderJudgeMarkdown(report) : flags.format === "csv" ? judgeCsvRows(report) : report
37031
+ });
37032
+ return report;
37033
+ }
37034
+ await Promise.resolve().then(() => init_jev());
37035
+ const key = (hooks?.env ?? process.env).TYPESAFE_API_KEY?.trim() ?? "";
37036
+ if (!isSafeJevReport(report, key)) {
37037
+ throw usageError("Unsafe Jev numerical report metadata. Remove secrets and local paths from the judge inputs.");
37038
+ }
37039
+ const advisory = await adviseWithJev({
37040
+ artifact: parsed.artifact,
37041
+ rubric: parsed.rubric,
37042
+ numerical: report,
37043
+ requestedModel,
37044
+ dryRun: flags["dry-run"] === true,
37045
+ cachePath: flags.cache,
37046
+ transport: hooks?.transport,
37047
+ env: hooks?.env,
37048
+ nowMs: hooks?.nowMs,
37049
+ sleep: hooks?.sleep
37050
+ });
37051
+ const bundle = { numericalReport: report, advisory };
37052
+ if (flags.format === "md") {
37053
+ const markdown = [
37054
+ renderJudgeMarkdown(report),
37055
+ `- Jev: ${advisory.status} ${advisory.reason}`,
37056
+ `- Endpoint: ${advisory.endpoint}`,
37057
+ `- Planned requests: ${advisory.plannedRequests}`,
37058
+ "",
37059
+ "## Jev answers",
37060
+ "",
37061
+ ...advisory.answers.map((answer) => {
37062
+ const value = answer.noul ?? answer.choice ?? answer.score ?? "none";
37063
+ const confidence = answer.confidence === null ? "" : `; confidence=${answer.confidence}`;
37064
+ return `- ${answer.id}: ${answer.type}=${value}${confidence}; ${answer.disposition}`;
37065
+ }),
37066
+ ...advisory.answers.length === 0 ? ["None."] : []
37067
+ ].join(`
37068
+ `);
37069
+ if (flags.out) {
37070
+ const outPath = resolve7(flags.out);
37071
+ await writeTextFile(outPath, `${markdown}
37072
+ `);
37073
+ console.log(`Wrote md output to ${outPath}`);
37074
+ } else {
37075
+ await writeStdout(`${markdown}
37076
+ `);
37077
+ }
37078
+ return bundle;
37079
+ }
37080
+ await writeOutput({
37081
+ format: flags.format,
37082
+ outPath: flags.out,
37083
+ data: bundle,
37084
+ meta: buildOutputMeta({
37085
+ command: "judge",
37086
+ pluginDigest: {},
37087
+ schemaRef: outputSchemaRefForCommand("judge.jev")
37088
+ })
37089
+ });
37090
+ return bundle;
37091
+ }
37092
+ var judge_default = defineCommand({
37093
+ name: "judge",
37094
+ description: "Advise on an analysis artifact with a versioned rules rubric",
37095
+ options: {
37096
+ rubric: option(string2(), { description: "Versioned rubric JSON path" }),
37097
+ provider: option(_enum(["rules", "jev"]).default("rules"), {
37098
+ description: "Advisory provider. rules is local. jev is an opt-in network provider."
37099
+ }),
37100
+ "dry-run": option(exports_coerce.boolean().default(false), {
37101
+ description: "Show the Jev request plan without calling the network"
37102
+ }),
37103
+ model: option(string2().trim().min(1).default("jev-latest"), {
37104
+ description: "Requested Jev model. jev-latest is an alias, not a pinned id."
37105
+ }),
37106
+ cache: option(string2().optional(), { description: "Optional Jev advisory cache path" }),
37107
+ out: option(string2().optional(), { description: "Output path" }),
37108
+ format: option(_enum(["json", "md", "csv"]).default("json"), { description: "Output format" })
37109
+ },
37110
+ async handler({ flags, positional }) {
37111
+ const analysisPath = positional[0];
37112
+ if (!analysisPath) {
37113
+ throw usageError("Usage: idk judge <analysis.json> --rubric <rubric.json> --provider rules");
37114
+ }
37115
+ await runJudge(flags, analysisPath);
36822
37116
  }
36823
- return clamp013((endWorthLog10 - startWorthLog10) / span);
36824
- }
36825
- function estimateLtvPerUser(args) {
36826
- const { config, horizonDays } = args;
36827
- const progression = clamp013(args.progression);
36828
- const arppuDaily = config.revenue.arppuDaily * (1 + progression * config.revenue.progressionRevenueLift);
36829
- const adArpDau = config.revenue.adArpDau * (1 + progression * config.revenue.progressionRevenueLift * 0.5);
36830
- const payerConversion = config.revenue.payerConversion;
36831
- const fullDays = Math.floor(Math.max(0, horizonDays));
36832
- const remainder = Math.max(0, horizonDays - fullDays);
36833
- let gross = 0;
36834
- for (let day = 1;day <= fullDays; day++) {
36835
- const active = retentionAtDay(config.retention, day);
36836
- gross += active * (payerConversion * arppuDaily + adArpDau);
37117
+ });
37118
+
37119
+ // src/commands/calibrate.ts
37120
+ init_zod();
37121
+ init_errors3();
37122
+ init_outputMeta();
37123
+ import { extname as extname3, resolve as resolve8 } from "path";
37124
+
37125
+ // src/lib/ltvCalibration.ts
37126
+ function assertFiniteNumbers(value, path) {
37127
+ if (typeof value === "number" && !Number.isFinite(value)) {
37128
+ throw new RangeError(`Calibration requires finite numbers; numeric overflow at ${path}`);
36837
37129
  }
36838
- if (remainder > 0) {
36839
- const active = retentionAtDay(config.retention, fullDays + 1);
36840
- gross += remainder * active * (payerConversion * arppuDaily + adArpDau);
37130
+ if (typeof value === "object" && value !== null) {
37131
+ for (const [key, child] of Object.entries(value))
37132
+ assertFiniteNumbers(child, `${path}.${key}`);
36841
37133
  }
36842
- const net = gross * (1 - config.revenue.platformFeeRate) * config.revenue.grossMarginRate;
36843
- const ltv = net - config.acquisition.cpi;
36844
- return {
36845
- cumulativeGrossRevenuePerUser: gross,
36846
- cumulativeNetRevenuePerUser: net,
36847
- cumulativeLtvPerUser: ltv
36848
- };
36849
37134
  }
36850
- // src/lib/ltvUncertainty.ts
36851
37135
  function clamp014(v) {
36852
37136
  if (v < 0)
36853
37137
  return 0;
@@ -36864,278 +37148,260 @@ function clampCorrelation2(v) {
36864
37148
  return 1;
36865
37149
  return v;
36866
37150
  }
36867
- function mulberry323(seed) {
36868
- let a = seed >>> 0;
36869
- return () => {
36870
- a |= 0;
36871
- a = a + 1831565813 | 0;
36872
- let t = Math.imul(a ^ a >>> 15, 1 | a);
36873
- t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
36874
- return ((t ^ t >>> 14) >>> 0) / 4294967296;
36875
- };
36876
- }
36877
- function randNormal(rng) {
36878
- const u1 = Math.max(0.000000000001, rng());
36879
- const u2 = Math.max(0.000000000001, rng());
36880
- return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
36881
- }
36882
- function buildCorrelationMatrix(base) {
36883
- const rc = clampCorrelation2(base.retentionConversion);
36884
- const ra = clampCorrelation2(base.retentionArppu);
36885
- const rd = clampCorrelation2(base.retentionAd);
36886
- const ca = clampCorrelation2(base.conversionArppu);
36887
- const cd = clampCorrelation2(base.conversionAd);
36888
- const ad = clampCorrelation2(base.arppuAd);
36889
- return [
36890
- [1, rc, ra, rd],
36891
- [rc, 1, ca, cd],
36892
- [ra, ca, 1, ad],
36893
- [rd, cd, ad, 1]
36894
- ];
36895
- }
36896
- function withOffDiagonalScale(matrix, alpha) {
36897
- return matrix.map((row, i) => row.map((v, j) => {
36898
- if (i === j)
36899
- return 1;
36900
- return v * alpha;
36901
- }));
36902
- }
36903
- function cholesky4(matrix) {
36904
- const n = 4;
36905
- const l = Array.from({ length: n }, () => Array.from({ length: n }, () => 0));
36906
- for (let i = 0;i < n; i++) {
36907
- for (let j = 0;j <= i; j++) {
36908
- let sum = 0;
36909
- for (let k = 0;k < j; k++) {
36910
- sum += (l[i]?.[k] ?? 0) * (l[j]?.[k] ?? 0);
36911
- }
36912
- if (i === j) {
36913
- const val = (matrix[i]?.[i] ?? 0) - sum;
36914
- if (val <= 0.000000001)
36915
- return null;
36916
- l[i][j] = Math.sqrt(val);
36917
- } else {
36918
- const denom = l[j]?.[j] ?? 0;
36919
- if (Math.abs(denom) < 0.000000001)
36920
- return null;
36921
- l[i][j] = ((matrix[i]?.[j] ?? 0) - sum) / denom;
36922
- }
36923
- }
36924
- }
36925
- return l;
37151
+ function mean(values) {
37152
+ if (values.length === 0)
37153
+ return 0;
37154
+ return values.reduce((a, b) => a + b, 0) / values.length;
36926
37155
  }
36927
- function multiplyLowerTriangular(l, z) {
36928
- const out = [0, 0, 0, 0];
36929
- for (let i = 0;i < 4; i++) {
36930
- let sum = 0;
36931
- for (let j = 0;j <= i; j++) {
36932
- sum += (l[i]?.[j] ?? 0) * (z[j] ?? 0);
36933
- }
36934
- out[i] = sum;
37156
+ function variance(values) {
37157
+ if (values.length === 0)
37158
+ return 0;
37159
+ const m = mean(values);
37160
+ let acc = 0;
37161
+ for (const v of values) {
37162
+ const d = v - m;
37163
+ acc += d * d;
36935
37164
  }
36936
- return out;
37165
+ return acc / values.length;
36937
37166
  }
36938
- function sampleCorrelatedNormals(rng, corr) {
36939
- const z = [randNormal(rng), randNormal(rng), randNormal(rng), randNormal(rng)];
36940
- const matrix = buildCorrelationMatrix(corr);
36941
- let l = cholesky4(matrix);
36942
- let alpha = 0.99;
36943
- while (!l && alpha > 0.000001) {
36944
- l = cholesky4(withOffDiagonalScale(matrix, alpha));
36945
- alpha *= 0.8;
36946
- }
36947
- if (!l) {
37167
+ function estimateCorrelation(x, y) {
37168
+ const n = Math.min(x.length, y.length);
37169
+ if (n < 3) {
36948
37170
  return {
36949
- retention: z[0] ?? 0,
36950
- conversion: z[1] ?? 0,
36951
- arppu: z[2] ?? 0,
36952
- ad: z[3] ?? 0
37171
+ raw: 0,
37172
+ value: 0,
37173
+ confidence: 0,
37174
+ shrinkage: 0,
37175
+ sampleSize: n,
37176
+ varianceX: 0,
37177
+ varianceY: 0
36953
37178
  };
36954
37179
  }
36955
- const c = multiplyLowerTriangular(l, z);
36956
- return {
36957
- retention: c[0] ?? 0,
36958
- conversion: c[1] ?? 0,
36959
- arppu: c[2] ?? 0,
36960
- ad: c[3] ?? 0
36961
- };
36962
- }
36963
- function applyLogNormalShock(mean, sigma, z) {
36964
- if (sigma <= 0)
36965
- return mean;
36966
- return mean * Math.exp(sigma * z - 0.5 * sigma * sigma);
36967
- }
36968
- function makeUncertainConfig(base, rng) {
36969
- const shock = sampleCorrelatedNormals(rng, base.uncertainty.correlation);
36970
- const d1 = clamp014(applyLogNormalShock(base.retention.d1, base.uncertainty.sigma.retention, shock.retention));
36971
- const d7 = Math.min(d1, clamp014(applyLogNormalShock(base.retention.d7, base.uncertainty.sigma.retention, shock.retention)));
36972
- const d30 = Math.min(d7, clamp014(applyLogNormalShock(base.retention.d30, base.uncertainty.sigma.retention, shock.retention)));
36973
- const d90 = Math.min(d30, clamp014(applyLogNormalShock(base.retention.d90, base.uncertainty.sigma.retention, shock.retention)));
36974
- return {
36975
- ...base,
36976
- retention: {
36977
- ...base.retention,
36978
- d1,
36979
- d7,
36980
- d30,
36981
- d90
36982
- },
36983
- revenue: {
36984
- ...base.revenue,
36985
- payerConversion: clamp014(applyLogNormalShock(base.revenue.payerConversion, base.uncertainty.sigma.conversion, shock.conversion)),
36986
- arppuDaily: Math.max(0, applyLogNormalShock(base.revenue.arppuDaily, base.uncertainty.sigma.arppu, shock.arppu)),
36987
- adArpDau: Math.max(0, applyLogNormalShock(base.revenue.adArpDau, base.uncertainty.sigma.ad, shock.ad))
36988
- }
36989
- };
36990
- }
36991
- function quantile2(values, q) {
36992
- if (values.length === 0)
36993
- return 0;
36994
- const sorted = [...values].sort((a, b) => a - b);
36995
- const idx = Math.max(0, Math.min(sorted.length - 1, Math.floor((sorted.length - 1) * q)));
36996
- return sorted[idx] ?? 0;
36997
- }
36998
- function estimateLtvDistribution(args) {
36999
- const rng = mulberry323(args.seed);
37000
- const grossSamples = [];
37001
- const netSamples = [];
37002
- const ltvSamples = [];
37003
- const draws = Math.max(1, args.draws);
37004
- for (let i = 0;i < draws; i++) {
37005
- const sampled = makeUncertainConfig(args.config, rng);
37006
- const point = estimateLtvPerUser({
37007
- config: sampled,
37008
- horizonDays: args.horizonDays,
37009
- progression: args.progression
37010
- });
37011
- grossSamples.push(point.cumulativeGrossRevenuePerUser);
37012
- netSamples.push(point.cumulativeNetRevenuePerUser);
37013
- ltvSamples.push(point.cumulativeLtvPerUser);
37014
- }
37015
- const avg = (arr) => arr.reduce((a, b) => a + b, 0) / Math.max(1, arr.length);
37016
- const byQuantile = {};
37017
- for (const q of args.quantiles) {
37018
- byQuantile[`q${Math.round(q * 100)}`] = quantile2(ltvSamples, q);
37019
- }
37020
- return {
37021
- mean: {
37022
- cumulativeGrossRevenuePerUser: avg(grossSamples),
37023
- cumulativeNetRevenuePerUser: avg(netSamples),
37024
- cumulativeLtvPerUser: avg(ltvSamples)
37025
- },
37026
- quantiles: byQuantile
37027
- };
37028
- }
37029
- // src/lib/cohortObservation.ts
37030
- var RETENTION_DAYS = [1, 7, 30, 90];
37031
- function observeCohort(rows, observedThroughDay) {
37032
- if (!Number.isSafeInteger(observedThroughDay) || observedThroughDay < 0)
37033
- throw new Error("observed-through-day must be a nonnegative safe integer");
37034
- if (rows.length === 0)
37035
- throw new Error("Observed cohort requires at least one install record");
37036
- const users = new Map;
37037
- for (const row of rows) {
37038
- if (!row.userId.trim() || !Number.isSafeInteger(row.day) || row.day < 0 || row.day > observedThroughDay) {
37039
- throw new Error("Observed cohort requires a user ID and integer days within observed-through-day");
37040
- }
37041
- if (![row.iapRevenue, row.adRevenue, ...row.acquisitionCost === undefined ? [] : [row.acquisitionCost]].every((v) => Number.isFinite(v) && v >= 0)) {
37042
- throw new Error("Observed cohort revenue and acquisition costs must be finite and nonnegative");
37043
- }
37044
- if (row.active !== undefined && typeof row.active !== "boolean")
37045
- throw new Error("Observed cohort active must be boolean");
37046
- const user = users.get(row.userId) ?? { days: new Set, active: new Set };
37047
- if (user.days.has(row.day))
37048
- throw new Error("Observed cohort requires one aggregated row per user and day");
37049
- user.days.add(row.day);
37050
- if (row.active ?? true)
37051
- user.active.add(row.day);
37052
- users.set(row.userId, user);
37180
+ const sx = x.slice(0, n);
37181
+ const sy = y.slice(0, n);
37182
+ const mx = mean(sx);
37183
+ const my = mean(sy);
37184
+ let cov = 0;
37185
+ let vx = 0;
37186
+ let vy = 0;
37187
+ for (let i = 0;i < n; i++) {
37188
+ const dx = (sx[i] ?? 0) - mx;
37189
+ const dy = (sy[i] ?? 0) - my;
37190
+ cov += dx * dy;
37191
+ vx += dx * dx;
37192
+ vy += dy * dy;
37053
37193
  }
37054
- for (const user of users.values()) {
37055
- if (!user.days.has(0))
37056
- throw new Error("Observed cohort requires a day 0 install record for every user, including inactive users");
37194
+ const varianceX = vx / n;
37195
+ const varianceY = vy / n;
37196
+ if (vx <= 0.000000000001 || vy <= 0.000000000001) {
37197
+ return {
37198
+ raw: 0,
37199
+ value: 0,
37200
+ confidence: 0,
37201
+ shrinkage: 0,
37202
+ sampleSize: n,
37203
+ varianceX,
37204
+ varianceY
37205
+ };
37057
37206
  }
37058
- const n = users.size;
37059
- const observation = {
37060
- observedThroughDay,
37061
- users: n,
37062
- retention: RETENTION_DAYS.map((day) => {
37063
- if (day > observedThroughDay)
37064
- return { day, status: "not-observed", users: n, activeUsers: null, rate: null, interval95: null };
37065
- const activeUsers = [...users.values()].filter((user) => user.active.has(day)).length;
37066
- const rate = activeUsers / n;
37067
- const z2 = 1.959963984540054 ** 2;
37068
- const denominator = 1 + z2 / n;
37069
- const center = (rate + z2 / (2 * n)) / denominator;
37070
- const radius = Math.sqrt(z2 * (rate * (1 - rate) / n + z2 / (4 * n * n))) / denominator;
37071
- 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) } };
37072
- })
37207
+ const raw = clampCorrelation2(cov / Math.sqrt(vx) / Math.sqrt(vy));
37208
+ const ci95 = 1.96 / Math.sqrt(Math.max(1, n - 3));
37209
+ const sampleConfidence = clamp014(1 - ci95);
37210
+ const varianceConfidenceX = varianceX / (varianceX + 0.01);
37211
+ const varianceConfidenceY = varianceY / (varianceY + 0.01);
37212
+ const varianceConfidence = Math.sqrt(varianceConfidenceX * varianceConfidenceY);
37213
+ const confidence = clamp014(sampleConfidence * varianceConfidence);
37214
+ const shrinkage = confidence;
37215
+ const value = raw * shrinkage;
37216
+ return {
37217
+ raw,
37218
+ value,
37219
+ confidence,
37220
+ shrinkage,
37221
+ sampleSize: n,
37222
+ varianceX,
37223
+ varianceY
37073
37224
  };
37074
- assertCohortObservation(observation);
37075
- return observation;
37076
37225
  }
37077
- function assertCohortObservation(observation) {
37078
- if (!Number.isSafeInteger(observation.users) || observation.users < 1 || !Number.isSafeInteger(observation.observedThroughDay) || observation.observedThroughDay < 0) {
37079
- throw new Error("Invalid cohort population or observation cutoff");
37226
+ function calibrateMonetization(rows) {
37227
+ if (rows.length === 0) {
37228
+ throw new Error("calibration rows cannot be empty");
37080
37229
  }
37081
- if (observation.retention.length !== RETENTION_DAYS.length)
37082
- throw new Error("Cohort retention must contain D1, D7, D30 and D90");
37083
- for (const [index, point] of observation.retention.entries()) {
37084
- if (point.day !== RETENTION_DAYS[index] || point.users !== observation.users)
37085
- throw new Error("Cohort retention days and population must match the observation");
37086
- const observed = point.day <= observation.observedThroughDay;
37087
- if (point.status !== (observed ? "observed" : "not-observed"))
37088
- throw new Error("Cohort retention status must match the observation cutoff");
37089
- if (!observed) {
37090
- if (point.activeUsers !== null || point.rate !== null || point.interval95 !== null)
37091
- throw new Error("Future retention must remain unobserved");
37092
- continue;
37230
+ const users = new Map;
37231
+ for (const row of rows) {
37232
+ const day = Math.max(0, Math.floor(row.day));
37233
+ const u = users.get(row.userId) ?? { days: new Set, iap: 0, ad: 0, acq: undefined };
37234
+ if (row.active ?? true)
37235
+ u.days.add(day);
37236
+ u.iap += Math.max(0, row.iapRevenue);
37237
+ u.ad += Math.max(0, row.adRevenue);
37238
+ if (row.acquisitionCost !== undefined && Number.isFinite(row.acquisitionCost)) {
37239
+ u.acq = (u.acq ?? 0) + Math.max(0, row.acquisitionCost);
37093
37240
  }
37094
- if (point.activeUsers === null || !Number.isSafeInteger(point.activeUsers) || point.activeUsers < 0 || point.activeUsers > point.users || point.rate !== point.activeUsers / point.users) {
37095
- throw new Error("Retained users and retention rate must match the installed population");
37241
+ users.set(row.userId, u);
37242
+ }
37243
+ const cohortSize = Math.max(1, users.size);
37244
+ const dayActiveCount = (targetDay) => {
37245
+ let count = 0;
37246
+ for (const user of users.values())
37247
+ if (user.days.has(targetDay))
37248
+ count += 1;
37249
+ return count;
37250
+ };
37251
+ const d1 = dayActiveCount(1) / cohortSize;
37252
+ const d7 = dayActiveCount(7) / cohortSize;
37253
+ const d30 = dayActiveCount(30) / cohortSize;
37254
+ const d90 = dayActiveCount(90) / cohortSize;
37255
+ let totalIap = 0;
37256
+ let totalAd = 0;
37257
+ let payerUsers = 0;
37258
+ let activeUserDays = 0;
37259
+ let payerDays = 0;
37260
+ let totalAcq = 0;
37261
+ let acqUsers = 0;
37262
+ for (const user of users.values()) {
37263
+ totalIap += user.iap;
37264
+ totalAd += user.ad;
37265
+ if (user.iap > 0)
37266
+ payerUsers += 1;
37267
+ activeUserDays += user.days.size;
37268
+ for (const day of user.days) {
37269
+ if (day >= 0 && user.iap > 0)
37270
+ payerDays += 1;
37096
37271
  }
37097
- const interval = point.interval95;
37098
- 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) {
37099
- throw new Error("Retention confidence interval must be finite and contain the observed rate");
37272
+ if (user.acq !== undefined) {
37273
+ totalAcq += user.acq;
37274
+ acqUsers += 1;
37100
37275
  }
37101
37276
  }
37102
- }
37103
- function observedRetentionRates(observation) {
37104
- return Object.fromEntries(observation.retention.filter((row) => row.rate !== null).map((row) => [`d${row.day}`, row.rate]));
37105
- }
37106
- function planRetentionPatch(observation, assumptions = {}) {
37107
- const rates = observedRetentionRates(observation);
37108
- if (Object.keys(rates).length === 0)
37109
- return { status: "unavailable", retention: {}, warnings: ["No retention day has been fully observed."] };
37110
- const merged = { ...assumptions, ...rates };
37111
- const warnings = [];
37112
- for (const day of RETENTION_DAYS) {
37113
- if (merged[`d${day}`] === undefined)
37114
- 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.`);
37115
- }
37116
- let previous;
37117
- for (const day of RETENTION_DAYS) {
37118
- const rate = merged[`d${day}`];
37119
- if (rate === undefined)
37120
- continue;
37121
- if (previous && previous.rate < rate)
37122
- 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.`);
37123
- previous = { day, rate };
37277
+ const payerConversion = payerUsers / cohortSize;
37278
+ const arppuDaily = totalIap / Math.max(1, payerDays);
37279
+ const adArpDau = totalAd / Math.max(1, activeUserDays);
37280
+ const cpi = totalAcq / Math.max(1, acqUsers);
37281
+ const userRetention = [];
37282
+ const userConversion = [];
37283
+ const userArppu = [];
37284
+ const userAdArpDau = [];
37285
+ for (const user of users.values()) {
37286
+ const activeDays = Math.max(1, user.days.size);
37287
+ const retentionScore = clamp014(activeDays / 30);
37288
+ const conversionScore = user.iap > 0 ? 1 : 0;
37289
+ const arppuScore = user.iap > 0 ? user.iap / activeDays : 0;
37290
+ const adScore = user.ad / activeDays;
37291
+ userRetention.push(retentionScore);
37292
+ userConversion.push(conversionScore);
37293
+ userArppu.push(arppuScore);
37294
+ userAdArpDau.push(adScore);
37124
37295
  }
37125
- return warnings.length ? { status: "requires-review", retention: {}, warnings } : { status: "ready", retention: rates, warnings };
37126
- }
37127
- function compareRetention(observation, assumptions) {
37128
- return observation.retention.map((row) => {
37129
- const assumption = assumptions[`d${row.day}`] ?? null;
37130
- return {
37131
- day: row.day,
37132
- assumption,
37133
- observed: row.rate,
37134
- differencePercentagePoints: row.rate === null || assumption === null ? null : (row.rate - assumption) * 100,
37135
- status: row.rate === null ? "not-observed" : assumption === null ? "unconfigured" : "compared"
37136
- };
37137
- });
37296
+ const estimatedCorrelationPair = {
37297
+ retentionConversion: estimateCorrelation(userRetention, userConversion),
37298
+ retentionArppu: estimateCorrelation(userRetention, userArppu),
37299
+ retentionAd: estimateCorrelation(userRetention, userAdArpDau),
37300
+ conversionArppu: estimateCorrelation(userConversion, userArppu),
37301
+ conversionAd: estimateCorrelation(userConversion, userAdArpDau),
37302
+ arppuAd: estimateCorrelation(userArppu, userAdArpDau)
37303
+ };
37304
+ const estimatedCorrelation = {
37305
+ retentionConversion: estimatedCorrelationPair.retentionConversion.value,
37306
+ retentionArppu: estimatedCorrelationPair.retentionArppu.value,
37307
+ retentionAd: estimatedCorrelationPair.retentionAd.value,
37308
+ conversionArppu: estimatedCorrelationPair.conversionArppu.value,
37309
+ conversionAd: estimatedCorrelationPair.conversionAd.value,
37310
+ arppuAd: estimatedCorrelationPair.arppuAd.value
37311
+ };
37312
+ const estimatedCorrelationRaw = {
37313
+ retentionConversion: estimatedCorrelationPair.retentionConversion.raw,
37314
+ retentionArppu: estimatedCorrelationPair.retentionArppu.raw,
37315
+ retentionAd: estimatedCorrelationPair.retentionAd.raw,
37316
+ conversionArppu: estimatedCorrelationPair.conversionArppu.raw,
37317
+ conversionAd: estimatedCorrelationPair.conversionAd.raw,
37318
+ arppuAd: estimatedCorrelationPair.arppuAd.raw
37319
+ };
37320
+ const correlationConfidence = {
37321
+ retentionConversion: estimatedCorrelationPair.retentionConversion.confidence,
37322
+ retentionArppu: estimatedCorrelationPair.retentionArppu.confidence,
37323
+ retentionAd: estimatedCorrelationPair.retentionAd.confidence,
37324
+ conversionArppu: estimatedCorrelationPair.conversionArppu.confidence,
37325
+ conversionAd: estimatedCorrelationPair.conversionAd.confidence,
37326
+ arppuAd: estimatedCorrelationPair.arppuAd.confidence
37327
+ };
37328
+ const result = {
37329
+ monetization: {
37330
+ cohorts: {
37331
+ baseUsers: cohortSize
37332
+ },
37333
+ retention: {
37334
+ d1: clamp014(d1),
37335
+ d7: clamp014(Math.min(d1, d7)),
37336
+ d30: clamp014(Math.min(d1, d7, d30)),
37337
+ d90: clamp014(Math.min(d1, d7, d30, d90)),
37338
+ longTailDailyDecay: 0.02
37339
+ },
37340
+ revenue: {
37341
+ payerConversion: clamp014(payerConversion),
37342
+ arppuDaily: Math.max(0, arppuDaily),
37343
+ adArpDau: Math.max(0, adArpDau),
37344
+ platformFeeRate: 0.3,
37345
+ grossMarginRate: 0.92
37346
+ },
37347
+ acquisition: {
37348
+ cpi: Math.max(0, cpi)
37349
+ },
37350
+ uncertainty: {
37351
+ enabled: true,
37352
+ draws: 300,
37353
+ quantiles: [0.5, 0.9],
37354
+ sigma: {
37355
+ retention: 0.08,
37356
+ conversion: 0.12,
37357
+ arppu: 0.2,
37358
+ ad: 0.15
37359
+ },
37360
+ correlation: {
37361
+ retentionConversion: estimatedCorrelation.retentionConversion,
37362
+ retentionArppu: estimatedCorrelation.retentionArppu,
37363
+ retentionAd: estimatedCorrelation.retentionAd,
37364
+ conversionArppu: estimatedCorrelation.conversionArppu,
37365
+ conversionAd: estimatedCorrelation.conversionAd,
37366
+ arppuAd: estimatedCorrelation.arppuAd
37367
+ }
37368
+ }
37369
+ },
37370
+ diagnostics: {
37371
+ users: cohortSize,
37372
+ totalRows: rows.length,
37373
+ totalIap,
37374
+ totalAd,
37375
+ activeUserDays,
37376
+ payerUsers,
37377
+ payerDays,
37378
+ retention: {
37379
+ d1,
37380
+ d7,
37381
+ d30,
37382
+ d90
37383
+ },
37384
+ userFeatureVariance: {
37385
+ retention: variance(userRetention),
37386
+ conversion: variance(userConversion),
37387
+ arppu: variance(userArppu),
37388
+ adArpDau: variance(userAdArpDau)
37389
+ },
37390
+ estimatedCorrelation,
37391
+ estimatedCorrelationRaw,
37392
+ correlationConfidence,
37393
+ correlationDiagnostics: estimatedCorrelationPair
37394
+ }
37395
+ };
37396
+ assertFiniteNumbers(result, "calibration");
37397
+ return result;
37138
37398
  }
37399
+ // src/lib/ltvProjection.ts
37400
+ init_dist2();
37401
+ // src/lib/ltvUncertainty.ts
37402
+ init_dist2();
37403
+ // src/lib/cohortObservation.ts
37404
+ init_dist2();
37139
37405
  function renderCohortMarkdown(observation, comparison, patch) {
37140
37406
  const percent = (value) => value === null ? "unavailable" : `${(value * 100).toFixed(2)}%`;
37141
37407
  const lines = [
@@ -37460,6 +37726,42 @@ var CLI_DESCRIPTION = import_package2.default.description ?? "Bun-first idle gam
37460
37726
  // src/balance/workflow.ts
37461
37727
  init_bun();
37462
37728
 
37729
+ // src/balance/pacingAmount.ts
37730
+ var MAX_DECIMAL_LENGTH = 200;
37731
+ var DECIMAL = /^([+-]?)(?:(\d+)(?:\.(\d*))?|\.(\d+))(?:[eE]([+-]?\d+))?$/;
37732
+ function pacingValue(value) {
37733
+ if (typeof value === "number")
37734
+ return Number.isFinite(value) ? Object.is(value, -0) ? 0 : value : undefined;
37735
+ return typeof value === "string" && value.length <= MAX_DECIMAL_LENGTH && DECIMAL.test(value) ? value : undefined;
37736
+ }
37737
+ function parts(value) {
37738
+ if (pacingValue(value) === undefined)
37739
+ throw new RangeError("Pacing value must be finite or bounded decimal text");
37740
+ const match = DECIMAL.exec(String(value));
37741
+ const fraction = match[3] ?? match[4] ?? "";
37742
+ const digits = `${match[2] ?? ""}${fraction}`.replace(/^0+/, "");
37743
+ const sign = digits.length === 0 ? 0 : match[1] === "-" ? -1 : 1;
37744
+ return {
37745
+ sign,
37746
+ digits: digits.replace(/0+$/, ""),
37747
+ magnitude: BigInt(match[5] ?? "0") - BigInt(fraction.length) + BigInt(digits.length)
37748
+ };
37749
+ }
37750
+ function comparePacingValues(left, right) {
37751
+ if (typeof left === "number" && typeof right === "number")
37752
+ return left < right ? -1 : left > right ? 1 : 0;
37753
+ const a = parts(left), b = parts(right);
37754
+ if (a.sign !== b.sign)
37755
+ return a.sign < b.sign ? -1 : 1;
37756
+ if (a.sign === 0)
37757
+ return 0;
37758
+ if (a.magnitude !== b.magnitude)
37759
+ return (a.magnitude < b.magnitude ? -1 : 1) * a.sign;
37760
+ const length = Math.max(a.digits.length, b.digits.length);
37761
+ const ad = a.digits.padEnd(length, "0"), bd = b.digits.padEnd(length, "0");
37762
+ return (ad < bd ? -1 : ad > bd ? 1 : 0) * a.sign;
37763
+ }
37764
+
37463
37765
  // src/balance/pacing.ts
37464
37766
  var PACING_LIMITS = Object.freeze({
37465
37767
  maxRuns: 1000,
@@ -37501,6 +37803,12 @@ function finite2(value, label) {
37501
37803
  invalid(`${label} must be finite`);
37502
37804
  return Object.is(value, -0) ? 0 : value;
37503
37805
  }
37806
+ function bound(value, label) {
37807
+ const parsed = pacingValue(value);
37808
+ if (parsed === undefined)
37809
+ invalid(`${label} must be finite or decimal text of at most 200 characters`);
37810
+ return parsed;
37811
+ }
37504
37812
  function limit(value, maximum, label) {
37505
37813
  if (value === undefined)
37506
37814
  return maximum;
@@ -37559,11 +37867,11 @@ function planPacingRuns(input) {
37559
37867
  if (metrics.has(metric))
37560
37868
  invalid(`duplicate target metric ${metric}`);
37561
37869
  metrics.add(metric);
37562
- const min = raw.min === undefined ? undefined : finite2(raw.min, `targets[${index}].min`);
37563
- const max = raw.max === undefined ? undefined : finite2(raw.max, `targets[${index}].max`);
37870
+ const min = raw.min === undefined ? undefined : bound(raw.min, `targets[${index}].min`);
37871
+ const max = raw.max === undefined ? undefined : bound(raw.max, `targets[${index}].max`);
37564
37872
  if (min === undefined && max === undefined)
37565
37873
  invalid(`target ${metric} needs min or max`);
37566
- if (min !== undefined && max !== undefined && min > max)
37874
+ if (min !== undefined && max !== undefined && comparePacingValues(min, max) > 0)
37567
37875
  invalid(`target ${metric} min exceeds max`);
37568
37876
  return Object.freeze({ metric, unit, ...min !== undefined ? { min } : {}, ...max !== undefined ? { max } : {} });
37569
37877
  }).sort((left, right) => compareText(left.metric, right.metric));
@@ -37642,11 +37950,12 @@ function checkTarget(target, metrics) {
37642
37950
  const value = metrics[target.metric];
37643
37951
  if (value === null)
37644
37952
  return { ...target, value, status: "unreached", message: "Metric not reached within the run horizon" };
37645
- if (typeof value !== "number" || !Number.isFinite(value)) {
37646
- return { ...target, value: null, status: "error", message: "Evaluator metric must be a finite number or null" };
37953
+ const parsed = pacingValue(value);
37954
+ if (parsed === undefined) {
37955
+ return { ...target, value: null, status: "error", message: "Evaluator metric must be a finite number, bounded decimal text or null" };
37647
37956
  }
37648
- const breached = target.min !== undefined && value < target.min || target.max !== undefined && value > target.max;
37649
- return { ...target, value: Object.is(value, -0) ? 0 : value, status: breached ? "breach" : "pass" };
37957
+ const breached = target.min !== undefined && comparePacingValues(parsed, target.min) < 0 || target.max !== undefined && comparePacingValues(parsed, target.max) > 0;
37958
+ return { ...target, value: parsed, status: breached ? "breach" : "pass" };
37650
37959
  }
37651
37960
  async function runPacingChecks(input, evaluator) {
37652
37961
  const plan = planPacingRuns(input);
@@ -38410,7 +38719,8 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38410
38719
  const loaded = await loadRegistriesFromFlags(flags);
38411
38720
  for (const [index, path] of Object.keys(loaded.pluginDigest).entries())
38412
38721
  inputFiles[`plugin${index}`] = path;
38413
- const context = { cliVersion: CLI_VERSION, bunVersion: Bun.version, pluginDigests: JSON.stringify(loaded.pluginDigest), engine: "number" };
38722
+ const engine = resolveEffectiveEngine({ requested: flags.engine });
38723
+ const context = { cliVersion: CLI_VERSION, bunVersion: Bun.version, pluginDigests: JSON.stringify(loaded.pluginDigest), engine: engine.effectiveId };
38414
38724
  const outputDir = resolve10(base, config.outputDir);
38415
38725
  if (check)
38416
38726
  return readCurrentBundle(outputDir, inputFiles, { context });
@@ -38447,7 +38757,7 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38447
38757
  const metricIds = new Set(config.metrics.map((metric) => metric.id));
38448
38758
  if (metricIds.size !== config.metrics.length)
38449
38759
  throw new Error("Metric IDs must be unique");
38450
- const targets = config.pacing.targets ?? [];
38760
+ const targets = plan.targets;
38451
38761
  for (const target of targets)
38452
38762
  if (!metricIds.has(target.metric))
38453
38763
  throw new Error(`Unknown target metric: ${target.metric}`);
@@ -38469,6 +38779,9 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38469
38779
  const unit = metric.kind === "firstAction" || metric.kind === "firstActionPrefix" ? "seconds" : metric.kind === "endWallet" || metric.kind === "endNetWorth" ? baselineDocument.unit.code : "count";
38470
38780
  if (target.unit !== unit)
38471
38781
  throw new Error(`Target ${target.metric} must use unit '${unit}'`);
38782
+ if (metric.kind !== "endWallet" && metric.kind !== "endNetWorth" && (typeof target.min === "string" || typeof target.max === "string")) {
38783
+ throw new Error(`Target ${target.metric} requires finite numeric time or count bounds`);
38784
+ }
38472
38785
  }
38473
38786
  const materialize = (patches) => {
38474
38787
  const candidate = applySheetCsv(template, schema, overlayCsv(canonicalCsv, patches));
@@ -38486,7 +38799,7 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38486
38799
  modelRegistry: loaded.modelRegistry,
38487
38800
  strategyRegistry: loaded.strategyRegistry,
38488
38801
  pluginDigest: loaded.pluginDigest,
38489
- engineRequest: "number",
38802
+ engineRequest: engine.effectiveId,
38490
38803
  seed: descriptor.seed
38491
38804
  });
38492
38805
  const opened = prepared.open("simulate", `simulate:${descriptor.seed}`, { durationSec: descriptor.horizonSec });
@@ -38508,6 +38821,13 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38508
38821
  throw new Error("Action history exceeded the result budget; pacing cannot be inferred from truncated history");
38509
38822
  const actions = run.actionsLog ?? [];
38510
38823
  const result = {};
38824
+ const monetary = (amount) => {
38825
+ const E = opened.scenario.ctx.E;
38826
+ if (!E.isFinite(amount))
38827
+ throw new Error("Monetary metric overflowed the selected engine");
38828
+ const decimal = E.toString(amount);
38829
+ return engine.effectiveId === "number" ? Number(decimal) : decimal;
38830
+ };
38511
38831
  for (const metric of config.metrics) {
38512
38832
  if (metric.kind === "firstAction" || metric.kind === "firstActionPrefix") {
38513
38833
  const action = actions.find((item) => metric.kind === "firstAction" ? item.actionId === metric.actionId : item.actionId.startsWith(metric.prefix));
@@ -38517,12 +38837,12 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38517
38837
  else if (metric.kind === "actionCount")
38518
38838
  result[metric.id] = actions.length;
38519
38839
  else if (metric.kind === "endWallet")
38520
- result[metric.id] = Number(run.end.wallet.money.amount);
38840
+ result[metric.id] = monetary(run.end.wallet.money.amount);
38521
38841
  else {
38522
38842
  const worth = opened.scenario.model.netWorth?.(opened.scenario.ctx, run.end);
38523
38843
  if (!worth)
38524
38844
  throw new Error("endNetWorth requires model.netWorth; wallet is not a substitute");
38525
- result[metric.id] = Number(worth.amount);
38845
+ result[metric.id] = monetary(worth.amount);
38526
38846
  }
38527
38847
  }
38528
38848
  return result;
@@ -38580,7 +38900,7 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38580
38900
  `);
38581
38901
  return { id: variant.id, scenario: name };
38582
38902
  });
38583
- addArtifact("variants.json", JSON.stringify({ version: 1, engine: "number", seeds: plan.seeds, horizonSec: horizon, strategy: plan.strategy, variants }, null, 2) + `
38903
+ addArtifact("variants.json", JSON.stringify({ version: 1, engine: engine.effectiveId, seeds: plan.seeds, horizonSec: horizon, strategy: plan.strategy, variants }, null, 2) + `
38584
38904
  `);
38585
38905
  }
38586
38906
  return artifacts;
@@ -38595,10 +38915,10 @@ function cell(value) {
38595
38915
  }
38596
38916
  function explain(result) {
38597
38917
  if (result.status === "breach" && result.value !== null) {
38598
- if (result.min !== undefined && result.value < result.min)
38599
- return `below the minimum by ${result.min - result.value} ${result.unit}`;
38600
- if (result.max !== undefined && result.value > result.max)
38601
- return `above the maximum by ${result.value - result.max} ${result.unit}`;
38918
+ if (result.min !== undefined && comparePacingValues(result.value, result.min) < 0)
38919
+ return typeof result.min === "number" && typeof result.value === "number" ? `below the minimum by ${result.min - result.value} ${result.unit}` : `below the minimum ${result.min} ${result.unit}`;
38920
+ if (result.max !== undefined && comparePacingValues(result.value, result.max) > 0)
38921
+ return typeof result.max === "number" && typeof result.value === "number" ? `above the maximum by ${result.value - result.max} ${result.unit}` : `above the maximum ${result.max} ${result.unit}`;
38602
38922
  }
38603
38923
  if (result.status === "pass")
38604
38924
  return "within the inclusive target bounds";
@@ -38637,6 +38957,7 @@ var balance_default = defineCommand({
38637
38957
  description: "Refresh a typed CSV balance sheet into scenario, pacing checks, and result sheets",
38638
38958
  options: {
38639
38959
  ...pluginOptions(),
38960
+ engine: option(string2().min(1).optional(), { description: "Numeric engine (default: number; explicit: breakInfinity)" }),
38640
38961
  check: option(exports_coerce.boolean().default(false), { description: "Check result freshness without rerunning simulations" }),
38641
38962
  format: option(_enum(["json", "md"]).default("json"), { description: "JSON manifest or human-readable target explanations" })
38642
38963
  },
@@ -39093,6 +39414,7 @@ function compareIdentity2(args) {
39093
39414
  return {
39094
39415
  a: side(args.scenarios.a),
39095
39416
  b: side(args.scenarios.b),
39417
+ ...args.engineId && args.engineId !== "number" ? { engineId: args.engineId } : {},
39096
39418
  pluginDigests: pluginDigestValues(args.loaded.pluginDigest),
39097
39419
  metrics,
39098
39420
  bundle: flags.bundle ?? null,
@@ -39101,9 +39423,16 @@ function compareIdentity2(args) {
39101
39423
  };
39102
39424
  }
39103
39425
  function measureScenario(args) {
39426
+ const requireFinite = (amount, label) => {
39427
+ if (!args.E.isFinite(amount))
39428
+ throw new RangeError(`compare ${label} must be finite in the selected engine`);
39429
+ return amount;
39430
+ };
39104
39431
  const runInput = args.compiled.fresh("economy");
39105
39432
  const run = runScenario(runInput);
39106
39433
  const endWorth = runInput.model.netWorth?.(runInput.ctx, run.end) ?? run.end.wallet.money;
39434
+ requireFinite(run.end.wallet.money.amount, "end money");
39435
+ requireFinite(endWorth.amount, "end net worth");
39107
39436
  let etaSeconds;
39108
39437
  let etaReached;
39109
39438
  if (args.targetWorth) {
@@ -39112,7 +39441,12 @@ function measureScenario(args) {
39112
39441
  unit: etaInput.ctx.unit,
39113
39442
  suffix: { kind: "alphaInfinite", minLen: 2 }
39114
39443
  }).amount;
39115
- const reachedFn = (s) => args.E.cmp((etaInput.model.netWorth?.(etaInput.ctx, s) ?? s.wallet.money).amount, target) >= 0;
39444
+ requireFinite(target, "ETA target");
39445
+ const reachedFn = (s) => {
39446
+ requireFinite(s.wallet.money.amount, "ETA money");
39447
+ const worth = etaInput.model.netWorth?.(etaInput.ctx, s) ?? s.wallet.money;
39448
+ return args.E.cmp(requireFinite(worth.amount, "ETA net worth"), target) >= 0;
39449
+ };
39116
39450
  const etaRun = runScenario({
39117
39451
  ...etaInput,
39118
39452
  run: {
@@ -39394,6 +39728,7 @@ var compare_default = defineCommand({
39394
39728
  description: "Compare two scenarios via measured simulation metrics",
39395
39729
  options: {
39396
39730
  ...pluginOptions(),
39731
+ engine: option(string2().min(1).optional(), { description: "Numeric engine (default: number; explicit: breakInfinity)" }),
39397
39732
  duration: option(exports_coerce.number().optional(), { description: "Override durationSec" }),
39398
39733
  step: option(exports_coerce.number().optional(), { description: "Override stepSec" }),
39399
39734
  strategy: option(strategySchema, { description: "Override strategy id (greedy|planner|scripted)" }),
@@ -39449,7 +39784,8 @@ var compare_default = defineCommand({
39449
39784
  throw usageError("metric=timeToMilestone requires --milestone-key <key>");
39450
39785
  }
39451
39786
  const scenarios = { a: aScenario, b: bScenario };
39452
- const identity = compareIdentity2({ scenarios, flags, loaded });
39787
+ const engine = resolveEffectiveEngine({ requested: flags.engine });
39788
+ const identity = compareIdentity2({ scenarios, flags, loaded, engineId: engine.effectiveId });
39453
39789
  const effectiveSeed = flags.seed ?? defaultRunSeed({
39454
39790
  base: {
39455
39791
  command: "compare",
@@ -39469,9 +39805,12 @@ var compare_default = defineCommand({
39469
39805
  milestoneKey: undefined
39470
39806
  }
39471
39807
  },
39472
- runs: [{ identity, defaults: compareIdentity2({ scenarios, flags: { "max-duration": DEFAULT_MAX_DURATION }, loaded }) }]
39808
+ runs: [{ identity, defaults: {
39809
+ ...compareIdentity2({ scenarios, flags: { "max-duration": DEFAULT_MAX_DURATION }, loaded }),
39810
+ engineId: "number"
39811
+ } }]
39473
39812
  });
39474
- const E = createNumberEngine();
39813
+ const E = engine.engine;
39475
39814
  const aCompiled = compileComparableScenario({
39476
39815
  scenario: aScenario,
39477
39816
  E,
@@ -40329,7 +40668,7 @@ function parseHorizons(raw) {
40329
40668
  }
40330
40669
  var DEFAULT_LTV_HORIZONS = "30m,2h,24h,7d,30d,90d";
40331
40670
  function ltvUncertaintyDraws(scenario, draws) {
40332
- const uncertainty = deriveMonetizationConfig(scenario).uncertainty;
40671
+ const uncertainty = deriveMonetizationConfig2(scenario).uncertainty;
40333
40672
  return uncertainty.enabled || draws !== undefined ? draws ?? uncertainty.draws : null;
40334
40673
  }
40335
40674
  function ltvStageInputs(scenario, horizons, draws) {
@@ -40391,7 +40730,7 @@ function runLtvAnalysis(args) {
40391
40730
  const horizons = parseHorizons(args.horizonsRaw);
40392
40731
  const stepSec = args.step ?? args.compiled.run.stepSec;
40393
40732
  const runFast = resolveFastMode(args.fast, args.compiled.run.fast);
40394
- const monetizationConfig = deriveMonetizationConfig(args.scenario);
40733
+ const monetizationConfig = deriveMonetizationConfig2(args.scenario);
40395
40734
  const uncertainEnabled = monetizationConfig.uncertainty.enabled || args.draws !== undefined;
40396
40735
  const draws = args.draws ?? monetizationConfig.uncertainty.draws;
40397
40736
  let state = args.compiled.initial;
@@ -40451,12 +40790,12 @@ function runLtvAnalysis(args) {
40451
40790
  const endWorthLog10 = args.compiled.ctx.E.absLog10(worth.amount);
40452
40791
  const progression = progressionFactor(startWorthLog10, endWorthLog10, monetizationConfig.revenue.progressionLogSpan);
40453
40792
  const horizonDays = h.seconds / 86400;
40454
- const point = estimateLtvPerUser({
40793
+ const point = estimateLtvPerUser2({
40455
40794
  config: monetizationConfig,
40456
40795
  horizonDays,
40457
40796
  progression
40458
40797
  });
40459
- const distribution = uncertainEnabled && draws > 1 ? estimateLtvDistribution({
40798
+ const distribution = uncertainEnabled && draws > 1 ? estimateLtvDistribution2({
40460
40799
  config: monetizationConfig,
40461
40800
  horizonDays,
40462
40801
  progression,
@@ -42109,6 +42448,7 @@ var tune_default = defineCommand({
42109
42448
  description: "Tune strategy parameters with objective scoring",
42110
42449
  options: {
42111
42450
  ...pluginOptions(),
42451
+ engine: option(string2().min(1).optional(), { description: "Numeric engine (default: number; explicit: breakInfinity)" }),
42112
42452
  tune: option(string2().optional(), { description: "TuneSpec file path (.json|.yaml)" }),
42113
42453
  wizard: option(exports_coerce.boolean().default(false), {
42114
42454
  description: "Interactively generate a TuneSpec before tuning"
@@ -42164,11 +42504,14 @@ var tune_default = defineCommand({
42164
42504
  const tuneSpecInput = wizardResult?.tuneSpec ?? await readScenarioFile(tunePath);
42165
42505
  const baselinePath = flags["baseline-artifact"] ? resolve21(flags["baseline-artifact"]) : undefined;
42166
42506
  const baselineRaw = baselinePath ? await readJsonFile(baselinePath) : undefined;
42507
+ const engine = resolveEffectiveEngine({ requested: flags.engine });
42508
+ const engineIdentity = engine.effectiveId === "number" ? {} : { engineId: engine.effectiveId };
42167
42509
  const seed = flags.seed ?? deriveDeterministicSeed({
42168
42510
  command: "tune",
42169
42511
  scenario: scenarioInput,
42170
42512
  tuneSpec: tuneSpecInput,
42171
42513
  options: {
42514
+ ...engineIdentity,
42172
42515
  baselineArtifact: baselineRaw === undefined ? undefined : hashContent(baselineRaw),
42173
42516
  regressionTolerance: flags["regression-tolerance"]
42174
42517
  }
@@ -42176,7 +42519,7 @@ var tune_default = defineCommand({
42176
42519
  const runId = flags["run-id"] ?? deriveDeterministicRunId({
42177
42520
  command: "tune",
42178
42521
  seed,
42179
- scope: { scenario: scenarioInput, tuneSpec: tuneSpecInput, pluginDigests: Object.values(loaded.pluginDigest) }
42522
+ scope: { scenario: scenarioInput, tuneSpec: tuneSpecInput, pluginDigests: Object.values(loaded.pluginDigest), ...engineIdentity }
42180
42523
  });
42181
42524
  const outputMeta = buildOutputMeta({
42182
42525
  command: "tune",
@@ -42188,7 +42531,7 @@ var tune_default = defineCommand({
42188
42531
  pluginDigest: loaded.pluginDigest
42189
42532
  });
42190
42533
  const result = await cmdTune({
42191
- E: createNumberEngine(),
42534
+ E: engine.engine,
42192
42535
  scenarioInput,
42193
42536
  tuneSpecInput,
42194
42537
  modelRegistry: loaded.modelRegistry,
@@ -44105,4 +44448,4 @@ export {
44105
44448
  cli2 as cli
44106
44449
  };
44107
44450
 
44108
- //# debugId=4AAADDD1336AF66164756E2164756E21
44451
+ //# debugId=0936659FB5366DD764756E2164756E21