@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/README.md +25 -0
- package/dist/main.js +440 -178
- package/dist/main.js.map +21 -16
- package/dist/runtime/workers/entry.js +297 -25
- package/dist/runtime/workers/entry.js.map +12 -8
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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/
|
|
12976
|
-
function
|
|
12977
|
-
|
|
12978
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
13364
|
-
endNetWorthLog10
|
|
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 =
|
|
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
|
|
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 ?
|
|
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
|
|
19357
|
-
|
|
19358
|
-
|
|
19359
|
-
|
|
19360
|
-
|
|
19361
|
-
|
|
19362
|
-
|
|
19363
|
-
const
|
|
19364
|
-
|
|
19365
|
-
|
|
19366
|
-
|
|
19367
|
-
|
|
19368
|
-
|
|
19369
|
-
|
|
19370
|
-
|
|
19371
|
-
|
|
19372
|
-
|
|
19373
|
-
|
|
19374
|
-
|
|
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
|
|
19519
|
+
return s.initial.wallet.amount;
|
|
19380
19520
|
case "endNetWorth":
|
|
19381
|
-
return
|
|
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.
|
|
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.
|
|
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: "
|
|
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
|
-
|
|
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 :
|
|
37644
|
-
const max = raw.max === undefined ? undefined :
|
|
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 >
|
|
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
|
-
|
|
37727
|
-
|
|
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 &&
|
|
37730
|
-
return { ...target, value:
|
|
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
|
|
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 =
|
|
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:
|
|
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] =
|
|
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] =
|
|
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:
|
|
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
|
|
38680
|
-
return `below the minimum by ${result.min - result.value} ${result.unit}`;
|
|
38681
|
-
if (result.max !== undefined && result.value
|
|
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
|
-
|
|
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
|
|
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:
|
|
39808
|
+
runs: [{ identity, defaults: {
|
|
39809
|
+
...compareIdentity2({ scenarios, flags: { "max-duration": DEFAULT_MAX_DURATION }, loaded }),
|
|
39810
|
+
engineId: "number"
|
|
39811
|
+
} }]
|
|
39554
39812
|
});
|
|
39555
|
-
const E =
|
|
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:
|
|
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=
|
|
44451
|
+
//# debugId=0936659FB5366DD764756E2164756E21
|