hoi4-agent-tools 2.7.0 → 2.8.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 (38) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/dist/hoi4_agent_tools/mcp/tools/probability.js +9 -0
  4. package/dist/hoi4_agent_tools/probability/adapters.js +5 -0
  5. package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
  6. package/dist/hoi4_agent_tools/probability/scenario-state.js +165 -0
  7. package/dist/hoi4_agent_tools/probability/scope-pools.js +199 -0
  8. package/dist/hoi4_agent_tools/probability/simulation.js +4 -3
  9. package/dist/hoi4_agent_tools/probability/sweep.js +6 -2
  10. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +35 -28
  11. package/dist/hoi4_agent_tools/probability/value-evaluator.js +4 -0
  12. package/dist/hoi4_agent_tools/schemas/probability.js +90 -0
  13. package/dist/hoi4_agent_tools/version.js +1 -1
  14. package/docs/probability.md +46 -0
  15. package/docs/research/probability-adapter-evidence.md +2 -1
  16. package/examples/probability/scoped-dynamic-pools-evaluate-input.json +50 -0
  17. package/package.json +1 -1
  18. package/schemas/configuration.schema.json +1 -1
  19. package/schemas/continuous-focus-palette.schema.json +1 -1
  20. package/schemas/custom-weighted-pool.schema.json +1 -1
  21. package/schemas/event-feature-manifest.schema.json +1 -1
  22. package/schemas/focus-plan.schema.json +1 -1
  23. package/schemas/focus-planning-sidecar.schema.json +1 -1
  24. package/schemas/gui-animation-source.schema.json +1 -1
  25. package/schemas/gui-helper.schema.json +1 -1
  26. package/schemas/gui-scenario.schema.json +1 -1
  27. package/schemas/map-operation.schema.json +1 -1
  28. package/schemas/operation-result.schema.json +1 -1
  29. package/schemas/probability-analysis-result.schema.json +175 -66
  30. package/schemas/probability-compare-input.schema.json +251 -66
  31. package/schemas/probability-evaluate-input.schema.json +252 -67
  32. package/schemas/probability-inspect-input.schema.json +1 -1
  33. package/schemas/probability-render-input.schema.json +1 -1
  34. package/schemas/probability-scenario-set.schema.json +231 -46
  35. package/schemas/probability-sequence-input.schema.json +273 -88
  36. package/schemas/probability-simulate-input.schema.json +252 -67
  37. package/schemas/probability-sweep-input.schema.json +254 -69
  38. package/server.json +2 -2
@@ -1,4 +1,5 @@
1
1
  import { assignments, astPathFor, firstScalar, nodeLocation, } from '../core/source/index.js';
2
+ import { resolveScopeContext, rootScopeContext, rootStateValue, scopeIdentity, scopeStateValue, } from './scenario-state.js';
2
3
  function combineAnd(values) {
3
4
  if (values.includes('false'))
4
5
  return 'false';
@@ -51,19 +52,13 @@ function unresolved(assignment, candidate, reason) {
51
52
  ],
52
53
  };
53
54
  }
54
- function stateValue(scenario, key) {
55
- if (Object.hasOwn(scenario.state, key))
56
- return scenario.state[key];
57
- if (Object.hasOwn(scenario.state, `trigger.${key}`))
58
- return scenario.state[`trigger.${key}`];
59
- return undefined;
60
- }
61
- function evaluateCheckVariable(block, scenario, candidate, assignment) {
55
+ function evaluateCheckVariable(block, scenario, candidate, assignment, scopeContext) {
62
56
  const variable = firstScalar(block, 'var')?.value;
63
57
  const target = firstScalar(block, 'value')?.value;
64
58
  if (variable === undefined || target === undefined)
65
59
  return unresolved(assignment, candidate, 'check_variable requires declared var and value');
66
- const left = stateValue(scenario, `variable.${variable}`) ?? stateValue(scenario, variable);
60
+ const left = scopeStateValue(scenario, `variable.${variable}`, scopeContext) ??
61
+ scopeStateValue(scenario, variable, scopeContext);
67
62
  const leftNumber = numeric(left);
68
63
  const rightNumber = numeric(target);
69
64
  if (leftNumber === undefined || rightNumber === undefined)
@@ -82,13 +77,13 @@ function evaluateCheckVariable(block, scenario, candidate, assignment) {
82
77
  : leftNumber === rightNumber;
83
78
  return { state: result ? 'true' : 'false', unresolved: [] };
84
79
  }
85
- function evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack) {
80
+ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack, scopeContext) {
86
81
  const key = assignment.key.value;
87
82
  if (assignment.value.type === 'block') {
88
83
  if (key === 'AND')
89
- return evaluateTriggerBlock(assignment.value, scenario, candidate, definitions, helperStack);
84
+ return evaluateTriggerBlock(assignment.value, scenario, candidate, definitions, helperStack, scopeContext);
90
85
  if (key === 'OR') {
91
- const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack));
86
+ const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack, scopeContext));
92
87
  return {
93
88
  state: combineOr(children.map(({ state }) => state)),
94
89
  unresolved: children.flatMap(({ unresolved: items }) => items),
@@ -96,7 +91,7 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
96
91
  };
97
92
  }
98
93
  if (key === 'NOT' || key === 'NOR' || key === 'NAND') {
99
- const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack));
94
+ const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack, scopeContext));
100
95
  const base = key === 'NAND'
101
96
  ? combineAnd(children.map(({ state }) => state))
102
97
  : combineOr(children.map(({ state }) => state));
@@ -107,18 +102,25 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
107
102
  };
108
103
  }
109
104
  if (key === 'check_variable')
110
- return evaluateCheckVariable(assignment.value, scenario, candidate, assignment);
105
+ return evaluateCheckVariable(assignment.value, scenario, candidate, assignment, scopeContext);
111
106
  if (key === 'is_in_array' || key === 'is_variable_in_array') {
112
107
  const arrayId = firstScalar(assignment.value, 'array')?.value;
113
108
  const declaredValue = firstScalar(assignment.value, 'value')?.value;
114
109
  if (arrayId === undefined || declaredValue === undefined)
115
110
  return unresolved(assignment, candidate, `${key} requires array and value`);
116
- const array = stateValue(scenario, `array.${arrayId}`);
111
+ const array = scopeStateValue(scenario, `array.${arrayId}`, scopeContext) ??
112
+ rootStateValue(scenario, `array.${arrayId}`);
117
113
  if (!Array.isArray(array))
118
114
  return unresolved(assignment, candidate, `Scenario does not declare array ${arrayId}`);
119
- const compared = stateValue(scenario, declaredValue) ?? declaredValue;
115
+ const compared = ['THIS', 'ROOT', 'PREV'].includes(declaredValue.toUpperCase()) ||
116
+ resolveScopeContext(scenario, declaredValue, scopeContext) !== undefined
117
+ ? scopeIdentity(resolveScopeContext(scenario, declaredValue, scopeContext) ?? scopeContext)
118
+ : (scopeStateValue(scenario, declaredValue, scopeContext) ?? declaredValue);
120
119
  return { state: array.includes(compared) ? 'true' : 'false', unresolved: [] };
121
120
  }
121
+ const resolvedScope = resolveScopeContext(scenario, key, scopeContext);
122
+ if (resolvedScope !== undefined)
123
+ return evaluateTriggerBlock(assignment.value, scenario, candidate, definitions, helperStack, resolvedScope);
122
124
  const helper = definitions.scriptedTriggers.get(key);
123
125
  if (helper !== undefined) {
124
126
  if (helperStack.includes(key))
@@ -141,13 +143,13 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
141
143
  ...candidate,
142
144
  document: helper.document,
143
145
  provenance: [helperProvenance, ...candidate.provenance],
144
- }, definitions, [...helperStack, key]);
146
+ }, definitions, [...helperStack, key], scopeContext);
145
147
  return {
146
148
  ...evaluated,
147
149
  helperProvenance: [helperProvenance, ...(evaluated.helperProvenance ?? [])],
148
150
  };
149
151
  }
150
- const declaredScope = bool(stateValue(scenario, `scope.${key}`));
152
+ const declaredScope = bool(rootStateValue(scenario, `scope.${key}`));
151
153
  if (declaredScope !== undefined)
152
154
  return { state: declaredScope ? 'true' : 'false', unresolved: [] };
153
155
  return unresolved(assignment, candidate, `Scoped or compound trigger ${key} is not declared by the scenario`);
@@ -160,19 +162,24 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
160
162
  : { state: value ? 'true' : 'false', unresolved: [] };
161
163
  }
162
164
  if (key === 'has_country_flag' || key === 'has_global_flag' || key === 'has_state_flag') {
163
- return { state: scenario.flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
165
+ const flags = scopeContext.expression === 'ROOT' ? scenario.flags : scopeContext.binding.flags;
166
+ return { state: flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
164
167
  }
165
168
  if (key === 'has_event_target') {
169
+ const eventTargets = scopeContext.binding.eventTargets ?? scenario.eventTargets;
166
170
  return {
167
- state: Object.hasOwn(scenario.eventTargets ?? {}, right) ? 'true' : 'false',
171
+ state: Object.hasOwn(eventTargets ?? {}, right) ? 'true' : 'false',
168
172
  unresolved: [],
169
173
  };
170
174
  }
171
175
  if (key === 'tag' || key === 'original_tag') {
172
- if (scenario.actor === undefined)
176
+ const actor = scopeContext.expression === 'ROOT'
177
+ ? scenario.actor
178
+ : (scopeContext.binding.actor ?? scopeContext.binding.id);
179
+ if (actor === undefined)
173
180
  return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}`);
174
181
  return {
175
- state: compare(scenario.actor, right, assignment.operator.text) ? 'true' : 'false',
182
+ state: compare(actor, right, assignment.operator.text) ? 'true' : 'false',
176
183
  unresolved: [],
177
184
  };
178
185
  }
@@ -208,13 +215,13 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
208
215
  ...candidate,
209
216
  document: helper.document,
210
217
  provenance: [helperProvenance, ...candidate.provenance],
211
- }, definitions, [...helperStack, key]);
218
+ }, definitions, [...helperStack, key], scopeContext);
212
219
  return {
213
220
  ...evaluated,
214
221
  helperProvenance: [helperProvenance, ...(evaluated.helperProvenance ?? [])],
215
222
  };
216
223
  }
217
- const declared = stateValue(scenario, key);
224
+ const declared = scopeStateValue(scenario, key, scopeContext);
218
225
  if (declared === undefined)
219
226
  return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}`);
220
227
  const expectedBoolean = bool(right);
@@ -235,8 +242,8 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
235
242
  };
236
243
  return unresolved(assignment, candidate, `Trigger ${key} cannot compare the declared scenario value`);
237
244
  }
238
- function evaluateAssignment(assignment, scenario, candidate, definitions, helperStack) {
239
- const evaluated = evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack);
245
+ function evaluateAssignment(assignment, scenario, candidate, definitions, helperStack, scopeContext) {
246
+ const evaluated = evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack, scopeContext);
240
247
  const source = candidate.provenance[0];
241
248
  const provenance = candidate.document === undefined || source === undefined
242
249
  ? source
@@ -264,13 +271,13 @@ function evaluateAssignment(assignment, scenario, candidate, definitions, helper
264
271
  ],
265
272
  };
266
273
  }
267
- export function evaluateTriggerBlock(block, scenario, candidate, definitions, helperStack = []) {
274
+ export function evaluateTriggerBlock(block, scenario, candidate, definitions, helperStack = [], scopeContext = rootScopeContext(scenario)) {
268
275
  const override = scenario.candidateOverrides?.[candidate.id];
269
276
  if (override !== undefined)
270
277
  return { state: override ? 'true' : 'false', unresolved: [] };
271
278
  if (block === undefined)
272
279
  return { state: 'true', unresolved: [] };
273
- const children = assignments(block).map((assignment) => evaluateAssignment(assignment, scenario, candidate, definitions, helperStack));
280
+ const children = assignments(block).map((assignment) => evaluateAssignment(assignment, scenario, candidate, definitions, helperStack, scopeContext));
274
281
  return {
275
282
  state: combineAnd(children.map(({ state }) => state)),
276
283
  unresolved: children.flatMap(({ unresolved: items }) => items),
@@ -1,5 +1,6 @@
1
1
  import { assignments, astPathFor, nodeLocation, } from '../core/source/index.js';
2
2
  import { Rational } from './rational.js';
3
+ import { scopedExpressionValue } from './scenario-state.js';
3
4
  import { evaluateTriggerBlock } from './trigger-evaluator.js';
4
5
  function appendReference(evaluated, reference) {
5
6
  return {
@@ -51,6 +52,9 @@ function scenarioNumber(scenario, expression) {
51
52
  if (expression.startsWith('var:') &&
52
53
  Object.hasOwn(scenario.state, `variable.${expression.slice(4)}`))
53
54
  return scenario.state[`variable.${expression.slice(4)}`];
55
+ const scoped = scopedExpressionValue(scenario, expression);
56
+ if (scoped !== undefined)
57
+ return scoped;
54
58
  return undefined;
55
59
  }
56
60
  function expressionValue(expression, candidate, scenario, definitions, mtthStack) {
@@ -82,6 +82,62 @@ const uncertainInputSchema = z
82
82
  message: 'Range min must not exceed max',
83
83
  });
84
84
  });
85
+ const probabilityScopeBindingSchema = z
86
+ .object({
87
+ id: z.string().min(1).max(512),
88
+ type: z
89
+ .enum([
90
+ 'country',
91
+ 'state',
92
+ 'character',
93
+ 'unit_leader',
94
+ 'operative',
95
+ 'strategic_region',
96
+ 'province',
97
+ 'unknown',
98
+ ])
99
+ .optional(),
100
+ actor: z.string().max(256).optional(),
101
+ state: z.record(z.string().min(1).max(1024), scenarioValueSchema).default({}),
102
+ flags: z.array(z.string().min(1).max(512)).max(100_000).optional(),
103
+ eventTargets: z.record(z.string().max(512), z.string().max(512)).optional(),
104
+ weight: z.union([z.number().nonnegative(), z.string().min(1).max(1024)]).optional(),
105
+ })
106
+ .strict();
107
+ const probabilityScopePoolSchema = z
108
+ .object({
109
+ id: z.string().min(1).max(512),
110
+ bindAs: z.string().min(1).max(512).default('THIS'),
111
+ selection: z
112
+ .enum(['enumeration', 'uniform', 'proportional_categorical'])
113
+ .default('enumeration'),
114
+ complete: z.boolean().default(false),
115
+ filter: z
116
+ .object({
117
+ inlineClausewitz: z.string().min(1).max(1_048_576).optional(),
118
+ scriptedTrigger: z.string().min(1).max(512).optional(),
119
+ })
120
+ .strict()
121
+ .superRefine((filter, context) => {
122
+ if ((filter.inlineClausewitz === undefined) === (filter.scriptedTrigger === undefined))
123
+ context.addIssue({
124
+ code: 'custom',
125
+ message: 'Scope-pool filter requires exactly one inlineClausewitz or scriptedTrigger',
126
+ });
127
+ })
128
+ .optional(),
129
+ candidates: z.array(probabilityScopeBindingSchema).min(1).max(100_000),
130
+ })
131
+ .strict()
132
+ .superRefine((pool, context) => {
133
+ const ids = pool.candidates.map(({ id }) => id);
134
+ if (new Set(ids).size !== ids.length)
135
+ context.addIssue({
136
+ code: 'custom',
137
+ path: ['candidates'],
138
+ message: 'Scope-pool candidate IDs must be unique',
139
+ });
140
+ });
85
141
  export const probabilityScenarioSchema = z
86
142
  .object({
87
143
  id: z.string().min(1).max(512),
@@ -92,6 +148,8 @@ export const probabilityScenarioSchema = z
92
148
  state: z.record(z.string().min(1).max(1024), scenarioValueSchema),
93
149
  flags: z.array(z.string().min(1).max(512)).max(100_000).optional(),
94
150
  eventTargets: z.record(z.string().max(512), z.string().max(512)).optional(),
151
+ scopes: z.record(z.string().min(1).max(512), probabilityScopeBindingSchema).optional(),
152
+ scopePools: z.array(probabilityScopePoolSchema).max(100_000).optional(),
95
153
  candidateOverrides: z.record(z.string().max(512), z.boolean()).optional(),
96
154
  uncertainInputs: z.array(uncertainInputSchema).max(10_000).optional(),
97
155
  correlations: z
@@ -118,6 +176,13 @@ export const probabilityScenarioSchema = z
118
176
  })
119
177
  .strict()
120
178
  .superRefine((scenario, context) => {
179
+ const poolIds = scenario.scopePools?.map(({ id }) => id) ?? [];
180
+ if (new Set(poolIds).size !== poolIds.length)
181
+ context.addIssue({
182
+ code: 'custom',
183
+ path: ['scopePools'],
184
+ message: 'Scope-pool IDs must be unique within a scenario',
185
+ });
121
186
  const paths = scenario.uncertainInputs?.map(({ path }) => path) ?? [];
122
187
  if (new Set(paths).size !== paths.length)
123
188
  context.addIssue({
@@ -596,6 +661,30 @@ const candidateAnalysisSchema = z
596
661
  unresolved: z.array(probabilityUnresolvedSchema),
597
662
  })
598
663
  .strict();
664
+ const scopePoolCandidateAnalysisSchema = z
665
+ .object({
666
+ id: z.string(),
667
+ eligibility: z.enum(['true', 'false', 'unresolved']),
668
+ weight: rationalJsonSchema.optional(),
669
+ conditionalProbability: z.number().min(0).max(1).optional(),
670
+ exactConditionalProbability: rationalJsonSchema.optional(),
671
+ trace: z.array(valueTraceSchema),
672
+ unresolved: z.array(probabilityUnresolvedSchema),
673
+ })
674
+ .strict();
675
+ const scopePoolAnalysisSchema = z
676
+ .object({
677
+ id: z.string(),
678
+ bindAs: z.string(),
679
+ selection: z.enum(['enumeration', 'uniform', 'proportional_categorical']),
680
+ poolComplete: z.boolean(),
681
+ candidates: z.array(scopePoolCandidateAnalysisSchema),
682
+ eligibleCandidateIds: z.array(z.string()),
683
+ unresolvedCandidateIds: z.array(z.string()),
684
+ poolTotal: rationalJsonSchema.optional(),
685
+ unresolved: z.array(probabilityUnresolvedSchema),
686
+ })
687
+ .strict();
599
688
  const scenarioAnalysisSchema = z
600
689
  .object({
601
690
  id: z.string(),
@@ -623,6 +712,7 @@ const scenarioAnalysisSchema = z
623
712
  })
624
713
  .strict())
625
714
  .optional(),
715
+ scopePools: z.array(scopePoolAnalysisSchema).optional(),
626
716
  summary: z
627
717
  .object({
628
718
  topOutcomes: z.array(z.string()),
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '2.7.0';
3
+ export const PACKAGE_VERSION = '2.8.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
@@ -42,6 +42,50 @@ A scenario contains only state the caller is willing to declare. Missing values
42
42
  }
43
43
  ```
44
44
 
45
+ Use `scopes` to bind exact Clausewitz scope expressions. A binding carries its own actor, state, flags, and event targets, so a `FROM` trigger is evaluated against `FROM` rather than the root country. Chained names such as `FROM.FROM`, plus `ROOT`, `THIS`, `PREV`, `scope:name`, and `event_target:name`, are resolved without replacing their inner conditions with a single boolean. Scoped numeric expressions such as `FROM.variable_name` and uncertain paths such as `scopes.FROM.variable.variable_name` use the same binding.
46
+
47
+ Use `scopePools` when source logic builds a destination, target, opposition, donor, or other runtime candidate set. Each pool supplies a candidate catalog, the scope alias to bind, and either an inline trigger filter or a named scripted trigger. The result enumerates every eligible, excluded, and unresolved candidate. `uniform` and `proportional_categorical` pools also return exact shares when `complete` is true; an incomplete catalog is still enumerated but is not normalized.
48
+
49
+ ```json
50
+ {
51
+ "id": "relief_routes",
52
+ "state": {},
53
+ "scopes": {
54
+ "FROM": {
55
+ "id": "origin",
56
+ "type": "country",
57
+ "actor": "FRA",
58
+ "state": { "variable.relief_pressure": 7 }
59
+ }
60
+ },
61
+ "scopePools": [
62
+ {
63
+ "id": "relief_donor",
64
+ "bindAs": "FROM",
65
+ "selection": "proportional_categorical",
66
+ "complete": true,
67
+ "filter": {
68
+ "inlineClausewitz": "FROM = { check_variable = { var = relief_ready value = 1 compare = greater_than_or_equals } }"
69
+ },
70
+ "candidates": [
71
+ {
72
+ "id": "GER",
73
+ "actor": "GER",
74
+ "state": { "variable.relief_ready": 1, "variable.donor_weight": 1 },
75
+ "weight": "FROM.donor_weight"
76
+ },
77
+ {
78
+ "id": "ITA",
79
+ "actor": "ITA",
80
+ "state": { "variable.relief_ready": 1, "variable.donor_weight": 3 },
81
+ "weight": "FROM.donor_weight"
82
+ }
83
+ ]
84
+ }
85
+ ]
86
+ }
87
+ ```
88
+
45
89
  Focus, technology, and doctrine probabilities require a complete candidate pool. Their engine rule is an independent uniform score race, not weight divided by total weight. Focus scenarios use `focus.external_factors_complete: true` only after the caller has supplied every relevant prerequisite and strategy factor. Technology and doctrine scenarios use `technology.external_factors_complete: true` only after cost, date, bonus, strategy, and candidate effects are accounted for.
46
90
 
47
91
  Decision and mission adapters intentionally return scores and ranks without inventing normalized probabilities. Event-option and `random_list` adapters normalize only their complete local pools. Direct random remains an independent percentage. MTTH horizon chance uses the versioned game timing model and returns a bound when an inactive-to-active polling phase is unknown.
@@ -61,6 +105,8 @@ Authoritative JSON keeps these fields separate:
61
105
 
62
106
  Every candidate includes source provenance with a stable AST path, an ordered modifier trace, support level, and unresolved analysis. `external` support means the parsed candidate is valid but its surrounding game factors were not fully declared. The tool response stays compact; complete matrices, traces, simulations, comparisons, and visuals are linked MCP resources.
63
107
 
108
+ Every evaluated scope pool is stored with the scenario in authoritative JSON. Pool rows include eligibility, resolved weight, exact conditional share when supported, trigger traces, eligible IDs, unresolved IDs, and unresolved evidence. Compact MCP responses report pool and pool-candidate counts without copying the full catalog into the prompt.
109
+
64
110
  Nested `random_list` entries report both their conditional share inside the immediate list and their full path probability through every enclosing list. Dynamic parent paths remain explicit unresolved evidence.
65
111
 
66
112
  Deterministic simulation uses constant-memory Latin hypercube sampling by default, with seeded pseudo-random sampling available when requested. Numeric distributions can use a Gaussian copula correlation matrix. Discrete or categorical correlation requests are sampled independently and reported as unresolved instead of being approximated silently. Simulation reports Wilson intervals, effective sample count, global input importance, and HOI4 daily-hazard MTTH samples. Timing quantiles use a deterministic bounded reservoir and include their confidence basis and retained sample count.
@@ -55,7 +55,8 @@ When a game root is configured, adapter execution verifies `launcher-settings.js
55
55
  The analyzer reports, but does not guess or execute:
56
56
 
57
57
  - arbitrary effects used to calculate later weights;
58
- - unprovided scopes, event targets, game rules, DLC state, dynamic identifiers, meta effects, or scripted-localisation results;
58
+ - scope chains, event targets, game rules, DLC state, dynamic identifiers, meta effects, or scripted-localisation results that are neither present in source nor declared through structured scenario bindings;
59
+ - runtime scope pools whose candidate catalog is not declared; declared catalogs are filtered and enumerated exactly, while normalized shares additionally require explicit completeness;
59
60
  - parameterized scripted triggers whose arguments cannot be resolved from the selected source;
60
61
  - recursive MTTH variables or helper cycles;
61
62
  - incomplete focus, technology, doctrine, event-option, or random-list candidate pools;
@@ -0,0 +1,50 @@
1
+ {
2
+ "adapter": "event_option_ai_chance",
3
+ "source": {
4
+ "inlineClausewitz": "country_event = { id = scoped.1 option = { name = scoped.1.a ai_chance = { base = 1 modifier = { factor = 3 FROM = { tag = FRA } } } } option = { name = scoped.1.b ai_chance = { base = 1 } } }"
5
+ },
6
+ "scenarioSet": {
7
+ "schemaVersion": "1.0",
8
+ "id": "scoped_dynamic_pools",
9
+ "scenarios": [
10
+ {
11
+ "id": "france_origin",
12
+ "state": {},
13
+ "scopes": {
14
+ "FROM": {
15
+ "id": "origin",
16
+ "type": "country",
17
+ "actor": "FRA",
18
+ "state": {}
19
+ }
20
+ },
21
+ "scopePools": [
22
+ {
23
+ "id": "destination",
24
+ "bindAs": "FROM",
25
+ "selection": "uniform",
26
+ "complete": true,
27
+ "filter": {
28
+ "inlineClausewitz": "FROM = { is_ai = yes }"
29
+ },
30
+ "candidates": [
31
+ {
32
+ "id": "GER",
33
+ "type": "country",
34
+ "actor": "GER",
35
+ "state": { "is_ai": true }
36
+ },
37
+ {
38
+ "id": "ITA",
39
+ "type": "country",
40
+ "actor": "ITA",
41
+ "state": { "is_ai": true }
42
+ }
43
+ ]
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ },
49
+ "outputs": ["json", "unresolved"]
50
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
3
  "mcpName": "io.github.klimPaskov/hoi4-agent-tools",
4
- "version": "2.7.0",
4
+ "version": "2.8.0",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/configuration.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/continuous-focus-palette.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/custom-weighted-pool.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/event-feature-manifest.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/focus-plan.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/focus-planning-sidecar.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/gui-animation-source.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/gui-helper.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/gui-scenario.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/map-operation.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "oneOf": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.0/schemas/operation-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {