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,265 @@
1
+ import { ServiceError } from '../core/result.js';
2
+ import { conditionBreakpointValues, evaluateSurfaceScenarios } from './evaluation.js';
3
+ const MAX_SWEEP_POINTS = 250_000;
4
+ function declaredValues(surface, scenario, path, steps) {
5
+ const uncertain = scenario.uncertainInputs?.find((input) => input.path === path);
6
+ if (uncertain?.range !== undefined)
7
+ return [
8
+ ...new Set([
9
+ ...values(uncertain.range, steps),
10
+ ...conditionBreakpointValues(surface, uncertain.range.min, uncertain.range.max),
11
+ ]),
12
+ ].sort((left, right) => left - right);
13
+ if (uncertain?.alternatives !== undefined) {
14
+ const values = uncertain.alternatives.filter((value) => typeof value === 'number');
15
+ if (values.length > 0)
16
+ return [...new Set(values)].sort((left, right) => left - right);
17
+ }
18
+ const current = scenario.state[path];
19
+ if (typeof current === 'number')
20
+ return [current];
21
+ throw new ServiceError('PROBABILITY_SWEEP_RANGE_REQUIRED', 'Every sweep path requires a scenario range, numeric alternatives, or numeric state value', { scenarioId: scenario.id, path });
22
+ }
23
+ function values(range, steps) {
24
+ if (range.min === range.max)
25
+ return [range.min];
26
+ return Array.from({ length: steps }, (_, index) => range.min + ((range.max - range.min) * index) / (steps - 1));
27
+ }
28
+ function point(surface, scenario, definitions, path, value, assigned) {
29
+ const evaluated = evaluateSurfaceScenarios(surface, [{ ...scenario, state: { ...scenario.state, ...assigned }, uncertainInputs: [] }], definitions)[0];
30
+ return {
31
+ scenarioId: scenario.id,
32
+ path,
33
+ value,
34
+ ...(Object.keys(assigned).length > 1 ? { values: assigned } : {}),
35
+ candidates: evaluated.candidates.map((candidate) => ({
36
+ id: candidate.id,
37
+ ...(typeof candidate.rawValue === 'object' && candidate.rawValue !== null
38
+ ? { rawValue: candidate.rawValue.value }
39
+ : {}),
40
+ ...(candidate.conditionalProbability === undefined ||
41
+ candidate.conditionalProbability === null
42
+ ? {}
43
+ : { conditionalProbability: candidate.conditionalProbability }),
44
+ ...(candidate.rank === undefined || candidate.rank === null ? {} : { rank: candidate.rank }),
45
+ })),
46
+ };
47
+ }
48
+ function leader(point) {
49
+ return point.candidates.find(({ rank }) => rank === 1)?.id;
50
+ }
51
+ function discoverReversals(points) {
52
+ const output = [];
53
+ const groups = new Map();
54
+ for (const item of points) {
55
+ const key = `${item.scenarioId}:${item.path}`;
56
+ const group = groups.get(key) ?? [];
57
+ group.push(item);
58
+ groups.set(key, group);
59
+ }
60
+ for (const group of groups.values()) {
61
+ group.sort((left, right) => left.value - right.value);
62
+ for (let index = 1; index < group.length; index += 1) {
63
+ const before = group[index - 1];
64
+ const after = group[index];
65
+ const beforeLeader = leader(before);
66
+ const afterLeader = leader(after);
67
+ if (beforeLeader === undefined || afterLeader === undefined || beforeLeader === afterLeader)
68
+ continue;
69
+ output.push({
70
+ scenarioId: before.scenarioId,
71
+ path: before.path,
72
+ between: [before.value, after.value],
73
+ beforeLeader,
74
+ afterLeader,
75
+ });
76
+ }
77
+ }
78
+ return output;
79
+ }
80
+ const pointCandidateMaps = new WeakMap();
81
+ function candidateMetrics(point, candidateId) {
82
+ let candidates = pointCandidateMaps.get(point);
83
+ if (candidates === undefined) {
84
+ candidates = new Map(point.candidates.map((candidate) => [candidate.id, candidate]));
85
+ pointCandidateMaps.set(point, candidates);
86
+ }
87
+ const candidate = candidates.get(candidateId);
88
+ if (candidate === undefined)
89
+ return [];
90
+ return [
91
+ ...(candidate.rawValue === undefined
92
+ ? []
93
+ : [{ metric: 'raw_value', value: candidate.rawValue }]),
94
+ ...(candidate.conditionalProbability === undefined
95
+ ? []
96
+ : [
97
+ {
98
+ metric: 'conditional_probability',
99
+ value: candidate.conditionalProbability,
100
+ },
101
+ ]),
102
+ ];
103
+ }
104
+ function localElasticities(points) {
105
+ const output = [];
106
+ const groups = new Map();
107
+ for (const point of points) {
108
+ if (point.values !== undefined)
109
+ continue;
110
+ const key = `${point.scenarioId}\0${point.path}`;
111
+ groups.set(key, [...(groups.get(key) ?? []), point]);
112
+ }
113
+ for (const group of groups.values()) {
114
+ group.sort((left, right) => left.value - right.value);
115
+ for (let index = 1; index < group.length; index += 1) {
116
+ const before = group[index - 1];
117
+ const after = group[index];
118
+ const deltaInput = after.value - before.value;
119
+ if (deltaInput === 0)
120
+ continue;
121
+ for (const candidate of after.candidates) {
122
+ const afterMetrics = candidateMetrics(after, candidate.id);
123
+ const beforeMetrics = new Map(candidateMetrics(before, candidate.id).map(({ metric, value }) => [metric, value]));
124
+ for (const { metric, value: afterValue } of afterMetrics) {
125
+ const beforeValue = beforeMetrics.get(metric);
126
+ if (beforeValue === undefined)
127
+ continue;
128
+ const slope = (afterValue - beforeValue) / deltaInput;
129
+ const midpointInput = (before.value + after.value) / 2;
130
+ const midpointOutput = (beforeValue + afterValue) / 2;
131
+ output.push({
132
+ scenarioId: after.scenarioId,
133
+ path: after.path,
134
+ candidateId: candidate.id,
135
+ metric,
136
+ between: [before.value, after.value],
137
+ slope,
138
+ ...(midpointInput === 0 || midpointOutput === 0
139
+ ? {}
140
+ : { elasticity: (slope * midpointInput) / midpointOutput }),
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ return output;
147
+ }
148
+ function pairwiseInteractions(points) {
149
+ const output = [];
150
+ const groups = new Map();
151
+ for (const point of points) {
152
+ if (point.values === undefined)
153
+ continue;
154
+ const key = `${point.scenarioId}\0${point.path}`;
155
+ groups.set(key, [...(groups.get(key) ?? []), point]);
156
+ }
157
+ for (const group of groups.values()) {
158
+ const first = group[0];
159
+ if (first === undefined)
160
+ continue;
161
+ const paths = first.path.split('|');
162
+ if (paths.length !== 2)
163
+ continue;
164
+ const [leftPath, rightPath] = paths;
165
+ const leftValues = [...new Set(group.map(({ values }) => values[leftPath]))].sort((left, right) => left - right);
166
+ const rightValues = [...new Set(group.map(({ values }) => values[rightPath]))].sort((left, right) => left - right);
167
+ const byCoordinates = new Map(group.map((point) => [`${point.values[leftPath]}\0${point.values[rightPath]}`, point]));
168
+ for (let leftIndex = 1; leftIndex < leftValues.length; leftIndex += 1) {
169
+ for (let rightIndex = 1; rightIndex < rightValues.length; rightIndex += 1) {
170
+ const leftLow = leftValues[leftIndex - 1];
171
+ const leftHigh = leftValues[leftIndex];
172
+ const rightLow = rightValues[rightIndex - 1];
173
+ const rightHigh = rightValues[rightIndex];
174
+ const lowLow = byCoordinates.get(`${leftLow}\0${rightLow}`);
175
+ const highLow = byCoordinates.get(`${leftHigh}\0${rightLow}`);
176
+ const lowHigh = byCoordinates.get(`${leftLow}\0${rightHigh}`);
177
+ const highHigh = byCoordinates.get(`${leftHigh}\0${rightHigh}`);
178
+ for (const candidate of highHigh.candidates) {
179
+ for (const { metric, value: highHighValue } of candidateMetrics(highHigh, candidate.id)) {
180
+ const lowLowValue = candidateMetrics(lowLow, candidate.id).find((item) => item.metric === metric)?.value;
181
+ const highLowValue = candidateMetrics(highLow, candidate.id).find((item) => item.metric === metric)?.value;
182
+ const lowHighValue = candidateMetrics(lowHigh, candidate.id).find((item) => item.metric === metric)?.value;
183
+ if (lowLowValue === undefined ||
184
+ highLowValue === undefined ||
185
+ lowHighValue === undefined)
186
+ continue;
187
+ const mixedDifference = highHighValue - highLowValue - lowHighValue + lowLowValue;
188
+ output.push({
189
+ scenarioId: highHigh.scenarioId,
190
+ paths: [leftPath, rightPath],
191
+ candidateId: candidate.id,
192
+ metric,
193
+ cell: { left: [leftLow, leftHigh], right: [rightLow, rightHigh] },
194
+ mixedDifference,
195
+ mixedDifferencePerUnit: mixedDifference / ((leftHigh - leftLow) * (rightHigh - rightLow)),
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ return output;
203
+ }
204
+ export function sweepSurface(surface, scenarios, definitions, request, signal) {
205
+ const estimate = request.pairwise
206
+ ? scenarios.length *
207
+ Math.max(1, (request.paths.length * (request.paths.length - 1)) / 2) *
208
+ request.steps ** 2
209
+ : scenarios.length * request.paths.length * request.steps;
210
+ if (estimate > MAX_SWEEP_POINTS)
211
+ throw new ServiceError('PROBABILITY_SWEEP_LIMIT', 'Requested sweep exceeds the analysis point limit', {
212
+ estimatedPoints: estimate,
213
+ limit: MAX_SWEEP_POINTS,
214
+ });
215
+ const points = [];
216
+ for (const scenario of scenarios) {
217
+ if (request.pairwise && request.paths.length > 1) {
218
+ for (let leftIndex = 0; leftIndex < request.paths.length; leftIndex += 1) {
219
+ for (let rightIndex = leftIndex + 1; rightIndex < request.paths.length; rightIndex += 1) {
220
+ const leftPath = request.paths[leftIndex];
221
+ const rightPath = request.paths[rightIndex];
222
+ const leftValues = declaredValues(surface, scenario, leftPath, request.steps);
223
+ const rightValues = declaredValues(surface, scenario, rightPath, request.steps);
224
+ for (const left of leftValues) {
225
+ for (const right of rightValues) {
226
+ if ((points.length & 1023) === 0)
227
+ signal?.throwIfAborted();
228
+ if (points.length >= MAX_SWEEP_POINTS)
229
+ throw new ServiceError('PROBABILITY_SWEEP_LIMIT', 'Expanded threshold-aware sweep exceeds the analysis point limit', { limit: MAX_SWEEP_POINTS });
230
+ points.push(point(surface, scenario, definitions, `${leftPath}|${rightPath}`, left, {
231
+ [leftPath]: left,
232
+ [rightPath]: right,
233
+ }));
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ else {
240
+ for (const path of request.paths) {
241
+ for (const value of declaredValues(surface, scenario, path, request.steps)) {
242
+ if ((points.length & 1023) === 0)
243
+ signal?.throwIfAborted();
244
+ if (points.length >= MAX_SWEEP_POINTS)
245
+ throw new ServiceError('PROBABILITY_SWEEP_LIMIT', 'Expanded threshold-aware sweep exceeds the analysis point limit', { limit: MAX_SWEEP_POINTS });
246
+ points.push(point(surface, scenario, definitions, path, value, { [path]: value }));
247
+ }
248
+ }
249
+ }
250
+ }
251
+ const rankReversals = request.findRankReversals ? discoverReversals(points) : [];
252
+ const breakpoints = rankReversals.flatMap((reversal) => {
253
+ const [low, high] = reversal.between;
254
+ return points.filter(({ scenarioId, path, value }) => scenarioId === reversal.scenarioId &&
255
+ path === reversal.path &&
256
+ (value === low || value === high));
257
+ });
258
+ return {
259
+ points,
260
+ rankReversals,
261
+ breakpoints,
262
+ localElasticities: localElasticities(points),
263
+ pairwiseInteractions: pairwiseInteractions(points),
264
+ };
265
+ }
@@ -0,0 +1,280 @@
1
+ import { assignments, astPathFor, firstScalar, nodeLocation, } from '../core/source/index.js';
2
+ function combineAnd(values) {
3
+ if (values.includes('false'))
4
+ return 'false';
5
+ return values.includes('unresolved') ? 'unresolved' : 'true';
6
+ }
7
+ function combineOr(values) {
8
+ if (values.includes('true'))
9
+ return 'true';
10
+ return values.includes('unresolved') ? 'unresolved' : 'false';
11
+ }
12
+ function invert(value) {
13
+ return value === 'true' ? 'false' : value === 'false' ? 'true' : 'unresolved';
14
+ }
15
+ function bool(value) {
16
+ if (typeof value === 'boolean')
17
+ return value;
18
+ if (value === 'yes' || value === 'true')
19
+ return true;
20
+ if (value === 'no' || value === 'false')
21
+ return false;
22
+ return undefined;
23
+ }
24
+ function numeric(value) {
25
+ if (typeof value === 'number' && Number.isFinite(value))
26
+ return value;
27
+ if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value)))
28
+ return Number(value);
29
+ return undefined;
30
+ }
31
+ function compare(left, right, operator) {
32
+ if (typeof left === 'number' && typeof right === 'number') {
33
+ if (operator === '>')
34
+ return left > right;
35
+ if (operator === '<')
36
+ return left < right;
37
+ }
38
+ return operator === '!=' ? left !== right : left === right;
39
+ }
40
+ function unresolved(assignment, candidate, reason) {
41
+ return {
42
+ state: 'unresolved',
43
+ unresolved: [
44
+ {
45
+ code: 'TRIGGER_UNRESOLVED',
46
+ message: reason,
47
+ path: assignment.key.value,
48
+ candidateId: candidate.id,
49
+ ...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
50
+ },
51
+ ],
52
+ };
53
+ }
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) {
62
+ const variable = firstScalar(block, 'var')?.value;
63
+ const target = firstScalar(block, 'value')?.value;
64
+ if (variable === undefined || target === undefined)
65
+ return unresolved(assignment, candidate, 'check_variable requires declared var and value');
66
+ const left = stateValue(scenario, `variable.${variable}`) ?? stateValue(scenario, variable);
67
+ const leftNumber = numeric(left);
68
+ const rightNumber = numeric(target);
69
+ if (leftNumber === undefined || rightNumber === undefined)
70
+ return unresolved(assignment, candidate, `Scenario does not declare numeric variable ${variable}`);
71
+ const compareName = firstScalar(block, 'compare')?.value ?? 'equals';
72
+ const result = compareName === 'greater_than'
73
+ ? leftNumber > rightNumber
74
+ : compareName === 'greater_than_or_equals'
75
+ ? leftNumber >= rightNumber
76
+ : compareName === 'less_than'
77
+ ? leftNumber < rightNumber
78
+ : compareName === 'less_than_or_equals'
79
+ ? leftNumber <= rightNumber
80
+ : compareName === 'not_equals'
81
+ ? leftNumber !== rightNumber
82
+ : leftNumber === rightNumber;
83
+ return { state: result ? 'true' : 'false', unresolved: [] };
84
+ }
85
+ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack) {
86
+ const key = assignment.key.value;
87
+ if (assignment.value.type === 'block') {
88
+ if (key === 'AND')
89
+ return evaluateTriggerBlock(assignment.value, scenario, candidate, definitions, helperStack);
90
+ if (key === 'OR') {
91
+ const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack));
92
+ return {
93
+ state: combineOr(children.map(({ state }) => state)),
94
+ unresolved: children.flatMap(({ unresolved: items }) => items),
95
+ helperProvenance: children.flatMap(({ helperProvenance }) => helperProvenance ?? []),
96
+ };
97
+ }
98
+ if (key === 'NOT' || key === 'NOR' || key === 'NAND') {
99
+ const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack));
100
+ const base = key === 'NAND'
101
+ ? combineAnd(children.map(({ state }) => state))
102
+ : combineOr(children.map(({ state }) => state));
103
+ return {
104
+ state: invert(base),
105
+ unresolved: children.flatMap(({ unresolved: items }) => items),
106
+ helperProvenance: children.flatMap(({ helperProvenance }) => helperProvenance ?? []),
107
+ };
108
+ }
109
+ if (key === 'check_variable')
110
+ return evaluateCheckVariable(assignment.value, scenario, candidate, assignment);
111
+ if (key === 'is_in_array' || key === 'is_variable_in_array') {
112
+ const arrayId = firstScalar(assignment.value, 'array')?.value;
113
+ const declaredValue = firstScalar(assignment.value, 'value')?.value;
114
+ if (arrayId === undefined || declaredValue === undefined)
115
+ return unresolved(assignment, candidate, `${key} requires array and value`);
116
+ const array = stateValue(scenario, `array.${arrayId}`);
117
+ if (!Array.isArray(array))
118
+ return unresolved(assignment, candidate, `Scenario does not declare array ${arrayId}`);
119
+ const compared = stateValue(scenario, declaredValue) ?? declaredValue;
120
+ return { state: array.includes(compared) ? 'true' : 'false', unresolved: [] };
121
+ }
122
+ const helper = definitions.scriptedTriggers.get(key);
123
+ if (helper !== undefined) {
124
+ if (helperStack.includes(key))
125
+ return unresolved(assignment, candidate, `Scripted trigger recursion detected at ${key}`);
126
+ if (assignments(assignment.value).length > 0)
127
+ return unresolved(assignment, candidate, `Parameterized scripted trigger ${key} is not expanded without declared arguments`);
128
+ const helperProvenance = {
129
+ path: helper.file.displayPath,
130
+ rootKind: helper.file.rootKind,
131
+ loadOrder: helper.file.loadOrder,
132
+ sourceHash: helper.file.sha256,
133
+ location: nodeLocation(helper.document, helper.node, key),
134
+ ...(astPathFor(helper.document, helper.node) === undefined
135
+ ? {}
136
+ : { astPath: astPathFor(helper.document, helper.node) }),
137
+ symbol: key,
138
+ helperChain: [...helperStack, key],
139
+ };
140
+ const evaluated = evaluateTriggerBlock(helper.value, scenario, {
141
+ ...candidate,
142
+ document: helper.document,
143
+ provenance: [helperProvenance, ...candidate.provenance],
144
+ }, definitions, [...helperStack, key]);
145
+ return {
146
+ ...evaluated,
147
+ helperProvenance: [helperProvenance, ...(evaluated.helperProvenance ?? [])],
148
+ };
149
+ }
150
+ const declaredScope = bool(stateValue(scenario, `scope.${key}`));
151
+ if (declaredScope !== undefined)
152
+ return { state: declaredScope ? 'true' : 'false', unresolved: [] };
153
+ return unresolved(assignment, candidate, `Scoped or compound trigger ${key} is not declared by the scenario`);
154
+ }
155
+ const right = assignment.value.value;
156
+ if (key === 'always') {
157
+ const value = bool(right);
158
+ return value === undefined
159
+ ? unresolved(assignment, candidate, 'always trigger is not boolean')
160
+ : { state: value ? 'true' : 'false', unresolved: [] };
161
+ }
162
+ if (key === 'has_country_flag' || key === 'has_global_flag' || key === 'has_state_flag') {
163
+ return { state: scenario.flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
164
+ }
165
+ if (key === 'has_event_target') {
166
+ return {
167
+ state: Object.hasOwn(scenario.eventTargets ?? {}, right) ? 'true' : 'false',
168
+ unresolved: [],
169
+ };
170
+ }
171
+ if (key === 'tag' || key === 'original_tag') {
172
+ if (scenario.actor === undefined)
173
+ return unresolved(assignment, candidate, `Scenario does not declare actor for ${key}`);
174
+ return {
175
+ state: compare(scenario.actor, right, assignment.operator.text) ? 'true' : 'false',
176
+ unresolved: [],
177
+ };
178
+ }
179
+ if (key === 'date') {
180
+ if (scenario.date === undefined)
181
+ return unresolved(assignment, candidate, 'Scenario does not declare date');
182
+ const leftDate = Number(scenario.date.replaceAll('.', ''));
183
+ const rightDate = Number(right.replaceAll('.', ''));
184
+ if (!Number.isFinite(leftDate) || !Number.isFinite(rightDate))
185
+ return unresolved(assignment, candidate, 'Date comparison is not numeric');
186
+ return {
187
+ state: compare(leftDate, rightDate, assignment.operator.text) ? 'true' : 'false',
188
+ unresolved: [],
189
+ };
190
+ }
191
+ const helper = definitions.scriptedTriggers.get(key);
192
+ if (helper !== undefined && bool(right) === true) {
193
+ if (helperStack.includes(key))
194
+ return unresolved(assignment, candidate, `Scripted trigger recursion detected at ${key}`);
195
+ const helperProvenance = {
196
+ path: helper.file.displayPath,
197
+ rootKind: helper.file.rootKind,
198
+ loadOrder: helper.file.loadOrder,
199
+ sourceHash: helper.file.sha256,
200
+ location: nodeLocation(helper.document, helper.node, key),
201
+ ...(astPathFor(helper.document, helper.node) === undefined
202
+ ? {}
203
+ : { astPath: astPathFor(helper.document, helper.node) }),
204
+ symbol: key,
205
+ helperChain: [...helperStack, key],
206
+ };
207
+ const evaluated = evaluateTriggerBlock(helper.value, scenario, {
208
+ ...candidate,
209
+ document: helper.document,
210
+ provenance: [helperProvenance, ...candidate.provenance],
211
+ }, definitions, [...helperStack, key]);
212
+ return {
213
+ ...evaluated,
214
+ helperProvenance: [helperProvenance, ...(evaluated.helperProvenance ?? [])],
215
+ };
216
+ }
217
+ const declared = stateValue(scenario, key);
218
+ if (declared === undefined)
219
+ return unresolved(assignment, candidate, `Scenario does not declare trigger ${key}`);
220
+ const expectedBoolean = bool(right);
221
+ const actualBoolean = bool(declared);
222
+ if (expectedBoolean !== undefined && actualBoolean !== undefined)
223
+ return { state: expectedBoolean === actualBoolean ? 'true' : 'false', unresolved: [] };
224
+ const leftNumber = numeric(declared);
225
+ const rightNumber = numeric(right);
226
+ if (leftNumber !== undefined && rightNumber !== undefined)
227
+ return {
228
+ state: compare(leftNumber, rightNumber, assignment.operator.text) ? 'true' : 'false',
229
+ unresolved: [],
230
+ };
231
+ if (typeof declared === 'string')
232
+ return {
233
+ state: compare(declared, right, assignment.operator.text) ? 'true' : 'false',
234
+ unresolved: [],
235
+ };
236
+ return unresolved(assignment, candidate, `Trigger ${key} cannot compare the declared scenario value`);
237
+ }
238
+ function evaluateAssignment(assignment, scenario, candidate, definitions, helperStack) {
239
+ const evaluated = evaluateAssignmentCore(assignment, scenario, candidate, definitions, helperStack);
240
+ const source = candidate.provenance[0];
241
+ const provenance = candidate.document === undefined || source === undefined
242
+ ? source
243
+ : {
244
+ ...source,
245
+ location: nodeLocation(candidate.document, assignment, assignment.key.value),
246
+ ...(astPathFor(candidate.document, assignment) === undefined
247
+ ? {}
248
+ : { astPath: astPathFor(candidate.document, assignment) }),
249
+ symbol: assignment.key.value,
250
+ };
251
+ const expression = assignment.value.type === 'scalar'
252
+ ? `${assignment.key.value} ${assignment.operator.text} ${assignment.value.value}`
253
+ : assignment.key.value;
254
+ return {
255
+ ...evaluated,
256
+ trace: [
257
+ {
258
+ operation: 'eligibility',
259
+ expression,
260
+ applied: evaluated.state,
261
+ ...(provenance === undefined ? {} : { provenance }),
262
+ },
263
+ ...(evaluated.trace ?? []),
264
+ ],
265
+ };
266
+ }
267
+ export function evaluateTriggerBlock(block, scenario, candidate, definitions, helperStack = []) {
268
+ const override = scenario.candidateOverrides?.[candidate.id];
269
+ if (override !== undefined)
270
+ return { state: override ? 'true' : 'false', unresolved: [] };
271
+ if (block === undefined)
272
+ return { state: 'true', unresolved: [] };
273
+ const children = assignments(block).map((assignment) => evaluateAssignment(assignment, scenario, candidate, definitions, helperStack));
274
+ return {
275
+ state: combineAnd(children.map(({ state }) => state)),
276
+ unresolved: children.flatMap(({ unresolved: items }) => items),
277
+ helperProvenance: children.flatMap(({ helperProvenance }) => helperProvenance ?? []),
278
+ trace: children.flatMap(({ trace }) => trace ?? []),
279
+ };
280
+ }