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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/hoi4_agent_tools/gui/assets.js +15 -3
- package/dist/hoi4_agent_tools/gui/layout.js +270 -75
- package/dist/hoi4_agent_tools/gui/renderer.js +185 -22
- package/dist/hoi4_agent_tools/gui/source-graph.js +40 -11
- package/dist/hoi4_agent_tools/gui/studio.js +35 -7
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +9 -0
- package/dist/hoi4_agent_tools/probability/adapters.js +5 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
- package/dist/hoi4_agent_tools/probability/scenario-state.js +165 -0
- package/dist/hoi4_agent_tools/probability/scope-pools.js +199 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +4 -3
- package/dist/hoi4_agent_tools/probability/sweep.js +6 -2
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +35 -28
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +4 -0
- package/dist/hoi4_agent_tools/schemas/probability.js +90 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +5 -1
- package/docs/probability.md +46 -0
- package/docs/research/probability-adapter-evidence.md +2 -1
- package/examples/probability/scoped-dynamic-pools-evaluate-input.json +50 -0
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +175 -66
- package/schemas/probability-compare-input.schema.json +251 -66
- package/schemas/probability-evaluate-input.schema.json +252 -67
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +231 -46
- package/schemas/probability-sequence-input.schema.json +273 -88
- package/schemas/probability-simulate-input.schema.json +252 -67
- package/schemas/probability-sweep-input.schema.json +254 -69
- 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
|
-
|
|
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
|
-
|
|
248
|
+
sampled = withScenarioPathValue(sampled, input.path, value);
|
|
248
249
|
}
|
|
249
|
-
return { ...
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
171
|
+
state: Object.hasOwn(eventTargets ?? {}, right) ? 'true' : 'false',
|
|
168
172
|
unresolved: [],
|
|
169
173
|
};
|
|
170
174
|
}
|
|
171
175
|
if (key === 'tag' || key === 'original_tag') {
|
|
172
|
-
|
|
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(
|
|
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 =
|
|
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) {
|