hoi4-agent-tools 3.0.9 → 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.
- package/CHANGELOG.md +32 -1
- package/README.md +10 -2
- package/dist/bin/stdio.js +23 -12
- package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
- package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
- package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
- package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
- package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
- package/dist/hoi4_agent_tools/core/engine.js +48 -5
- package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
- package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
- package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
- package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
- package/dist/hoi4_agent_tools/core/index.js +47 -4
- package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
- package/dist/hoi4_agent_tools/core/job-service.js +112 -0
- package/dist/hoi4_agent_tools/core/job-store.js +641 -0
- package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
- package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
- package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
- package/dist/hoi4_agent_tools/core/result.js +1 -0
- package/dist/hoi4_agent_tools/core/scanner.js +15 -75
- package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
- package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
- package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
- package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
- package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
- package/dist/hoi4_agent_tools/core/transactions.js +80 -27
- package/dist/hoi4_agent_tools/decision/ai.js +104 -0
- package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
- package/dist/hoi4_agent_tools/decision/cost.js +177 -0
- package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
- package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
- package/dist/hoi4_agent_tools/decision/service.js +172 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
- package/dist/hoi4_agent_tools/event/graph.js +214 -290
- package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
- package/dist/hoi4_agent_tools/event/operations.js +179 -0
- package/dist/hoi4_agent_tools/event/service.js +67 -15
- package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
- package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
- package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
- package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
- package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
- package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
- package/dist/hoi4_agent_tools/impact/service.js +127 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
- package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
- package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
- package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
- package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
- package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
- package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
- package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
- package/dist/hoi4_agent_tools/probability/operations.js +183 -0
- package/dist/hoi4_agent_tools/probability/service.js +24 -3
- package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
- package/dist/hoi4_agent_tools/schemas/common.js +2 -0
- package/dist/hoi4_agent_tools/schemas/event.js +72 -1
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
- package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
- package/dist/hoi4_agent_tools/technology/graph.js +83 -78
- package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
- package/dist/hoi4_agent_tools/technology/operations.js +125 -0
- package/dist/hoi4_agent_tools/technology/service.js +89 -15
- package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/analysis.md +100 -0
- package/docs/development.md +2 -0
- package/docs/events.md +7 -2
- package/docs/helper-expansion.md +78 -0
- package/docs/http.md +2 -0
- package/docs/jobs.md +86 -0
- package/docs/technology.md +6 -2
- package/package.json +15 -6
- 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/decision-inspect-input.schema.json +290 -0
- 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/helper-expansion-request.schema.json +44 -0
- package/schemas/helper-expansion-summary.schema.json +70 -0
- package/schemas/impact-inspect-input.schema.json +203 -0
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- 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 +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { analysisSourceEvidence } from '../core/analysis-evidence.js';
|
|
2
|
+
import { publicArtifactLink } from '../core/artifacts.js';
|
|
3
|
+
import { canonicalJson, hashCanonical } from '../core/canonical.js';
|
|
4
|
+
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
5
|
+
import { emptyServiceResult, ServiceError } from '../core/result.js';
|
|
6
|
+
import { buildProposedSourceOverlay } from '../core/source-overlay.js';
|
|
7
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
8
|
+
import { inspectDecisionAi } from './ai.js';
|
|
9
|
+
import { compareDecisionScenarios, inspectDecisionScenario } from './comparison.js';
|
|
10
|
+
import { decisionSourceInventory, decisionTargetCatalog } from './source-inventory.js';
|
|
11
|
+
/** Authorized, read-only decision operation over the shared source scanner. */
|
|
12
|
+
export class DecisionAnalyzer {
|
|
13
|
+
engine;
|
|
14
|
+
constructor(engine) {
|
|
15
|
+
this.engine = engine;
|
|
16
|
+
}
|
|
17
|
+
async inspect(input) {
|
|
18
|
+
const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
|
|
19
|
+
if (input.refresh)
|
|
20
|
+
this.engine.invalidate(input.workspaceId);
|
|
21
|
+
const before = await this.engine.scan(input.workspaceId, {}, input.principal, input.signal);
|
|
22
|
+
if (input.expectedRevision !== undefined && before.revision !== input.expectedRevision)
|
|
23
|
+
throw new ServiceError('DECISION_SOURCE_STALE', 'Decision source revision changed since selection', {
|
|
24
|
+
expectedRevision: input.expectedRevision,
|
|
25
|
+
observedRevision: before.revision,
|
|
26
|
+
});
|
|
27
|
+
if (input.mode !== 'inventory' && input.id === undefined)
|
|
28
|
+
throw new ServiceError('DECISION_ID_REQUIRED', 'Decision analysis requires an identifier');
|
|
29
|
+
if (input.mode !== 'inventory' && input.scenarios.length === 0)
|
|
30
|
+
throw new ServiceError('DECISION_SCENARIOS_REQUIRED', 'Decision analysis requires scenarios');
|
|
31
|
+
if (input.mode === 'compare' && input.proposedSources === undefined)
|
|
32
|
+
throw new ServiceError('DECISION_PROPOSAL_REQUIRED', 'Decision comparison requires proposed sources');
|
|
33
|
+
if (input.mode !== 'compare' && input.proposedSources !== undefined)
|
|
34
|
+
throw new ServiceError('DECISION_PROPOSAL_MODE', 'Proposed sources require compare mode');
|
|
35
|
+
const inventory = decisionSourceInventory(before);
|
|
36
|
+
const overlay = input.proposedSources === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: buildProposedSourceOverlay(before, workspace, input.proposedSources);
|
|
39
|
+
const scenarios = input.scenarios.map((scenario) => ({
|
|
40
|
+
id: scenario.id,
|
|
41
|
+
state: scenario.state,
|
|
42
|
+
...(scenario.actor === undefined ? {} : { actor: scenario.actor }),
|
|
43
|
+
...(scenario.date === undefined ? {} : { date: scenario.date }),
|
|
44
|
+
...(scenario.flags === undefined ? {} : { flags: scenario.flags }),
|
|
45
|
+
...(scenario.eventTargets === undefined ? {} : { eventTargets: scenario.eventTargets }),
|
|
46
|
+
...(scenario.candidateOverrides === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { candidateOverrides: scenario.candidateOverrides }),
|
|
49
|
+
...(scenario.closedFlags === undefined ? {} : { closedFlags: scenario.closedFlags }),
|
|
50
|
+
...(scenario.scopes === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: {
|
|
53
|
+
scopes: Object.fromEntries(Object.entries(scenario.scopes).map(([key, binding]) => [
|
|
54
|
+
key,
|
|
55
|
+
{
|
|
56
|
+
id: binding.id,
|
|
57
|
+
state: binding.state,
|
|
58
|
+
...(binding.type === undefined ? {} : { type: binding.type }),
|
|
59
|
+
...(binding.actor === undefined ? {} : { actor: binding.actor }),
|
|
60
|
+
...(binding.flags === undefined ? {} : { flags: binding.flags }),
|
|
61
|
+
...(binding.eventTargets === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: { eventTargets: binding.eventTargets }),
|
|
64
|
+
...(binding.weight === undefined ? {} : { weight: binding.weight }),
|
|
65
|
+
},
|
|
66
|
+
])),
|
|
67
|
+
}),
|
|
68
|
+
}));
|
|
69
|
+
const analysis = input.mode === 'inventory'
|
|
70
|
+
? {
|
|
71
|
+
decisions: inventory.decisions.map((source) => ({
|
|
72
|
+
id: source.id,
|
|
73
|
+
category: source.category,
|
|
74
|
+
kind: source.kind,
|
|
75
|
+
path: source.path,
|
|
76
|
+
rootKind: source.rootKind,
|
|
77
|
+
loadOrder: source.loadOrder,
|
|
78
|
+
...(source.location === undefined ? {} : { location: source.location }),
|
|
79
|
+
targeting: decisionTargetCatalog(source),
|
|
80
|
+
fields: Object.fromEntries(Object.entries(source.fields)
|
|
81
|
+
.filter(([, occurrences]) => occurrences.length > 0)
|
|
82
|
+
.map(([field, occurrences]) => [field, occurrences.length])),
|
|
83
|
+
})),
|
|
84
|
+
categories: inventory.categories.map(({ id, path, rootKind, loadOrder, location }) => ({
|
|
85
|
+
id,
|
|
86
|
+
path,
|
|
87
|
+
rootKind,
|
|
88
|
+
loadOrder,
|
|
89
|
+
...(location === undefined ? {} : { location }),
|
|
90
|
+
})),
|
|
91
|
+
overrides: inventory.overrides,
|
|
92
|
+
unresolvedDefinitions: inventory.unresolvedDefinitions,
|
|
93
|
+
}
|
|
94
|
+
: input.mode === 'compare' && overlay !== undefined
|
|
95
|
+
? compareDecisionScenarios(before, overlay.snapshot, input.id, scenarios)
|
|
96
|
+
: scenarios.map((scenario) => ({
|
|
97
|
+
scenarioId: scenario.id,
|
|
98
|
+
inspection: inspectDecisionScenario(before, input.id, scenario),
|
|
99
|
+
}));
|
|
100
|
+
const ai = input.mode === 'inventory'
|
|
101
|
+
? undefined
|
|
102
|
+
: await inspectDecisionAi(this.engine, before, input.id, scenarios, overlay, input.principal, input.signal);
|
|
103
|
+
const sourceComplete = input.mode === 'inventory'
|
|
104
|
+
? inventory.complete
|
|
105
|
+
: input.mode === 'compare'
|
|
106
|
+
? 'complete' in analysis && analysis.complete
|
|
107
|
+
: inventory.complete &&
|
|
108
|
+
Array.isArray(analysis) &&
|
|
109
|
+
analysis.every(({ inspection }) => inspection !== null &&
|
|
110
|
+
inspection.gates.complete &&
|
|
111
|
+
inspection.cost.unresolved.length === 0 &&
|
|
112
|
+
inspection.lifecycle.unresolved.length === 0);
|
|
113
|
+
const complete = sourceComplete &&
|
|
114
|
+
(ai === undefined ||
|
|
115
|
+
('status' in ai &&
|
|
116
|
+
ai.status === 'complete' &&
|
|
117
|
+
!('proposedUnresolved' in ai) &&
|
|
118
|
+
!('baselineUnresolved' in ai)));
|
|
119
|
+
const report = {
|
|
120
|
+
schemaVersion: 'decision-analysis.v1',
|
|
121
|
+
mode: input.mode,
|
|
122
|
+
sourceRevision: before.revision,
|
|
123
|
+
...(overlay === undefined ? {} : { proposedRevision: overlay.snapshot.revision }),
|
|
124
|
+
...(input.id === undefined ? {} : { id: input.id }),
|
|
125
|
+
...(overlay === undefined ? {} : { proposedChanges: overlay.changes }),
|
|
126
|
+
analysis,
|
|
127
|
+
...(ai === undefined ? {} : { ai }),
|
|
128
|
+
};
|
|
129
|
+
const reportHash = hashCanonical(report);
|
|
130
|
+
const provenanceSources = analysisSourceEvidence(before, overlay?.snapshot);
|
|
131
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `decision-${reportHash.slice(0, 24)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
132
|
+
kind: 'decision-analysis',
|
|
133
|
+
toolVersion: PACKAGE_VERSION,
|
|
134
|
+
schemaVersion: 'decision-analysis.v1',
|
|
135
|
+
sourceHashes: provenanceSources.sourceHashes,
|
|
136
|
+
metadata: {
|
|
137
|
+
beforeRevision: before.revision,
|
|
138
|
+
...(overlay === undefined ? {} : { afterRevision: overlay.snapshot.revision }),
|
|
139
|
+
sourceInventory: provenanceSources.inventory,
|
|
140
|
+
},
|
|
141
|
+
}, 'Revision-pinned decision inventory or declared-scenario evidence', input.signal);
|
|
142
|
+
const result = emptyServiceResult(input.workspaceId, {
|
|
143
|
+
mode: input.mode,
|
|
144
|
+
sourceRevision: before.revision,
|
|
145
|
+
...(overlay === undefined ? {} : { proposedRevision: overlay.snapshot.revision }),
|
|
146
|
+
reportHash,
|
|
147
|
+
complete,
|
|
148
|
+
decisions: inventory.decisions.length,
|
|
149
|
+
categories: inventory.categories.length,
|
|
150
|
+
overrides: inventory.overrides.length,
|
|
151
|
+
scenarios: input.mode === 'inventory' ? 0 : input.scenarios.length,
|
|
152
|
+
unresolvedDefinitions: inventory.unresolvedDefinitions.length,
|
|
153
|
+
});
|
|
154
|
+
result.code = complete ? 'DECISION_ANALYZED' : 'DECISION_ANALYZED_PARTIAL';
|
|
155
|
+
result.artifacts = [publicArtifactLink(artifact)];
|
|
156
|
+
result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
|
|
157
|
+
setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
|
|
158
|
+
result.validation = {
|
|
159
|
+
passed: complete,
|
|
160
|
+
checks: [
|
|
161
|
+
{
|
|
162
|
+
id: 'decision-coverage',
|
|
163
|
+
passed: complete,
|
|
164
|
+
message: complete
|
|
165
|
+
? 'Decision evidence is complete under the declared scenarios'
|
|
166
|
+
: 'Decision evidence contains a missing definition, unbound scenario input, or unsupported source path; inspect the linked report',
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
}
|