hoi4-agent-tools 2.6.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 (44) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/hoi4_agent_tools/gui/assets.js +15 -3
  4. package/dist/hoi4_agent_tools/gui/layout.js +270 -75
  5. package/dist/hoi4_agent_tools/gui/renderer.js +185 -22
  6. package/dist/hoi4_agent_tools/gui/source-graph.js +40 -11
  7. package/dist/hoi4_agent_tools/gui/studio.js +35 -7
  8. package/dist/hoi4_agent_tools/mcp/tools/probability.js +9 -0
  9. package/dist/hoi4_agent_tools/probability/adapters.js +5 -0
  10. package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
  11. package/dist/hoi4_agent_tools/probability/scenario-state.js +165 -0
  12. package/dist/hoi4_agent_tools/probability/scope-pools.js +199 -0
  13. package/dist/hoi4_agent_tools/probability/simulation.js +4 -3
  14. package/dist/hoi4_agent_tools/probability/sweep.js +6 -2
  15. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +35 -28
  16. package/dist/hoi4_agent_tools/probability/value-evaluator.js +4 -0
  17. package/dist/hoi4_agent_tools/schemas/probability.js +90 -0
  18. package/dist/hoi4_agent_tools/version.js +1 -1
  19. package/docs/gui.md +5 -1
  20. package/docs/probability.md +46 -0
  21. package/docs/research/probability-adapter-evidence.md +2 -1
  22. package/examples/probability/scoped-dynamic-pools-evaluate-input.json +50 -0
  23. package/package.json +1 -1
  24. package/schemas/configuration.schema.json +1 -1
  25. package/schemas/continuous-focus-palette.schema.json +1 -1
  26. package/schemas/custom-weighted-pool.schema.json +1 -1
  27. package/schemas/event-feature-manifest.schema.json +1 -1
  28. package/schemas/focus-plan.schema.json +1 -1
  29. package/schemas/focus-planning-sidecar.schema.json +1 -1
  30. package/schemas/gui-animation-source.schema.json +1 -1
  31. package/schemas/gui-helper.schema.json +1 -1
  32. package/schemas/gui-scenario.schema.json +1 -1
  33. package/schemas/map-operation.schema.json +1 -1
  34. package/schemas/operation-result.schema.json +1 -1
  35. package/schemas/probability-analysis-result.schema.json +175 -66
  36. package/schemas/probability-compare-input.schema.json +251 -66
  37. package/schemas/probability-evaluate-input.schema.json +252 -67
  38. package/schemas/probability-inspect-input.schema.json +1 -1
  39. package/schemas/probability-render-input.schema.json +1 -1
  40. package/schemas/probability-scenario-set.schema.json +231 -46
  41. package/schemas/probability-sequence-input.schema.json +273 -88
  42. package/schemas/probability-simulate-input.schema.json +252 -67
  43. package/schemas/probability-sweep-input.schema.json +254 -69
  44. package/server.json +2 -2
@@ -0,0 +1,165 @@
1
+ function caseInsensitiveEntry(record, key) {
2
+ if (record === undefined)
3
+ return undefined;
4
+ if (Object.hasOwn(record, key))
5
+ return record[key];
6
+ const normalized = key.toLowerCase();
7
+ const found = Object.entries(record).find(([candidate]) => candidate.toLowerCase() === normalized);
8
+ return found?.[1];
9
+ }
10
+ export function rootScopeContext(scenario) {
11
+ return {
12
+ expression: 'ROOT',
13
+ binding: {
14
+ id: scenario.actor ?? 'ROOT',
15
+ ...(scenario.actor === undefined ? {} : { actor: scenario.actor }),
16
+ state: scenario.state,
17
+ ...(scenario.flags === undefined ? {} : { flags: scenario.flags }),
18
+ ...(scenario.eventTargets === undefined ? {} : { eventTargets: scenario.eventTargets }),
19
+ },
20
+ };
21
+ }
22
+ function explicitScope(scenario, expression, parent) {
23
+ const binding = caseInsensitiveEntry(scenario.scopes, expression);
24
+ return binding === undefined ? undefined : { expression, binding, parent };
25
+ }
26
+ export function resolveScopeContext(scenario, expression, current) {
27
+ const upper = expression.toUpperCase();
28
+ if (upper === 'ROOT')
29
+ return rootScopeContext(scenario);
30
+ if (upper === 'THIS')
31
+ return current;
32
+ if (upper === 'PREV')
33
+ return current.parent ?? rootScopeContext(scenario);
34
+ const chained = current.expression === 'ROOT' || current.expression === 'THIS'
35
+ ? expression
36
+ : `${current.expression}.${expression}`;
37
+ const chainedBinding = explicitScope(scenario, chained, current);
38
+ if (chainedBinding !== undefined)
39
+ return chainedBinding;
40
+ const direct = explicitScope(scenario, expression, current);
41
+ if (direct !== undefined)
42
+ return direct;
43
+ const targetMatch = /^(?:event_target|scope):(.+)$/iu.exec(expression);
44
+ const targetId = targetMatch === null
45
+ ? undefined
46
+ : (caseInsensitiveEntry(current.binding.eventTargets, targetMatch[1]) ??
47
+ caseInsensitiveEntry(scenario.eventTargets, targetMatch[1]));
48
+ if (targetId !== undefined) {
49
+ const declaredBinding = Object.values(scenario.scopes ?? {}).find((binding) => binding.id === targetId || binding.actor === targetId);
50
+ return {
51
+ expression,
52
+ binding: declaredBinding ?? { id: targetId, actor: targetId, state: {} },
53
+ parent: current,
54
+ };
55
+ }
56
+ return undefined;
57
+ }
58
+ export function scopeStateValue(scenario, key, current) {
59
+ if (Object.hasOwn(current.binding.state, key))
60
+ return current.binding.state[key];
61
+ if (Object.hasOwn(current.binding.state, `trigger.${key}`))
62
+ return current.binding.state[`trigger.${key}`];
63
+ if (current.expression !== 'ROOT')
64
+ return undefined;
65
+ if (Object.hasOwn(scenario.state, key))
66
+ return scenario.state[key];
67
+ if (Object.hasOwn(scenario.state, `trigger.${key}`))
68
+ return scenario.state[`trigger.${key}`];
69
+ return undefined;
70
+ }
71
+ export function rootStateValue(scenario, key) {
72
+ if (Object.hasOwn(scenario.state, key))
73
+ return scenario.state[key];
74
+ if (Object.hasOwn(scenario.state, `trigger.${key}`))
75
+ return scenario.state[`trigger.${key}`];
76
+ return undefined;
77
+ }
78
+ export function scopeIdentity(current) {
79
+ return current.binding.actor ?? current.binding.id;
80
+ }
81
+ export function scopedExpressionValue(scenario, expression) {
82
+ const scopes = Object.entries(scenario.scopes ?? {}).sort(([left], [right]) => right.length - left.length);
83
+ for (const [scope, binding] of scopes) {
84
+ for (const prefix of [`${scope}.`, `scope.${scope}.`, `scopes.${scope}.`]) {
85
+ if (!expression.toLowerCase().startsWith(prefix.toLowerCase()))
86
+ continue;
87
+ const key = expression.slice(prefix.length).replace(/^state\./u, '');
88
+ if (Object.hasOwn(binding.state, key))
89
+ return binding.state[key];
90
+ if (Object.hasOwn(binding.state, `variable.${key}`))
91
+ return binding.state[`variable.${key}`];
92
+ }
93
+ }
94
+ return undefined;
95
+ }
96
+ function updateScopeState(scenario, path, value) {
97
+ const scopes = Object.entries(scenario.scopes ?? {}).sort(([left], [right]) => right.length - left.length);
98
+ for (const [scope, binding] of scopes) {
99
+ for (const prefix of [`scope.${scope}.`, `scopes.${scope}.`]) {
100
+ if (!path.toLowerCase().startsWith(prefix.toLowerCase()))
101
+ continue;
102
+ const key = path.slice(prefix.length).replace(/^state\./u, '');
103
+ return {
104
+ ...scenario,
105
+ scopes: {
106
+ ...scenario.scopes,
107
+ [scope]: { ...binding, state: { ...binding.state, [key]: value } },
108
+ },
109
+ };
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ function updatePoolCandidateState(scenario, path, value) {
115
+ if (!path.startsWith('scopePools.'))
116
+ return undefined;
117
+ for (const [poolIndex, pool] of (scenario.scopePools ?? []).entries()) {
118
+ const poolPrefix = `scopePools.${pool.id}.`;
119
+ if (!path.startsWith(poolPrefix))
120
+ continue;
121
+ for (const [candidateIndex, candidate] of pool.candidates.entries()) {
122
+ const candidatePrefix = `${poolPrefix}${candidate.id}.`;
123
+ if (!path.startsWith(candidatePrefix))
124
+ continue;
125
+ const key = path.slice(candidatePrefix.length).replace(/^state\./u, '');
126
+ const candidates = [...pool.candidates];
127
+ candidates[candidateIndex] = {
128
+ ...candidate,
129
+ state: { ...candidate.state, [key]: value },
130
+ };
131
+ const pools = [...(scenario.scopePools ?? [])];
132
+ pools[poolIndex] = { ...pool, candidates };
133
+ return { ...scenario, scopePools: pools };
134
+ }
135
+ }
136
+ return undefined;
137
+ }
138
+ export function scenarioPathValue(scenario, path) {
139
+ const scoped = scopedExpressionValue(scenario, path);
140
+ if (scoped !== undefined)
141
+ return scoped;
142
+ if (path.startsWith('scopePools.')) {
143
+ for (const pool of scenario.scopePools ?? []) {
144
+ const poolPrefix = `scopePools.${pool.id}.`;
145
+ if (!path.startsWith(poolPrefix))
146
+ continue;
147
+ for (const candidate of pool.candidates) {
148
+ const candidatePrefix = `${poolPrefix}${candidate.id}.`;
149
+ if (!path.startsWith(candidatePrefix))
150
+ continue;
151
+ const key = path.slice(candidatePrefix.length).replace(/^state\./u, '');
152
+ if (Object.hasOwn(candidate.state, key))
153
+ return candidate.state[key];
154
+ }
155
+ }
156
+ }
157
+ return rootStateValue(scenario, path);
158
+ }
159
+ export function withScenarioPathValue(scenario, path, value) {
160
+ return (updateScopeState(scenario, path, value) ??
161
+ updatePoolCandidateState(scenario, path, value) ?? {
162
+ ...scenario,
163
+ state: { ...scenario.state, [path]: value },
164
+ });
165
+ }
@@ -0,0 +1,199 @@
1
+ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
2
+ import { childBlocks, parseClausewitz, } from '../core/source/index.js';
3
+ import { Rational, sumRationals } from './rational.js';
4
+ import { rootScopeContext, scopedExpressionValue, } from './scenario-state.js';
5
+ import { evaluateTriggerBlock } from './trigger-evaluator.js';
6
+ function filterSource(pool) {
7
+ if (pool.filter?.scriptedTrigger !== undefined)
8
+ return `filter = { ${pool.filter.scriptedTrigger} = yes }`;
9
+ const inline = pool.filter?.inlineClausewitz?.trim();
10
+ if (inline === undefined)
11
+ return undefined;
12
+ if (/^filter\s*=/iu.test(inline))
13
+ return inline;
14
+ return inline.startsWith('{') ? `filter = ${inline}` : `filter = { ${inline} }`;
15
+ }
16
+ function parseFilter(scenario, pool) {
17
+ const source = filterSource(pool);
18
+ const sourceHash = hashCanonical({ scenarioId: scenario.id, poolId: pool.id, source });
19
+ if (source === undefined)
20
+ return { sourceHash, unresolved: [] };
21
+ const path = `scenario:${scenario.id}/scope-pool:${pool.id}`;
22
+ try {
23
+ const document = parseClausewitz(Buffer.from(source, 'utf8'), path);
24
+ const block = childBlocks(document.root, 'filter')[0];
25
+ if (block !== undefined)
26
+ return { document, block, sourceHash, unresolved: [] };
27
+ }
28
+ catch (error) {
29
+ return {
30
+ sourceHash,
31
+ unresolved: [
32
+ {
33
+ code: 'SCOPE_POOL_FILTER_INVALID',
34
+ message: `Scope pool ${pool.id} filter could not be parsed`,
35
+ path: pool.id,
36
+ details: { reason: error instanceof Error ? error.message : String(error) },
37
+ },
38
+ ],
39
+ };
40
+ }
41
+ return {
42
+ sourceHash,
43
+ unresolved: [
44
+ {
45
+ code: 'SCOPE_POOL_FILTER_INVALID',
46
+ message: `Scope pool ${pool.id} filter does not contain a trigger block`,
47
+ path: pool.id,
48
+ },
49
+ ],
50
+ };
51
+ }
52
+ function scopedScenario(scenario, pool, binding) {
53
+ const bindAs = pool.bindAs ?? 'THIS';
54
+ if (bindAs === 'ROOT') {
55
+ const root = {
56
+ ...scenario,
57
+ actor: binding.actor ?? binding.id,
58
+ state: binding.state,
59
+ ...(binding.flags === undefined ? {} : { flags: binding.flags }),
60
+ ...(binding.eventTargets === undefined ? {} : { eventTargets: binding.eventTargets }),
61
+ scopes: { ...scenario.scopes, ROOT: binding, THIS: binding },
62
+ };
63
+ return { scenario: root, context: rootScopeContext(root) };
64
+ }
65
+ const selected = {
66
+ ...scenario,
67
+ scopes: {
68
+ ...scenario.scopes,
69
+ THIS: binding,
70
+ [bindAs]: binding,
71
+ },
72
+ };
73
+ return {
74
+ scenario: selected,
75
+ context: {
76
+ expression: 'THIS',
77
+ binding,
78
+ parent: rootScopeContext(scenario),
79
+ },
80
+ };
81
+ }
82
+ function candidateFor(scenario, pool, binding, filter) {
83
+ return {
84
+ id: binding.id,
85
+ adapterId: 'custom_weighted_pool',
86
+ sourceKind: 'dynamic_scope_pool_candidate',
87
+ defaultValue: '1',
88
+ ...(filter.document === undefined ? {} : { document: filter.document }),
89
+ provenance: [
90
+ {
91
+ path: `scenario:${scenario.id}/scope-pool:${pool.id}`,
92
+ rootKind: 'scenario',
93
+ loadOrder: 0,
94
+ sourceHash: filter.sourceHash,
95
+ symbol: binding.id,
96
+ },
97
+ ],
98
+ metadata: { poolId: pool.id, bindAs: pool.bindAs ?? 'THIS' },
99
+ };
100
+ }
101
+ function poolWeight(pool, binding, scenario) {
102
+ if (pool.selection === 'uniform')
103
+ return Rational.one;
104
+ const expression = binding.weight ?? 1;
105
+ const direct = Rational.parse(expression);
106
+ if (direct !== undefined)
107
+ return direct.compare(Rational.zero) < 0 ? undefined : direct;
108
+ if (typeof expression !== 'string')
109
+ return undefined;
110
+ const value = binding.state[expression] ??
111
+ binding.state[`variable.${expression}`] ??
112
+ scopedExpressionValue(scenario, expression) ??
113
+ scenario.state[expression] ??
114
+ scenario.state[`variable.${expression}`];
115
+ const parsed = typeof value === 'number' || typeof value === 'string' ? Rational.parse(value) : undefined;
116
+ return parsed !== undefined && parsed.compare(Rational.zero) >= 0 ? parsed : undefined;
117
+ }
118
+ function evaluatePool(scenario, pool, definitions) {
119
+ const filter = parseFilter(scenario, pool);
120
+ const candidates = [...pool.candidates]
121
+ .sort((left, right) => compareCodeUnits(left.id, right.id))
122
+ .map((binding) => {
123
+ const selected = scopedScenario(scenario, pool, binding);
124
+ const candidate = candidateFor(scenario, pool, binding, filter);
125
+ const eligibility = filter.unresolved.length > 0
126
+ ? { state: 'unresolved', unresolved: filter.unresolved }
127
+ : evaluateTriggerBlock(filter.block, selected.scenario, candidate, definitions, [], selected.context);
128
+ const unresolved = [...eligibility.unresolved];
129
+ const weight = pool.selection === 'enumeration' ? undefined : poolWeight(pool, binding, selected.scenario);
130
+ if (pool.selection !== 'enumeration' && weight === undefined)
131
+ unresolved.push({
132
+ code: 'SCOPE_POOL_WEIGHT_UNRESOLVED',
133
+ message: `Scope pool ${pool.id} candidate ${binding.id} has an unresolved weight`,
134
+ path: pool.id,
135
+ candidateId: binding.id,
136
+ details: { weight: binding.weight ?? 1 },
137
+ });
138
+ return {
139
+ id: binding.id,
140
+ eligibility: eligibility.state,
141
+ ...(weight === undefined ? {} : { weight: weight.toJSON() }),
142
+ trace: eligibility.trace ?? [],
143
+ unresolved,
144
+ };
145
+ });
146
+ const unresolved = [
147
+ ...filter.unresolved,
148
+ ...candidates.flatMap(({ unresolved: items }) => items),
149
+ ];
150
+ if (pool.selection !== 'enumeration' && !pool.complete)
151
+ unresolved.push({
152
+ code: 'SCOPE_POOL_INCOMPLETE',
153
+ message: `Scope pool ${pool.id} can be enumerated, but normalized probabilities require a complete candidate catalog`,
154
+ path: pool.id,
155
+ });
156
+ const resolved = candidates.every(({ eligibility, weight }) => eligibility !== 'unresolved' && (pool.selection === 'enumeration' || weight !== undefined));
157
+ const eligibleWeights = candidates.flatMap((candidate) => candidate.eligibility === 'true' && candidate.weight !== undefined
158
+ ? [Rational.parse(candidate.weight.decimal)]
159
+ : []);
160
+ const total = sumRationals(eligibleWeights);
161
+ if (pool.selection !== 'enumeration' &&
162
+ pool.complete &&
163
+ resolved &&
164
+ total.compare(Rational.zero) > 0) {
165
+ for (const candidate of candidates) {
166
+ const probability = candidate.eligibility === 'true' && candidate.weight !== undefined
167
+ ? Rational.parse(candidate.weight.decimal).divide(total)
168
+ : Rational.zero;
169
+ candidate.conditionalProbability = probability.toNumber();
170
+ candidate.exactConditionalProbability = probability.toJSON();
171
+ }
172
+ }
173
+ else if (pool.selection !== 'enumeration' && pool.complete && resolved && total.isZero())
174
+ unresolved.push({
175
+ code: 'SCOPE_POOL_ZERO_TOTAL',
176
+ message: `Scope pool ${pool.id} has no positive eligible weight`,
177
+ path: pool.id,
178
+ });
179
+ return {
180
+ id: pool.id,
181
+ bindAs: pool.bindAs ?? 'THIS',
182
+ selection: pool.selection,
183
+ poolComplete: pool.complete,
184
+ candidates,
185
+ eligibleCandidateIds: candidates
186
+ .filter(({ eligibility }) => eligibility === 'true')
187
+ .map(({ id }) => id),
188
+ unresolvedCandidateIds: candidates
189
+ .filter(({ eligibility, unresolved: items }) => eligibility === 'unresolved' || items.length > 0)
190
+ .map(({ id }) => id),
191
+ ...(pool.selection !== 'enumeration' && resolved ? { poolTotal: total.toJSON() } : {}),
192
+ unresolved,
193
+ };
194
+ }
195
+ export function evaluateScopePools(scenario, definitions) {
196
+ return [...(scenario.scopePools ?? [])]
197
+ .sort((left, right) => compareCodeUnits(left.id, right.id))
198
+ .map((pool) => evaluatePool(scenario, pool, definitions));
199
+ }
@@ -1,3 +1,4 @@
1
+ import { withScenarioPathValue } from './scenario-state.js';
1
2
  import { evaluateExactCandidates } from './evaluation.js';
2
3
  function recordUnresolved(unresolved, issue) {
3
4
  if (unresolved.some((existing) => existing.code === issue.code &&
@@ -222,7 +223,7 @@ function continuousSamples(scenario, samples, samplingMethod, random, unresolved
222
223
  }
223
224
  function sampledScenario(scenario, continuous, random, unresolved) {
224
225
  const inputs = scenario.uncertainInputs ?? [];
225
- const state = { ...scenario.state };
226
+ let sampled = { ...scenario, state: { ...scenario.state } };
226
227
  for (const input of inputs) {
227
228
  let value;
228
229
  const plannedUniform = continuous.get(input.path);
@@ -244,9 +245,9 @@ function sampledScenario(scenario, continuous, random, unresolved) {
244
245
  });
245
246
  }
246
247
  else
247
- state[input.path] = value;
248
+ sampled = withScenarioPathValue(sampled, input.path, value);
248
249
  }
249
- return { ...scenario, state, uncertainInputs: [] };
250
+ return { ...sampled, uncertainInputs: [] };
250
251
  }
251
252
  function quantileForConfidence(confidence) {
252
253
  return inverseNormal(0.5 + confidence / 2);
@@ -1,5 +1,6 @@
1
1
  import { ServiceError } from '../core/result.js';
2
2
  import { conditionBreakpointValues, evaluateSurfaceScenarios } from './evaluation.js';
3
+ import { scenarioPathValue, withScenarioPathValue } from './scenario-state.js';
3
4
  const MAX_SWEEP_POINTS = 250_000;
4
5
  function declaredValues(surface, scenario, path, steps) {
5
6
  const uncertain = scenario.uncertainInputs?.find((input) => input.path === path);
@@ -15,7 +16,7 @@ function declaredValues(surface, scenario, path, steps) {
15
16
  if (values.length > 0)
16
17
  return [...new Set(values)].sort((left, right) => left - right);
17
18
  }
18
- const current = scenario.state[path];
19
+ const current = scenarioPathValue(scenario, path);
19
20
  if (typeof current === 'number')
20
21
  return [current];
21
22
  throw new ServiceError('PROBABILITY_SWEEP_RANGE_REQUIRED', 'Every sweep path requires a scenario range, numeric alternatives, or numeric state value', { scenarioId: scenario.id, path });
@@ -26,7 +27,10 @@ function values(range, steps) {
26
27
  return Array.from({ length: steps }, (_, index) => range.min + ((range.max - range.min) * index) / (steps - 1));
27
28
  }
28
29
  function point(surface, scenario, definitions, path, value, assigned) {
29
- const evaluated = evaluateSurfaceScenarios(surface, [{ ...scenario, state: { ...scenario.state, ...assigned }, uncertainInputs: [] }], definitions)[0];
30
+ const base = { ...scenario, uncertainInputs: [] };
31
+ const evaluated = evaluateSurfaceScenarios(surface, [
32
+ Object.entries(assigned).reduce((updated, [assignedPath, assignedValue]) => withScenarioPathValue(updated, assignedPath, assignedValue), base),
33
+ ], definitions)[0];
30
34
  return {
31
35
  scenarioId: scenario.id,
32
36
  path,
@@ -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) {