hoi4-agent-tools 2.2.0 → 2.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/CHANGELOG.md +15 -0
- package/README.md +30 -21
- package/dist/bin/stdio.js +0 -4
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +5 -0
- package/dist/hoi4_agent_tools/core/index.js +57 -1
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/limits.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +2 -0
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +17 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +0 -530
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
import { assignments } from '../core/source/index.js';
|
|
3
|
+
import { Rational, sumRationals, uniformRaceProbabilities } from './rational.js';
|
|
4
|
+
import { evaluateTriggerBlock } from './trigger-evaluator.js';
|
|
5
|
+
import { evaluateCandidateValue } from './value-evaluator.js';
|
|
6
|
+
const BRANCH_LIMIT = 4_096;
|
|
7
|
+
function numericState(value) {
|
|
8
|
+
return typeof value === 'number' || typeof value === 'string' ? Rational.parse(value) : undefined;
|
|
9
|
+
}
|
|
10
|
+
function externalFactor(surface, candidate, scenario) {
|
|
11
|
+
let factor = Rational.one;
|
|
12
|
+
const trace = [];
|
|
13
|
+
const unresolved = [];
|
|
14
|
+
if (surface.adapter.id === 'national_focus_ai_will_do') {
|
|
15
|
+
const sourceFactor = numericState(scenario.state[`focus_factor.${candidate.id}`] ??
|
|
16
|
+
scenario.state[`ai_strategy.focus_factor.${candidate.id}`]);
|
|
17
|
+
if (sourceFactor !== undefined) {
|
|
18
|
+
const before = factor;
|
|
19
|
+
factor = factor.multiply(sourceFactor);
|
|
20
|
+
trace.push({
|
|
21
|
+
operation: 'external_factor',
|
|
22
|
+
expression: `focus_factor.${candidate.id}`,
|
|
23
|
+
applied: 'true',
|
|
24
|
+
value: sourceFactor.toJSON(),
|
|
25
|
+
before: before.toJSON(),
|
|
26
|
+
after: factor.toJSON(),
|
|
27
|
+
note: 'Declared aggregate AI strategy focus factor',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (scenario.state[`focus_prerequisite_completed.${candidate.id}`] === true) {
|
|
31
|
+
const prerequisite = new Rational(3n, 2n);
|
|
32
|
+
const before = factor;
|
|
33
|
+
factor = factor.multiply(prerequisite);
|
|
34
|
+
trace.push({
|
|
35
|
+
operation: 'external_factor',
|
|
36
|
+
expression: `focus_prerequisite_completed.${candidate.id}`,
|
|
37
|
+
applied: 'true',
|
|
38
|
+
value: prerequisite.toJSON(),
|
|
39
|
+
before: before.toJSON(),
|
|
40
|
+
after: factor.toJSON(),
|
|
41
|
+
note: 'Verified recently-completed prerequisite multiplier',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (scenario.state['focus.external_factors_complete'] !== true) {
|
|
45
|
+
unresolved.push({
|
|
46
|
+
code: 'FOCUS_EXTERNAL_FACTORS_UNDECLARED',
|
|
47
|
+
message: 'Exact focus selection requires the scenario to declare that strategy-plan and prerequisite factors are complete',
|
|
48
|
+
path: 'focus.external_factors_complete',
|
|
49
|
+
candidateId: candidate.id,
|
|
50
|
+
...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (surface.adapter.id === 'technology_ai_will_do' ||
|
|
55
|
+
surface.adapter.id === 'doctrine_ai_will_do') {
|
|
56
|
+
const percentage = numericState(scenario.state[`research_weight_factor.${candidate.id}`] ??
|
|
57
|
+
scenario.state['research_weight_factor.all']);
|
|
58
|
+
if (percentage !== undefined) {
|
|
59
|
+
const multiplier = Rational.one.add(percentage.divide(new Rational(100n)));
|
|
60
|
+
const before = factor;
|
|
61
|
+
factor = factor.multiply(multiplier.max(Rational.zero));
|
|
62
|
+
trace.push({
|
|
63
|
+
operation: 'external_factor',
|
|
64
|
+
expression: `research_weight_factor.${candidate.id}`,
|
|
65
|
+
applied: 'true',
|
|
66
|
+
value: multiplier.toJSON(),
|
|
67
|
+
before: before.toJSON(),
|
|
68
|
+
after: factor.toJSON(),
|
|
69
|
+
note: 'Declared research_weight_factor percentage converted to a score multiplier',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (scenario.state['technology.external_factors_complete'] !== true) {
|
|
73
|
+
unresolved.push({
|
|
74
|
+
code: 'TECHNOLOGY_EXTERNAL_FACTORS_UNDECLARED',
|
|
75
|
+
message: 'Exact technology selection requires all research cost, bonus, date, strategy, and candidate factors to be declared complete',
|
|
76
|
+
path: 'technology.external_factors_complete',
|
|
77
|
+
candidateId: candidate.id,
|
|
78
|
+
...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { factor, trace, unresolved };
|
|
83
|
+
}
|
|
84
|
+
export function evaluateExactCandidates(surface, scenario, definitions) {
|
|
85
|
+
const candidates = surface.candidates.map((candidate) => {
|
|
86
|
+
const eligibility = evaluateTriggerBlock(candidate.eligibilityBlock, scenario, candidate, definitions);
|
|
87
|
+
const value = evaluateCandidateValue(candidate, scenario, definitions);
|
|
88
|
+
const external = externalFactor(surface, candidate, scenario);
|
|
89
|
+
const exactValue = eligibility.state === 'false'
|
|
90
|
+
? Rational.zero
|
|
91
|
+
: value.value?.multiply(external.factor).max(Rational.zero);
|
|
92
|
+
return {
|
|
93
|
+
id: candidate.id,
|
|
94
|
+
eligibility: eligibility.state,
|
|
95
|
+
...(exactValue === undefined ? {} : { value: exactValue }),
|
|
96
|
+
trace: [
|
|
97
|
+
...(candidate.eligibilityBlock === undefined
|
|
98
|
+
? [
|
|
99
|
+
{
|
|
100
|
+
operation: 'eligibility',
|
|
101
|
+
expression: 'implicit true',
|
|
102
|
+
applied: eligibility.state,
|
|
103
|
+
...(candidate.provenance[0] === undefined
|
|
104
|
+
? {}
|
|
105
|
+
: { provenance: candidate.provenance[0] }),
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
: (eligibility.trace ?? [])),
|
|
109
|
+
...value.trace,
|
|
110
|
+
...external.trace,
|
|
111
|
+
],
|
|
112
|
+
unresolved: [...eligibility.unresolved, ...value.unresolved, ...external.unresolved],
|
|
113
|
+
provenance: [
|
|
114
|
+
...candidate.provenance,
|
|
115
|
+
...(eligibility.helperProvenance ?? []),
|
|
116
|
+
...(value.referencedProvenance ?? []),
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
const eligible = candidates.filter(({ eligibility, value }) => eligibility === 'true' && value !== undefined);
|
|
121
|
+
if (surface.adapter.selectionRule === 'proportional_categorical' && surface.poolComplete) {
|
|
122
|
+
const poolResolved = candidates.every(({ eligibility, value }) => eligibility !== 'unresolved' && value !== undefined);
|
|
123
|
+
const total = sumRationals(eligible.map(({ value }) => value));
|
|
124
|
+
if (poolResolved && !total.isZero()) {
|
|
125
|
+
for (const candidate of candidates)
|
|
126
|
+
candidate.probability =
|
|
127
|
+
candidate.eligibility === 'true' && candidate.value !== undefined
|
|
128
|
+
? candidate.value.divide(total)
|
|
129
|
+
: Rational.zero;
|
|
130
|
+
}
|
|
131
|
+
else if (poolResolved &&
|
|
132
|
+
surface.adapter.id === 'event_option_ai_chance' &&
|
|
133
|
+
candidates.length > 0) {
|
|
134
|
+
for (const [index, candidate] of candidates.entries())
|
|
135
|
+
candidate.probability = index === 0 ? Rational.one : Rational.zero;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (surface.adapter.selectionRule === 'uniform_score_race' && surface.poolComplete) {
|
|
139
|
+
const externalComplete = candidates.every(({ unresolved: items }) => items.every(({ code }) => code !== 'FOCUS_EXTERNAL_FACTORS_UNDECLARED' &&
|
|
140
|
+
code !== 'TECHNOLOGY_EXTERNAL_FACTORS_UNDECLARED'));
|
|
141
|
+
if (externalComplete &&
|
|
142
|
+
candidates.every(({ eligibility, value }) => eligibility !== 'unresolved' && value !== undefined)) {
|
|
143
|
+
const probabilities = uniformRaceProbabilities(candidates.map(({ value }) => value));
|
|
144
|
+
for (const [index, probability] of probabilities.entries())
|
|
145
|
+
candidates[index].probability = probability;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (surface.adapter.selectionRule === 'independent_chance') {
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
if (candidate.value === undefined)
|
|
151
|
+
continue;
|
|
152
|
+
candidate.probability = candidate.value
|
|
153
|
+
.divide(new Rational(100n))
|
|
154
|
+
.max(Rational.zero)
|
|
155
|
+
.min(Rational.one);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (surface.adapter.id === 'random_list') {
|
|
159
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
160
|
+
if (candidate.probability === undefined)
|
|
161
|
+
continue;
|
|
162
|
+
let pathProbability = candidate.probability;
|
|
163
|
+
let pathResolved = true;
|
|
164
|
+
for (const parent of surface.candidates[index]?.parentRandomPools ?? []) {
|
|
165
|
+
const parentSurface = {
|
|
166
|
+
id: parent.id,
|
|
167
|
+
adapter: surface.adapter,
|
|
168
|
+
candidates: parent.candidates,
|
|
169
|
+
poolComplete: true,
|
|
170
|
+
sourceRevision: surface.sourceRevision,
|
|
171
|
+
sourceHash: surface.sourceHash,
|
|
172
|
+
filesScanned: surface.filesScanned,
|
|
173
|
+
unsupported: [],
|
|
174
|
+
};
|
|
175
|
+
const parentEvaluated = evaluateExactCandidates(parentSurface, scenario, definitions);
|
|
176
|
+
const selected = parentEvaluated[parent.selectedEntryIndex];
|
|
177
|
+
if (selected?.probability === undefined || selected.unresolved.length > 0) {
|
|
178
|
+
pathResolved = false;
|
|
179
|
+
candidate.unresolved.push({
|
|
180
|
+
code: 'NESTED_RANDOM_PATH_UNRESOLVED',
|
|
181
|
+
message: `Parent random-list path ${parent.id} cannot be evaluated exactly`,
|
|
182
|
+
candidateId: candidate.id,
|
|
183
|
+
...(selected?.provenance[0] === undefined
|
|
184
|
+
? {}
|
|
185
|
+
: { provenance: selected.provenance[0] }),
|
|
186
|
+
details: { parentRandomListId: parent.id },
|
|
187
|
+
});
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
pathProbability = pathProbability.multiply(selected.probability);
|
|
191
|
+
candidate.provenance.push(...selected.provenance);
|
|
192
|
+
}
|
|
193
|
+
if (pathResolved)
|
|
194
|
+
candidate.pathProbability = pathProbability;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return candidates;
|
|
198
|
+
}
|
|
199
|
+
function scalarThresholds(block) {
|
|
200
|
+
if (block === undefined)
|
|
201
|
+
return [];
|
|
202
|
+
const values = [];
|
|
203
|
+
for (const assignment of assignments(block)) {
|
|
204
|
+
if (assignment.value.type === 'scalar') {
|
|
205
|
+
const value = Number(assignment.value.value);
|
|
206
|
+
if (Number.isFinite(value))
|
|
207
|
+
values.push(value);
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
values.push(...scalarThresholds(assignment.value));
|
|
211
|
+
}
|
|
212
|
+
return values;
|
|
213
|
+
}
|
|
214
|
+
export function boundedRangeValues(surface, min, max) {
|
|
215
|
+
const values = new Set([min, max]);
|
|
216
|
+
for (const candidate of surface.candidates) {
|
|
217
|
+
for (const threshold of [
|
|
218
|
+
...scalarThresholds(candidate.weightBlock),
|
|
219
|
+
...scalarThresholds(candidate.eligibilityBlock),
|
|
220
|
+
]) {
|
|
221
|
+
if (threshold < min || threshold > max)
|
|
222
|
+
continue;
|
|
223
|
+
values.add(threshold);
|
|
224
|
+
const epsilon = Math.max(1e-9, Math.abs(threshold) * 1e-9);
|
|
225
|
+
if (threshold - epsilon >= min)
|
|
226
|
+
values.add(threshold - epsilon);
|
|
227
|
+
if (threshold + epsilon <= max)
|
|
228
|
+
values.add(threshold + epsilon);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return [...values].sort((left, right) => left - right);
|
|
232
|
+
}
|
|
233
|
+
function triggerThresholds(block) {
|
|
234
|
+
if (block === undefined)
|
|
235
|
+
return [];
|
|
236
|
+
const values = [];
|
|
237
|
+
for (const assignment of assignments(block)) {
|
|
238
|
+
if (assignment.key.value === 'check_variable' && assignment.value.type === 'block') {
|
|
239
|
+
const threshold = assignments(assignment.value).find(({ key }) => key.value === 'value');
|
|
240
|
+
if (threshold?.value.type === 'scalar') {
|
|
241
|
+
const value = Number(threshold.value.value);
|
|
242
|
+
if (Number.isFinite(value))
|
|
243
|
+
values.push(value);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (assignment.value.type === 'block')
|
|
247
|
+
values.push(...triggerThresholds(assignment.value));
|
|
248
|
+
}
|
|
249
|
+
return values;
|
|
250
|
+
}
|
|
251
|
+
export function conditionBreakpointValues(surface, min, max) {
|
|
252
|
+
const values = new Set();
|
|
253
|
+
for (const candidate of surface.candidates)
|
|
254
|
+
for (const threshold of [
|
|
255
|
+
...triggerThresholds(candidate.weightBlock),
|
|
256
|
+
...triggerThresholds(candidate.eligibilityBlock),
|
|
257
|
+
]) {
|
|
258
|
+
if (threshold < min || threshold > max)
|
|
259
|
+
continue;
|
|
260
|
+
values.add(threshold);
|
|
261
|
+
const epsilon = Math.max(1e-9, Math.abs(threshold) * 1e-9);
|
|
262
|
+
if (threshold - epsilon >= min)
|
|
263
|
+
values.add(threshold - epsilon);
|
|
264
|
+
if (threshold + epsilon <= max)
|
|
265
|
+
values.add(threshold + epsilon);
|
|
266
|
+
}
|
|
267
|
+
return [...values].sort((left, right) => left - right);
|
|
268
|
+
}
|
|
269
|
+
function scenarioBranches(surface, scenario) {
|
|
270
|
+
let branches = [{ ...scenario, state: { ...scenario.state } }];
|
|
271
|
+
let bounded = false;
|
|
272
|
+
const unresolved = [];
|
|
273
|
+
for (const input of scenario.uncertainInputs ?? []) {
|
|
274
|
+
let values = [];
|
|
275
|
+
if (input.alternatives !== undefined)
|
|
276
|
+
values = input.alternatives;
|
|
277
|
+
else if (input.range !== undefined) {
|
|
278
|
+
values = boundedRangeValues(surface, input.range.min, input.range.max);
|
|
279
|
+
bounded = true;
|
|
280
|
+
}
|
|
281
|
+
else if (input.distribution !== undefined) {
|
|
282
|
+
unresolved.push({
|
|
283
|
+
code: 'DISTRIBUTION_REQUIRES_SIMULATION',
|
|
284
|
+
message: `Distribution for ${input.path} requires seeded simulation`,
|
|
285
|
+
path: input.path,
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
else if (input.unresolved === true) {
|
|
290
|
+
unresolved.push({
|
|
291
|
+
code: 'SCENARIO_INPUT_UNRESOLVED',
|
|
292
|
+
message: `Scenario explicitly leaves ${input.path} unresolved`,
|
|
293
|
+
path: input.path,
|
|
294
|
+
});
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (values.length === 0)
|
|
298
|
+
continue;
|
|
299
|
+
if (branches.length * values.length > BRANCH_LIMIT) {
|
|
300
|
+
unresolved.push({
|
|
301
|
+
code: 'SCENARIO_BRANCH_LIMIT',
|
|
302
|
+
message: `Exact alternative expansion exceeds ${BRANCH_LIMIT} branches`,
|
|
303
|
+
path: input.path,
|
|
304
|
+
details: { currentBranches: branches.length, alternatives: values.length },
|
|
305
|
+
});
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
branches = branches.flatMap((branch) => values.map((value) => ({ ...branch, state: { ...branch.state, [input.path]: value } })));
|
|
309
|
+
if (values.length > 1)
|
|
310
|
+
bounded = true;
|
|
311
|
+
}
|
|
312
|
+
return { branches, bounded, unresolved };
|
|
313
|
+
}
|
|
314
|
+
function supportLevel(surface, bounded, unresolved, hasExactProbability) {
|
|
315
|
+
if (surface.adapter.selectionRule === 'score_only')
|
|
316
|
+
return 'score_only';
|
|
317
|
+
if (bounded)
|
|
318
|
+
return 'bounded';
|
|
319
|
+
const externalCodes = new Set([
|
|
320
|
+
'FOCUS_EXTERNAL_FACTORS_UNDECLARED',
|
|
321
|
+
'TECHNOLOGY_EXTERNAL_FACTORS_UNDECLARED',
|
|
322
|
+
]);
|
|
323
|
+
if (unresolved.length > 0 &&
|
|
324
|
+
unresolved.every(({ code }) => externalCodes.has(code)) &&
|
|
325
|
+
!hasExactProbability)
|
|
326
|
+
return 'external';
|
|
327
|
+
if (unresolved.length > 0 && !hasExactProbability)
|
|
328
|
+
return 'unsupported';
|
|
329
|
+
return 'exact';
|
|
330
|
+
}
|
|
331
|
+
function rankCandidates(candidates) {
|
|
332
|
+
const ranked = candidates
|
|
333
|
+
.filter(({ rawValue, eligibility }) => rawValue !== undefined && rawValue !== null && eligibility !== 'false')
|
|
334
|
+
.sort((left, right) => {
|
|
335
|
+
const leftValue = typeof left.rawValue === 'object' && left.rawValue !== null
|
|
336
|
+
? left.rawValue.value
|
|
337
|
+
: Number(left.rawValue);
|
|
338
|
+
const rightValue = typeof right.rawValue === 'object' && right.rawValue !== null
|
|
339
|
+
? right.rawValue.value
|
|
340
|
+
: Number(right.rawValue);
|
|
341
|
+
return rightValue - leftValue || compareCodeUnits(left.id, right.id);
|
|
342
|
+
});
|
|
343
|
+
for (const [index, candidate] of ranked.entries())
|
|
344
|
+
candidate.rank = index + 1;
|
|
345
|
+
}
|
|
346
|
+
function scenarioSummary(candidates) {
|
|
347
|
+
const eligible = candidates
|
|
348
|
+
.filter(({ eligibility }) => eligibility === 'true')
|
|
349
|
+
.sort((left, right) => (left.rank ?? Number.MAX_SAFE_INTEGER) - (right.rank ?? Number.MAX_SAFE_INTEGER) ||
|
|
350
|
+
compareCodeUnits(left.id, right.id));
|
|
351
|
+
const metric = (candidate) => {
|
|
352
|
+
if (candidate.conditionalProbability !== undefined && candidate.conditionalProbability !== null)
|
|
353
|
+
return { value: candidate.conditionalProbability, kind: 'probability' };
|
|
354
|
+
return {
|
|
355
|
+
value: typeof candidate.rawValue === 'object' && candidate.rawValue !== null
|
|
356
|
+
? candidate.rawValue.value
|
|
357
|
+
: (candidate.rawInterval?.max ?? 0),
|
|
358
|
+
kind: 'raw_value',
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
const [leader, runnerUp] = eligible;
|
|
362
|
+
let closestRankReversal;
|
|
363
|
+
if (leader !== undefined && runnerUp !== undefined) {
|
|
364
|
+
const leaderMetric = metric(leader);
|
|
365
|
+
const runnerUpMetric = metric(runnerUp);
|
|
366
|
+
if (leaderMetric.kind === runnerUpMetric.kind)
|
|
367
|
+
closestRankReversal = {
|
|
368
|
+
candidates: [leader.id, runnerUp.id],
|
|
369
|
+
gap: Math.abs(leaderMetric.value - runnerUpMetric.value),
|
|
370
|
+
metric: leaderMetric.kind,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const dominantFactors = (leader?.trace ?? [])
|
|
374
|
+
.filter(({ applied, operation }) => applied === 'true' && operation !== 'eligibility')
|
|
375
|
+
.map(({ operation, expression }) => `${operation} ${expression}`)
|
|
376
|
+
.slice(-5);
|
|
377
|
+
return {
|
|
378
|
+
topOutcomes: eligible.slice(0, 3).map(({ id }) => id),
|
|
379
|
+
bottomEligibleOutcomes: eligible
|
|
380
|
+
.slice(-3)
|
|
381
|
+
.reverse()
|
|
382
|
+
.map(({ id }) => id),
|
|
383
|
+
impossibleOutcomes: candidates
|
|
384
|
+
.filter(({ eligibility, conditionalProbability }) => eligibility === 'false' || conditionalProbability === 0)
|
|
385
|
+
.map(({ id }) => id),
|
|
386
|
+
unresolvedOutcomes: candidates
|
|
387
|
+
.filter(({ eligibility, unresolved }) => eligibility === 'unresolved' || unresolved.length > 0)
|
|
388
|
+
.map(({ id }) => id),
|
|
389
|
+
dominantFactors,
|
|
390
|
+
...(closestRankReversal === undefined ? {} : { closestRankReversal }),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function scheduledMtthChance(surface, scenario, definitions, horizonDays) {
|
|
394
|
+
if (surface.adapter.id !== 'event_mean_time_to_happen' || surface.candidates.length !== 1)
|
|
395
|
+
return { unresolved: [] };
|
|
396
|
+
let state = {
|
|
397
|
+
...scenario,
|
|
398
|
+
state: { ...scenario.state },
|
|
399
|
+
flags: [...(scenario.flags ?? [])],
|
|
400
|
+
};
|
|
401
|
+
const schedule = [...(scenario.schedule ?? [])]
|
|
402
|
+
.filter(({ atDay }) => atDay >= 0 && atDay <= horizonDays)
|
|
403
|
+
.sort((left, right) => left.atDay - right.atDay);
|
|
404
|
+
const boundaries = [0, ...schedule.map(({ atDay }) => atDay), horizonDays];
|
|
405
|
+
let minimumHazard = 0;
|
|
406
|
+
let maximumHazard = 0;
|
|
407
|
+
let previousEligibility;
|
|
408
|
+
const unresolved = [];
|
|
409
|
+
const intervals = [];
|
|
410
|
+
for (let index = 0; index + 1 < boundaries.length; index += 1) {
|
|
411
|
+
const start = boundaries[index];
|
|
412
|
+
const end = boundaries[index + 1];
|
|
413
|
+
if (index > 0) {
|
|
414
|
+
const change = schedule[index - 1];
|
|
415
|
+
if (change !== undefined) {
|
|
416
|
+
state = {
|
|
417
|
+
...state,
|
|
418
|
+
state: { ...state.state, ...change.set },
|
|
419
|
+
flags: [
|
|
420
|
+
...new Set([
|
|
421
|
+
...(state.flags ?? []).filter((flag) => !change.clearFlags?.includes(flag)),
|
|
422
|
+
...(change.addFlags ?? []),
|
|
423
|
+
]),
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (end <= start)
|
|
429
|
+
continue;
|
|
430
|
+
const [candidate] = evaluateExactCandidates(surface, state, definitions);
|
|
431
|
+
if (candidate?.eligibility === 'false') {
|
|
432
|
+
intervals.push({
|
|
433
|
+
startDay: start,
|
|
434
|
+
endDay: end,
|
|
435
|
+
eligibility: 'false',
|
|
436
|
+
minimumHazardContribution: 0,
|
|
437
|
+
maximumHazardContribution: 0,
|
|
438
|
+
});
|
|
439
|
+
previousEligibility = 'false';
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (candidate?.eligibility !== 'true' ||
|
|
443
|
+
candidate.value === undefined ||
|
|
444
|
+
candidate.value.compare(Rational.zero) <= 0) {
|
|
445
|
+
intervals.push({
|
|
446
|
+
startDay: start,
|
|
447
|
+
endDay: end,
|
|
448
|
+
eligibility: candidate?.eligibility ?? 'unresolved',
|
|
449
|
+
minimumHazardContribution: 0,
|
|
450
|
+
maximumHazardContribution: 0,
|
|
451
|
+
});
|
|
452
|
+
unresolved.push(...(candidate?.unresolved ?? []));
|
|
453
|
+
previousEligibility = candidate?.eligibility;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const duration = end - start;
|
|
457
|
+
const mtth = candidate.value.toNumber();
|
|
458
|
+
let intervalMinimumHazard;
|
|
459
|
+
let intervalMaximumHazard;
|
|
460
|
+
if (previousEligibility === 'false') {
|
|
461
|
+
const declaredPhase = state.state['mtth.poll_phase_days'];
|
|
462
|
+
if (typeof declaredPhase === 'number' && declaredPhase >= 0 && declaredPhase <= 20) {
|
|
463
|
+
const activeDays = Math.max(0, duration - declaredPhase);
|
|
464
|
+
intervalMinimumHazard = activeDays / mtth;
|
|
465
|
+
intervalMaximumHazard = activeDays / mtth;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
intervalMinimumHazard = Math.max(0, duration - 20) / mtth;
|
|
469
|
+
intervalMaximumHazard = duration / mtth;
|
|
470
|
+
unresolved.push({
|
|
471
|
+
code: 'MTTH_POLL_PHASE_UNDECLARED',
|
|
472
|
+
message: 'Trigger became true after inactivity; the verified 20-day inactive polling phase is not declared, so horizon chance is bounded',
|
|
473
|
+
candidateId: candidate.id,
|
|
474
|
+
...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
|
|
475
|
+
details: { transitionDay: start, pollWindowDays: 20 },
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
intervalMinimumHazard = duration / mtth;
|
|
481
|
+
intervalMaximumHazard = duration / mtth;
|
|
482
|
+
}
|
|
483
|
+
minimumHazard += intervalMinimumHazard;
|
|
484
|
+
maximumHazard += intervalMaximumHazard;
|
|
485
|
+
intervals.push({
|
|
486
|
+
startDay: start,
|
|
487
|
+
endDay: end,
|
|
488
|
+
eligibility: 'true',
|
|
489
|
+
effectiveMtthDays: mtth,
|
|
490
|
+
minimumHazardContribution: intervalMinimumHazard,
|
|
491
|
+
maximumHazardContribution: intervalMaximumHazard,
|
|
492
|
+
});
|
|
493
|
+
previousEligibility = 'true';
|
|
494
|
+
}
|
|
495
|
+
const minimumChance = 1 - 2 ** -minimumHazard;
|
|
496
|
+
const maximumChance = 1 - 2 ** -maximumHazard;
|
|
497
|
+
if (minimumHazard !== maximumHazard)
|
|
498
|
+
return {
|
|
499
|
+
chanceInterval: { low: minimumChance, high: maximumChance },
|
|
500
|
+
survivalInterval: { low: 1 - maximumChance, high: 1 - minimumChance },
|
|
501
|
+
intervals,
|
|
502
|
+
unresolved,
|
|
503
|
+
};
|
|
504
|
+
if (unresolved.length > 0)
|
|
505
|
+
return { intervals, unresolved };
|
|
506
|
+
return { chance: maximumChance, survival: 1 - maximumChance, intervals, unresolved: [] };
|
|
507
|
+
}
|
|
508
|
+
export function evaluateSurfaceScenarios(surface, scenarios, definitions, horizonDays) {
|
|
509
|
+
return scenarios.map((scenario) => {
|
|
510
|
+
const expanded = scenarioBranches(surface, scenario);
|
|
511
|
+
const branchResults = expanded.branches.map((branch) => evaluateExactCandidates(surface, branch, definitions));
|
|
512
|
+
const candidates = surface.candidates.map((sourceCandidate, candidateIndex) => {
|
|
513
|
+
const evaluated = branchResults.map((branch) => branch[candidateIndex]).filter(Boolean);
|
|
514
|
+
const values = evaluated.flatMap(({ value }) => (value === undefined ? [] : [value]));
|
|
515
|
+
const probabilities = evaluated.flatMap(({ probability }) => probability === undefined ? [] : [probability]);
|
|
516
|
+
const pathProbabilities = evaluated.flatMap(({ pathProbability }) => pathProbability === undefined ? [] : [pathProbability]);
|
|
517
|
+
const unresolved = [
|
|
518
|
+
...expanded.unresolved,
|
|
519
|
+
...evaluated.flatMap(({ unresolved: items }) => items),
|
|
520
|
+
];
|
|
521
|
+
const provenance = [
|
|
522
|
+
...new Map(evaluated
|
|
523
|
+
.flatMap(({ provenance: items }) => items)
|
|
524
|
+
.map((item) => [JSON.stringify(item), item])).values(),
|
|
525
|
+
];
|
|
526
|
+
const eligibilityStates = new Set(evaluated.map(({ eligibility }) => eligibility));
|
|
527
|
+
const eligibility = eligibilityStates.size === 1 ? (evaluated[0]?.eligibility ?? 'unresolved') : 'unresolved';
|
|
528
|
+
const minValue = values.reduce((minimum, value) => value.min(minimum), values[0] ?? Rational.zero);
|
|
529
|
+
const maxValue = values.reduce((maximum, value) => value.max(maximum), values[0] ?? Rational.zero);
|
|
530
|
+
const minProbability = probabilities.reduce((minimum, value) => value.min(minimum), probabilities[0] ?? Rational.zero);
|
|
531
|
+
const maxProbability = probabilities.reduce((maximum, value) => value.max(maximum), probabilities[0] ?? Rational.zero);
|
|
532
|
+
const minPathProbability = pathProbabilities.reduce((minimum, value) => value.min(minimum), pathProbabilities[0] ?? Rational.zero);
|
|
533
|
+
const maxPathProbability = pathProbabilities.reduce((maximum, value) => value.max(maximum), pathProbabilities[0] ?? Rational.zero);
|
|
534
|
+
const exactRaw = values.length > 0 && values.every((value) => value.compare(values[0]) === 0);
|
|
535
|
+
const exactProbability = probabilities.length === branchResults.length &&
|
|
536
|
+
probabilities.length > 0 &&
|
|
537
|
+
probabilities.every((value) => value.compare(probabilities[0]) === 0);
|
|
538
|
+
const exactPathProbability = pathProbabilities.length === branchResults.length &&
|
|
539
|
+
pathProbabilities.length > 0 &&
|
|
540
|
+
pathProbabilities.every((value) => value.compare(pathProbabilities[0]) === 0);
|
|
541
|
+
const support = supportLevel(surface, expanded.bounded, unresolved, exactProbability);
|
|
542
|
+
const result = {
|
|
543
|
+
id: sourceCandidate.id,
|
|
544
|
+
eligibility,
|
|
545
|
+
supportLevel: support,
|
|
546
|
+
...(exactRaw ? { rawValue: values[0].toJSON() } : {}),
|
|
547
|
+
...(values.length > 0 && !exactRaw
|
|
548
|
+
? { rawInterval: { min: minValue.toNumber(), max: maxValue.toNumber() } }
|
|
549
|
+
: {}),
|
|
550
|
+
...(exactProbability
|
|
551
|
+
? {
|
|
552
|
+
conditionalProbability: probabilities[0].toNumber(),
|
|
553
|
+
exactConditionalProbability: probabilities[0].toJSON(),
|
|
554
|
+
}
|
|
555
|
+
: probabilities.length > 0
|
|
556
|
+
? {
|
|
557
|
+
conditionalProbabilityInterval: {
|
|
558
|
+
low: minProbability.toNumber(),
|
|
559
|
+
high: maxProbability.toNumber(),
|
|
560
|
+
},
|
|
561
|
+
}
|
|
562
|
+
: { conditionalProbability: null }),
|
|
563
|
+
...(surface.adapter.id !== 'random_list'
|
|
564
|
+
? {}
|
|
565
|
+
: exactPathProbability
|
|
566
|
+
? {
|
|
567
|
+
pathProbability: pathProbabilities[0].toNumber(),
|
|
568
|
+
exactPathProbability: pathProbabilities[0].toJSON(),
|
|
569
|
+
}
|
|
570
|
+
: pathProbabilities.length > 0
|
|
571
|
+
? {
|
|
572
|
+
pathProbabilityInterval: {
|
|
573
|
+
low: minPathProbability.toNumber(),
|
|
574
|
+
high: maxPathProbability.toNumber(),
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
: { pathProbability: null }),
|
|
578
|
+
...(surface.adapter.id === 'event_mean_time_to_happen' && exactRaw
|
|
579
|
+
? {
|
|
580
|
+
effectiveMtthDays: values[0].toNumber(),
|
|
581
|
+
timingModel: 'verified daily discrete checks with median parameter',
|
|
582
|
+
...(scenario.schedule?.length
|
|
583
|
+
? {}
|
|
584
|
+
: {
|
|
585
|
+
timingQuantilesDays: {
|
|
586
|
+
p10: -values[0].toNumber() * Math.log2(0.9),
|
|
587
|
+
p50: values[0].toNumber(),
|
|
588
|
+
p90: -values[0].toNumber() * Math.log2(0.1),
|
|
589
|
+
p95: -values[0].toNumber() * Math.log2(0.05),
|
|
590
|
+
},
|
|
591
|
+
}),
|
|
592
|
+
}
|
|
593
|
+
: {}),
|
|
594
|
+
rank: null,
|
|
595
|
+
trace: evaluated[0]?.trace ?? [],
|
|
596
|
+
provenance: provenance.length === 0 ? sourceCandidate.provenance : provenance,
|
|
597
|
+
unresolved,
|
|
598
|
+
};
|
|
599
|
+
return result;
|
|
600
|
+
});
|
|
601
|
+
rankCandidates(candidates);
|
|
602
|
+
const allUnresolved = [
|
|
603
|
+
...surface.unsupported,
|
|
604
|
+
...expanded.unresolved,
|
|
605
|
+
...candidates.flatMap(({ unresolved }) => unresolved),
|
|
606
|
+
];
|
|
607
|
+
const timing = horizonDays === undefined
|
|
608
|
+
? { unresolved: [] }
|
|
609
|
+
: scheduledMtthChance(surface, scenario, definitions, horizonDays);
|
|
610
|
+
allUnresolved.push(...timing.unresolved);
|
|
611
|
+
if (timing.chance !== undefined && candidates[0] !== undefined)
|
|
612
|
+
candidates[0].cumulativeChance = timing.chance;
|
|
613
|
+
if (timing.chanceInterval !== undefined && candidates[0] !== undefined)
|
|
614
|
+
Object.assign(candidates[0], {
|
|
615
|
+
cumulativeChanceInterval: timing.chanceInterval,
|
|
616
|
+
supportLevel: 'bounded',
|
|
617
|
+
});
|
|
618
|
+
const exactValues = candidates.flatMap(({ rawValue }) => typeof rawValue === 'object' && rawValue !== null ? [Rational.parse(rawValue.decimal)] : []);
|
|
619
|
+
return {
|
|
620
|
+
id: scenario.id,
|
|
621
|
+
...(scenario.label === undefined ? {} : { label: scenario.label }),
|
|
622
|
+
...(scenario.prevalence === undefined ? {} : { prevalence: scenario.prevalence }),
|
|
623
|
+
poolComplete: surface.poolComplete,
|
|
624
|
+
supportLevel: candidates.some(({ supportLevel: support }) => support === 'unsupported')
|
|
625
|
+
? 'unsupported'
|
|
626
|
+
: candidates.some(({ supportLevel: support }) => support === 'external')
|
|
627
|
+
? 'external'
|
|
628
|
+
: timing.chanceInterval !== undefined ||
|
|
629
|
+
candidates.some(({ supportLevel: support }) => support === 'bounded')
|
|
630
|
+
? 'bounded'
|
|
631
|
+
: surface.adapter.selectionRule === 'score_only'
|
|
632
|
+
? 'score_only'
|
|
633
|
+
: 'exact',
|
|
634
|
+
candidates,
|
|
635
|
+
...(exactValues.length === candidates.length &&
|
|
636
|
+
surface.adapter.selectionRule === 'proportional_categorical'
|
|
637
|
+
? { poolTotal: sumRationals(exactValues).toJSON() }
|
|
638
|
+
: {}),
|
|
639
|
+
...(horizonDays === undefined ? {} : { horizonDays }),
|
|
640
|
+
...(timing.survival === undefined ? {} : { survivalChance: timing.survival }),
|
|
641
|
+
...(timing.survivalInterval === undefined
|
|
642
|
+
? {}
|
|
643
|
+
: { survivalChanceInterval: timing.survivalInterval }),
|
|
644
|
+
...(timing.intervals === undefined ? {} : { timingIntervals: timing.intervals }),
|
|
645
|
+
summary: scenarioSummary(candidates),
|
|
646
|
+
unresolved: allUnresolved,
|
|
647
|
+
};
|
|
648
|
+
});
|
|
649
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './adapters.js';
|
|
2
|
+
export * from './compare.js';
|
|
3
|
+
export * from './evaluation.js';
|
|
4
|
+
export * from './model.js';
|
|
5
|
+
export * from './rational.js';
|
|
6
|
+
export * from './render.js';
|
|
7
|
+
export * from './sequence.js';
|
|
8
|
+
export * from './service.js';
|
|
9
|
+
export * from './simulation.js';
|
|
10
|
+
export * from './source-analysis.js';
|
|
11
|
+
export * from './sweep.js';
|
|
12
|
+
export * from './trigger-evaluator.js';
|
|
13
|
+
export * from './value-evaluator.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const PROBABILITY_SCHEMA_VERSION = 1;
|
|
2
|
+
export const PROBABILITY_ADAPTER_VERSION = 'hoi4-1.19.2.v1';
|
|
3
|
+
export const VERIFIED_GAME_VERSION = 'Operation Postern 1.19.2.0 (d245)';
|
|
4
|
+
export const VERIFIED_GAME_RAW_VERSION = '1.19.2.0';
|
|
5
|
+
export const VERIFIED_GAME_CHECKSUM = 'd245';
|