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.
Files changed (62) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +30 -21
  3. package/dist/bin/stdio.js +0 -4
  4. package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
  5. package/dist/hoi4_agent_tools/core/engine.js +5 -0
  6. package/dist/hoi4_agent_tools/core/index.js +57 -1
  7. package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
  8. package/dist/hoi4_agent_tools/core/source/index.js +1 -0
  9. package/dist/hoi4_agent_tools/event/limits.js +1 -1
  10. package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
  11. package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
  12. package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
  13. package/dist/hoi4_agent_tools/probability/compare.js +178 -0
  14. package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
  15. package/dist/hoi4_agent_tools/probability/index.js +13 -0
  16. package/dist/hoi4_agent_tools/probability/model.js +5 -0
  17. package/dist/hoi4_agent_tools/probability/rational.js +145 -0
  18. package/dist/hoi4_agent_tools/probability/render.js +307 -0
  19. package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
  20. package/dist/hoi4_agent_tools/probability/service.js +1168 -0
  21. package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
  22. package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
  23. package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
  24. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
  25. package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
  26. package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
  27. package/dist/hoi4_agent_tools/version.js +1 -1
  28. package/docs/README.md +1 -0
  29. package/docs/development.md +2 -0
  30. package/docs/probability.md +80 -0
  31. package/docs/research/probability-adapter-evidence.md +64 -0
  32. package/examples/clients/codex-config.toml +3 -0
  33. package/examples/clients/generic.json +8 -0
  34. package/examples/probability/adaptive-event-pool.json +92 -0
  35. package/examples/probability/adaptive-event-sequence-input.json +57 -0
  36. package/examples/probability/event-mtth-scenarios.json +57 -0
  37. package/examples/probability/event-mtth-simulate-input.json +37 -0
  38. package/examples/probability/focus-route-scenarios.json +61 -0
  39. package/examples/probability/focus-route-sweep-input.json +36 -0
  40. package/package.json +17 -8
  41. package/schemas/configuration.schema.json +747 -0
  42. package/schemas/continuous-focus-palette.schema.json +325 -0
  43. package/schemas/custom-weighted-pool.schema.json +298 -0
  44. package/schemas/event-feature-manifest.schema.json +63 -0
  45. package/schemas/focus-plan.schema.json +927 -0
  46. package/schemas/focus-planning-sidecar.schema.json +407 -0
  47. package/schemas/gui-animation-source.schema.json +176 -0
  48. package/schemas/gui-helper.schema.json +301 -0
  49. package/schemas/gui-scenario.schema.json +298 -0
  50. package/schemas/map-operation.schema.json +2284 -0
  51. package/schemas/operation-result.schema.json +327 -0
  52. package/schemas/probability-analysis-result.schema.json +2211 -0
  53. package/schemas/probability-compare-input.schema.json +664 -0
  54. package/schemas/probability-evaluate-input.schema.json +665 -0
  55. package/schemas/probability-inspect-input.schema.json +97 -0
  56. package/schemas/probability-render-input.schema.json +141 -0
  57. package/schemas/probability-scenario-set.schema.json +430 -0
  58. package/schemas/probability-sequence-input.schema.json +886 -0
  59. package/schemas/probability-simulate-input.schema.json +691 -0
  60. package/schemas/probability-sweep-input.schema.json +702 -0
  61. package/server.json +3 -3
  62. 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';