@idlekit/cli 0.5.1 → 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 {
@@ -19797,6 +19944,151 @@ var init_public = __esm(() => {
19797
19944
  init_uncertainty();
19798
19945
  });
19799
19946
 
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) };
19955
+ }
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);
19978
+ }
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");
19982
+ }
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;
19997
+ }
19998
+ function object2(value) {
19999
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20000
+ }
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");
20004
+ }
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
+ }
20030
+ }
20031
+ }
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;
20040
+ }
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
+ }
20050
+ }
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 };
20072
+ }
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
+ });
20086
+ }
20087
+ var RETENTION_DAYS;
20088
+ var init_cohort = __esm(() => {
20089
+ RETENTION_DAYS = Object.freeze([1, 7, 30, 90]);
20090
+ });
20091
+
19800
20092
  // ../core/dist/index.js
19801
20093
  var init_dist2 = __esm(() => {
19802
20094
  init_types();
@@ -19863,6 +20155,7 @@ var init_dist2 = __esm(() => {
19863
20155
  init_flow();
19864
20156
  init_jevAdvisory();
19865
20157
  init_public();
20158
+ init_cohort();
19866
20159
  });
19867
20160
 
19868
20161
  // src/runtime/bun.ts
@@ -19928,7 +20221,7 @@ var init_bun = __esm(() => {
19928
20221
  var require_package = __commonJS(function(exports, module) {
19929
20222
  module.exports = {
19930
20223
  name: "@idlekit/cli",
19931
- version: "0.5.1",
20224
+ version: "0.6.0",
19932
20225
  description: "Bun-first idk CLI for idle game scenario validation, simulation, tuning, and reporting.",
19933
20226
  license: "MIT",
19934
20227
  type: "module",
@@ -19981,7 +20274,7 @@ var require_package = __commonJS(function(exports, module) {
19981
20274
  "@clack/prompts": "1.8.1",
19982
20275
  "@gunshi/plugin-completion": "0.37.3",
19983
20276
  "@gunshi/plugin-suggestion": "0.37.3",
19984
- "@idlekit/core": "0.5.0",
20277
+ "@idlekit/core": "0.6.0",
19985
20278
  ajv: "^8.20.0",
19986
20279
  gunshi: "0.37.3",
19987
20280
  "proper-lockfile": "4.1.2",
@@ -34920,8 +35213,8 @@ function ownedCount(state) {
34920
35213
  }
34921
35214
  function finiteLinearAmount(E, value, label) {
34922
35215
  if (!E.isFinite(value)) {
34923
- 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.`, {
34924
- 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."
34925
35218
  });
34926
35219
  }
34927
35220
  return value;
@@ -37108,115 +37401,7 @@ init_dist2();
37108
37401
  // src/lib/ltvUncertainty.ts
37109
37402
  init_dist2();
37110
37403
  // src/lib/cohortObservation.ts
37111
- var RETENTION_DAYS = [1, 7, 30, 90];
37112
- function observeCohort(rows, observedThroughDay) {
37113
- if (!Number.isSafeInteger(observedThroughDay) || observedThroughDay < 0)
37114
- throw new Error("observed-through-day must be a nonnegative safe integer");
37115
- if (rows.length === 0)
37116
- throw new Error("Observed cohort requires at least one install record");
37117
- const users = new Map;
37118
- for (const row of rows) {
37119
- if (!row.userId.trim() || !Number.isSafeInteger(row.day) || row.day < 0 || row.day > observedThroughDay) {
37120
- throw new Error("Observed cohort requires a user ID and integer days within observed-through-day");
37121
- }
37122
- if (![row.iapRevenue, row.adRevenue, ...row.acquisitionCost === undefined ? [] : [row.acquisitionCost]].every((v) => Number.isFinite(v) && v >= 0)) {
37123
- throw new Error("Observed cohort revenue and acquisition costs must be finite and nonnegative");
37124
- }
37125
- if (row.active !== undefined && typeof row.active !== "boolean")
37126
- throw new Error("Observed cohort active must be boolean");
37127
- const user = users.get(row.userId) ?? { days: new Set, active: new Set };
37128
- if (user.days.has(row.day))
37129
- throw new Error("Observed cohort requires one aggregated row per user and day");
37130
- user.days.add(row.day);
37131
- if (row.active ?? true)
37132
- user.active.add(row.day);
37133
- users.set(row.userId, user);
37134
- }
37135
- for (const user of users.values()) {
37136
- if (!user.days.has(0))
37137
- throw new Error("Observed cohort requires a day 0 install record for every user, including inactive users");
37138
- }
37139
- const n = users.size;
37140
- const observation = {
37141
- observedThroughDay,
37142
- users: n,
37143
- retention: RETENTION_DAYS.map((day) => {
37144
- if (day > observedThroughDay)
37145
- return { day, status: "not-observed", users: n, activeUsers: null, rate: null, interval95: null };
37146
- const activeUsers = [...users.values()].filter((user) => user.active.has(day)).length;
37147
- const rate = activeUsers / n;
37148
- const z2 = 1.959963984540054 ** 2;
37149
- const denominator = 1 + z2 / n;
37150
- const center = (rate + z2 / (2 * n)) / denominator;
37151
- const radius = Math.sqrt(z2 * (rate * (1 - rate) / n + z2 / (4 * n * n))) / denominator;
37152
- return { day, status: "observed", users: n, activeUsers, rate, interval95: { min: activeUsers === 0 ? 0 : Math.max(0, center - radius), max: activeUsers === n ? 1 : Math.min(1, center + radius) } };
37153
- })
37154
- };
37155
- assertCohortObservation(observation);
37156
- return observation;
37157
- }
37158
- function assertCohortObservation(observation) {
37159
- if (!Number.isSafeInteger(observation.users) || observation.users < 1 || !Number.isSafeInteger(observation.observedThroughDay) || observation.observedThroughDay < 0) {
37160
- throw new Error("Invalid cohort population or observation cutoff");
37161
- }
37162
- if (observation.retention.length !== RETENTION_DAYS.length)
37163
- throw new Error("Cohort retention must contain D1, D7, D30 and D90");
37164
- for (const [index, point] of observation.retention.entries()) {
37165
- if (point.day !== RETENTION_DAYS[index] || point.users !== observation.users)
37166
- throw new Error("Cohort retention days and population must match the observation");
37167
- const observed = point.day <= observation.observedThroughDay;
37168
- if (point.status !== (observed ? "observed" : "not-observed"))
37169
- throw new Error("Cohort retention status must match the observation cutoff");
37170
- if (!observed) {
37171
- if (point.activeUsers !== null || point.rate !== null || point.interval95 !== null)
37172
- throw new Error("Future retention must remain unobserved");
37173
- continue;
37174
- }
37175
- if (point.activeUsers === null || !Number.isSafeInteger(point.activeUsers) || point.activeUsers < 0 || point.activeUsers > point.users || point.rate !== point.activeUsers / point.users) {
37176
- throw new Error("Retained users and retention rate must match the installed population");
37177
- }
37178
- const interval = point.interval95;
37179
- if (point.rate === null || !interval || !Number.isFinite(interval.min) || !Number.isFinite(interval.max) || interval.min < 0 || interval.max > 1 || interval.min > point.rate || interval.max < point.rate) {
37180
- throw new Error("Retention confidence interval must be finite and contain the observed rate");
37181
- }
37182
- }
37183
- }
37184
- function observedRetentionRates(observation) {
37185
- return Object.fromEntries(observation.retention.filter((row) => row.rate !== null).map((row) => [`d${row.day}`, row.rate]));
37186
- }
37187
- function planRetentionPatch(observation, assumptions = {}) {
37188
- const rates = observedRetentionRates(observation);
37189
- if (Object.keys(rates).length === 0)
37190
- return { status: "unavailable", retention: {}, warnings: ["No retention day has been fully observed."] };
37191
- const merged = { ...assumptions, ...rates };
37192
- const warnings = [];
37193
- for (const day of RETENTION_DAYS) {
37194
- if (merged[`d${day}`] === undefined)
37195
- warnings.push(`Unobserved D${day} has no explicit retained assumption. An LTV default could conflict with the observations; supply this assumption in --scenario or observe this day before applying a patch.`);
37196
- }
37197
- let previous;
37198
- for (const day of RETENTION_DAYS) {
37199
- const rate = merged[`d${day}`];
37200
- if (rate === undefined)
37201
- continue;
37202
- if (previous && previous.rate < rate)
37203
- warnings.push(`D${previous.day}=${previous.rate} is below D${day}=${rate}. LTV requires d1 >= d7 >= d30 >= d90; review observations and retained assumptions before applying a patch.`);
37204
- previous = { day, rate };
37205
- }
37206
- return warnings.length ? { status: "requires-review", retention: {}, warnings } : { status: "ready", retention: rates, warnings };
37207
- }
37208
- function compareRetention(observation, assumptions) {
37209
- return observation.retention.map((row) => {
37210
- const assumption = assumptions[`d${row.day}`] ?? null;
37211
- return {
37212
- day: row.day,
37213
- assumption,
37214
- observed: row.rate,
37215
- differencePercentagePoints: row.rate === null || assumption === null ? null : (row.rate - assumption) * 100,
37216
- status: row.rate === null ? "not-observed" : assumption === null ? "unconfigured" : "compared"
37217
- };
37218
- });
37219
- }
37404
+ init_dist2();
37220
37405
  function renderCohortMarkdown(observation, comparison, patch) {
37221
37406
  const percent = (value) => value === null ? "unavailable" : `${(value * 100).toFixed(2)}%`;
37222
37407
  const lines = [
@@ -37541,6 +37726,42 @@ var CLI_DESCRIPTION = import_package2.default.description ?? "Bun-first idle gam
37541
37726
  // src/balance/workflow.ts
37542
37727
  init_bun();
37543
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
+
37544
37765
  // src/balance/pacing.ts
37545
37766
  var PACING_LIMITS = Object.freeze({
37546
37767
  maxRuns: 1000,
@@ -37582,6 +37803,12 @@ function finite2(value, label) {
37582
37803
  invalid(`${label} must be finite`);
37583
37804
  return Object.is(value, -0) ? 0 : value;
37584
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
+ }
37585
37812
  function limit(value, maximum, label) {
37586
37813
  if (value === undefined)
37587
37814
  return maximum;
@@ -37640,11 +37867,11 @@ function planPacingRuns(input) {
37640
37867
  if (metrics.has(metric))
37641
37868
  invalid(`duplicate target metric ${metric}`);
37642
37869
  metrics.add(metric);
37643
- const min = raw.min === undefined ? undefined : finite2(raw.min, `targets[${index}].min`);
37644
- 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`);
37645
37872
  if (min === undefined && max === undefined)
37646
37873
  invalid(`target ${metric} needs min or max`);
37647
- if (min !== undefined && max !== undefined && min > max)
37874
+ if (min !== undefined && max !== undefined && comparePacingValues(min, max) > 0)
37648
37875
  invalid(`target ${metric} min exceeds max`);
37649
37876
  return Object.freeze({ metric, unit, ...min !== undefined ? { min } : {}, ...max !== undefined ? { max } : {} });
37650
37877
  }).sort((left, right) => compareText(left.metric, right.metric));
@@ -37723,11 +37950,12 @@ function checkTarget(target, metrics) {
37723
37950
  const value = metrics[target.metric];
37724
37951
  if (value === null)
37725
37952
  return { ...target, value, status: "unreached", message: "Metric not reached within the run horizon" };
37726
- if (typeof value !== "number" || !Number.isFinite(value)) {
37727
- 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" };
37728
37956
  }
37729
- const breached = target.min !== undefined && value < target.min || target.max !== undefined && value > target.max;
37730
- 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" };
37731
37959
  }
37732
37960
  async function runPacingChecks(input, evaluator) {
37733
37961
  const plan = planPacingRuns(input);
@@ -38491,7 +38719,8 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38491
38719
  const loaded = await loadRegistriesFromFlags(flags);
38492
38720
  for (const [index, path] of Object.keys(loaded.pluginDigest).entries())
38493
38721
  inputFiles[`plugin${index}`] = path;
38494
- 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 };
38495
38724
  const outputDir = resolve10(base, config.outputDir);
38496
38725
  if (check)
38497
38726
  return readCurrentBundle(outputDir, inputFiles, { context });
@@ -38528,7 +38757,7 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38528
38757
  const metricIds = new Set(config.metrics.map((metric) => metric.id));
38529
38758
  if (metricIds.size !== config.metrics.length)
38530
38759
  throw new Error("Metric IDs must be unique");
38531
- const targets = config.pacing.targets ?? [];
38760
+ const targets = plan.targets;
38532
38761
  for (const target of targets)
38533
38762
  if (!metricIds.has(target.metric))
38534
38763
  throw new Error(`Unknown target metric: ${target.metric}`);
@@ -38550,6 +38779,9 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38550
38779
  const unit = metric.kind === "firstAction" || metric.kind === "firstActionPrefix" ? "seconds" : metric.kind === "endWallet" || metric.kind === "endNetWorth" ? baselineDocument.unit.code : "count";
38551
38780
  if (target.unit !== unit)
38552
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
+ }
38553
38785
  }
38554
38786
  const materialize = (patches) => {
38555
38787
  const candidate = applySheetCsv(template, schema, overlayCsv(canonicalCsv, patches));
@@ -38567,7 +38799,7 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38567
38799
  modelRegistry: loaded.modelRegistry,
38568
38800
  strategyRegistry: loaded.strategyRegistry,
38569
38801
  pluginDigest: loaded.pluginDigest,
38570
- engineRequest: "number",
38802
+ engineRequest: engine.effectiveId,
38571
38803
  seed: descriptor.seed
38572
38804
  });
38573
38805
  const opened = prepared.open("simulate", `simulate:${descriptor.seed}`, { durationSec: descriptor.horizonSec });
@@ -38589,6 +38821,13 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38589
38821
  throw new Error("Action history exceeded the result budget; pacing cannot be inferred from truncated history");
38590
38822
  const actions = run.actionsLog ?? [];
38591
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
+ };
38592
38831
  for (const metric of config.metrics) {
38593
38832
  if (metric.kind === "firstAction" || metric.kind === "firstActionPrefix") {
38594
38833
  const action = actions.find((item) => metric.kind === "firstAction" ? item.actionId === metric.actionId : item.actionId.startsWith(metric.prefix));
@@ -38598,12 +38837,12 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38598
38837
  else if (metric.kind === "actionCount")
38599
38838
  result[metric.id] = actions.length;
38600
38839
  else if (metric.kind === "endWallet")
38601
- result[metric.id] = Number(run.end.wallet.money.amount);
38840
+ result[metric.id] = monetary(run.end.wallet.money.amount);
38602
38841
  else {
38603
38842
  const worth = opened.scenario.model.netWorth?.(opened.scenario.ctx, run.end);
38604
38843
  if (!worth)
38605
38844
  throw new Error("endNetWorth requires model.netWorth; wallet is not a substitute");
38606
- result[metric.id] = Number(worth.amount);
38845
+ result[metric.id] = monetary(worth.amount);
38607
38846
  }
38608
38847
  }
38609
38848
  return result;
@@ -38661,7 +38900,7 @@ async function refreshBalanceWorkflow(configPath, flags, check = false) {
38661
38900
  `);
38662
38901
  return { id: variant.id, scenario: name };
38663
38902
  });
38664
- 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) + `
38665
38904
  `);
38666
38905
  }
38667
38906
  return artifacts;
@@ -38676,10 +38915,10 @@ function cell(value) {
38676
38915
  }
38677
38916
  function explain(result) {
38678
38917
  if (result.status === "breach" && result.value !== null) {
38679
- if (result.min !== undefined && result.value < result.min)
38680
- return `below the minimum by ${result.min - result.value} ${result.unit}`;
38681
- if (result.max !== undefined && result.value > result.max)
38682
- return `above the maximum by ${result.value - result.max} ${result.unit}`;
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}`;
38683
38922
  }
38684
38923
  if (result.status === "pass")
38685
38924
  return "within the inclusive target bounds";
@@ -38718,6 +38957,7 @@ var balance_default = defineCommand({
38718
38957
  description: "Refresh a typed CSV balance sheet into scenario, pacing checks, and result sheets",
38719
38958
  options: {
38720
38959
  ...pluginOptions(),
38960
+ engine: option(string2().min(1).optional(), { description: "Numeric engine (default: number; explicit: breakInfinity)" }),
38721
38961
  check: option(exports_coerce.boolean().default(false), { description: "Check result freshness without rerunning simulations" }),
38722
38962
  format: option(_enum(["json", "md"]).default("json"), { description: "JSON manifest or human-readable target explanations" })
38723
38963
  },
@@ -39174,6 +39414,7 @@ function compareIdentity2(args) {
39174
39414
  return {
39175
39415
  a: side(args.scenarios.a),
39176
39416
  b: side(args.scenarios.b),
39417
+ ...args.engineId && args.engineId !== "number" ? { engineId: args.engineId } : {},
39177
39418
  pluginDigests: pluginDigestValues(args.loaded.pluginDigest),
39178
39419
  metrics,
39179
39420
  bundle: flags.bundle ?? null,
@@ -39182,9 +39423,16 @@ function compareIdentity2(args) {
39182
39423
  };
39183
39424
  }
39184
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
+ };
39185
39431
  const runInput = args.compiled.fresh("economy");
39186
39432
  const run = runScenario(runInput);
39187
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");
39188
39436
  let etaSeconds;
39189
39437
  let etaReached;
39190
39438
  if (args.targetWorth) {
@@ -39193,7 +39441,12 @@ function measureScenario(args) {
39193
39441
  unit: etaInput.ctx.unit,
39194
39442
  suffix: { kind: "alphaInfinite", minLen: 2 }
39195
39443
  }).amount;
39196
- 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
+ };
39197
39450
  const etaRun = runScenario({
39198
39451
  ...etaInput,
39199
39452
  run: {
@@ -39475,6 +39728,7 @@ var compare_default = defineCommand({
39475
39728
  description: "Compare two scenarios via measured simulation metrics",
39476
39729
  options: {
39477
39730
  ...pluginOptions(),
39731
+ engine: option(string2().min(1).optional(), { description: "Numeric engine (default: number; explicit: breakInfinity)" }),
39478
39732
  duration: option(exports_coerce.number().optional(), { description: "Override durationSec" }),
39479
39733
  step: option(exports_coerce.number().optional(), { description: "Override stepSec" }),
39480
39734
  strategy: option(strategySchema, { description: "Override strategy id (greedy|planner|scripted)" }),
@@ -39530,7 +39784,8 @@ var compare_default = defineCommand({
39530
39784
  throw usageError("metric=timeToMilestone requires --milestone-key <key>");
39531
39785
  }
39532
39786
  const scenarios = { a: aScenario, b: bScenario };
39533
- const identity = compareIdentity2({ scenarios, flags, loaded });
39787
+ const engine = resolveEffectiveEngine({ requested: flags.engine });
39788
+ const identity = compareIdentity2({ scenarios, flags, loaded, engineId: engine.effectiveId });
39534
39789
  const effectiveSeed = flags.seed ?? defaultRunSeed({
39535
39790
  base: {
39536
39791
  command: "compare",
@@ -39550,9 +39805,12 @@ var compare_default = defineCommand({
39550
39805
  milestoneKey: undefined
39551
39806
  }
39552
39807
  },
39553
- 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
+ } }]
39554
39812
  });
39555
- const E = createNumberEngine();
39813
+ const E = engine.engine;
39556
39814
  const aCompiled = compileComparableScenario({
39557
39815
  scenario: aScenario,
39558
39816
  E,
@@ -42190,6 +42448,7 @@ var tune_default = defineCommand({
42190
42448
  description: "Tune strategy parameters with objective scoring",
42191
42449
  options: {
42192
42450
  ...pluginOptions(),
42451
+ engine: option(string2().min(1).optional(), { description: "Numeric engine (default: number; explicit: breakInfinity)" }),
42193
42452
  tune: option(string2().optional(), { description: "TuneSpec file path (.json|.yaml)" }),
42194
42453
  wizard: option(exports_coerce.boolean().default(false), {
42195
42454
  description: "Interactively generate a TuneSpec before tuning"
@@ -42245,11 +42504,14 @@ var tune_default = defineCommand({
42245
42504
  const tuneSpecInput = wizardResult?.tuneSpec ?? await readScenarioFile(tunePath);
42246
42505
  const baselinePath = flags["baseline-artifact"] ? resolve21(flags["baseline-artifact"]) : undefined;
42247
42506
  const baselineRaw = baselinePath ? await readJsonFile(baselinePath) : undefined;
42507
+ const engine = resolveEffectiveEngine({ requested: flags.engine });
42508
+ const engineIdentity = engine.effectiveId === "number" ? {} : { engineId: engine.effectiveId };
42248
42509
  const seed = flags.seed ?? deriveDeterministicSeed({
42249
42510
  command: "tune",
42250
42511
  scenario: scenarioInput,
42251
42512
  tuneSpec: tuneSpecInput,
42252
42513
  options: {
42514
+ ...engineIdentity,
42253
42515
  baselineArtifact: baselineRaw === undefined ? undefined : hashContent(baselineRaw),
42254
42516
  regressionTolerance: flags["regression-tolerance"]
42255
42517
  }
@@ -42257,7 +42519,7 @@ var tune_default = defineCommand({
42257
42519
  const runId = flags["run-id"] ?? deriveDeterministicRunId({
42258
42520
  command: "tune",
42259
42521
  seed,
42260
- scope: { scenario: scenarioInput, tuneSpec: tuneSpecInput, pluginDigests: Object.values(loaded.pluginDigest) }
42522
+ scope: { scenario: scenarioInput, tuneSpec: tuneSpecInput, pluginDigests: Object.values(loaded.pluginDigest), ...engineIdentity }
42261
42523
  });
42262
42524
  const outputMeta = buildOutputMeta({
42263
42525
  command: "tune",
@@ -42269,7 +42531,7 @@ var tune_default = defineCommand({
42269
42531
  pluginDigest: loaded.pluginDigest
42270
42532
  });
42271
42533
  const result = await cmdTune({
42272
- E: createNumberEngine(),
42534
+ E: engine.engine,
42273
42535
  scenarioInput,
42274
42536
  tuneSpecInput,
42275
42537
  modelRegistry: loaded.modelRegistry,
@@ -44186,4 +44448,4 @@ export {
44186
44448
  cli2 as cli
44187
44449
  };
44188
44450
 
44189
- //# debugId=A3CAAFDF1955C7F064756E2164756E21
44451
+ //# debugId=0936659FB5366DD764756E2164756E21