@idlekit/core 0.2.0 → 0.3.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/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/scenario/compile.d.ts +25 -0
- package/dist/scenario/compile.js +60 -0
- package/dist/scenario/compile.js.map +1 -1
- package/dist/scenario/scenarioContract.d.ts +152 -0
- package/dist/scenario/scenarioContract.js +2815 -0
- package/dist/scenario/scenarioContract.js.map +1 -0
- package/dist/sim/analysis/analyzers.d.ts +245 -0
- package/dist/sim/analysis/analyzers.js +1076 -0
- package/dist/sim/analysis/analyzers.js.map +1 -0
- package/dist/sim/analysis/contracts.d.ts +147 -0
- package/dist/sim/analysis/contracts.js +1230 -0
- package/dist/sim/analysis/contracts.js.map +1 -0
- package/dist/sim/analysis/distribution.d.ts +199 -0
- package/dist/sim/analysis/distribution.js +1246 -0
- package/dist/sim/analysis/distribution.js.map +1 -0
- package/dist/sim/analysis/eta.d.ts +118 -0
- package/dist/sim/analysis/eta.js +968 -70
- package/dist/sim/analysis/eta.js.map +1 -1
- package/dist/sim/analysis/experiment.d.ts +300 -0
- package/dist/sim/analysis/experiment.js +2206 -0
- package/dist/sim/analysis/experiment.js.map +1 -0
- package/dist/sim/analysis/growth.d.ts +2 -0
- package/dist/sim/analysis/growth.js +5 -1
- package/dist/sim/analysis/growth.js.map +1 -1
- package/dist/sim/analysis/growthFit.d.ts +183 -0
- package/dist/sim/analysis/growthFit.js +1798 -0
- package/dist/sim/analysis/growthFit.js.map +1 -0
- package/dist/sim/analysis/inspect.d.ts +236 -0
- package/dist/sim/analysis/inspect.js +388 -0
- package/dist/sim/analysis/inspect.js.map +1 -0
- package/dist/sim/analysis/metrics.d.ts +91 -0
- package/dist/sim/analysis/metrics.js +266 -0
- package/dist/sim/analysis/metrics.js.map +1 -0
- package/dist/sim/analysis/prestigeCycle.d.ts +6 -0
- package/dist/sim/analysis/prestigeCycle.js +10 -1
- package/dist/sim/analysis/prestigeCycle.js.map +1 -1
- package/dist/sim/analysis/registry.d.ts +41 -0
- package/dist/sim/analysis/registry.js +54 -0
- package/dist/sim/analysis/registry.js.map +1 -0
- package/dist/sim/analysis/resetActions.d.ts +7 -0
- package/dist/sim/analysis/resetActions.js +45 -0
- package/dist/sim/analysis/resetActions.js.map +1 -0
- package/dist/sim/analysis/resetCycle.d.ts +175 -0
- package/dist/sim/analysis/resetCycle.js +1009 -0
- package/dist/sim/analysis/resetCycle.js.map +1 -0
- package/dist/sim/analysis/resetOptimization.d.ts +198 -0
- package/dist/sim/analysis/resetOptimization.js +2187 -0
- package/dist/sim/analysis/resetOptimization.js.map +1 -0
- package/dist/sim/analysis/sensitivity.d.ts +73 -0
- package/dist/sim/analysis/sensitivity.js +160 -0
- package/dist/sim/analysis/sensitivity.js.map +1 -0
- package/dist/sim/monteCarlo.d.ts +12 -0
- package/dist/sim/monteCarlo.js +33 -4
- package/dist/sim/monteCarlo.js.map +1 -1
- package/dist/sim/observation.d.ts +22 -0
- package/dist/sim/observation.js +33 -0
- package/dist/sim/observation.js.map +1 -1
- package/dist/sim/offline.js +9 -2
- package/dist/sim/offline.js.map +1 -1
- package/dist/sim/random.d.ts +15 -0
- package/dist/sim/random.js +28 -0
- package/dist/sim/random.js.map +1 -1
- package/dist/sim/session.d.ts +7 -0
- package/dist/sim/session.js +13 -0
- package/dist/sim/session.js.map +1 -1
- package/dist/sim/simulator.js +7 -1
- package/dist/sim/simulator.js.map +1 -1
- package/dist/sim/step.d.ts +67 -1
- package/dist/sim/step.js +100 -18
- package/dist/sim/step.js.map +1 -1
- package/dist/sim/strategy/opt/freshRun.d.ts +3 -0
- package/dist/sim/strategy/opt/freshRun.js +6 -0
- package/dist/sim/strategy/opt/freshRun.js.map +1 -0
- package/dist/sim/strategy/opt/runner.js +2 -2
- package/dist/sim/strategy/opt/runner.js.map +1 -1
- package/dist/sim/strategy/planner.d.ts +26 -0
- package/dist/sim/strategy/planner.js +30 -0
- package/dist/sim/strategy/planner.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import * as _validateReport_1 from "typia/lib/internal/_validateReport";
|
|
2
|
+
import * as _createStandardSchema_1 from "typia/lib/internal/_createStandardSchema";
|
|
3
|
+
import typia, {} from "typia";
|
|
4
|
+
import { recordPrestigeReset } from "../constraints";
|
|
5
|
+
import { emitActionFacts, emitCommittedResets } from "../observation";
|
|
6
|
+
import { runScenario } from "../simulator";
|
|
7
|
+
import { applyDecisions } from "../step";
|
|
8
|
+
import { runElapsedSec, } from "../types";
|
|
9
|
+
import { deepClonePreservingPrototype } from "../../utils/deepClone";
|
|
10
|
+
import { withoutResetActions } from "./resetActions";
|
|
11
|
+
/**
|
|
12
|
+
* Reset-cycle execution v1.
|
|
13
|
+
* `analyzePrestigeCycle` stays the legacy interval scan.
|
|
14
|
+
* This artifact does not copy that scan's stability or break-even fields.
|
|
15
|
+
*/
|
|
16
|
+
const RESET_CYCLE_ALGORITHM_VERSION = "reset-cycle-v1";
|
|
17
|
+
/**
|
|
18
|
+
* Rounding slack for the horizon check, in units of the magnitudes being added. Summed runner seconds carry a few
|
|
19
|
+
* ULPs of error, such as 0.1 three times against a 0.3 horizon. A real overshoot, even 5e-7 seconds past 1000, is
|
|
20
|
+
* far larger.
|
|
21
|
+
*/
|
|
22
|
+
const HORIZON_ULPS = 64;
|
|
23
|
+
/** The largest share of an interval the horizon slack may cover. A visible overshoot is never rounding. */
|
|
24
|
+
const HORIZON_SHARE = 1e-6;
|
|
25
|
+
const RESET_CYCLE_SCHEMA_ID = "https://idlekit.dev/schemas/reset.cycle.v1.schema.json";
|
|
26
|
+
/**
|
|
27
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset Each cycle commits a reset through the action payment path.
|
|
28
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset #22d7a8a Re-read the section: three cycles leave three resets, and the reset does not mint income.
|
|
29
|
+
*/
|
|
30
|
+
export const resetCycleExecution = "committed-reset-transition";
|
|
31
|
+
/**
|
|
32
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-refused-reset A refused cost, condition, or cooldown pays no reward.
|
|
33
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-refused-reset #5bbd1e4 Re-read the section: cannot-apply, insufficient funds, and cooldown are recorded, and the point stock stays put.
|
|
34
|
+
*/
|
|
35
|
+
export const resetCycleSameContract = "prestige-and-custom-reset";
|
|
36
|
+
/**
|
|
37
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-reward-delta Reward gained by the reset does not include the stock already held.
|
|
38
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-reward-delta #ed22025 Re-read the section: opening points of 100 stay out of the delta.
|
|
39
|
+
*/
|
|
40
|
+
export const resetCycleRewardDelta = "reward-delta-excludes-opening-stock";
|
|
41
|
+
export function resetCycleSchemaId() {
|
|
42
|
+
return RESET_CYCLE_SCHEMA_ID;
|
|
43
|
+
}
|
|
44
|
+
export function resetCycleAlgorithmVersion() {
|
|
45
|
+
return RESET_CYCLE_ALGORITHM_VERSION;
|
|
46
|
+
}
|
|
47
|
+
export class ResetCycleRejected extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "ResetCycleRejected";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/*
|
|
54
|
+
* typia runs on first call, not at module load. A plugin that the bundled CLI loads
|
|
55
|
+
* imports core source without the transform, and a module-load call would throw there.
|
|
56
|
+
*/
|
|
57
|
+
let resetCycleValidator;
|
|
58
|
+
/**
|
|
59
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset The generated validator accepts this artifact and rejects a missing status.
|
|
60
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset #22d7a8a Re-read the section: the wire object is reset cycle v1.
|
|
61
|
+
*/
|
|
62
|
+
export const validateResetCycleArtifact = (input) => (resetCycleValidator ??= (() => {
|
|
63
|
+
const _ve0 = "Readonly<{ schemaVersion: 1; algorithmVersion: \"reset-cycle-v1\"; status: ResetCycleStatus; policy: \"explicit\" | \"fixed-interval\"; appliedResets: number; originSec: number; globalElapsedSec: number; assumptions: readonly string[]; warnings: readonly string[]; cycles: readonly Readonly<...>[]; }>";
|
|
64
|
+
const _ve1 = "Readonly<{ index: number; startSec: number; endSec: number; resetAtSec: number | null; durationSec: number; rewardDelta: string; accumulatedReward: string; resetSkippedReason?: ResetSkipReason | undefined; carried: Readonly<...>; }>";
|
|
65
|
+
const _ve2 = "ReadonlyArray<Readonly<{ index: number; startSec: number; endSec: number; resetAtSec: number | null; durationSec: number; rewardDelta: string; accumulatedReward: string; resetSkippedReason?: ResetSkipReason | undefined; carried: Readonly<...>; }>>";
|
|
66
|
+
const _io0 = input => 1 === input.schemaVersion && "reset-cycle-v1" === input.algorithmVersion && ("censored" === input.status || "incomplete" === input.status || "ok" === input.status || "unsupported" === input.status) && ("explicit" === input.policy || "fixed-interval" === input.policy) && "number" === typeof input.appliedResets && "number" === typeof input.originSec && "number" === typeof input.globalElapsedSec && (Array.isArray(input.assumptions) && input.assumptions.every(elem => "string" === typeof elem)) && (Array.isArray(input.warnings) && input.warnings.every(elem => "string" === typeof elem)) && (Array.isArray(input.cycles) && input.cycles.every(elem => "object" === typeof elem && null !== elem && _io1(elem)));
|
|
67
|
+
const _io1 = input => "number" === typeof input.index && "number" === typeof input.startSec && "number" === typeof input.endSec && (null === input.resetAtSec || "number" === typeof input.resetAtSec) && "number" === typeof input.durationSec && "string" === typeof input.rewardDelta && "string" === typeof input.accumulatedReward && (undefined === input.resetSkippedReason || "budget" === input.resetSkippedReason || "cannot-apply" === input.resetSkippedReason || "cooldown" === input.resetSkippedReason || "insufficient-funds" === input.resetSkippedReason || "missing-adapter" === input.resetSkippedReason || "rejected" === input.resetSkippedReason) && ("object" === typeof input.carried && null !== input.carried && _io2(input.carried));
|
|
68
|
+
const _io2 = input => "string" === typeof input.points && "string" === typeof input.multiplier && "number" === typeof input.count && "string" === typeof input.reward;
|
|
69
|
+
const _vo0 = (input, _path, _exceptionable = true) => [1 === input.schemaVersion || _report(_exceptionable, {
|
|
70
|
+
path: _path + ".schemaVersion",
|
|
71
|
+
expected: "1",
|
|
72
|
+
value: input.schemaVersion
|
|
73
|
+
}), "reset-cycle-v1" === input.algorithmVersion || _report(_exceptionable, {
|
|
74
|
+
path: _path + ".algorithmVersion",
|
|
75
|
+
expected: "\"reset-cycle-v1\"",
|
|
76
|
+
value: input.algorithmVersion
|
|
77
|
+
}), "censored" === input.status || "incomplete" === input.status || "ok" === input.status || "unsupported" === input.status || _report(_exceptionable, {
|
|
78
|
+
path: _path + ".status",
|
|
79
|
+
expected: "(\"censored\" | \"incomplete\" | \"ok\" | \"unsupported\")",
|
|
80
|
+
value: input.status
|
|
81
|
+
}), "explicit" === input.policy || "fixed-interval" === input.policy || _report(_exceptionable, {
|
|
82
|
+
path: _path + ".policy",
|
|
83
|
+
expected: "(\"explicit\" | \"fixed-interval\")",
|
|
84
|
+
value: input.policy
|
|
85
|
+
}), "number" === typeof input.appliedResets || _report(_exceptionable, {
|
|
86
|
+
path: _path + ".appliedResets",
|
|
87
|
+
expected: "number",
|
|
88
|
+
value: input.appliedResets
|
|
89
|
+
}), "number" === typeof input.originSec || _report(_exceptionable, {
|
|
90
|
+
path: _path + ".originSec",
|
|
91
|
+
expected: "number",
|
|
92
|
+
value: input.originSec
|
|
93
|
+
}), "number" === typeof input.globalElapsedSec || _report(_exceptionable, {
|
|
94
|
+
path: _path + ".globalElapsedSec",
|
|
95
|
+
expected: "number",
|
|
96
|
+
value: input.globalElapsedSec
|
|
97
|
+
}), (Array.isArray(input.assumptions) || _report(_exceptionable, {
|
|
98
|
+
path: _path + ".assumptions",
|
|
99
|
+
expected: "ReadonlyArray<string>",
|
|
100
|
+
value: input.assumptions
|
|
101
|
+
})) && input.assumptions.map((elem, _index4) => "string" === typeof elem || _report(_exceptionable, {
|
|
102
|
+
path: _path + ".assumptions[" + _index4 + "]",
|
|
103
|
+
expected: "string",
|
|
104
|
+
value: elem
|
|
105
|
+
})).every(flag => flag) || _report(_exceptionable, {
|
|
106
|
+
path: _path + ".assumptions",
|
|
107
|
+
expected: "ReadonlyArray<string>",
|
|
108
|
+
value: input.assumptions
|
|
109
|
+
}), (Array.isArray(input.warnings) || _report(_exceptionable, {
|
|
110
|
+
path: _path + ".warnings",
|
|
111
|
+
expected: "ReadonlyArray<string>",
|
|
112
|
+
value: input.warnings
|
|
113
|
+
})) && input.warnings.map((elem, _index5) => "string" === typeof elem || _report(_exceptionable, {
|
|
114
|
+
path: _path + ".warnings[" + _index5 + "]",
|
|
115
|
+
expected: "string",
|
|
116
|
+
value: elem
|
|
117
|
+
})).every(flag => flag) || _report(_exceptionable, {
|
|
118
|
+
path: _path + ".warnings",
|
|
119
|
+
expected: "ReadonlyArray<string>",
|
|
120
|
+
value: input.warnings
|
|
121
|
+
}), (Array.isArray(input.cycles) || _report(_exceptionable, {
|
|
122
|
+
path: _path + ".cycles",
|
|
123
|
+
expected: _ve2,
|
|
124
|
+
value: input.cycles
|
|
125
|
+
})) && input.cycles.map((elem, _index6) => ("object" === typeof elem && null !== elem || _report(_exceptionable, {
|
|
126
|
+
path: _path + ".cycles[" + _index6 + "]",
|
|
127
|
+
expected: _ve1,
|
|
128
|
+
value: elem
|
|
129
|
+
})) && _vo1(elem, _path + ".cycles[" + _index6 + "]", true && _exceptionable) || _report(_exceptionable, {
|
|
130
|
+
path: _path + ".cycles[" + _index6 + "]",
|
|
131
|
+
expected: _ve1,
|
|
132
|
+
value: elem
|
|
133
|
+
})).every(flag => flag) || _report(_exceptionable, {
|
|
134
|
+
path: _path + ".cycles",
|
|
135
|
+
expected: _ve2,
|
|
136
|
+
value: input.cycles
|
|
137
|
+
})].every(flag => flag);
|
|
138
|
+
const _vo1 = (input, _path, _exceptionable = true) => ["number" === typeof input.index || _report(_exceptionable, {
|
|
139
|
+
path: _path + ".index",
|
|
140
|
+
expected: "number",
|
|
141
|
+
value: input.index
|
|
142
|
+
}), "number" === typeof input.startSec || _report(_exceptionable, {
|
|
143
|
+
path: _path + ".startSec",
|
|
144
|
+
expected: "number",
|
|
145
|
+
value: input.startSec
|
|
146
|
+
}), "number" === typeof input.endSec || _report(_exceptionable, {
|
|
147
|
+
path: _path + ".endSec",
|
|
148
|
+
expected: "number",
|
|
149
|
+
value: input.endSec
|
|
150
|
+
}), null === input.resetAtSec || "number" === typeof input.resetAtSec || _report(_exceptionable, {
|
|
151
|
+
path: _path + ".resetAtSec",
|
|
152
|
+
expected: "(null | number)",
|
|
153
|
+
value: input.resetAtSec
|
|
154
|
+
}), "number" === typeof input.durationSec || _report(_exceptionable, {
|
|
155
|
+
path: _path + ".durationSec",
|
|
156
|
+
expected: "number",
|
|
157
|
+
value: input.durationSec
|
|
158
|
+
}), "string" === typeof input.rewardDelta || _report(_exceptionable, {
|
|
159
|
+
path: _path + ".rewardDelta",
|
|
160
|
+
expected: "string",
|
|
161
|
+
value: input.rewardDelta
|
|
162
|
+
}), "string" === typeof input.accumulatedReward || _report(_exceptionable, {
|
|
163
|
+
path: _path + ".accumulatedReward",
|
|
164
|
+
expected: "string",
|
|
165
|
+
value: input.accumulatedReward
|
|
166
|
+
}), undefined === input.resetSkippedReason || "budget" === input.resetSkippedReason || "cannot-apply" === input.resetSkippedReason || "cooldown" === input.resetSkippedReason || "insufficient-funds" === input.resetSkippedReason || "missing-adapter" === input.resetSkippedReason || "rejected" === input.resetSkippedReason || _report(_exceptionable, {
|
|
167
|
+
path: _path + ".resetSkippedReason",
|
|
168
|
+
expected: "(\"budget\" | \"cannot-apply\" | \"cooldown\" | \"insufficient-funds\" | \"missing-adapter\" | \"rejected\" | undefined)",
|
|
169
|
+
value: input.resetSkippedReason
|
|
170
|
+
}), ("object" === typeof input.carried && null !== input.carried || _report(_exceptionable, {
|
|
171
|
+
path: _path + ".carried",
|
|
172
|
+
expected: "Readonly<{ points: string; multiplier: string; count: number; reward: string; }>",
|
|
173
|
+
value: input.carried
|
|
174
|
+
})) && _vo2(input.carried, _path + ".carried", true && _exceptionable) || _report(_exceptionable, {
|
|
175
|
+
path: _path + ".carried",
|
|
176
|
+
expected: "Readonly<{ points: string; multiplier: string; count: number; reward: string; }>",
|
|
177
|
+
value: input.carried
|
|
178
|
+
})].every(flag => flag);
|
|
179
|
+
const _vo2 = (input, _path, _exceptionable = true) => ["string" === typeof input.points || _report(_exceptionable, {
|
|
180
|
+
path: _path + ".points",
|
|
181
|
+
expected: "string",
|
|
182
|
+
value: input.points
|
|
183
|
+
}), "string" === typeof input.multiplier || _report(_exceptionable, {
|
|
184
|
+
path: _path + ".multiplier",
|
|
185
|
+
expected: "string",
|
|
186
|
+
value: input.multiplier
|
|
187
|
+
}), "number" === typeof input.count || _report(_exceptionable, {
|
|
188
|
+
path: _path + ".count",
|
|
189
|
+
expected: "number",
|
|
190
|
+
value: input.count
|
|
191
|
+
}), "string" === typeof input.reward || _report(_exceptionable, {
|
|
192
|
+
path: _path + ".reward",
|
|
193
|
+
expected: "string",
|
|
194
|
+
value: input.reward
|
|
195
|
+
})].every(flag => flag);
|
|
196
|
+
const __is = input => "object" === typeof input && null !== input && _io0(input);
|
|
197
|
+
let errors;
|
|
198
|
+
let _report;
|
|
199
|
+
return _createStandardSchema_1._createStandardSchema(input => {
|
|
200
|
+
if (false === __is(input)) {
|
|
201
|
+
errors = [];
|
|
202
|
+
_report = _validateReport_1._validateReport(errors);
|
|
203
|
+
((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input || _report(true, {
|
|
204
|
+
path: _path + "",
|
|
205
|
+
expected: _ve0,
|
|
206
|
+
value: input
|
|
207
|
+
})) && _vo0(input, _path + "", true) || _report(true, {
|
|
208
|
+
path: _path + "",
|
|
209
|
+
expected: _ve0,
|
|
210
|
+
value: input
|
|
211
|
+
}))(input, "$input", true);
|
|
212
|
+
const success = 0 === errors.length;
|
|
213
|
+
return success ? {
|
|
214
|
+
success,
|
|
215
|
+
data: input
|
|
216
|
+
} : {
|
|
217
|
+
success,
|
|
218
|
+
errors,
|
|
219
|
+
data: input
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
success: true,
|
|
224
|
+
data: input
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
})())(input);
|
|
228
|
+
function buildResetCycleJsonSchema() {
|
|
229
|
+
return {
|
|
230
|
+
components: {
|
|
231
|
+
schemas: {
|
|
232
|
+
"ReadonlyschemaVersion1algorithmVersionreset-cycle-v1statusResetCycleStatuspolicyexplicitfixed-intervalappliedResetsnumberoriginSecnumberglobalElapsedSecnumberassumptionsreadonlystringwarningsreadonlystringcyclesreadonlyReadonly---": {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {
|
|
235
|
+
schemaVersion: {
|
|
236
|
+
"const": 1
|
|
237
|
+
},
|
|
238
|
+
algorithmVersion: {
|
|
239
|
+
"const": "reset-cycle-v1"
|
|
240
|
+
},
|
|
241
|
+
status: {
|
|
242
|
+
$ref: "#/components/schemas/ResetCycleStatus"
|
|
243
|
+
},
|
|
244
|
+
policy: {
|
|
245
|
+
oneOf: [
|
|
246
|
+
{
|
|
247
|
+
"const": "explicit"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"const": "fixed-interval"
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
appliedResets: {
|
|
255
|
+
type: "number"
|
|
256
|
+
},
|
|
257
|
+
originSec: {
|
|
258
|
+
type: "number"
|
|
259
|
+
},
|
|
260
|
+
globalElapsedSec: {
|
|
261
|
+
type: "number"
|
|
262
|
+
},
|
|
263
|
+
assumptions: {
|
|
264
|
+
type: "array",
|
|
265
|
+
items: {
|
|
266
|
+
type: "string"
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
warnings: {
|
|
270
|
+
type: "array",
|
|
271
|
+
items: {
|
|
272
|
+
type: "string"
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
cycles: {
|
|
276
|
+
type: "array",
|
|
277
|
+
items: {
|
|
278
|
+
$ref: "#/components/schemas/ReadonlyindexnumberstartSecnumberendSecnumberresetAtSecnumbernulldurationSecnumberrewardDeltastringaccumulatedRewardstringresetSkippedReasonResetSkipReasonundefinedcarriedReadonly---"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
required: [
|
|
283
|
+
"schemaVersion",
|
|
284
|
+
"algorithmVersion",
|
|
285
|
+
"status",
|
|
286
|
+
"policy",
|
|
287
|
+
"appliedResets",
|
|
288
|
+
"originSec",
|
|
289
|
+
"globalElapsedSec",
|
|
290
|
+
"assumptions",
|
|
291
|
+
"warnings",
|
|
292
|
+
"cycles"
|
|
293
|
+
],
|
|
294
|
+
additionalProperties: false
|
|
295
|
+
},
|
|
296
|
+
ResetCycleStatus: {
|
|
297
|
+
oneOf: [
|
|
298
|
+
{
|
|
299
|
+
"const": "censored"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
"const": "incomplete"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"const": "ok"
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
"const": "unsupported"
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
"ReadonlyindexnumberstartSecnumberendSecnumberresetAtSecnumbernulldurationSecnumberrewardDeltastringaccumulatedRewardstringresetSkippedReasonResetSkipReasonundefinedcarriedReadonly---": {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
index: {
|
|
316
|
+
type: "number"
|
|
317
|
+
},
|
|
318
|
+
startSec: {
|
|
319
|
+
type: "number"
|
|
320
|
+
},
|
|
321
|
+
endSec: {
|
|
322
|
+
type: "number"
|
|
323
|
+
},
|
|
324
|
+
resetAtSec: {
|
|
325
|
+
oneOf: [
|
|
326
|
+
{
|
|
327
|
+
type: "null"
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
type: "number"
|
|
331
|
+
}
|
|
332
|
+
]
|
|
333
|
+
},
|
|
334
|
+
durationSec: {
|
|
335
|
+
type: "number"
|
|
336
|
+
},
|
|
337
|
+
rewardDelta: {
|
|
338
|
+
type: "string"
|
|
339
|
+
},
|
|
340
|
+
accumulatedReward: {
|
|
341
|
+
type: "string"
|
|
342
|
+
},
|
|
343
|
+
resetSkippedReason: {
|
|
344
|
+
oneOf: [
|
|
345
|
+
{
|
|
346
|
+
"const": "budget"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
"const": "cannot-apply"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
"const": "cooldown"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"const": "insufficient-funds"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"const": "missing-adapter"
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
"const": "rejected"
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
carried: {
|
|
366
|
+
$ref: "#/components/schemas/Readonlypointsstringmultiplierstringcountnumberrewardstring"
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
required: [
|
|
370
|
+
"index",
|
|
371
|
+
"startSec",
|
|
372
|
+
"endSec",
|
|
373
|
+
"resetAtSec",
|
|
374
|
+
"durationSec",
|
|
375
|
+
"rewardDelta",
|
|
376
|
+
"accumulatedReward",
|
|
377
|
+
"carried"
|
|
378
|
+
],
|
|
379
|
+
additionalProperties: false
|
|
380
|
+
},
|
|
381
|
+
Readonlypointsstringmultiplierstringcountnumberrewardstring: {
|
|
382
|
+
type: "object",
|
|
383
|
+
properties: {
|
|
384
|
+
points: {
|
|
385
|
+
type: "string"
|
|
386
|
+
},
|
|
387
|
+
multiplier: {
|
|
388
|
+
type: "string"
|
|
389
|
+
},
|
|
390
|
+
count: {
|
|
391
|
+
type: "number"
|
|
392
|
+
},
|
|
393
|
+
reward: {
|
|
394
|
+
type: "string"
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
required: [
|
|
398
|
+
"points",
|
|
399
|
+
"multiplier",
|
|
400
|
+
"count",
|
|
401
|
+
"reward"
|
|
402
|
+
],
|
|
403
|
+
additionalProperties: false
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
schema: {
|
|
408
|
+
type: "object",
|
|
409
|
+
properties: {
|
|
410
|
+
schemaVersion: {
|
|
411
|
+
"const": 1
|
|
412
|
+
},
|
|
413
|
+
algorithmVersion: {
|
|
414
|
+
"const": "reset-cycle-v1"
|
|
415
|
+
},
|
|
416
|
+
status: {
|
|
417
|
+
$ref: "#/components/schemas/ResetCycleStatus"
|
|
418
|
+
},
|
|
419
|
+
policy: {
|
|
420
|
+
oneOf: [
|
|
421
|
+
{
|
|
422
|
+
"const": "explicit"
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"const": "fixed-interval"
|
|
426
|
+
}
|
|
427
|
+
]
|
|
428
|
+
},
|
|
429
|
+
appliedResets: {
|
|
430
|
+
type: "number"
|
|
431
|
+
},
|
|
432
|
+
originSec: {
|
|
433
|
+
type: "number"
|
|
434
|
+
},
|
|
435
|
+
globalElapsedSec: {
|
|
436
|
+
type: "number"
|
|
437
|
+
},
|
|
438
|
+
assumptions: {
|
|
439
|
+
type: "array",
|
|
440
|
+
items: {
|
|
441
|
+
type: "string"
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
warnings: {
|
|
445
|
+
type: "array",
|
|
446
|
+
items: {
|
|
447
|
+
type: "string"
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
cycles: {
|
|
451
|
+
type: "array",
|
|
452
|
+
items: {
|
|
453
|
+
$ref: "#/components/schemas/ReadonlyindexnumberstartSecnumberendSecnumberresetAtSecnumbernulldurationSecnumberrewardDeltastringaccumulatedRewardstringresetSkippedReasonResetSkipReasonundefinedcarriedReadonly---"
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
required: [
|
|
458
|
+
"schemaVersion",
|
|
459
|
+
"algorithmVersion",
|
|
460
|
+
"status",
|
|
461
|
+
"policy",
|
|
462
|
+
"appliedResets",
|
|
463
|
+
"originSec",
|
|
464
|
+
"globalElapsedSec",
|
|
465
|
+
"assumptions",
|
|
466
|
+
"warnings",
|
|
467
|
+
"cycles"
|
|
468
|
+
],
|
|
469
|
+
additionalProperties: false
|
|
470
|
+
},
|
|
471
|
+
version: "3.1"
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
let resetCycleJsonSchemaUnit;
|
|
475
|
+
/**
|
|
476
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset The structural schema is OpenAPI 3.1 from typia.
|
|
477
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset #22d7a8a Re-read the section: schema version is 3.1.
|
|
478
|
+
*/
|
|
479
|
+
export function resetCycleJsonSchema() {
|
|
480
|
+
resetCycleJsonSchemaUnit ??= buildResetCycleJsonSchema();
|
|
481
|
+
return resetCycleJsonSchemaUnit;
|
|
482
|
+
}
|
|
483
|
+
function assertPositiveInt(label, value) {
|
|
484
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
485
|
+
throw new ResetCycleRejected(`reset cycle ${label} must be a positive integer`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function assertNonNegativeInt(label, value) {
|
|
489
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
490
|
+
throw new ResetCycleRejected(`reset cycle ${label} must be an integer >= 0`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/** The runner treats a duration at or below this as already complete, so such an interval would play no time. */
|
|
494
|
+
const RUNNER_MIN_DURATION_SEC = 1e-12;
|
|
495
|
+
function assertPositiveTime(label, value) {
|
|
496
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !(value > 0)) {
|
|
497
|
+
throw new ResetCycleRejected(`reset cycle ${label} must be a finite number > 0`);
|
|
498
|
+
}
|
|
499
|
+
if (value <= RUNNER_MIN_DURATION_SEC) {
|
|
500
|
+
throw new ResetCycleRejected(`reset cycle ${label} must be greater than the runner's ${RUNNER_MIN_DURATION_SEC} second resolution`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/** Adapters this module built from the model. A caller's adapter may reuse their ids, so identity decides. */
|
|
504
|
+
const builtinActionAdapters = new WeakSet();
|
|
505
|
+
const builtinSpecAdapters = new WeakSet();
|
|
506
|
+
function actionAdapter(model, kind) {
|
|
507
|
+
const adapter = {
|
|
508
|
+
id: kind === "prestige" ? "prestige.action" : "custom.action",
|
|
509
|
+
kind,
|
|
510
|
+
preserved: ["points", "multiplier", "count"],
|
|
511
|
+
action(ctx, state) {
|
|
512
|
+
return model.actions(ctx, state).find((candidate) => candidate.kind === kind);
|
|
513
|
+
},
|
|
514
|
+
reward(_ctx, state) {
|
|
515
|
+
return state.prestige.points;
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
builtinActionAdapters.add(adapter);
|
|
519
|
+
return adapter;
|
|
520
|
+
}
|
|
521
|
+
function specAdapter(model) {
|
|
522
|
+
const spec = model.prestige;
|
|
523
|
+
if (!spec)
|
|
524
|
+
return undefined;
|
|
525
|
+
const adapter = {
|
|
526
|
+
id: "prestige.spec",
|
|
527
|
+
kind: "prestige",
|
|
528
|
+
preserved: ["points", "multiplier", "count"],
|
|
529
|
+
action(ctx, state) {
|
|
530
|
+
return {
|
|
531
|
+
id: "prestige.spec",
|
|
532
|
+
kind: "prestige",
|
|
533
|
+
canApply: () => true,
|
|
534
|
+
cost: () => null,
|
|
535
|
+
apply: (applyCtx, applyState) => {
|
|
536
|
+
const gained = spec.pointsFrom(applyCtx, applyState);
|
|
537
|
+
return spec.resetState(applyCtx, applyState, gained);
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
reward(_ctx, state) {
|
|
542
|
+
return state.prestige.points;
|
|
543
|
+
},
|
|
544
|
+
};
|
|
545
|
+
builtinSpecAdapters.add(adapter);
|
|
546
|
+
return adapter;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Prestige action wins when both an action and a spec exist.
|
|
550
|
+
* The spec functions are not called on that path, so the bonus is not applied twice.
|
|
551
|
+
* A model with neither needs the caller to pass an adapter.
|
|
552
|
+
*
|
|
553
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset The default adapter uses the prestige action, or the spec when no action exists.
|
|
554
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset #22d7a8a Re-read the section: an action and a spec are not both applied.
|
|
555
|
+
*/
|
|
556
|
+
export function resetAdapterFor(scenario, explicit, state = scenario.initial, ctx = scenario.ctx) {
|
|
557
|
+
if (explicit)
|
|
558
|
+
return explicit;
|
|
559
|
+
// Read the state and context where the reset would happen. A model can list its prestige action only later,
|
|
560
|
+
// or only under the live constraints.
|
|
561
|
+
const discovered = scenario.model.actions(ctx, state);
|
|
562
|
+
if (discovered.some((candidate) => candidate.kind === "prestige")) {
|
|
563
|
+
return actionAdapter(scenario.model, "prestige");
|
|
564
|
+
}
|
|
565
|
+
return specAdapter(scenario.model);
|
|
566
|
+
}
|
|
567
|
+
function skipReason(fact, eventReason) {
|
|
568
|
+
if (eventReason === "cannotApply")
|
|
569
|
+
return "cannot-apply";
|
|
570
|
+
if (eventReason === "insufficientFunds")
|
|
571
|
+
return "insufficient-funds";
|
|
572
|
+
if (eventReason === "cooldown")
|
|
573
|
+
return "cooldown";
|
|
574
|
+
if (!fact)
|
|
575
|
+
return "rejected";
|
|
576
|
+
return "rejected";
|
|
577
|
+
}
|
|
578
|
+
function carried(E, state, reward) {
|
|
579
|
+
return {
|
|
580
|
+
points: E.toString(state.prestige.points),
|
|
581
|
+
multiplier: E.toString(state.prestige.multiplier),
|
|
582
|
+
count: state.prestige.count,
|
|
583
|
+
reward: E.toString(reward),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Run a fixed interval or an explicit list of reset durations.
|
|
588
|
+
* Each completed cycle ends in one committed reset. A refused reset is kept and not forced.
|
|
589
|
+
*
|
|
590
|
+
* @evidence docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset The executor chains cycle end into the next cycle start.
|
|
591
|
+
* @evidenceReview docs/requirements/active/reset-cycle-execution.md#req-pr10-committed-reset #22d7a8a Re-read the section: carried points and multiplier are the next cycle's start, and a split resume matches one run.
|
|
592
|
+
*/
|
|
593
|
+
export function executeResetCycles(args) {
|
|
594
|
+
const { scenario } = args;
|
|
595
|
+
const { E } = scenario.ctx;
|
|
596
|
+
if (args.policy.kind === "fixed-interval") {
|
|
597
|
+
assertPositiveInt("cycles", args.policy.cycles);
|
|
598
|
+
assertPositiveTime("intervalSec", args.policy.intervalSec);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
assertPositiveInt("steps", args.policy.steps.length);
|
|
602
|
+
for (const step of args.policy.steps)
|
|
603
|
+
assertPositiveTime("intervalSec", step.durationSec);
|
|
604
|
+
}
|
|
605
|
+
// maxCycles is a cap, like horizonSec and budgetSteps, so 0 censors the run before any reset.
|
|
606
|
+
if (args.maxCycles !== undefined)
|
|
607
|
+
assertNonNegativeInt("maxCycles", args.maxCycles);
|
|
608
|
+
if (args.horizonSec !== undefined && (typeof args.horizonSec !== "number" || !Number.isFinite(args.horizonSec) || args.horizonSec < 0)) {
|
|
609
|
+
throw new ResetCycleRejected("reset cycle horizonSec must be a finite number >= 0");
|
|
610
|
+
}
|
|
611
|
+
if (args.budgetSteps !== undefined &&
|
|
612
|
+
(typeof args.budgetSteps !== "number" || !Number.isInteger(args.budgetSteps) || args.budgetSteps < 0)) {
|
|
613
|
+
throw new ResetCycleRejected("reset cycle budgetSteps must be an integer >= 0");
|
|
614
|
+
}
|
|
615
|
+
// A checkpoint from another contract or version is not read as this one.
|
|
616
|
+
if (args.resume &&
|
|
617
|
+
(args.resume.contract !== "idlekit.reset-cycle-checkpoint" || args.resume.version !== 1)) {
|
|
618
|
+
throw new ResetCycleRejected("reset cycle resume needs an idlekit.reset-cycle-checkpoint version 1");
|
|
619
|
+
}
|
|
620
|
+
// A state belongs to the model that produced it. Another model would read it under different rules.
|
|
621
|
+
if (args.resume &&
|
|
622
|
+
(args.resume.modelId !== undefined || args.resume.modelVersion !== undefined) &&
|
|
623
|
+
(args.resume.modelId !== scenario.model.id || args.resume.modelVersion !== scenario.model.version)) {
|
|
624
|
+
throw new ResetCycleRejected(`reset cycle checkpoint belongs to model ${String(args.resume.modelId)}@${String(args.resume.modelVersion)}, not ${scenario.model.id}@${scenario.model.version}`);
|
|
625
|
+
}
|
|
626
|
+
// accumulatedReward sums one adapter's reward. Another adapter would add a different metric to it.
|
|
627
|
+
if (args.resume &&
|
|
628
|
+
(args.resume.adapterId !== args.adapter?.id || args.resume.adapterKind !== args.adapter?.kind)) {
|
|
629
|
+
throw new ResetCycleRejected(`reset cycle checkpoint accumulates adapter ${String(args.resume.adapterId)}, not ${String(args.adapter?.id)}`);
|
|
630
|
+
}
|
|
631
|
+
const originSec = args.resume?.originSec ?? args.resume?.state.t ?? scenario.initial.t;
|
|
632
|
+
// A guard can stop before the runner validates the clock, so the clock fields are checked here.
|
|
633
|
+
const clockFields = [originSec, args.resume?.state.t ?? scenario.initial.t, args.resume?.elapsedSec ?? 0];
|
|
634
|
+
if (clockFields.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
635
|
+
throw new ResetCycleRejected("reset cycle clock fields must be finite numbers");
|
|
636
|
+
}
|
|
637
|
+
// Resume from copies, so in-place edits during the run cannot rewrite the checkpoint.
|
|
638
|
+
let state = args.resume ? deepClonePreservingPrototype(args.resume.state) : scenario.initial;
|
|
639
|
+
let constraints = args.resume?.constraints
|
|
640
|
+
? deepClonePreservingPrototype(args.resume.constraints)
|
|
641
|
+
: scenario.constraints;
|
|
642
|
+
// An RNG snapshot is opaque, so it is only checkpointed and restored under a named, versioned generator.
|
|
643
|
+
// The id must be nonempty and the version finite, or the recorded identity could not match itself on resume.
|
|
644
|
+
if (args.rng &&
|
|
645
|
+
(typeof args.rng.id !== "string" ||
|
|
646
|
+
args.rng.id.length === 0 ||
|
|
647
|
+
typeof args.rng.stateVersion !== "number" ||
|
|
648
|
+
!Number.isFinite(args.rng.stateVersion))) {
|
|
649
|
+
throw new ResetCycleRejected("reset cycle rng needs an id and a stateVersion");
|
|
650
|
+
}
|
|
651
|
+
let accumulated = args.resume?.accumulatedReward ?? E.zero();
|
|
652
|
+
let nextIndex = args.resume?.nextIndex ?? 0;
|
|
653
|
+
// Every checkpoint field is checked before any restore hook runs, so a rejected resume leaves the caller's strategy
|
|
654
|
+
// and RNG where they were.
|
|
655
|
+
if (typeof nextIndex !== "number" || !Number.isInteger(nextIndex) || nextIndex < 0) {
|
|
656
|
+
throw new ResetCycleRejected("reset cycle checkpoint nextIndex must be an integer >= 0");
|
|
657
|
+
}
|
|
658
|
+
// Elapsed seconds are summed from runner results. Absolute t can round away from simulated time.
|
|
659
|
+
let elapsed = args.resume?.elapsedSec ?? state.t - originSec;
|
|
660
|
+
// Elapsed seconds count forward from the origin. A negative count would let a resume pass the horizon.
|
|
661
|
+
if (elapsed < 0)
|
|
662
|
+
throw new ResetCycleRejected("reset cycle checkpoint elapsedSec must be >= 0");
|
|
663
|
+
if (!E.isFinite(accumulated))
|
|
664
|
+
throw new ResetCycleRejected("reset cycle checkpoint accumulatedReward is not finite");
|
|
665
|
+
// Seconds a budget already played of the next cycle. That cycle keeps its index and plays only the rest.
|
|
666
|
+
let pending = args.resume?.pendingSec ?? 0;
|
|
667
|
+
if (typeof pending !== "number" || !Number.isFinite(pending) || pending < 0) {
|
|
668
|
+
throw new ResetCycleRejected("reset cycle checkpoint pendingSec must be a finite number >= 0");
|
|
669
|
+
}
|
|
670
|
+
// A resumed cut cycle starts where it started before the cut. Older checkpoints place it pendingSec before t.
|
|
671
|
+
const resumedStart = args.resume?.pendingStartSec;
|
|
672
|
+
if (pending > 0 && resumedStart !== undefined && (typeof resumedStart !== "number" || !Number.isFinite(resumedStart))) {
|
|
673
|
+
throw new ResetCycleRejected("reset cycle checkpoint pendingStartSec must be a finite number");
|
|
674
|
+
}
|
|
675
|
+
const resumedDuration = args.resume?.pendingDurationSec;
|
|
676
|
+
if (pending > 0 &&
|
|
677
|
+
resumedDuration !== undefined &&
|
|
678
|
+
(typeof resumedDuration !== "number" || !Number.isFinite(resumedDuration) || !(resumedDuration > 0))) {
|
|
679
|
+
throw new ResetCycleRejected("reset cycle checkpoint pendingDurationSec must be a finite number > 0");
|
|
680
|
+
}
|
|
681
|
+
// A cut cycle has played less than its planned duration, or it would have reset before the cut.
|
|
682
|
+
if (pending > 0 && resumedDuration !== undefined && pending >= resumedDuration) {
|
|
683
|
+
throw new ResetCycleRejected("reset cycle checkpoint pendingSec must be less than pendingDurationSec");
|
|
684
|
+
}
|
|
685
|
+
// A checkpoint that runs no cycle is written again with the same cut-cycle metadata.
|
|
686
|
+
let pendingDuration = pending > 0 ? resumedDuration : undefined;
|
|
687
|
+
let pendingStart = pending > 0 ? (resumedStart ?? state.t - pending) : undefined;
|
|
688
|
+
// A saved snapshot needs something to restore it into. Dropping it would resume on a different stream.
|
|
689
|
+
if (args.resume && "strategyState" in args.resume) {
|
|
690
|
+
if (!scenario.strategy?.restoreState) {
|
|
691
|
+
throw new ResetCycleRejected("reset cycle checkpoint has strategyState, but the strategy has no restoreState");
|
|
692
|
+
}
|
|
693
|
+
// An opaque snapshot belongs to the strategy, and the state version, that wrote it.
|
|
694
|
+
if (args.resume.strategyId !== scenario.strategy.id ||
|
|
695
|
+
args.resume.strategyStateVersion !== scenario.strategy.stateVersion) {
|
|
696
|
+
throw new ResetCycleRejected(`reset cycle checkpoint strategyState was written by ${String(args.resume.strategyId)}@${String(args.resume.strategyStateVersion)}, not ${String(scenario.strategy.id)}@${String(scenario.strategy.stateVersion)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else if (args.resume && scenario.strategy?.snapshotState && scenario.strategy.restoreState) {
|
|
700
|
+
// A restorable strategy resumed without its snapshot would start from its initial state.
|
|
701
|
+
throw new ResetCycleRejected("reset cycle checkpoint has no strategyState for a strategy that restores one");
|
|
702
|
+
}
|
|
703
|
+
if (args.resume && "rngState" in args.resume) {
|
|
704
|
+
if (!args.rng)
|
|
705
|
+
throw new ResetCycleRejected("reset cycle checkpoint has rngState, but no rng was passed");
|
|
706
|
+
// An opaque RNG snapshot belongs to the generator, and the state version, that wrote it. A snapshot without them
|
|
707
|
+
// is ambiguous and is rejected.
|
|
708
|
+
if (args.resume.rngId !== args.rng.id || args.resume.rngStateVersion !== args.rng.stateVersion) {
|
|
709
|
+
throw new ResetCycleRejected(`reset cycle checkpoint rngState was written by ${String(args.resume.rngId)}@${String(args.resume.rngStateVersion)}, not ${String(args.rng.id)}@${String(args.rng.stateVersion)}`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Restore from copies, so a restore that keeps or edits its argument cannot change the checkpoint.
|
|
713
|
+
// Presence is the key, not the value: an undefined snapshot is still restored.
|
|
714
|
+
if (args.resume && "strategyState" in args.resume) {
|
|
715
|
+
scenario.strategy?.restoreState?.call(scenario.strategy, deepClonePreservingPrototype(args.resume.strategyState));
|
|
716
|
+
}
|
|
717
|
+
if (args.resume && "rngState" in args.resume) {
|
|
718
|
+
args.rng?.restore(deepClonePreservingPrototype(args.resume.rngState));
|
|
719
|
+
}
|
|
720
|
+
// The context the runner and the boundary action see: the scenario context with the live constraints.
|
|
721
|
+
const liveCtx = () => ({ ...scenario.ctx, ...(constraints ? { constraints } : {}) });
|
|
722
|
+
const startAdapter = resetAdapterFor(scenario, args.adapter, state, liveCtx());
|
|
723
|
+
const warnings = ["legacy interval scan fields are not reset evidence"];
|
|
724
|
+
if (scenario.strategy?.snapshotState && !scenario.strategy.restoreState) {
|
|
725
|
+
warnings.push("strategy state is not checkpointed: snapshotState has no restoreState");
|
|
726
|
+
}
|
|
727
|
+
const requested = args.policy.kind === "fixed-interval" ? args.policy.cycles : args.policy.steps.length;
|
|
728
|
+
const rows = [];
|
|
729
|
+
let guard = "ok";
|
|
730
|
+
let remainingBudget = args.budgetSteps;
|
|
731
|
+
const rewardOf = (adapter, current) => adapter ? adapter.reward(liveCtx(), current) : current.prestige.points;
|
|
732
|
+
while (rows.length < requested && guard === "ok") {
|
|
733
|
+
if (args.maxCycles !== undefined && rows.length >= args.maxCycles) {
|
|
734
|
+
guard = "censored";
|
|
735
|
+
warnings.push("max-cycles");
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
const duration = args.policy.kind === "fixed-interval" ? args.policy.intervalSec : args.policy.steps[rows.length].durationSec;
|
|
739
|
+
// horizonSec is elapsed time from the study origin, not an absolute t.
|
|
740
|
+
// The slack covers the rounding of this addition only: a few ULPs of the magnitudes involved, so a zero horizon
|
|
741
|
+
// admits no positive interval and a long horizon admits no real overshoot.
|
|
742
|
+
// It also stays below a millionth of the interval, so at a large horizon, where the ULP-based bound grows to
|
|
743
|
+
// seconds, a real overshoot of the interval is still censored.
|
|
744
|
+
const slack = args.horizonSec === undefined
|
|
745
|
+
? 0
|
|
746
|
+
: Math.min(HORIZON_ULPS * Number.EPSILON * Math.max(Math.abs(elapsed) + Math.abs(duration), Math.abs(args.horizonSec)), duration * HORIZON_SHARE);
|
|
747
|
+
// A cut cycle resumes under the duration it was planned with. Another duration would change where it resets.
|
|
748
|
+
if (pending > 0 && args.resume?.pendingDurationSec !== undefined && duration !== args.resume.pendingDurationSec) {
|
|
749
|
+
throw new ResetCycleRejected(`reset cycle checkpoint cut a ${args.resume.pendingDurationSec}-second cycle, not a ${duration}-second one`);
|
|
750
|
+
}
|
|
751
|
+
// An older checkpoint without pendingDurationSec is checked against the planned duration here.
|
|
752
|
+
if (pending > 0 && pending >= duration) {
|
|
753
|
+
throw new ResetCycleRejected(`reset cycle checkpoint played ${pending} seconds of a ${duration}-second cycle`);
|
|
754
|
+
}
|
|
755
|
+
const remainingSec = duration - pending;
|
|
756
|
+
if (args.horizonSec !== undefined && elapsed + remainingSec > args.horizonSec + slack) {
|
|
757
|
+
guard = "censored";
|
|
758
|
+
warnings.push("horizon");
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
const startSec = pending > 0 && pendingStart !== undefined ? pendingStart : state.t;
|
|
762
|
+
const intervalAdapter = args.adapter ?? resetAdapterFor(scenario, undefined, state, liveCtx()) ?? startAdapter;
|
|
763
|
+
const { maxSteps: _ignoredMax, until: _ignoredUntil, trace: _ignoredTrace, ...runRest } = scenario.run;
|
|
764
|
+
const run = runScenario({
|
|
765
|
+
...scenario,
|
|
766
|
+
model: withoutResetActions(scenario.model, intervalAdapter),
|
|
767
|
+
initial: state,
|
|
768
|
+
...(constraints ? { constraints } : {}),
|
|
769
|
+
run: {
|
|
770
|
+
...runRest,
|
|
771
|
+
durationSec: remainingSec,
|
|
772
|
+
...(remainingBudget !== undefined ? { maxSteps: remainingBudget } : {}),
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
if (remainingBudget !== undefined)
|
|
776
|
+
remainingBudget = Math.max(0, remainingBudget - (run.stop?.steps ?? 0));
|
|
777
|
+
// The runner's elapsed seconds, not an end.t difference that rounds at a large t.
|
|
778
|
+
const playedNow = runElapsedSec(run);
|
|
779
|
+
elapsed += playedNow;
|
|
780
|
+
// The whole cycle's seconds, including what an earlier budget-cut call played.
|
|
781
|
+
const played = pending + playedNow;
|
|
782
|
+
pending = 0;
|
|
783
|
+
state = run.end;
|
|
784
|
+
// nextBoundary checks the duration before the budget, so a budget stop never completed the interval.
|
|
785
|
+
const shortBudget = run.stop?.reason === "budget";
|
|
786
|
+
if (shortBudget) {
|
|
787
|
+
rows.push({
|
|
788
|
+
index: nextIndex,
|
|
789
|
+
startSec,
|
|
790
|
+
endSec: state.t,
|
|
791
|
+
resetAtSec: null,
|
|
792
|
+
durationSec: played,
|
|
793
|
+
rewardDelta: E.toString(E.zero()),
|
|
794
|
+
accumulatedReward: E.toString(accumulated),
|
|
795
|
+
resetSkippedReason: "budget",
|
|
796
|
+
carried: carried(E, state, rewardOf(intervalAdapter, state)),
|
|
797
|
+
});
|
|
798
|
+
// The cycle is not finished. A resume continues it under the same index for the remaining seconds.
|
|
799
|
+
pending = played;
|
|
800
|
+
pendingDuration = duration;
|
|
801
|
+
pendingStart = startSec;
|
|
802
|
+
guard = "censored";
|
|
803
|
+
warnings.push("budget");
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
// Resolve the adapter where the reset happens. A prestige action can appear only after this interval.
|
|
807
|
+
// The boundary action sees the live constraints, as it would inside runScenario, and so does discovery.
|
|
808
|
+
const boundaryCtx = liveCtx();
|
|
809
|
+
const adapter = args.adapter ?? resetAdapterFor(scenario, undefined, state, boundaryCtx);
|
|
810
|
+
if (!adapter) {
|
|
811
|
+
rows.push({
|
|
812
|
+
index: nextIndex,
|
|
813
|
+
startSec,
|
|
814
|
+
endSec: state.t,
|
|
815
|
+
resetAtSec: null,
|
|
816
|
+
durationSec: played,
|
|
817
|
+
rewardDelta: E.toString(E.zero()),
|
|
818
|
+
accumulatedReward: E.toString(accumulated),
|
|
819
|
+
resetSkippedReason: "missing-adapter",
|
|
820
|
+
carried: carried(E, state, state.prestige.points),
|
|
821
|
+
});
|
|
822
|
+
nextIndex += 1;
|
|
823
|
+
guard = "unsupported";
|
|
824
|
+
warnings.push("explicit adapter required");
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
const specNote = "prestige spec was not applied because a prestige action commits the reset";
|
|
828
|
+
if (builtinActionAdapters.has(adapter) && adapter.kind === "prestige" && scenario.model.prestige && !warnings.includes(specNote)) {
|
|
829
|
+
warnings.push(specNote);
|
|
830
|
+
}
|
|
831
|
+
const beforeReward = adapter.reward(boundaryCtx, state);
|
|
832
|
+
if (!E.isFinite(beforeReward))
|
|
833
|
+
throw new ResetCycleRejected(`reset adapter ${adapter.id} reward is not finite`);
|
|
834
|
+
const action = adapter.action(boundaryCtx, state);
|
|
835
|
+
// A reset is a prestige or custom action. Another kind would be paid and applied without a reset fact.
|
|
836
|
+
if (action && action.kind !== "prestige" && action.kind !== "custom") {
|
|
837
|
+
throw new ResetCycleRejected(`reset adapter ${adapter.id} returned a ${action.kind} action, not a reset`);
|
|
838
|
+
}
|
|
839
|
+
const kinds = new Set();
|
|
840
|
+
if (action)
|
|
841
|
+
kinds.add(action.kind);
|
|
842
|
+
const applied = applyDecisions({
|
|
843
|
+
ctx: boundaryCtx,
|
|
844
|
+
model: scenario.model,
|
|
845
|
+
state,
|
|
846
|
+
decisions: action ? [{ action }] : [],
|
|
847
|
+
constraints,
|
|
848
|
+
resetKinds: kinds,
|
|
849
|
+
// The boundary happens at the executor's summed elapsed seconds, not at a rounded reward timestamp.
|
|
850
|
+
// It stays within the segment's wall end, as wallNow keeps a step's reset there.
|
|
851
|
+
...(boundaryCtx.clocks
|
|
852
|
+
? {
|
|
853
|
+
wallT: Number.isFinite(boundaryCtx.clocks.wallEndT)
|
|
854
|
+
? Math.min(boundaryCtx.clocks.wallT + elapsed, boundaryCtx.clocks.wallEndT)
|
|
855
|
+
: boundaryCtx.clocks.wallT + elapsed,
|
|
856
|
+
}
|
|
857
|
+
: {}),
|
|
858
|
+
// The adapter built this action for the current state. A spec or custom adapter action is not in model.actions.
|
|
859
|
+
resolveAgainstModel: false,
|
|
860
|
+
});
|
|
861
|
+
// The boundary decision is an action fact too, applied or refused, as it would be inside runScenario, and its
|
|
862
|
+
// events reach the scenario emitter as a step's events do. They are announced only once the boundary is valid,
|
|
863
|
+
// so a rejected reset leaves no applied action behind with observers.
|
|
864
|
+
const observing = scenario.run.observation?.enabled !== false;
|
|
865
|
+
const boundaryT = state.t;
|
|
866
|
+
const announce = () => {
|
|
867
|
+
if (observing)
|
|
868
|
+
emitActionFacts(scenario.run.observer, boundaryT, applied.events);
|
|
869
|
+
if (applied.events.length > 0)
|
|
870
|
+
boundaryCtx.emit?.(applied.events);
|
|
871
|
+
};
|
|
872
|
+
const skipped = applied.events.find((event) => event.type === "action.skipped");
|
|
873
|
+
const fact = applied.committedResets[0];
|
|
874
|
+
if (!fact || skipped) {
|
|
875
|
+
announce();
|
|
876
|
+
const reason = skipReason(fact, skipped && skipped.type === "action.skipped" ? skipped.reason : undefined);
|
|
877
|
+
rows.push({
|
|
878
|
+
index: nextIndex,
|
|
879
|
+
startSec,
|
|
880
|
+
endSec: state.t,
|
|
881
|
+
resetAtSec: null,
|
|
882
|
+
durationSec: played,
|
|
883
|
+
rewardDelta: E.toString(E.zero()),
|
|
884
|
+
accumulatedReward: E.toString(accumulated),
|
|
885
|
+
// An adapter is present here. An action it cannot offer now is a refusal, not a missing adapter.
|
|
886
|
+
resetSkippedReason: action ? reason : "cannot-apply",
|
|
887
|
+
carried: carried(E, state, beforeReward),
|
|
888
|
+
});
|
|
889
|
+
nextIndex += 1;
|
|
890
|
+
guard = "incomplete";
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
// The boundary does not advance time. A reset that moved t would split the clock from the elapsed seconds.
|
|
894
|
+
if (applied.state.t !== state.t) {
|
|
895
|
+
throw new ResetCycleRejected(`reset action ${String(action?.id)} changed t from ${state.t} to ${applied.state.t}`);
|
|
896
|
+
}
|
|
897
|
+
state = applied.state;
|
|
898
|
+
// The carried prestige fields are written to the row and the checkpoint, so they must stay finite.
|
|
899
|
+
if (!Number.isFinite(state.prestige.count) ||
|
|
900
|
+
!E.isFinite(state.prestige.points) ||
|
|
901
|
+
!E.isFinite(state.prestige.multiplier)) {
|
|
902
|
+
throw new ResetCycleRejected(`reset action ${String(action?.id)} left nonfinite prestige fields`);
|
|
903
|
+
}
|
|
904
|
+
// stepOnce keeps maxMoneyEver at or above the wallet. The boundary action does not pass through it.
|
|
905
|
+
if (E.cmp(state.wallet.money.amount, state.maxMoneyEver.amount) > 0) {
|
|
906
|
+
state = { ...state, maxMoneyEver: state.wallet.money };
|
|
907
|
+
}
|
|
908
|
+
const afterReward = adapter.reward(boundaryCtx, state);
|
|
909
|
+
const delta = E.sub(afterReward, beforeReward);
|
|
910
|
+
if (!E.isFinite(afterReward) || !E.isFinite(delta)) {
|
|
911
|
+
throw new ResetCycleRejected(`reset adapter ${adapter.id} reward is not finite after the reset`);
|
|
912
|
+
}
|
|
913
|
+
// Finite deltas can still sum past the engine's range. The total is stored and resumed, so it must stay finite.
|
|
914
|
+
const total = E.add(accumulated, delta);
|
|
915
|
+
if (!E.isFinite(total)) {
|
|
916
|
+
throw new ResetCycleRejected(`reset adapter ${adapter.id} accumulated reward is not finite`);
|
|
917
|
+
}
|
|
918
|
+
accumulated = total;
|
|
919
|
+
announce();
|
|
920
|
+
if (action?.kind === "prestige") {
|
|
921
|
+
constraints = recordPrestigeReset(constraints, fact.t, scenario.run.onPrestigeReset);
|
|
922
|
+
}
|
|
923
|
+
const rewardFact = { ...fact, rewardDelta: E.toString(delta) };
|
|
924
|
+
if (observing)
|
|
925
|
+
emitCommittedResets(scenario.run.observer, [rewardFact]);
|
|
926
|
+
// Only the spec adapter built here applied the spec. A caller's adapter with the same id did not.
|
|
927
|
+
if (builtinSpecAdapters.has(adapter) && scenario.model.prestige) {
|
|
928
|
+
const declared = scenario.model.prestige.multiplierFrom(boundaryCtx, state.prestige.points);
|
|
929
|
+
const multiplierNote = "multiplierFrom was not applied; resetState owns the multiplier";
|
|
930
|
+
if (E.cmp(declared, state.prestige.multiplier) !== 0 && !warnings.includes(multiplierNote)) {
|
|
931
|
+
warnings.push(multiplierNote);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
rows.push({
|
|
935
|
+
index: nextIndex,
|
|
936
|
+
startSec,
|
|
937
|
+
endSec: state.t,
|
|
938
|
+
resetAtSec: fact.t,
|
|
939
|
+
durationSec: played,
|
|
940
|
+
rewardDelta: E.toString(delta),
|
|
941
|
+
accumulatedReward: E.toString(accumulated),
|
|
942
|
+
carried: carried(E, state, afterReward),
|
|
943
|
+
});
|
|
944
|
+
nextIndex += 1;
|
|
945
|
+
}
|
|
946
|
+
const appliedResets = rows.filter((row) => row.resetAtSec !== null).length;
|
|
947
|
+
const artifact = {
|
|
948
|
+
schemaVersion: 1,
|
|
949
|
+
algorithmVersion: RESET_CYCLE_ALGORITHM_VERSION,
|
|
950
|
+
status: guard,
|
|
951
|
+
policy: args.policy.kind,
|
|
952
|
+
appliedResets,
|
|
953
|
+
originSec,
|
|
954
|
+
globalElapsedSec: elapsed,
|
|
955
|
+
assumptions: [
|
|
956
|
+
...(guard === "unsupported" ? ["no prestige action or prestige spec was found at the reset boundary"] : []),
|
|
957
|
+
"reward delta is the adapter metric after the reset minus the metric before it",
|
|
958
|
+
"cycle-local duration is the simulated seconds of the interval",
|
|
959
|
+
"global elapsed is the simulated seconds summed since the study origin",
|
|
960
|
+
"the reset action does not receive an income tick",
|
|
961
|
+
],
|
|
962
|
+
warnings,
|
|
963
|
+
cycles: rows,
|
|
964
|
+
};
|
|
965
|
+
return {
|
|
966
|
+
artifact,
|
|
967
|
+
state,
|
|
968
|
+
...(constraints ? { constraints } : {}),
|
|
969
|
+
checkpoint: {
|
|
970
|
+
...checkpointOf(scenario, args.rng, state, constraints, nextIndex, originSec, accumulated, elapsed),
|
|
971
|
+
...(args.adapter ? { adapterId: args.adapter.id, adapterKind: args.adapter.kind } : {}),
|
|
972
|
+
...(pending > 0 ? { pendingSec: pending, pendingDurationSec: pendingDuration, pendingStartSec: pendingStart } : {}),
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
function checkpointOf(scenario, rng, state, constraints, nextIndex, originSec, accumulatedReward, elapsedSec) {
|
|
977
|
+
// Copy live snapshot objects, so later strategy or RNG use cannot rewrite a returned checkpoint.
|
|
978
|
+
// A strategy snapshot is kept only when the strategy can restore it.
|
|
979
|
+
const restorable = !!(scenario.strategy?.snapshotState && scenario.strategy.restoreState);
|
|
980
|
+
const strategyState = restorable ? deepClonePreservingPrototype(scenario.strategy?.snapshotState?.()) : undefined;
|
|
981
|
+
const rngState = deepClonePreservingPrototype(rng?.snapshot());
|
|
982
|
+
return {
|
|
983
|
+
contract: "idlekit.reset-cycle-checkpoint",
|
|
984
|
+
version: 1,
|
|
985
|
+
modelId: scenario.model.id,
|
|
986
|
+
modelVersion: scenario.model.version,
|
|
987
|
+
state: deepClonePreservingPrototype(state),
|
|
988
|
+
...(constraints ? { constraints: deepClonePreservingPrototype(constraints) } : {}),
|
|
989
|
+
nextIndex,
|
|
990
|
+
originSec,
|
|
991
|
+
elapsedSec,
|
|
992
|
+
accumulatedReward,
|
|
993
|
+
...(restorable
|
|
994
|
+
? {
|
|
995
|
+
strategyState,
|
|
996
|
+
...(scenario.strategy?.id !== undefined ? { strategyId: scenario.strategy.id } : {}),
|
|
997
|
+
...(scenario.strategy?.stateVersion !== undefined ? { strategyStateVersion: scenario.strategy.stateVersion } : {}),
|
|
998
|
+
}
|
|
999
|
+
: {}),
|
|
1000
|
+
...(rng
|
|
1001
|
+
? {
|
|
1002
|
+
rngState,
|
|
1003
|
+
rngId: rng.id,
|
|
1004
|
+
rngStateVersion: rng.stateVersion,
|
|
1005
|
+
}
|
|
1006
|
+
: {}),
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
//# sourceMappingURL=resetCycle.js.map
|