@idlekit/core 0.2.1 → 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.
Files changed (82) hide show
  1. package/dist/index.d.ts +12 -0
  2. package/dist/index.js +12 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/scenario/compile.d.ts +25 -0
  5. package/dist/scenario/compile.js +60 -0
  6. package/dist/scenario/compile.js.map +1 -1
  7. package/dist/scenario/scenarioContract.d.ts +152 -0
  8. package/dist/scenario/scenarioContract.js +2815 -0
  9. package/dist/scenario/scenarioContract.js.map +1 -0
  10. package/dist/sim/analysis/analyzers.d.ts +245 -0
  11. package/dist/sim/analysis/analyzers.js +1076 -0
  12. package/dist/sim/analysis/analyzers.js.map +1 -0
  13. package/dist/sim/analysis/contracts.d.ts +147 -0
  14. package/dist/sim/analysis/contracts.js +1230 -0
  15. package/dist/sim/analysis/contracts.js.map +1 -0
  16. package/dist/sim/analysis/distribution.d.ts +199 -0
  17. package/dist/sim/analysis/distribution.js +1246 -0
  18. package/dist/sim/analysis/distribution.js.map +1 -0
  19. package/dist/sim/analysis/eta.d.ts +118 -0
  20. package/dist/sim/analysis/eta.js +968 -70
  21. package/dist/sim/analysis/eta.js.map +1 -1
  22. package/dist/sim/analysis/experiment.d.ts +300 -0
  23. package/dist/sim/analysis/experiment.js +2206 -0
  24. package/dist/sim/analysis/experiment.js.map +1 -0
  25. package/dist/sim/analysis/growth.d.ts +2 -0
  26. package/dist/sim/analysis/growth.js +5 -1
  27. package/dist/sim/analysis/growth.js.map +1 -1
  28. package/dist/sim/analysis/growthFit.d.ts +183 -0
  29. package/dist/sim/analysis/growthFit.js +1798 -0
  30. package/dist/sim/analysis/growthFit.js.map +1 -0
  31. package/dist/sim/analysis/inspect.d.ts +236 -0
  32. package/dist/sim/analysis/inspect.js +388 -0
  33. package/dist/sim/analysis/inspect.js.map +1 -0
  34. package/dist/sim/analysis/metrics.d.ts +91 -0
  35. package/dist/sim/analysis/metrics.js +266 -0
  36. package/dist/sim/analysis/metrics.js.map +1 -0
  37. package/dist/sim/analysis/prestigeCycle.d.ts +6 -0
  38. package/dist/sim/analysis/prestigeCycle.js +10 -1
  39. package/dist/sim/analysis/prestigeCycle.js.map +1 -1
  40. package/dist/sim/analysis/registry.d.ts +41 -0
  41. package/dist/sim/analysis/registry.js +54 -0
  42. package/dist/sim/analysis/registry.js.map +1 -0
  43. package/dist/sim/analysis/resetActions.d.ts +7 -0
  44. package/dist/sim/analysis/resetActions.js +45 -0
  45. package/dist/sim/analysis/resetActions.js.map +1 -0
  46. package/dist/sim/analysis/resetCycle.d.ts +175 -0
  47. package/dist/sim/analysis/resetCycle.js +1009 -0
  48. package/dist/sim/analysis/resetCycle.js.map +1 -0
  49. package/dist/sim/analysis/resetOptimization.d.ts +198 -0
  50. package/dist/sim/analysis/resetOptimization.js +2187 -0
  51. package/dist/sim/analysis/resetOptimization.js.map +1 -0
  52. package/dist/sim/analysis/sensitivity.d.ts +73 -0
  53. package/dist/sim/analysis/sensitivity.js +160 -0
  54. package/dist/sim/analysis/sensitivity.js.map +1 -0
  55. package/dist/sim/monteCarlo.d.ts +12 -0
  56. package/dist/sim/monteCarlo.js +33 -4
  57. package/dist/sim/monteCarlo.js.map +1 -1
  58. package/dist/sim/observation.d.ts +22 -0
  59. package/dist/sim/observation.js +33 -0
  60. package/dist/sim/observation.js.map +1 -1
  61. package/dist/sim/offline.js +9 -2
  62. package/dist/sim/offline.js.map +1 -1
  63. package/dist/sim/random.d.ts +15 -0
  64. package/dist/sim/random.js +28 -0
  65. package/dist/sim/random.js.map +1 -1
  66. package/dist/sim/session.d.ts +7 -0
  67. package/dist/sim/session.js +13 -0
  68. package/dist/sim/session.js.map +1 -1
  69. package/dist/sim/simulator.js +7 -1
  70. package/dist/sim/simulator.js.map +1 -1
  71. package/dist/sim/step.d.ts +67 -1
  72. package/dist/sim/step.js +100 -18
  73. package/dist/sim/step.js.map +1 -1
  74. package/dist/sim/strategy/opt/freshRun.d.ts +3 -0
  75. package/dist/sim/strategy/opt/freshRun.js +6 -0
  76. package/dist/sim/strategy/opt/freshRun.js.map +1 -0
  77. package/dist/sim/strategy/opt/runner.js +2 -2
  78. package/dist/sim/strategy/opt/runner.js.map +1 -1
  79. package/dist/sim/strategy/planner.d.ts +26 -0
  80. package/dist/sim/strategy/planner.js +30 -0
  81. package/dist/sim/strategy/planner.js.map +1 -1
  82. package/package.json +2 -2
@@ -1,26 +1,886 @@
1
+ import * as _validateReport_1 from "typia/lib/internal/_validateReport";
2
+ import * as _createStandardSchema_1 from "typia/lib/internal/_createStandardSchema";
3
+ import { parseMoney } from "../../notation/parseMoney";
1
4
  import { runScenario } from "../simulator";
2
5
  import { assertHorizonReached, runElapsedSec } from "../types";
3
- import { parseMoney } from "../../notation/parseMoney";
4
- function targetReached(scenario, targetKind, threshold, run) {
5
- const { E } = scenario.ctx;
6
- if (targetKind === "money") {
7
- return E.cmp(run.end.wallet.money.amount, threshold) >= 0;
6
+ import { evaluateGoal, moneyAtLeast, netWorthAtLeast, walletMoneyMetric, } from "./metrics";
7
+ import typia, {} from "typia";
8
+ /**
9
+ * ETA estimate v1.
10
+ * Legacy `ETAResult.seconds` stays a number. This object carries status.
11
+ * DX-02 may wrap these functions. It should not grow a second ETA engine.
12
+ */
13
+ const ETA_SUFFIX = { kind: "alphaInfinite", minLen: 2 };
14
+ const ETA_ALGORITHM_VERSION = "eta-estimate-v1";
15
+ const ETA_SCHEMA_ID = "https://idlekit.dev/schemas/eta.estimate.v1.schema.json";
16
+ /**
17
+ * @evidence docs/requirements/active/eta-numeric-and-goal-semantics.md#req-pr09-engine-ratio The time ratio is divided inside the Engine before any seconds number.
18
+ * @evidenceReview docs/requirements/active/eta-numeric-and-goal-semantics.md#req-pr09-engine-ratio #fe46221 Re-read the section: 1e402 over 1e400 is 100 seconds, not a narrowed division.
19
+ */
20
+ export const etaEngineRatio = "engine-divn-before-number";
21
+ /**
22
+ * @evidence docs/requirements/active/eta-numeric-and-goal-semantics.md#req-pr09-exact-local Exact needs a constant derivative, declared strict comparison, and no purchase, reset, evolve, or policy boundary.
23
+ * @evidenceReview docs/requirements/active/eta-numeric-and-goal-semantics.md#req-pr09-exact-local #f2b843f Re-read the section: exact needs comparisonIsExact true, tolerant or unknown comparisons remain local, and net worth is not money income.
24
+ */
25
+ export const etaExactOrLocal = "exact-or-analytic-local";
26
+ /**
27
+ * @evidence docs/requirements/active/eta-numeric-and-goal-semantics.md#req-pr09-censored-observation A missed horizon is censored. Fixed-step time is the first observation.
28
+ * @evidenceReview docs/requirements/active/eta-numeric-and-goal-semantics.md#req-pr09-censored-observation #a644d35 Re-read the section: seconds is null, the legacy field stays the horizon, and there is no unreachable claim.
29
+ */
30
+ export const etaCensoredObservation = "censored-fixed-step";
31
+ export function etaEstimateSchemaId() {
32
+ return ETA_SCHEMA_ID;
33
+ }
34
+ export function etaEstimateAlgorithmVersion() {
35
+ return ETA_ALGORITHM_VERSION;
36
+ }
37
+ /*
38
+ * typia runs on first call, not at module load. A plugin that the bundled CLI loads
39
+ * imports core source without the transform, and a module-load call would throw there.
40
+ */
41
+ let etaEstimateValidator;
42
+ export const validateEtaEstimate = (input) => (etaEstimateValidator ??= (() => {
43
+ const _ip0 = input => 1 === input["schemaVersion"];
44
+ const _ip1 = input => "eta-estimate-v1" === input["algorithmVersion"];
45
+ const _ip2 = input => "string" === typeof input["goalId"];
46
+ const _ip3 = input => "number" === typeof input["goalVersion"];
47
+ const _ip4 = input => "string" === typeof input["metricId"];
48
+ const _ip5 = input => "string" === typeof input["meaning"];
49
+ const _ip6 = input => "interval" === input["timeAxis"];
50
+ const _ip7 = input => "object" === typeof input["target"] && null !== input["target"] && _io1(input["target"]);
51
+ const _ip8 = input => "censored" === input["status"] || "error" === input["status"] || "insufficient-data" === input["status"] || "invalid" === input["status"] || "ok" === input["status"] || "unknown" === input["status"] || "unsupported" === input["status"];
52
+ const _ip9 = input => undefined === input["method"] || "analytic-local" === input["method"] || "exact" === input["method"] || "fixed-step" === input["method"] || "heuristic-fit" === input["method"] || "monte-carlo" === input["method"];
53
+ const _ip10 = input => null === input["seconds"] || "number" === typeof input["seconds"];
54
+ const _ip11 = input => undefined === input["secondsDecimal"] || "string" === typeof input["secondsDecimal"];
55
+ const _ip12 = input => undefined === input["observationResolutionSec"] || "number" === typeof input["observationResolutionSec"];
56
+ const _ip13 = input => undefined === input["bracket"] || "object" === typeof input["bracket"] && null !== input["bracket"] && _io2(input["bracket"]);
57
+ const _ip14 = input => Array.isArray(input["assumptions"]) && input["assumptions"].every(elem => "string" === typeof elem);
58
+ const _ip15 = input => Array.isArray(input["warnings"]) && input["warnings"].every(elem => "string" === typeof elem);
59
+ const _ip16 = input => undefined === input["horizonSec"] || "number" === typeof input["horizonSec"];
60
+ const _ip17 = input => undefined === input["compare"] || "object" === typeof input["compare"] && null !== input["compare"] && _io3(input["compare"]);
61
+ const _ve0 = "Readonly<{ schemaVersion: 1; algorithmVersion: \"eta-estimate-v1\"; goalId: string; goalVersion: number; metricId: string; meaning: string; timeAxis: \"interval\"; target: Readonly<{ kind: \"money\" | \"netWorth\"; decimal: string; unit: string; }>; ... 9 more ...; compare?: Readonly<...> | undefined; }>";
62
+ const _vp0 = (input, _path, _exceptionable = true) => 1 === input["schemaVersion"] || _report(_exceptionable, {
63
+ path: _path + ".schemaVersion",
64
+ expected: "1",
65
+ value: input["schemaVersion"]
66
+ });
67
+ const _vp1 = (input, _path, _exceptionable = true) => "eta-estimate-v1" === input["algorithmVersion"] || _report(_exceptionable, {
68
+ path: _path + ".algorithmVersion",
69
+ expected: "\"eta-estimate-v1\"",
70
+ value: input["algorithmVersion"]
71
+ });
72
+ const _vp2 = (input, _path, _exceptionable = true) => "string" === typeof input["goalId"] || _report(_exceptionable, {
73
+ path: _path + ".goalId",
74
+ expected: "string",
75
+ value: input["goalId"]
76
+ });
77
+ const _vp3 = (input, _path, _exceptionable = true) => "number" === typeof input["goalVersion"] || _report(_exceptionable, {
78
+ path: _path + ".goalVersion",
79
+ expected: "number",
80
+ value: input["goalVersion"]
81
+ });
82
+ const _vp4 = (input, _path, _exceptionable = true) => "string" === typeof input["metricId"] || _report(_exceptionable, {
83
+ path: _path + ".metricId",
84
+ expected: "string",
85
+ value: input["metricId"]
86
+ });
87
+ const _vp5 = (input, _path, _exceptionable = true) => "string" === typeof input["meaning"] || _report(_exceptionable, {
88
+ path: _path + ".meaning",
89
+ expected: "string",
90
+ value: input["meaning"]
91
+ });
92
+ const _vp6 = (input, _path, _exceptionable = true) => "interval" === input["timeAxis"] || _report(_exceptionable, {
93
+ path: _path + ".timeAxis",
94
+ expected: "\"interval\"",
95
+ value: input["timeAxis"]
96
+ });
97
+ const _vp7 = (input, _path, _exceptionable = true) => ("object" === typeof input["target"] && null !== input["target"] || _report(_exceptionable, {
98
+ path: _path + ".target",
99
+ expected: "Readonly<{ kind: \"money\" | \"netWorth\"; decimal: string; unit: string; }>",
100
+ value: input["target"]
101
+ })) && _vo1(input["target"], _path + ".target", true && _exceptionable) || _report(_exceptionable, {
102
+ path: _path + ".target",
103
+ expected: "Readonly<{ kind: \"money\" | \"netWorth\"; decimal: string; unit: string; }>",
104
+ value: input["target"]
105
+ });
106
+ const _vp8 = (input, _path, _exceptionable = true) => "censored" === input["status"] || "error" === input["status"] || "insufficient-data" === input["status"] || "invalid" === input["status"] || "ok" === input["status"] || "unknown" === input["status"] || "unsupported" === input["status"] || _report(_exceptionable, {
107
+ path: _path + ".status",
108
+ expected: "(\"censored\" | \"error\" | \"insufficient-data\" | \"invalid\" | \"ok\" | \"unknown\" | \"unsupported\")",
109
+ value: input["status"]
110
+ });
111
+ const _vp9 = (input, _path, _exceptionable = true) => undefined === input["method"] || "analytic-local" === input["method"] || "exact" === input["method"] || "fixed-step" === input["method"] || "heuristic-fit" === input["method"] || "monte-carlo" === input["method"] || _report(_exceptionable, {
112
+ path: _path + ".method",
113
+ expected: "(\"analytic-local\" | \"exact\" | \"fixed-step\" | \"heuristic-fit\" | \"monte-carlo\" | undefined)",
114
+ value: input["method"]
115
+ });
116
+ const _vp10 = (input, _path, _exceptionable = true) => null === input["seconds"] || "number" === typeof input["seconds"] || _report(_exceptionable, {
117
+ path: _path + ".seconds",
118
+ expected: "(null | number)",
119
+ value: input["seconds"]
120
+ });
121
+ const _vp11 = (input, _path, _exceptionable = true) => undefined === input["secondsDecimal"] || "string" === typeof input["secondsDecimal"] || _report(_exceptionable, {
122
+ path: _path + ".secondsDecimal",
123
+ expected: "(string | undefined)",
124
+ value: input["secondsDecimal"]
125
+ });
126
+ const _vp12 = (input, _path, _exceptionable = true) => undefined === input["observationResolutionSec"] || "number" === typeof input["observationResolutionSec"] || _report(_exceptionable, {
127
+ path: _path + ".observationResolutionSec",
128
+ expected: "(number | undefined)",
129
+ value: input["observationResolutionSec"]
130
+ });
131
+ const _vp13 = (input, _path, _exceptionable = true) => undefined === input["bracket"] || ("object" === typeof input["bracket"] && null !== input["bracket"] || _report(_exceptionable, {
132
+ path: _path + ".bracket",
133
+ expected: "(Readonly<{ lowerSec: number; upperSec: number; }> | undefined)",
134
+ value: input["bracket"]
135
+ })) && _vo2(input["bracket"], _path + ".bracket", true && _exceptionable) || _report(_exceptionable, {
136
+ path: _path + ".bracket",
137
+ expected: "(Readonly<{ lowerSec: number; upperSec: number; }> | undefined)",
138
+ value: input["bracket"]
139
+ });
140
+ const _vp14 = (input, _path, _exceptionable = true) => (Array.isArray(input["assumptions"]) || _report(_exceptionable, {
141
+ path: _path + ".assumptions",
142
+ expected: "ReadonlyArray<string>",
143
+ value: input["assumptions"]
144
+ })) && input["assumptions"].map((elem, _index3) => "string" === typeof elem || _report(_exceptionable, {
145
+ path: _path + ".assumptions[" + _index3 + "]",
146
+ expected: "string",
147
+ value: elem
148
+ })).every(flag => flag) || _report(_exceptionable, {
149
+ path: _path + ".assumptions",
150
+ expected: "ReadonlyArray<string>",
151
+ value: input["assumptions"]
152
+ });
153
+ const _vp15 = (input, _path, _exceptionable = true) => (Array.isArray(input["warnings"]) || _report(_exceptionable, {
154
+ path: _path + ".warnings",
155
+ expected: "ReadonlyArray<string>",
156
+ value: input["warnings"]
157
+ })) && input["warnings"].map((elem, _index4) => "string" === typeof elem || _report(_exceptionable, {
158
+ path: _path + ".warnings[" + _index4 + "]",
159
+ expected: "string",
160
+ value: elem
161
+ })).every(flag => flag) || _report(_exceptionable, {
162
+ path: _path + ".warnings",
163
+ expected: "ReadonlyArray<string>",
164
+ value: input["warnings"]
165
+ });
166
+ const _vp16 = (input, _path, _exceptionable = true) => undefined === input["horizonSec"] || "number" === typeof input["horizonSec"] || _report(_exceptionable, {
167
+ path: _path + ".horizonSec",
168
+ expected: "(number | undefined)",
169
+ value: input["horizonSec"]
170
+ });
171
+ const _vp17 = (input, _path, _exceptionable = true) => undefined === input["compare"] || ("object" === typeof input["compare"] && null !== input["compare"] || _report(_exceptionable, {
172
+ path: _path + ".compare",
173
+ expected: _ve1,
174
+ value: input["compare"]
175
+ })) && _vo3(input["compare"], _path + ".compare", true && _exceptionable) || _report(_exceptionable, {
176
+ path: _path + ".compare",
177
+ expected: _ve1,
178
+ value: input["compare"]
179
+ });
180
+ const _ve1 = "(Readonly<{ localMethod: AnalysisMethod | null; fixedStepMethod: \"fixed-step\"; localSec: number | null; fixedStepSec: number | null; diffSec: number | null; }> | undefined)";
181
+ const _io0 = input => _ip0(input) && _ip1(input) && _ip2(input) && _ip3(input) && _ip4(input) && _ip5(input) && _ip6(input) && _ip7(input) && _ip8(input) && _ip9(input) && _ip10(input) && _ip11(input) && _ip12(input) && _ip13(input) && _ip14(input) && _ip15(input) && _ip16(input) && _ip17(input);
182
+ const _io1 = input => ("money" === input.kind || "netWorth" === input.kind) && "string" === typeof input.decimal && "string" === typeof input.unit;
183
+ const _io2 = input => "number" === typeof input.lowerSec && "number" === typeof input.upperSec;
184
+ const _io3 = input => (null === input.localMethod || "analytic-local" === input.localMethod || "exact" === input.localMethod || "fixed-step" === input.localMethod || "heuristic-fit" === input.localMethod || "monte-carlo" === input.localMethod) && "fixed-step" === input.fixedStepMethod && (null === input.localSec || "number" === typeof input.localSec) && (null === input.fixedStepSec || "number" === typeof input.fixedStepSec) && (null === input.diffSec || "number" === typeof input.diffSec);
185
+ const _vo0 = (input, _path, _exceptionable = true) => [_vp0(input, _path, true && _exceptionable), _vp1(input, _path, true && _exceptionable), _vp2(input, _path, true && _exceptionable), _vp3(input, _path, true && _exceptionable), _vp4(input, _path, true && _exceptionable), _vp5(input, _path, true && _exceptionable), _vp6(input, _path, true && _exceptionable), _vp7(input, _path, true && _exceptionable), _vp8(input, _path, true && _exceptionable), _vp9(input, _path, true && _exceptionable), _vp10(input, _path, true && _exceptionable), _vp11(input, _path, true && _exceptionable), _vp12(input, _path, true && _exceptionable), _vp13(input, _path, true && _exceptionable), _vp14(input, _path, true && _exceptionable), _vp15(input, _path, true && _exceptionable), _vp16(input, _path, true && _exceptionable), _vp17(input, _path, true && _exceptionable)].every(flag => flag);
186
+ const _vo1 = (input, _path, _exceptionable = true) => ["money" === input.kind || "netWorth" === input.kind || _report(_exceptionable, {
187
+ path: _path + ".kind",
188
+ expected: "(\"money\" | \"netWorth\")",
189
+ value: input.kind
190
+ }), "string" === typeof input.decimal || _report(_exceptionable, {
191
+ path: _path + ".decimal",
192
+ expected: "string",
193
+ value: input.decimal
194
+ }), "string" === typeof input.unit || _report(_exceptionable, {
195
+ path: _path + ".unit",
196
+ expected: "string",
197
+ value: input.unit
198
+ })].every(flag => flag);
199
+ const _vo2 = (input, _path, _exceptionable = true) => ["number" === typeof input.lowerSec || _report(_exceptionable, {
200
+ path: _path + ".lowerSec",
201
+ expected: "number",
202
+ value: input.lowerSec
203
+ }), "number" === typeof input.upperSec || _report(_exceptionable, {
204
+ path: _path + ".upperSec",
205
+ expected: "number",
206
+ value: input.upperSec
207
+ })].every(flag => flag);
208
+ const _vo3 = (input, _path, _exceptionable = true) => [null === input.localMethod || "analytic-local" === input.localMethod || "exact" === input.localMethod || "fixed-step" === input.localMethod || "heuristic-fit" === input.localMethod || "monte-carlo" === input.localMethod || _report(_exceptionable, {
209
+ path: _path + ".localMethod",
210
+ expected: "(\"analytic-local\" | \"exact\" | \"fixed-step\" | \"heuristic-fit\" | \"monte-carlo\" | null)",
211
+ value: input.localMethod
212
+ }), "fixed-step" === input.fixedStepMethod || _report(_exceptionable, {
213
+ path: _path + ".fixedStepMethod",
214
+ expected: "\"fixed-step\"",
215
+ value: input.fixedStepMethod
216
+ }), null === input.localSec || "number" === typeof input.localSec || _report(_exceptionable, {
217
+ path: _path + ".localSec",
218
+ expected: "(null | number)",
219
+ value: input.localSec
220
+ }), null === input.fixedStepSec || "number" === typeof input.fixedStepSec || _report(_exceptionable, {
221
+ path: _path + ".fixedStepSec",
222
+ expected: "(null | number)",
223
+ value: input.fixedStepSec
224
+ }), null === input.diffSec || "number" === typeof input.diffSec || _report(_exceptionable, {
225
+ path: _path + ".diffSec",
226
+ expected: "(null | number)",
227
+ value: input.diffSec
228
+ })].every(flag => flag);
229
+ const __is = input => "object" === typeof input && null !== input && _io0(input);
230
+ let errors;
231
+ let _report;
232
+ return _createStandardSchema_1._createStandardSchema(input => {
233
+ if (false === __is(input)) {
234
+ errors = [];
235
+ _report = _validateReport_1._validateReport(errors);
236
+ ((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input || _report(true, {
237
+ path: _path + "",
238
+ expected: _ve0,
239
+ value: input
240
+ })) && _vo0(input, _path + "", true) || _report(true, {
241
+ path: _path + "",
242
+ expected: _ve0,
243
+ value: input
244
+ }))(input, "$input", true);
245
+ const success = 0 === errors.length;
246
+ return success ? {
247
+ success,
248
+ data: input
249
+ } : {
250
+ success,
251
+ errors,
252
+ data: input
253
+ };
254
+ }
255
+ return {
256
+ success: true,
257
+ data: input
258
+ };
259
+ });
260
+ })())(input);
261
+ function buildEtaEstimateJsonSchema() {
262
+ return {
263
+ components: {
264
+ schemas: {
265
+ "ReadonlyschemaVersion1algorithmVersioneta-estimate-v1goalIdstringgoalVersionnumbermetricIdstringmeaningstringtimeAxisintervaltargetReadonlykindmoneynetWorthdecimalstringunitstring---9more---compareReadonly---undefined": {
266
+ type: "object",
267
+ properties: {
268
+ schemaVersion: {
269
+ "const": 1
270
+ },
271
+ algorithmVersion: {
272
+ "const": "eta-estimate-v1"
273
+ },
274
+ goalId: {
275
+ type: "string"
276
+ },
277
+ goalVersion: {
278
+ type: "number"
279
+ },
280
+ metricId: {
281
+ type: "string"
282
+ },
283
+ meaning: {
284
+ type: "string"
285
+ },
286
+ timeAxis: {
287
+ "const": "interval"
288
+ },
289
+ target: {
290
+ $ref: "#/components/schemas/ReadonlykindmoneynetWorthdecimalstringunitstring",
291
+ description: "The goal threshold this estimate answers, as an engine decimal string and its unit."
292
+ },
293
+ status: {
294
+ $ref: "#/components/schemas/AnalysisStatus"
295
+ },
296
+ method: {
297
+ oneOf: [
298
+ {
299
+ "const": "analytic-local"
300
+ },
301
+ {
302
+ "const": "exact"
303
+ },
304
+ {
305
+ "const": "fixed-step"
306
+ },
307
+ {
308
+ "const": "heuristic-fit"
309
+ },
310
+ {
311
+ "const": "monte-carlo"
312
+ }
313
+ ]
314
+ },
315
+ seconds: {
316
+ oneOf: [
317
+ {
318
+ type: "null"
319
+ },
320
+ {
321
+ type: "number"
322
+ }
323
+ ]
324
+ },
325
+ secondsDecimal: {
326
+ type: "string"
327
+ },
328
+ observationResolutionSec: {
329
+ type: "number"
330
+ },
331
+ bracket: {
332
+ $ref: "#/components/schemas/ReadonlylowerSecnumberupperSecnumber"
333
+ },
334
+ assumptions: {
335
+ type: "array",
336
+ items: {
337
+ type: "string"
338
+ }
339
+ },
340
+ warnings: {
341
+ type: "array",
342
+ items: {
343
+ type: "string"
344
+ }
345
+ },
346
+ horizonSec: {
347
+ type: "number"
348
+ },
349
+ compare: {
350
+ $ref: "#/components/schemas/ReadonlylocalMethodAnalysisMethodnullfixedStepMethodfixed-steplocalSecnumbernullfixedStepSecnumbernulldiffSecnumbernull"
351
+ }
352
+ },
353
+ required: [
354
+ "schemaVersion",
355
+ "algorithmVersion",
356
+ "goalId",
357
+ "goalVersion",
358
+ "metricId",
359
+ "meaning",
360
+ "timeAxis",
361
+ "target",
362
+ "status",
363
+ "seconds",
364
+ "assumptions",
365
+ "warnings"
366
+ ],
367
+ additionalProperties: false
368
+ },
369
+ ReadonlykindmoneynetWorthdecimalstringunitstring: {
370
+ type: "object",
371
+ properties: {
372
+ kind: {
373
+ oneOf: [
374
+ {
375
+ "const": "money"
376
+ },
377
+ {
378
+ "const": "netWorth"
379
+ }
380
+ ]
381
+ },
382
+ decimal: {
383
+ type: "string"
384
+ },
385
+ unit: {
386
+ type: "string"
387
+ }
388
+ },
389
+ required: [
390
+ "kind",
391
+ "decimal",
392
+ "unit"
393
+ ],
394
+ additionalProperties: false
395
+ },
396
+ AnalysisStatus: {
397
+ oneOf: [
398
+ {
399
+ "const": "censored"
400
+ },
401
+ {
402
+ "const": "error"
403
+ },
404
+ {
405
+ "const": "insufficient-data"
406
+ },
407
+ {
408
+ "const": "invalid"
409
+ },
410
+ {
411
+ "const": "ok"
412
+ },
413
+ {
414
+ "const": "unknown"
415
+ },
416
+ {
417
+ "const": "unsupported"
418
+ }
419
+ ]
420
+ },
421
+ ReadonlylowerSecnumberupperSecnumber: {
422
+ type: "object",
423
+ properties: {
424
+ lowerSec: {
425
+ type: "number"
426
+ },
427
+ upperSec: {
428
+ type: "number"
429
+ }
430
+ },
431
+ required: [
432
+ "lowerSec",
433
+ "upperSec"
434
+ ],
435
+ additionalProperties: false
436
+ },
437
+ "ReadonlylocalMethodAnalysisMethodnullfixedStepMethodfixed-steplocalSecnumbernullfixedStepSecnumbernulldiffSecnumbernull": {
438
+ type: "object",
439
+ properties: {
440
+ localMethod: {
441
+ oneOf: [
442
+ {
443
+ type: "null"
444
+ },
445
+ {
446
+ "const": "analytic-local"
447
+ },
448
+ {
449
+ "const": "exact"
450
+ },
451
+ {
452
+ "const": "fixed-step"
453
+ },
454
+ {
455
+ "const": "heuristic-fit"
456
+ },
457
+ {
458
+ "const": "monte-carlo"
459
+ }
460
+ ]
461
+ },
462
+ fixedStepMethod: {
463
+ "const": "fixed-step"
464
+ },
465
+ localSec: {
466
+ oneOf: [
467
+ {
468
+ type: "null"
469
+ },
470
+ {
471
+ type: "number"
472
+ }
473
+ ]
474
+ },
475
+ fixedStepSec: {
476
+ oneOf: [
477
+ {
478
+ type: "null"
479
+ },
480
+ {
481
+ type: "number"
482
+ }
483
+ ]
484
+ },
485
+ diffSec: {
486
+ oneOf: [
487
+ {
488
+ type: "null"
489
+ },
490
+ {
491
+ type: "number"
492
+ }
493
+ ]
494
+ }
495
+ },
496
+ required: [
497
+ "localMethod",
498
+ "fixedStepMethod",
499
+ "localSec",
500
+ "fixedStepSec",
501
+ "diffSec"
502
+ ],
503
+ additionalProperties: false
504
+ }
505
+ }
506
+ },
507
+ schema: {
508
+ type: "object",
509
+ properties: {
510
+ schemaVersion: {
511
+ "const": 1
512
+ },
513
+ algorithmVersion: {
514
+ "const": "eta-estimate-v1"
515
+ },
516
+ goalId: {
517
+ type: "string"
518
+ },
519
+ goalVersion: {
520
+ type: "number"
521
+ },
522
+ metricId: {
523
+ type: "string"
524
+ },
525
+ meaning: {
526
+ type: "string"
527
+ },
528
+ timeAxis: {
529
+ "const": "interval"
530
+ },
531
+ target: {
532
+ $ref: "#/components/schemas/ReadonlykindmoneynetWorthdecimalstringunitstring",
533
+ description: "The goal threshold this estimate answers, as an engine decimal string and its unit."
534
+ },
535
+ status: {
536
+ $ref: "#/components/schemas/AnalysisStatus"
537
+ },
538
+ method: {
539
+ oneOf: [
540
+ {
541
+ "const": "analytic-local"
542
+ },
543
+ {
544
+ "const": "exact"
545
+ },
546
+ {
547
+ "const": "fixed-step"
548
+ },
549
+ {
550
+ "const": "heuristic-fit"
551
+ },
552
+ {
553
+ "const": "monte-carlo"
554
+ }
555
+ ]
556
+ },
557
+ seconds: {
558
+ oneOf: [
559
+ {
560
+ type: "null"
561
+ },
562
+ {
563
+ type: "number"
564
+ }
565
+ ]
566
+ },
567
+ secondsDecimal: {
568
+ type: "string"
569
+ },
570
+ observationResolutionSec: {
571
+ type: "number"
572
+ },
573
+ bracket: {
574
+ $ref: "#/components/schemas/ReadonlylowerSecnumberupperSecnumber"
575
+ },
576
+ assumptions: {
577
+ type: "array",
578
+ items: {
579
+ type: "string"
580
+ }
581
+ },
582
+ warnings: {
583
+ type: "array",
584
+ items: {
585
+ type: "string"
586
+ }
587
+ },
588
+ horizonSec: {
589
+ type: "number"
590
+ },
591
+ compare: {
592
+ $ref: "#/components/schemas/ReadonlylocalMethodAnalysisMethodnullfixedStepMethodfixed-steplocalSecnumbernullfixedStepSecnumbernulldiffSecnumbernull"
593
+ }
594
+ },
595
+ required: [
596
+ "schemaVersion",
597
+ "algorithmVersion",
598
+ "goalId",
599
+ "goalVersion",
600
+ "metricId",
601
+ "meaning",
602
+ "timeAxis",
603
+ "target",
604
+ "status",
605
+ "seconds",
606
+ "assumptions",
607
+ "warnings"
608
+ ],
609
+ additionalProperties: false
610
+ },
611
+ version: "3.1"
612
+ };
613
+ }
614
+ let etaEstimateJsonSchemaUnit;
615
+ export function etaEstimateJsonSchema() {
616
+ etaEstimateJsonSchemaUnit ??= buildEtaEstimateJsonSchema();
617
+ return etaEstimateJsonSchemaUnit;
618
+ }
619
+ function finiteSeconds(value) {
620
+ if (!Number.isFinite(value) || value < 0)
621
+ return undefined;
622
+ return value;
623
+ }
624
+ /**
625
+ * Divide the gap by the rate in the Engine, then narrow.
626
+ * `toNumber` sees only that ratio, and only when it is a finite seconds count.
627
+ */
628
+ export function etaSecondsFromEngineRatio(E, target, current, rate, provenConstant) {
629
+ if (!E.isFinite(target) || !E.isFinite(current) || !E.isFinite(rate)) {
630
+ return { status: "invalid", seconds: null };
631
+ }
632
+ if (E.cmp(rate, E.zero()) <= 0) {
633
+ return {
634
+ status: provenConstant ? "unsupported" : "insufficient-data",
635
+ seconds: null,
636
+ };
637
+ }
638
+ const ratio = E.divN(E.sub(target, current), rate);
639
+ if (!E.isFinite(ratio))
640
+ return { status: "invalid", seconds: null, secondsDecimal: E.toString(ratio) };
641
+ const magnitude = E.absLog10(ratio);
642
+ if (Number.isFinite(magnitude) && magnitude > 308) {
643
+ return { status: "invalid", seconds: null, secondsDecimal: E.toString(ratio) };
644
+ }
645
+ const seconds = finiteSeconds(E.toNumber(ratio));
646
+ if (seconds === undefined) {
647
+ return { status: "invalid", seconds: null, secondsDecimal: E.toString(ratio) };
8
648
  }
9
- const worth = scenario.model.netWorth?.(scenario.ctx, run.end) ?? run.end.wallet.money;
10
- return E.cmp(worth.amount, threshold) >= 0;
649
+ // The engine division itself can lose a positive gap, before narrowing sees it.
650
+ if (seconds === 0 && !Number.isFinite(magnitude) && E.cmp(target, current) > 0) {
651
+ return { status: "invalid", seconds: null };
652
+ }
653
+ // A nonzero ratio below the number range narrows to 0, which would read as already reached.
654
+ // absLog10 is finite only for a nonzero value; cmp against zero can round such a ratio to equal.
655
+ // The engine's own division can also lose the ratio, so a nonzero gap is checked as well.
656
+ if (seconds === 0 && (Number.isFinite(magnitude) || Number.isFinite(E.absLog10(E.sub(target, current))))) {
657
+ return { status: "invalid", seconds: null, secondsDecimal: E.toString(ratio) };
658
+ }
659
+ return { status: "ok", seconds };
11
660
  }
12
- export function etaSimulate(args) {
13
- const threshold = parseMoney(args.scenario.ctx.E, args.target.value, {
14
- unit: args.scenario.ctx.unit,
15
- suffix: { kind: "alphaInfinite", minLen: 2 },
661
+ export function parseEtaAmount(scenario, value) {
662
+ return parseMoney(scenario.ctx.E, value, {
663
+ unit: scenario.ctx.unit,
664
+ suffix: ETA_SUFFIX,
16
665
  }).amount;
17
- const until = (s) => {
18
- if (args.target.kind === "money") {
19
- return args.scenario.ctx.E.cmp(s.wallet.money.amount, threshold) >= 0;
666
+ }
667
+ export function etaGoalForTarget(scenario, target) {
668
+ const threshold = parseEtaAmount(scenario, target.value);
669
+ const wire = scenario.ctx.E.toString(threshold);
670
+ const identity = { kind: target.kind, decimal: wire, unit: scenario.ctx.unit.code };
671
+ if (target.kind === "money") {
672
+ const metric = walletMoneyMetric();
673
+ return { goal: moneyAtLeast(wire), threshold, target: identity, metricId: metric.id, meaning: metric.meaning };
674
+ }
675
+ return {
676
+ goal: netWorthAtLeast({
677
+ target: wire,
678
+ netWorth: (state, ctx) => scenario.model.netWorth?.(ctx, state)?.amount,
679
+ fallbackToWallet: scenario.model.netWorth === undefined,
680
+ }),
681
+ threshold,
682
+ target: identity,
683
+ metricId: "netWorth",
684
+ meaning: "Current net worth amount",
685
+ };
686
+ }
687
+ const COMPARISON_BOUNDARY = "goal comparison uses a tolerance or has no exactness declaration";
688
+ function exactBlocks(scenario) {
689
+ const blocks = [];
690
+ const hint = scenario.model.analytic?.(scenario.ctx, scenario.initial);
691
+ if (hint?.incomeKind !== "constant")
692
+ blocks.push("income derivative is not confirmed constant");
693
+ if (scenario.model.actions(scenario.ctx, scenario.initial).length > 0) {
694
+ blocks.push("an action can change income");
695
+ }
696
+ if (scenario.model.evolve)
697
+ blocks.push("evolve can change the state inside a step");
698
+ if (scenario.model.prestige)
699
+ blocks.push("prestige reset can make the metric non-monotonic");
700
+ if (scenario.strategy)
701
+ blocks.push("a strategy can apply a decision");
702
+ if (scenario.constraints)
703
+ blocks.push("a policy constraint can change the schedule");
704
+ if (scenario.ctx.tickPolicy.maxLogGap !== undefined) {
705
+ blocks.push("tick settlement can drop or hold income below maxLogGap");
706
+ }
707
+ if (scenario.ctx.tickPolicy.mode === "accumulate" &&
708
+ scenario.ctx.E.cmp(scenario.initial.wallet.bucket, scenario.ctx.E.zero()) !== 0) {
709
+ blocks.push("a pending accumulated bucket can flush into the wallet on the next tick");
710
+ }
711
+ if (scenario.ctx.E.comparisonIsExact !== true)
712
+ blocks.push(COMPARISON_BOUNDARY);
713
+ return blocks;
714
+ }
715
+ function baseEstimate(binding, status, extra = {}) {
716
+ return {
717
+ schemaVersion: 1,
718
+ algorithmVersion: ETA_ALGORITHM_VERSION,
719
+ goalId: binding.goal.id,
720
+ goalVersion: binding.goal.version,
721
+ metricId: binding.metricId,
722
+ meaning: binding.meaning,
723
+ timeAxis: "interval",
724
+ target: binding.target,
725
+ status,
726
+ seconds: extra.seconds ?? null,
727
+ assumptions: extra.assumptions ?? [],
728
+ warnings: extra.warnings ?? [],
729
+ ...(extra.method !== undefined ? { method: extra.method } : {}),
730
+ ...(extra.secondsDecimal !== undefined ? { secondsDecimal: extra.secondsDecimal } : {}),
731
+ ...(extra.observationResolutionSec !== undefined
732
+ ? { observationResolutionSec: extra.observationResolutionSec }
733
+ : {}),
734
+ ...(extra.bracket !== undefined ? { bracket: extra.bracket } : {}),
735
+ ...(extra.horizonSec !== undefined ? { horizonSec: extra.horizonSec } : {}),
736
+ ...(extra.compare !== undefined ? { compare: extra.compare } : {}),
737
+ };
738
+ }
739
+ function confidenceFor(estimate) {
740
+ if (estimate.status !== "ok")
741
+ return "low";
742
+ if (estimate.method === "exact")
743
+ return "high";
744
+ return "medium";
745
+ }
746
+ function legacyInfinity() {
747
+ return Number.POSITIVE_INFINITY;
748
+ }
749
+ function analyticEstimate(scenario, target) {
750
+ const { E } = scenario.ctx;
751
+ const binding = etaGoalForTarget(scenario, target);
752
+ const blocks = exactBlocks(scenario);
753
+ const proven = blocks.length === 0 && target.kind === "money";
754
+ const provenIncome = blocks.every((block) => block === COMPARISON_BOUNDARY) && target.kind === "money";
755
+ const evaluation = evaluateGoal(binding.goal, scenario.initial, scenario.ctx);
756
+ // cmp reads NaN as reached. A non-finite wallet or target falls through to the engine ratio, which is invalid.
757
+ const already = evaluation.reached === true &&
758
+ E.isFinite(binding.threshold) &&
759
+ E.isFinite(scenario.initial.wallet.money.amount);
760
+ if (target.kind === "netWorth" && scenario.model.netWorth) {
761
+ const current = scenario.model.netWorth(scenario.ctx, scenario.initial).amount;
762
+ // cmp reads NaN as already reached, so a non-finite side is checked first.
763
+ if (E.isFinite(current) && E.isFinite(binding.threshold) && E.cmp(current, binding.threshold) >= 0) {
764
+ return {
765
+ reached: true,
766
+ seconds: 0,
767
+ estimate: baseEstimate(binding, "ok", {
768
+ seconds: 0,
769
+ assumptions: [
770
+ "target already reached at t=0",
771
+ "net worth was read directly; money income is not its derivative",
772
+ ],
773
+ }),
774
+ };
20
775
  }
21
- const worth = args.scenario.model.netWorth?.(args.scenario.ctx, s) ?? s.wallet.money;
22
- return args.scenario.ctx.E.cmp(worth.amount, threshold) >= 0;
776
+ const rate = scenario.model.income(scenario.ctx, scenario.initial).amount;
777
+ const local = etaSecondsFromEngineRatio(E, binding.threshold, current, rate, false);
778
+ return {
779
+ reached: local.seconds !== null,
780
+ seconds: local.seconds ?? legacyInfinity(),
781
+ estimate: baseEstimate(binding, "unsupported", {
782
+ assumptions: [
783
+ "unsupported-local",
784
+ "net worth has its own value, so money income is not its derivative",
785
+ "legacy seconds keeps the old initial-income ratio and is not a net worth result",
786
+ ...blocks,
787
+ ],
788
+ ...(local.secondsDecimal !== undefined ? { secondsDecimal: local.secondsDecimal } : {}),
789
+ }),
790
+ };
791
+ }
792
+ if (target.kind === "netWorth") {
793
+ const wallet = scenario.initial.wallet.money.amount;
794
+ const local = already
795
+ ? { status: "ok", seconds: 0 }
796
+ : etaSecondsFromEngineRatio(E, binding.threshold, wallet, scenario.model.income(scenario.ctx, scenario.initial).amount, false);
797
+ return {
798
+ reached: already,
799
+ seconds: local.seconds ?? legacyInfinity(),
800
+ estimate: baseEstimate(binding, "unsupported", {
801
+ assumptions: [
802
+ "unsupported-local",
803
+ "netWorth is absent; the legacy seconds field reuses wallet money and is not a net worth derivative",
804
+ ...(evaluation.assumptions ?? []),
805
+ ],
806
+ }),
807
+ };
808
+ }
809
+ const current = scenario.initial.wallet.money.amount;
810
+ if (already) {
811
+ const method = proven ? "exact" : "analytic-local";
812
+ return {
813
+ reached: true,
814
+ seconds: 0,
815
+ estimate: baseEstimate(binding, "ok", {
816
+ method,
817
+ seconds: 0,
818
+ assumptions: ["target already reached at t=0", ...blocks],
819
+ }),
820
+ };
821
+ }
822
+ const income = scenario.model.income(scenario.ctx, scenario.initial);
823
+ if (income.unit.code !== scenario.initial.wallet.money.unit.code ||
824
+ income.unit.code !== scenario.ctx.unit.code) {
825
+ return {
826
+ reached: false,
827
+ seconds: legacyInfinity(),
828
+ estimate: baseEstimate(binding, "invalid", {
829
+ assumptions: ["income unit differs from the wallet or target unit and cannot be settled"],
830
+ }),
831
+ };
832
+ }
833
+ const rate = income.amount;
834
+ const ratio = etaSecondsFromEngineRatio(E, binding.threshold, current, rate, provenIncome);
835
+ if (ratio.status !== "ok" || ratio.seconds === null) {
836
+ const assumptions = [
837
+ ratio.status === "unsupported"
838
+ ? "constant non-positive income does not reach a higher target; the estimate makes no reachability claim"
839
+ : ratio.status === "insufficient-data"
840
+ ? "the local rate is not positive; that is not proof the target stays out of reach"
841
+ : "the engine ratio is not a finite seconds count",
842
+ ...blocks,
843
+ ];
844
+ return {
845
+ reached: false,
846
+ seconds: legacyInfinity(),
847
+ estimate: baseEstimate(binding, ratio.status, {
848
+ assumptions,
849
+ ...(ratio.secondsDecimal !== undefined ? { secondsDecimal: ratio.secondsDecimal } : {}),
850
+ }),
851
+ };
852
+ }
853
+ const method = proven ? "exact" : "analytic-local";
854
+ return {
855
+ reached: true,
856
+ seconds: ratio.seconds,
857
+ estimate: baseEstimate(binding, "ok", {
858
+ method,
859
+ seconds: ratio.seconds,
860
+ assumptions: [
861
+ method === "exact"
862
+ ? "constant money derivative with no purchase, reset, evolve, or policy boundary"
863
+ : "analytic-local estimate from the initial rate only",
864
+ ...blocks,
865
+ ],
866
+ }),
23
867
  };
868
+ }
869
+ function bracketFor(continuous, observed) {
870
+ if (continuous === null || !Number.isFinite(observed))
871
+ return undefined;
872
+ const lower = Math.min(continuous, observed);
873
+ const upper = Math.max(continuous, observed);
874
+ return { lowerSec: lower, upperSec: upper };
875
+ }
876
+ export function etaSimulate(args) {
877
+ const binding = etaGoalForTarget(args.scenario, args.target);
878
+ const { E } = args.scenario.ctx;
879
+ const netWorthOf = args.target.kind === "netWorth" ? args.scenario.model.netWorth : undefined;
880
+ const observedAmount = (state) => netWorthOf ? netWorthOf(args.scenario.ctx, state).amount : state.wallet.money.amount;
881
+ // cmp reads NaN as reached, so a non-finite threshold or observed amount never ends the run as the goal.
882
+ const finiteAt = (state) => E.isFinite(binding.threshold) && E.isFinite(observedAmount(state));
883
+ const until = (state) => finiteAt(state) && binding.goal.met(state, args.scenario.ctx) === true;
24
884
  const scenario = {
25
885
  ...args.scenario,
26
886
  run: {
@@ -33,73 +893,111 @@ export function etaSimulate(args) {
33
893
  const run = runScenario(scenario);
34
894
  // A budget stop is neither the target nor maxDurationSec.
35
895
  assertHorizonReached(run, "etaSimulate");
36
- const reached = targetReached(scenario, args.target.kind, threshold, run);
896
+ const reached = finiteAt(run.end) && evaluateGoal(binding.goal, run.end, scenario.ctx).reached === true;
897
+ const nonFinite = !reached && !finiteAt(run.end);
898
+ // Without model.netWorth the legacy goal reads wallet money. That is not a net worth observation.
899
+ const walletProxy = args.target.kind === "netWorth" && args.scenario.model.netWorth === undefined;
900
+ const observed = reached ? runElapsedSec(run) : args.maxDurationSec;
901
+ const resolution = args.scenario.run.stepSec;
902
+ const blocks = exactBlocks(args.scenario);
903
+ const monotonic = blocks.length === 0 && args.target.kind === "money";
904
+ const continuousSeconds = monotonic ? analyticEstimate(args.scenario, args.target).estimate.seconds : null;
905
+ const assumptions = [
906
+ "fixed-step first observation, not a continuous crossing",
907
+ ...(reached ? [] : ["horizon ended before the goal was observed"]),
908
+ ...blocks,
909
+ ];
910
+ const bracket = reached && continuousSeconds !== null ? bracketFor(continuousSeconds, observed) : undefined;
911
+ const estimate = nonFinite
912
+ ? baseEstimate(binding, "invalid", {
913
+ observationResolutionSec: resolution,
914
+ assumptions: [...assumptions, "the threshold or the observed amount is not finite, so the goal is not evaluated"],
915
+ })
916
+ : walletProxy
917
+ ? baseEstimate(binding, "unsupported", {
918
+ observationResolutionSec: resolution,
919
+ assumptions: [
920
+ ...assumptions,
921
+ "unsupported-local",
922
+ "netWorth is absent; the legacy seconds field reuses wallet money and is not a net worth observation",
923
+ ],
924
+ })
925
+ : !reached
926
+ ? baseEstimate(binding, "censored", {
927
+ horizonSec: args.maxDurationSec,
928
+ observationResolutionSec: resolution,
929
+ assumptions,
930
+ })
931
+ : baseEstimate(binding, "ok", {
932
+ method: "fixed-step",
933
+ seconds: observed,
934
+ observationResolutionSec: resolution,
935
+ ...(bracket !== undefined ? { bracket } : {}),
936
+ assumptions: bracket
937
+ ? assumptions
938
+ : [...assumptions, "no reach bracket; a hidden crossing is not ruled out"],
939
+ });
37
940
  return {
38
941
  reached,
39
- seconds: reached ? runElapsedSec(run) : args.maxDurationSec,
942
+ seconds: observed,
40
943
  mode: "simulate",
41
- confidence: "high",
42
- assumptions: ["Direct simulation over maxDurationSec"],
944
+ confidence: confidenceFor(estimate),
945
+ assumptions: ["Direct simulation over maxDurationSec", ...assumptions],
43
946
  run: args.includeRun ? run : undefined,
947
+ estimate,
44
948
  };
45
949
  }
46
- function analyticUnavailable(reason) {
950
+ export function etaAnalytic(args) {
951
+ const local = analyticEstimate(args.scenario, args.target);
952
+ let estimate = local.estimate;
953
+ if (args.fallback) {
954
+ const stepped = etaSimulate({
955
+ scenario: args.scenario,
956
+ target: args.target,
957
+ maxDurationSec: args.fallback.maxDurationSec,
958
+ });
959
+ const localSec = local.estimate.status === "ok" ? local.estimate.seconds : null;
960
+ const fixedStepSec = stepped.estimate.status === "ok" ? stepped.estimate.seconds : null;
961
+ estimate = {
962
+ ...local.estimate,
963
+ compare: {
964
+ localMethod: local.estimate.method ?? null,
965
+ fixedStepMethod: "fixed-step",
966
+ localSec,
967
+ fixedStepSec,
968
+ diffSec: localSec !== null && fixedStepSec !== null ? fixedStepSec - localSec : null,
969
+ },
970
+ };
971
+ }
47
972
  return {
48
- reached: false,
49
- seconds: Number.POSITIVE_INFINITY,
973
+ reached: local.reached,
974
+ seconds: local.seconds,
50
975
  mode: "analytic",
51
- confidence: "low",
52
- assumptions: [reason],
976
+ confidence: confidenceFor(estimate),
977
+ assumptions: [...estimate.assumptions],
978
+ estimate,
53
979
  };
54
980
  }
55
- export function etaAnalytic(args) {
56
- const { scenario } = args;
57
- const { E } = scenario.ctx;
58
- const currMoney = scenario.initial.wallet.money.amount;
59
- const currWorth = (scenario.model.netWorth?.(scenario.ctx, scenario.initial) ?? scenario.initial.wallet.money)
60
- .amount;
61
- const rate = scenario.model.income(scenario.ctx, scenario.initial).amount;
62
- // Same notation as etaSimulate. `E.from("1aa")` is NaN, and cmp reads NaN as already reached.
63
- const threshold = parseMoney(E, args.target.value, {
64
- unit: scenario.ctx.unit,
65
- suffix: { kind: "alphaInfinite", minLen: 2 },
66
- }).amount;
67
- const current = args.target.kind === "money" ? currMoney : currWorth;
68
- if (!E.isFinite(current) || !E.isFinite(threshold)) {
69
- return analyticUnavailable("Current or target amount is not finite; analytic estimate unavailable");
981
+ export function validateEtaEstimateSemantics(estimate) {
982
+ const issues = [];
983
+ if (estimate.status === "censored" && estimate.seconds !== null) {
984
+ issues.push("censored estimate carries seconds");
70
985
  }
71
- if (E.cmp(current, threshold) >= 0) {
72
- return {
73
- reached: true,
74
- seconds: 0,
75
- mode: "analytic",
76
- confidence: "high",
77
- assumptions: ["Target already reached at t=0"],
78
- };
986
+ else if (estimate.status !== "ok" && estimate.seconds !== null) {
987
+ issues.push(`${estimate.status} estimate carries seconds`);
79
988
  }
80
- const r = E.toNumber(rate);
81
- if (!Number.isFinite(r)) {
82
- return analyticUnavailable("Initial income is not finite; analytic estimate unavailable");
989
+ if (estimate.status === "ok" && (estimate.seconds === null || !Number.isFinite(estimate.seconds))) {
990
+ issues.push("ok estimate has no finite seconds");
83
991
  }
84
- if (!(r > 0)) {
85
- return analyticUnavailable("Non-positive initial income; analytic estimate unavailable");
992
+ if (estimate.status === "ok" && estimate.seconds !== null && estimate.seconds < 0) {
993
+ issues.push("ok estimate has negative seconds");
86
994
  }
87
- const diff = E.toNumber(E.sub(threshold, current));
88
- const seconds = Math.max(0, diff / r);
89
- if (!Number.isFinite(seconds)) {
90
- return analyticUnavailable("Remaining amount does not fit a finite number; analytic estimate unavailable");
91
- }
92
- const hint = scenario.model.analytic?.(scenario.ctx, scenario.initial);
93
- const confidence = hint?.incomeKind === "constant" ? "high" : "medium";
94
- return {
95
- reached: true,
96
- seconds,
97
- mode: "analytic",
98
- confidence,
99
- assumptions: [
100
- "Income assumed near-constant in local interval",
101
- hint ? `Model analytic hint: ${hint.incomeKind ?? "custom"}` : "No analytic hint provided",
102
- ],
103
- };
995
+ if (estimate.method === "exact" && estimate.status !== "ok")
996
+ issues.push("exact method is not ok");
997
+ if (estimate.bracket && estimate.status !== "ok")
998
+ issues.push("bracket on a non-ok estimate");
999
+ if (JSON.stringify(estimate).includes("unreachable"))
1000
+ issues.push("estimate says unreachable");
1001
+ return issues;
104
1002
  }
105
1003
  //# sourceMappingURL=eta.js.map