hoi4-agent-tools 3.1.0 → 3.2.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 (57) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +6 -2
  3. package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
  4. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  5. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
  6. package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
  7. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  8. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  9. package/dist/hoi4_agent_tools/core/job-worker-host.js +2 -0
  10. package/dist/hoi4_agent_tools/core/job-worker.js +2 -0
  11. package/dist/hoi4_agent_tools/core/operation-tasks.js +5 -0
  12. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  13. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  14. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  15. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  16. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  17. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  18. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  19. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  20. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  21. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  22. package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
  23. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  24. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +2 -0
  25. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +2 -0
  26. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  27. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  28. package/dist/hoi4_agent_tools/version.js +1 -1
  29. package/docs/README.md +1 -0
  30. package/docs/analysis.md +100 -0
  31. package/docs/jobs.md +2 -1
  32. package/package.json +5 -2
  33. package/schemas/configuration.schema.json +1 -1
  34. package/schemas/continuous-focus-palette.schema.json +1 -1
  35. package/schemas/custom-weighted-pool.schema.json +1 -1
  36. package/schemas/decision-inspect-input.schema.json +290 -0
  37. package/schemas/event-feature-manifest.schema.json +1 -1
  38. package/schemas/focus-plan.schema.json +1 -1
  39. package/schemas/focus-planning-sidecar.schema.json +1 -1
  40. package/schemas/gui-animation-source.schema.json +1 -1
  41. package/schemas/gui-helper.schema.json +1 -1
  42. package/schemas/gui-scenario.schema.json +1 -1
  43. package/schemas/helper-expansion-request.schema.json +1 -1
  44. package/schemas/helper-expansion-summary.schema.json +1 -1
  45. package/schemas/impact-inspect-input.schema.json +203 -0
  46. package/schemas/map-operation.schema.json +1 -1
  47. package/schemas/operation-result.schema.json +1 -1
  48. package/schemas/probability-analysis-result.schema.json +1 -1
  49. package/schemas/probability-compare-input.schema.json +1 -1
  50. package/schemas/probability-evaluate-input.schema.json +1 -1
  51. package/schemas/probability-inspect-input.schema.json +1 -1
  52. package/schemas/probability-render-input.schema.json +1 -1
  53. package/schemas/probability-scenario-set.schema.json +1 -1
  54. package/schemas/probability-sequence-input.schema.json +1 -1
  55. package/schemas/probability-simulate-input.schema.json +1 -1
  56. package/schemas/probability-sweep-input.schema.json +1 -1
  57. package/server.json +3 -3
@@ -0,0 +1,177 @@
1
+ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
2
+ import { evaluateTriggerBlock, resolveConditionNumber } from '../core/condition-evaluator.js';
3
+ import { rootScopeContext } from '../core/scenario-state.js';
4
+ import { assignments, nodeLocation } from '../core/source/index.js';
5
+ function scalar(assignment) {
6
+ return assignment?.value.type === 'scalar' ? assignment.value.value : undefined;
7
+ }
8
+ function unresolved(source, message) {
9
+ return {
10
+ code: 'DECISION_COST_UNRESOLVED',
11
+ message,
12
+ candidateId: source.id,
13
+ provenance: {
14
+ path: source.path,
15
+ rootKind: source.rootKind,
16
+ loadOrder: source.loadOrder,
17
+ sourceHash: source.sourceHash,
18
+ ...(source.location === undefined ? {} : { location: source.location }),
19
+ symbol: source.id,
20
+ },
21
+ };
22
+ }
23
+ function directPayments(snapshot, source, scenario, definitions) {
24
+ const paymentEffects = [];
25
+ const conditionalOrHelperEffects = [];
26
+ for (const effect of source.fields.complete_effect) {
27
+ if (effect.value.type !== 'block') {
28
+ conditionalOrHelperEffects.push('malformed complete_effect');
29
+ continue;
30
+ }
31
+ for (const entry of assignments(effect.value)) {
32
+ const key = entry.key.value;
33
+ let resource;
34
+ let operation;
35
+ let expression;
36
+ if (key === 'add_political_power' || key === 'add_command_power') {
37
+ resource = key.slice(4);
38
+ operation = 'add';
39
+ expression = scalar(entry);
40
+ }
41
+ else if (key === 'add_to_variable' || key === 'subtract_from_variable') {
42
+ if (entry.value.type === 'block') {
43
+ const values = assignments(entry.value).filter(({ key: field }) => field.value !== 'tooltip');
44
+ if (values.length === 1) {
45
+ resource = `variable.${values[0].key.value}`;
46
+ operation = key === 'add_to_variable' ? 'add' : 'subtract';
47
+ expression = scalar(values[0]);
48
+ }
49
+ }
50
+ }
51
+ if (resource !== undefined && operation !== undefined) {
52
+ const amount = expression === undefined
53
+ ? undefined
54
+ : resolveConditionNumber(expression, scenario, rootScopeContext(scenario), definitions, source.path);
55
+ const normalizedOperation = amount !== undefined && amount < 0
56
+ ? operation === 'add'
57
+ ? 'subtract'
58
+ : 'add'
59
+ : operation;
60
+ paymentEffects.push({
61
+ resource,
62
+ amount: amount === undefined ? null : Math.abs(amount),
63
+ operation: normalizedOperation,
64
+ scope: 'ROOT',
65
+ location: nodeLocation(source.document, entry, key),
66
+ });
67
+ if (amount === undefined)
68
+ conditionalOrHelperEffects.push(`dynamic payment: ${key}`);
69
+ }
70
+ else if (key === 'add_political_power' ||
71
+ key === 'add_command_power' ||
72
+ key === 'add_to_variable' ||
73
+ key === 'subtract_from_variable') {
74
+ conditionalOrHelperEffects.push(`unparsed payment: ${key}`);
75
+ }
76
+ else if (snapshot.index.find('scripted_effect', key) !== undefined ||
77
+ ['if', 'else_if', 'else', 'random', 'random_list', 'ROOT', 'FROM', 'meta_effect'].includes(key)) {
78
+ conditionalOrHelperEffects.push(key);
79
+ }
80
+ }
81
+ }
82
+ return { paymentEffects, conditionalOrHelperEffects };
83
+ }
84
+ /** Cost and payment evidence is source-bounded; a custom cost is never treated as automatic payment. */
85
+ export function inspectDecisionCost(snapshot, source, scenario) {
86
+ const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
87
+ const base = directPayments(snapshot, source, scenario, definitions);
88
+ const cost = source.fields.cost;
89
+ const custom = source.fields.custom_cost_trigger;
90
+ const findings = [];
91
+ const deductions = base.paymentEffects.filter(({ operation }) => operation === 'subtract');
92
+ const seenDeductions = new Set();
93
+ for (const deduction of deductions) {
94
+ if (seenDeductions.has(deduction.resource))
95
+ findings.push(unresolved(source, `Multiple direct payments deduct ${deduction.resource}`));
96
+ seenDeductions.add(deduction.resource);
97
+ }
98
+ if (cost.length > 1 || custom.length > 1 || (cost.length > 0 && custom.length > 0)) {
99
+ findings.push(unresolved(source, 'Conflicting or repeated decision cost declarations'));
100
+ return {
101
+ kind: 'invalid',
102
+ expression: null,
103
+ amount: null,
104
+ affordable: 'unresolved',
105
+ ...base,
106
+ unresolved: findings,
107
+ };
108
+ }
109
+ if (cost.length === 1) {
110
+ if (deductions.some(({ resource }) => resource === 'political_power'))
111
+ findings.push(unresolved(source, 'Engine political-power cost also has an explicit political-power deduction'));
112
+ const expression = scalar(cost[0]);
113
+ const amount = expression === undefined
114
+ ? undefined
115
+ : resolveConditionNumber(expression, scenario, rootScopeContext(scenario), definitions, source.path);
116
+ const politicalPower = resolveConditionNumber('political_power', scenario, rootScopeContext(scenario), definitions, source.path);
117
+ if (amount === undefined || amount < 0)
118
+ findings.push(unresolved(source, 'Engine political-power cost is not a resolved nonnegative number'));
119
+ if (politicalPower === undefined)
120
+ findings.push(unresolved(source, 'Scenario does not declare current political power'));
121
+ return {
122
+ kind: 'engine_political_power',
123
+ expression: expression ?? null,
124
+ amount: amount ?? null,
125
+ affordable: amount === undefined || amount < 0 || politicalPower === undefined
126
+ ? 'unresolved'
127
+ : politicalPower >= amount
128
+ ? 'true'
129
+ : 'false',
130
+ ...base,
131
+ unresolved: findings,
132
+ };
133
+ }
134
+ if (custom.length === 1) {
135
+ const trigger = custom[0].value;
136
+ const evaluated = trigger.type === 'block'
137
+ ? evaluateTriggerBlock(trigger, { ...scenario, closedFlags: scenario.closedFlags ?? false }, {
138
+ id: source.id,
139
+ document: source.document,
140
+ provenance: [
141
+ {
142
+ path: source.path,
143
+ rootKind: source.rootKind,
144
+ loadOrder: source.loadOrder,
145
+ sourceHash: source.sourceHash,
146
+ ...(source.location === undefined ? {} : { location: source.location }),
147
+ symbol: source.id,
148
+ },
149
+ ],
150
+ }, definitions)
151
+ : {
152
+ state: 'unresolved',
153
+ unresolved: [unresolved(source, 'custom_cost_trigger is not a block')],
154
+ };
155
+ findings.push(...evaluated.unresolved);
156
+ if (base.conditionalOrHelperEffects.length > 0)
157
+ findings.push(unresolved(source, 'Custom payment crosses a conditional, helper, dynamic, or unparsed effect path'));
158
+ if (base.paymentEffects.length === 0 && base.conditionalOrHelperEffects.length === 0)
159
+ findings.push(unresolved(source, 'Custom cost has no recognized direct payment effect; other effect semantics remain unverified'));
160
+ return {
161
+ kind: 'custom',
162
+ expression: scalar(source.fields.custom_cost_text[0]) ?? null,
163
+ amount: null,
164
+ affordable: evaluated.state,
165
+ ...base,
166
+ unresolved: findings,
167
+ };
168
+ }
169
+ return {
170
+ kind: 'none',
171
+ expression: null,
172
+ amount: 0,
173
+ affordable: 'true',
174
+ ...base,
175
+ unresolved: findings,
176
+ };
177
+ }
@@ -0,0 +1,256 @@
1
+ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
2
+ import { evaluateTriggerBlock } from '../core/condition-evaluator.js';
3
+ import { assignments } from '../core/source/index.js';
4
+ import { decisionSourceInventory, } from './source-inventory.js';
5
+ function combine(values) {
6
+ if (values.includes('false'))
7
+ return 'false';
8
+ return values.includes('unresolved') ? 'unresolved' : 'true';
9
+ }
10
+ function combineAny(values) {
11
+ if (values.includes('true'))
12
+ return 'true';
13
+ return values.includes('unresolved') ? 'unresolved' : 'false';
14
+ }
15
+ function unresolved(message, candidateId) {
16
+ return {
17
+ state: 'unresolved',
18
+ unresolved: [{ code: 'DECISION_UNRESOLVED', message, candidateId }],
19
+ };
20
+ }
21
+ function scalar(assignment) {
22
+ return assignment?.value.type === 'scalar' ? assignment.value.value : undefined;
23
+ }
24
+ function block(assignment) {
25
+ return assignment?.value.type === 'block' ? assignment.value : undefined;
26
+ }
27
+ function subject(source) {
28
+ return {
29
+ id: source.id,
30
+ document: source.document,
31
+ provenance: [
32
+ {
33
+ path: source.path,
34
+ rootKind: source.rootKind,
35
+ loadOrder: source.loadOrder,
36
+ sourceHash: source.sourceHash,
37
+ ...(source.location === undefined ? {} : { location: source.location }),
38
+ symbol: source.id,
39
+ },
40
+ ],
41
+ };
42
+ }
43
+ export function evaluateDecisionField(source, name, scenario, definitions) {
44
+ const occurrences = source.fields[name];
45
+ if (occurrences.length > 1)
46
+ return unresolved(`Multiple ${name} blocks in ${source.path}`, source.id);
47
+ const assignment = occurrences[0];
48
+ if (assignment !== undefined && assignment.value.type !== 'block')
49
+ return unresolved(`${name} is not a trigger block`, source.id);
50
+ return evaluateTriggerBlock(block(assignment), scenario, subject(source), definitions);
51
+ }
52
+ function evaluateCategoryField(categories, name, scenario, definitions, decisionId) {
53
+ const definitionsWithField = categories.filter(({ fields }) => fields[name].length > 0);
54
+ if (definitionsWithField.length > 1)
55
+ return unresolved(`Multiple active category fragments define ${name}; merge semantics are unresolved`, decisionId);
56
+ const source = definitionsWithField[0];
57
+ return source === undefined
58
+ ? { state: 'true', unresolved: [] }
59
+ : evaluateDecisionField(source, name, scenario, definitions);
60
+ }
61
+ function explicitTargets(source, scenario) {
62
+ const targets = source.fields.targets;
63
+ const arrays = source.fields.target_array;
64
+ const stateFilters = source.fields.state_trigger;
65
+ const activeStateFilters = stateFilters.filter((assignment) => scalar(assignment) !== 'no');
66
+ const targeted = targets.length +
67
+ arrays.length +
68
+ activeStateFilters.length +
69
+ source.fields.target_root_trigger.length +
70
+ source.fields.target_trigger.length;
71
+ if (targeted === 0)
72
+ return { state: 'true', unresolved: [] };
73
+ const target = scenario.scopes?.FROM;
74
+ if (target === undefined)
75
+ return unresolved('Targeted decision requires an explicit FROM scope binding', source.id);
76
+ const targetId = target.actor ?? target.id;
77
+ if (targets.length + arrays.length + stateFilters.length === 0)
78
+ return { state: 'true', unresolved: [] };
79
+ const candidateChecks = [];
80
+ const stateChecks = [];
81
+ const findings = [];
82
+ for (const assignment of targets) {
83
+ if (assignment.value.type !== 'block') {
84
+ findings.push({
85
+ code: 'DECISION_UNRESOLVED',
86
+ message: 'targets must be a block',
87
+ candidateId: source.id,
88
+ });
89
+ candidateChecks.push('unresolved');
90
+ continue;
91
+ }
92
+ const listed = assignment.value.entries
93
+ .filter((entry) => entry.type === 'scalar')
94
+ .map((entry) => entry.value);
95
+ const state = scalar(assignments(assignment.value, 'state')[0]);
96
+ if (state !== undefined)
97
+ listed.push(state);
98
+ candidateChecks.push(listed.includes(targetId) ? 'true' : 'false');
99
+ }
100
+ for (const assignment of arrays) {
101
+ const name = scalar(assignment);
102
+ const value = name === undefined ? undefined : scenario.state[`array.${name}`];
103
+ if (!Array.isArray(value)) {
104
+ findings.push({
105
+ code: 'DECISION_UNRESOLVED',
106
+ message: `Scenario does not declare target array ${name ?? '<unknown>'}`,
107
+ candidateId: source.id,
108
+ });
109
+ candidateChecks.push('unresolved');
110
+ }
111
+ else
112
+ candidateChecks.push(value.includes(targetId) ? 'true' : 'false');
113
+ }
114
+ for (const assignment of activeStateFilters) {
115
+ const filter = scalar(assignment);
116
+ if (target.type === undefined || target.type === 'unknown') {
117
+ findings.push({
118
+ code: 'DECISION_UNRESOLVED',
119
+ message: 'Scenario does not declare the target scope type',
120
+ candidateId: source.id,
121
+ });
122
+ stateChecks.push('unresolved');
123
+ continue;
124
+ }
125
+ if (target.type !== 'state') {
126
+ stateChecks.push('false');
127
+ continue;
128
+ }
129
+ if (filter === 'yes' || filter === 'any') {
130
+ stateChecks.push('true');
131
+ continue;
132
+ }
133
+ if (filter === 'any_owned_state' || filter === 'any_controlled_state') {
134
+ const key = filter === 'any_owned_state' ? 'owner' : 'controller';
135
+ const declared = target.state[key];
136
+ if (typeof declared !== 'string' || scenario.actor === undefined) {
137
+ findings.push({
138
+ code: 'DECISION_UNRESOLVED',
139
+ message: `State target needs declared ${key} and actor for ${filter}`,
140
+ candidateId: source.id,
141
+ });
142
+ stateChecks.push('unresolved');
143
+ }
144
+ else
145
+ stateChecks.push(declared === scenario.actor ? 'true' : 'false');
146
+ continue;
147
+ }
148
+ if (filter !== undefined && /^[a-z][a-z0-9_]*$/u.test(filter)) {
149
+ const continent = target.state.continent;
150
+ if (typeof continent !== 'string') {
151
+ findings.push({
152
+ code: 'DECISION_UNRESOLVED',
153
+ message: `State target needs a declared continent for ${filter}`,
154
+ candidateId: source.id,
155
+ });
156
+ stateChecks.push('unresolved');
157
+ }
158
+ else
159
+ stateChecks.push(continent === filter ? 'true' : 'false');
160
+ continue;
161
+ }
162
+ findings.push({
163
+ code: 'DECISION_UNRESOLVED',
164
+ message: `State target filter ${filter ?? '<dynamic>'} is unsupported`,
165
+ candidateId: source.id,
166
+ });
167
+ stateChecks.push('unresolved');
168
+ }
169
+ return {
170
+ state: combine([
171
+ ...(candidateChecks.length === 0 ? [] : [combineAny(candidateChecks)]),
172
+ ...stateChecks,
173
+ ]),
174
+ unresolved: findings,
175
+ };
176
+ }
177
+ /** Evaluates only declared source and scenario facts; it does not execute a decision. */
178
+ export function evaluateDecisionGates(snapshot, id, scenario) {
179
+ const inventory = decisionSourceInventory(snapshot);
180
+ const source = inventory.decisions.find((candidate) => candidate.id === id);
181
+ if (source === undefined)
182
+ throw new Error(`Decision ${id} has no active source definition`);
183
+ const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
184
+ const guardedScenario = {
185
+ ...scenario,
186
+ closedFlags: scenario.closedFlags ?? false,
187
+ };
188
+ const actorOnlyScenario = {
189
+ ...guardedScenario,
190
+ scopes: Object.fromEntries(Object.entries(guardedScenario.scopes ?? {}).filter(([scope]) => scope !== 'FROM')),
191
+ };
192
+ const categories = inventory.categories.filter(({ id: category }) => category === source.category);
193
+ const categoryAllowed = evaluateCategoryField(categories, 'allowed', actorOnlyScenario, definitions, id);
194
+ const categoryVisible = evaluateCategoryField(categories, 'visible', guardedScenario, definitions, id);
195
+ const categoryAvailable = evaluateCategoryField(categories, 'available', guardedScenario, definitions, id);
196
+ const decisionAllowed = evaluateDecisionField(source, 'allowed', actorOnlyScenario, definitions);
197
+ const decisionAvailable = evaluateDecisionField(source, 'available', guardedScenario, definitions);
198
+ const decisionVisible = source.kind === 'mission'
199
+ ? null
200
+ : evaluateDecisionField(source, 'visible', guardedScenario, definitions);
201
+ const targetRoot = evaluateDecisionField(source, 'target_root_trigger', actorOnlyScenario, definitions);
202
+ const targetSelection = explicitTargets(source, guardedScenario);
203
+ const targetTrigger = evaluateDecisionField(source, 'target_trigger', guardedScenario, definitions);
204
+ const activation = source.kind === 'mission'
205
+ ? evaluateDecisionField(source, 'activation', guardedScenario, definitions)
206
+ : null;
207
+ const eligible = combine([
208
+ categoryAllowed.state,
209
+ decisionAllowed.state,
210
+ targetRoot.state,
211
+ targetSelection.state,
212
+ targetTrigger.state,
213
+ ...(activation === null ? [] : [activation.state]),
214
+ ]);
215
+ const visible = decisionVisible === null
216
+ ? null
217
+ : combine([eligible, categoryVisible.state, decisionVisible.state]);
218
+ const available = combine([eligible, categoryAvailable.state, decisionAvailable.state]);
219
+ const gates = [
220
+ categoryAllowed,
221
+ decisionAllowed,
222
+ categoryAvailable,
223
+ decisionAvailable,
224
+ targetRoot,
225
+ targetSelection,
226
+ targetTrigger,
227
+ ...(decisionVisible === null ? [] : [decisionVisible]),
228
+ ...(decisionVisible === null ? [] : [categoryVisible]),
229
+ ...(activation === null ? [] : [activation]),
230
+ ];
231
+ return {
232
+ sourceRevision: snapshot.revision,
233
+ id,
234
+ category: source.category,
235
+ kind: source.kind,
236
+ actor: scenario.actor ?? null,
237
+ target: scenario.scopes?.FROM === undefined
238
+ ? null
239
+ : (scenario.scopes.FROM.actor ?? scenario.scopes.FROM.id),
240
+ categoryAllowed,
241
+ decisionAllowed,
242
+ categoryVisible,
243
+ decisionVisible,
244
+ categoryAvailable,
245
+ decisionAvailable,
246
+ targetRoot,
247
+ targetSelection,
248
+ targetTrigger,
249
+ activation,
250
+ eligible,
251
+ visible,
252
+ available,
253
+ complete: inventory.complete && gates.every(({ state }) => state !== 'unresolved'),
254
+ unresolved: gates.flatMap(({ unresolved: findings }) => findings),
255
+ };
256
+ }
@@ -0,0 +1,114 @@
1
+ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
2
+ import { resolveConditionNumber } from '../core/condition-evaluator.js';
3
+ import { rootScopeContext } from '../core/scenario-state.js';
4
+ import { evaluateDecisionField } from './evaluation.js';
5
+ function scalar(assignment) {
6
+ return assignment?.value.type === 'scalar' ? assignment.value.value : undefined;
7
+ }
8
+ function boolean(value) {
9
+ if (value === 'yes')
10
+ return true;
11
+ if (value === 'no')
12
+ return false;
13
+ return null;
14
+ }
15
+ /** Static lifecycle paths and durations, with no claim that a campaign reached them. */
16
+ export function inspectDecisionLifecycle(snapshot, source, scenario) {
17
+ const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
18
+ const unresolved = [];
19
+ const duration = (field) => {
20
+ const entries = source.fields[field];
21
+ if (entries.length === 0)
22
+ return null;
23
+ if (entries.length > 1)
24
+ unresolved.push(`Multiple ${field} values in ${source.path}`);
25
+ const expression = scalar(entries[0]);
26
+ const days = expression === undefined
27
+ ? undefined
28
+ : resolveConditionNumber(expression, scenario, rootScopeContext(scenario), definitions, source.path);
29
+ if (days === undefined ||
30
+ !Number.isInteger(days) ||
31
+ (field === 'days_remove' ? days < -1 : days < 0))
32
+ unresolved.push(`${field} is not a supported day count`);
33
+ return { expression: expression ?? null, days: days ?? null };
34
+ };
35
+ const fireOnlyOnceExpression = scalar(source.fields.fire_only_once[0]);
36
+ const selectableExpression = scalar(source.fields.selectable_mission[0]);
37
+ const fireOnlyOnce = fireOnlyOnceExpression === undefined ? false : boolean(fireOnlyOnceExpression);
38
+ const selectableMission = selectableExpression === undefined ? false : boolean(selectableExpression);
39
+ if (source.fields.fire_only_once.length > 1 || (fireOnlyOnceExpression && fireOnlyOnce === null))
40
+ unresolved.push('fire_only_once is repeated or not a recognized Boolean');
41
+ if (source.fields.selectable_mission.length > 1 ||
42
+ (selectableExpression && selectableMission === null))
43
+ unresolved.push('selectable_mission is repeated or not a recognized Boolean');
44
+ const cooldown = duration('days_re_enable');
45
+ const removal = duration('days_remove');
46
+ const missionTimeout = duration('days_mission_timeout');
47
+ if (source.kind === 'mission' && missionTimeout === null)
48
+ unresolved.push('Mission classification has no timeout');
49
+ if (source.kind !== 'mission' && source.fields.timeout_effect.length > 0)
50
+ unresolved.push('A non-mission declares timeout_effect');
51
+ const guardedScenario = {
52
+ ...scenario,
53
+ closedFlags: scenario.closedFlags ?? false,
54
+ };
55
+ const removalTrigger = source.fields.remove_trigger.length === 0
56
+ ? null
57
+ : evaluateDecisionField(source, 'remove_trigger', guardedScenario, definitions);
58
+ const cancellationTrigger = source.fields.cancel_trigger.length === 0
59
+ ? null
60
+ : evaluateDecisionField(source, 'cancel_trigger', guardedScenario, definitions);
61
+ for (const result of [removalTrigger, cancellationTrigger])
62
+ if (result !== null)
63
+ unresolved.push(...result.unresolved.map(({ message }) => message));
64
+ const cancelIfNotVisibleExpression = scalar(source.fields.cancel_if_not_visible[0]);
65
+ const cancelIfNotVisible = cancelIfNotVisibleExpression === undefined ? false : boolean(cancelIfNotVisibleExpression);
66
+ if (source.fields.cancel_if_not_visible.length > 1 ||
67
+ (cancelIfNotVisibleExpression !== undefined && cancelIfNotVisible === null))
68
+ unresolved.push('cancel_if_not_visible is repeated or not a recognized Boolean');
69
+ if (cancelIfNotVisible && source.kind === 'mission')
70
+ unresolved.push('cancel_if_not_visible on a mission has unsupported visibility semantics');
71
+ const visible = cancelIfNotVisible && source.kind !== 'mission'
72
+ ? evaluateDecisionField(source, 'visible', guardedScenario, definitions)
73
+ : null;
74
+ if (visible !== null)
75
+ unresolved.push(...visible.unresolved.map(({ message }) => message));
76
+ const visibilityCancellation = visible === null
77
+ ? null
78
+ : visible.state === 'true'
79
+ ? 'false'
80
+ : visible.state === 'false'
81
+ ? 'true'
82
+ : 'unresolved';
83
+ const cancellationStates = [
84
+ cancellationTrigger?.state ?? 'false',
85
+ visibilityCancellation ?? 'false',
86
+ ];
87
+ const cancellationTriggerState = cancellationStates.includes('true')
88
+ ? 'true'
89
+ : cancellationStates.includes('unresolved')
90
+ ? 'unresolved'
91
+ : 'false';
92
+ return {
93
+ kind: source.kind,
94
+ fireOnlyOnce,
95
+ selectableMission: source.kind === 'mission' ? selectableMission : null,
96
+ cooldown,
97
+ removal,
98
+ missionTimeout,
99
+ hasActivation: source.fields.activation.length > 0,
100
+ hasCompletionEffect: source.fields.complete_effect.length > 0,
101
+ hasRemovalTrigger: source.fields.remove_trigger.length > 0,
102
+ hasRemovalEffect: source.fields.remove_effect.length > 0,
103
+ hasCancellationTrigger: source.fields.cancel_trigger.length > 0,
104
+ hasCancellationEffect: source.fields.cancel_effect.length > 0,
105
+ hasTimeoutEffect: source.fields.timeout_effect.length > 0,
106
+ ignoredMissionVisible: source.kind === 'mission' && source.fields.visible.length > 0,
107
+ removalTrigger,
108
+ cancellationTrigger,
109
+ cancelIfNotVisible,
110
+ visibilityCancellation,
111
+ cancellationTriggerState,
112
+ unresolved,
113
+ };
114
+ }