hoi4-agent-tools 2.1.1 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +32 -18
- package/dist/bin/stdio.js +2 -1
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +14 -0
- package/dist/hoi4_agent_tools/core/index.js +349 -2
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/limits.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +37 -13
- package/dist/hoi4_agent_tools/mcp/server/create.js +5 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +39 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +368 -0
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +77 -0
- package/dist/hoi4_agent_tools/technology/compare.js +240 -0
- package/dist/hoi4_agent_tools/technology/graph.js +1196 -0
- package/dist/hoi4_agent_tools/technology/index.js +7 -0
- package/dist/hoi4_agent_tools/technology/model.js +2 -0
- package/dist/hoi4_agent_tools/technology/queries.js +300 -0
- package/dist/hoi4_agent_tools/technology/render.js +641 -0
- package/dist/hoi4_agent_tools/technology/service.js +686 -0
- package/dist/hoi4_agent_tools/technology/source-analysis.js +967 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +2 -0
- package/docs/development.md +7 -1
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +1 -1
- package/docs/technology.md +90 -0
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +26 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { PROBABILITY_ADAPTER_VERSION, VERIFIED_GAME_VERSION, } from './model.js';
|
|
2
|
+
const evidenceRoot = 'docs/research/probability-adapter-evidence.md';
|
|
3
|
+
const sources = {
|
|
4
|
+
event_mean_time_to_happen: {
|
|
5
|
+
id: 'event_mean_time_to_happen',
|
|
6
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
7
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
8
|
+
selectionRule: 'median_daily_hazard',
|
|
9
|
+
capabilities: {
|
|
10
|
+
eligibility: true,
|
|
11
|
+
rawScore: true,
|
|
12
|
+
normalizedProbability: false,
|
|
13
|
+
timeDistribution: true,
|
|
14
|
+
sequence: false,
|
|
15
|
+
},
|
|
16
|
+
completePoolRequired: false,
|
|
17
|
+
confidence: 'documented_with_runtime_boundary',
|
|
18
|
+
sourceBlockTypes: ['event mean_time_to_happen'],
|
|
19
|
+
evaluationCadence: 'daily while active; inactive triggers are polled every 20 days',
|
|
20
|
+
scopeExpectations: ['event root scope declared by the scenario'],
|
|
21
|
+
supportedExpressions: [
|
|
22
|
+
'base/add/factor',
|
|
23
|
+
'days/months/years',
|
|
24
|
+
'modifiers',
|
|
25
|
+
'script constants',
|
|
26
|
+
'MTTH variables',
|
|
27
|
+
],
|
|
28
|
+
evidence: [evidenceRoot],
|
|
29
|
+
limitations: [
|
|
30
|
+
'The adapter models the verified daily median-hazard check only while the event trigger remains true.',
|
|
31
|
+
'The inactive-trigger 20-day polling cadence is reported separately and is not invented from missing scheduled state.',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
event_option_ai_chance: {
|
|
35
|
+
id: 'event_option_ai_chance',
|
|
36
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
37
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
38
|
+
selectionRule: 'proportional_categorical',
|
|
39
|
+
capabilities: {
|
|
40
|
+
eligibility: true,
|
|
41
|
+
rawScore: true,
|
|
42
|
+
normalizedProbability: true,
|
|
43
|
+
timeDistribution: false,
|
|
44
|
+
sequence: false,
|
|
45
|
+
},
|
|
46
|
+
completePoolRequired: true,
|
|
47
|
+
confidence: 'documented',
|
|
48
|
+
sourceBlockTypes: ['event option ai_chance'],
|
|
49
|
+
evaluationCadence: 'when the event option pool is selected',
|
|
50
|
+
scopeExpectations: ['event option root scope declared by the scenario'],
|
|
51
|
+
supportedExpressions: ['base/add/factor', 'modifiers', 'script constants', 'scripted triggers'],
|
|
52
|
+
evidence: [evidenceRoot],
|
|
53
|
+
limitations: [
|
|
54
|
+
'Runtime selection uses the engine d100 path; sub-one-percent outcomes are flagged.',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
decision_ai_will_do: {
|
|
58
|
+
id: 'decision_ai_will_do',
|
|
59
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
60
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
61
|
+
selectionRule: 'score_only',
|
|
62
|
+
capabilities: {
|
|
63
|
+
eligibility: true,
|
|
64
|
+
rawScore: true,
|
|
65
|
+
normalizedProbability: false,
|
|
66
|
+
timeDistribution: false,
|
|
67
|
+
sequence: false,
|
|
68
|
+
},
|
|
69
|
+
completePoolRequired: false,
|
|
70
|
+
confidence: 'documented_with_runtime_boundary',
|
|
71
|
+
sourceBlockTypes: ['decision ai_will_do'],
|
|
72
|
+
evaluationCadence: 'decision AI evaluation cadence; selection probability is not modeled',
|
|
73
|
+
scopeExpectations: ['country and target scopes declared by the scenario'],
|
|
74
|
+
supportedExpressions: ['base/add/factor', 'modifiers', 'script constants', 'scripted triggers'],
|
|
75
|
+
evidence: [evidenceRoot],
|
|
76
|
+
limitations: [
|
|
77
|
+
'Decision scores are not normalized without a documented complete selection rule.',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
mission_ai_will_do: {
|
|
81
|
+
id: 'mission_ai_will_do',
|
|
82
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
83
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
84
|
+
selectionRule: 'score_only',
|
|
85
|
+
capabilities: {
|
|
86
|
+
eligibility: true,
|
|
87
|
+
rawScore: true,
|
|
88
|
+
normalizedProbability: false,
|
|
89
|
+
timeDistribution: false,
|
|
90
|
+
sequence: false,
|
|
91
|
+
},
|
|
92
|
+
completePoolRequired: false,
|
|
93
|
+
confidence: 'documented_with_runtime_boundary',
|
|
94
|
+
sourceBlockTypes: ['mission ai_will_do'],
|
|
95
|
+
evaluationCadence: 'mission AI evaluation cadence; selection probability is not modeled',
|
|
96
|
+
scopeExpectations: ['country and target scopes declared by the scenario'],
|
|
97
|
+
supportedExpressions: ['base/add/factor', 'modifiers', 'script constants', 'scripted triggers'],
|
|
98
|
+
evidence: [evidenceRoot],
|
|
99
|
+
limitations: [
|
|
100
|
+
'Mission scores are not normalized without a documented complete selection rule.',
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
national_focus_ai_will_do: {
|
|
104
|
+
id: 'national_focus_ai_will_do',
|
|
105
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
106
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
107
|
+
selectionRule: 'uniform_score_race',
|
|
108
|
+
capabilities: {
|
|
109
|
+
eligibility: true,
|
|
110
|
+
rawScore: true,
|
|
111
|
+
normalizedProbability: true,
|
|
112
|
+
timeDistribution: false,
|
|
113
|
+
sequence: false,
|
|
114
|
+
},
|
|
115
|
+
completePoolRequired: true,
|
|
116
|
+
confidence: 'documented_with_runtime_boundary',
|
|
117
|
+
sourceBlockTypes: ['national focus ai_will_do'],
|
|
118
|
+
evaluationCadence: 'next-focus selection',
|
|
119
|
+
scopeExpectations: ['country scope and complete available-focus pool declared by the scenario'],
|
|
120
|
+
supportedExpressions: [
|
|
121
|
+
'base/add/factor',
|
|
122
|
+
'modifiers',
|
|
123
|
+
'script constants',
|
|
124
|
+
'scripted triggers',
|
|
125
|
+
'declared focus strategy factors',
|
|
126
|
+
],
|
|
127
|
+
evidence: [evidenceRoot],
|
|
128
|
+
limitations: [
|
|
129
|
+
'Ordered AI strategy plans can override weighted focus selection.',
|
|
130
|
+
'The prerequisite-completion multiplier and external focus factors require declared scenario inputs.',
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
technology_ai_will_do: {
|
|
134
|
+
id: 'technology_ai_will_do',
|
|
135
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
136
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
137
|
+
selectionRule: 'uniform_score_race',
|
|
138
|
+
capabilities: {
|
|
139
|
+
eligibility: true,
|
|
140
|
+
rawScore: true,
|
|
141
|
+
normalizedProbability: true,
|
|
142
|
+
timeDistribution: false,
|
|
143
|
+
sequence: false,
|
|
144
|
+
},
|
|
145
|
+
completePoolRequired: true,
|
|
146
|
+
confidence: 'documented_with_runtime_boundary',
|
|
147
|
+
sourceBlockTypes: ['technology ai_will_do'],
|
|
148
|
+
evaluationCadence: 'research-slot technology selection',
|
|
149
|
+
scopeExpectations: [
|
|
150
|
+
'country scope and complete eligible technology pool declared by the scenario',
|
|
151
|
+
],
|
|
152
|
+
supportedExpressions: [
|
|
153
|
+
'base/add/factor',
|
|
154
|
+
'modifiers',
|
|
155
|
+
'script constants',
|
|
156
|
+
'scripted triggers',
|
|
157
|
+
'declared research strategy factors',
|
|
158
|
+
],
|
|
159
|
+
evidence: [evidenceRoot],
|
|
160
|
+
limitations: [
|
|
161
|
+
'Exact selection probability requires the complete eligible technology pool and every external research factor.',
|
|
162
|
+
'Research duration is distinct from selection score.',
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
doctrine_ai_will_do: {
|
|
166
|
+
id: 'doctrine_ai_will_do',
|
|
167
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
168
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
169
|
+
selectionRule: 'uniform_score_race',
|
|
170
|
+
capabilities: {
|
|
171
|
+
eligibility: true,
|
|
172
|
+
rawScore: true,
|
|
173
|
+
normalizedProbability: true,
|
|
174
|
+
timeDistribution: false,
|
|
175
|
+
sequence: false,
|
|
176
|
+
},
|
|
177
|
+
completePoolRequired: true,
|
|
178
|
+
confidence: 'documented_with_runtime_boundary',
|
|
179
|
+
sourceBlockTypes: ['doctrine ai_will_do'],
|
|
180
|
+
evaluationCadence: 'research-slot doctrine selection',
|
|
181
|
+
scopeExpectations: [
|
|
182
|
+
'country scope and complete eligible doctrine pool declared by the scenario',
|
|
183
|
+
],
|
|
184
|
+
supportedExpressions: [
|
|
185
|
+
'base/add/factor',
|
|
186
|
+
'modifiers',
|
|
187
|
+
'script constants',
|
|
188
|
+
'scripted triggers',
|
|
189
|
+
'declared research strategy factors',
|
|
190
|
+
],
|
|
191
|
+
evidence: [evidenceRoot],
|
|
192
|
+
limitations: [
|
|
193
|
+
'Exact probability requires a declared complete eligible doctrine candidate pool.',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
direct_random: {
|
|
197
|
+
id: 'direct_random',
|
|
198
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
199
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
200
|
+
selectionRule: 'independent_chance',
|
|
201
|
+
capabilities: {
|
|
202
|
+
eligibility: false,
|
|
203
|
+
rawScore: true,
|
|
204
|
+
normalizedProbability: true,
|
|
205
|
+
timeDistribution: false,
|
|
206
|
+
sequence: false,
|
|
207
|
+
},
|
|
208
|
+
completePoolRequired: false,
|
|
209
|
+
confidence: 'documented',
|
|
210
|
+
sourceBlockTypes: ['random chance'],
|
|
211
|
+
evaluationCadence: 'when the enclosing random effect executes',
|
|
212
|
+
scopeExpectations: [
|
|
213
|
+
'enclosing effect scope declared by the scenario when conditions require it',
|
|
214
|
+
],
|
|
215
|
+
supportedExpressions: ['numeric chance', 'script constants', 'declared variables'],
|
|
216
|
+
evidence: [evidenceRoot],
|
|
217
|
+
limitations: [
|
|
218
|
+
'The chance is an independent percentage and is never normalized against nearby blocks.',
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
random_list: {
|
|
222
|
+
id: 'random_list',
|
|
223
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
224
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
225
|
+
selectionRule: 'proportional_categorical',
|
|
226
|
+
capabilities: {
|
|
227
|
+
eligibility: true,
|
|
228
|
+
rawScore: true,
|
|
229
|
+
normalizedProbability: true,
|
|
230
|
+
timeDistribution: false,
|
|
231
|
+
sequence: false,
|
|
232
|
+
},
|
|
233
|
+
completePoolRequired: true,
|
|
234
|
+
confidence: 'documented',
|
|
235
|
+
sourceBlockTypes: ['random_list entry'],
|
|
236
|
+
evaluationCadence: 'when the enclosing random_list executes',
|
|
237
|
+
scopeExpectations: ['enclosing effect scope declared by the scenario'],
|
|
238
|
+
supportedExpressions: [
|
|
239
|
+
'entry weights',
|
|
240
|
+
'weight modifiers',
|
|
241
|
+
'script constants',
|
|
242
|
+
'scripted triggers',
|
|
243
|
+
],
|
|
244
|
+
evidence: [evidenceRoot],
|
|
245
|
+
limitations: [
|
|
246
|
+
'Dynamic entry keys and effect-derived weights remain unresolved unless declared.',
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
ai_strategy_factor: {
|
|
250
|
+
id: 'ai_strategy_factor',
|
|
251
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
252
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
253
|
+
selectionRule: 'score_only',
|
|
254
|
+
capabilities: {
|
|
255
|
+
eligibility: true,
|
|
256
|
+
rawScore: true,
|
|
257
|
+
normalizedProbability: false,
|
|
258
|
+
timeDistribution: false,
|
|
259
|
+
sequence: false,
|
|
260
|
+
},
|
|
261
|
+
completePoolRequired: false,
|
|
262
|
+
confidence: 'documented_with_runtime_boundary',
|
|
263
|
+
sourceBlockTypes: ['ai_strategy factor'],
|
|
264
|
+
evaluationCadence: 'when the downstream AI selection surface reads the strategy',
|
|
265
|
+
scopeExpectations: ['strategy target and owning country declared by the source or scenario'],
|
|
266
|
+
supportedExpressions: ['numeric strategy value', 'script constants', 'declared variables'],
|
|
267
|
+
evidence: [evidenceRoot],
|
|
268
|
+
limitations: [
|
|
269
|
+
'Supported strategy factors modify a target score; a strategy definition is not itself a categorical pool.',
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
custom_weighted_pool: {
|
|
273
|
+
id: 'custom_weighted_pool',
|
|
274
|
+
version: PROBABILITY_ADAPTER_VERSION,
|
|
275
|
+
gameVersion: VERIFIED_GAME_VERSION,
|
|
276
|
+
selectionRule: 'custom_declared',
|
|
277
|
+
capabilities: {
|
|
278
|
+
eligibility: true,
|
|
279
|
+
rawScore: true,
|
|
280
|
+
normalizedProbability: true,
|
|
281
|
+
timeDistribution: true,
|
|
282
|
+
sequence: true,
|
|
283
|
+
},
|
|
284
|
+
completePoolRequired: true,
|
|
285
|
+
confidence: 'manifest_defined',
|
|
286
|
+
sourceBlockTypes: ['custom weighted-pool manifest'],
|
|
287
|
+
evaluationCadence: 'manifest-declared cadence',
|
|
288
|
+
scopeExpectations: ['manifest state only'],
|
|
289
|
+
supportedExpressions: ['declared arithmetic', 'declared conditions', 'declared transitions'],
|
|
290
|
+
evidence: [evidenceRoot],
|
|
291
|
+
limitations: [
|
|
292
|
+
'Only state and transitions explicitly declared by the manifest are modeled.',
|
|
293
|
+
'No gameplay effects are executed or inferred.',
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
function normalizationRule(selectionRule) {
|
|
298
|
+
switch (selectionRule) {
|
|
299
|
+
case 'proportional_categorical':
|
|
300
|
+
return [
|
|
301
|
+
'Normalize non-negative eligible values only when the local candidate pool is complete.',
|
|
302
|
+
];
|
|
303
|
+
case 'uniform_score_race':
|
|
304
|
+
return ['Use the independent-uniform maximum-score race only for a complete eligible pool.'];
|
|
305
|
+
case 'independent_chance':
|
|
306
|
+
return ['Clamp the resolved percentage to [0,100] and never normalize against neighbors.'];
|
|
307
|
+
case 'median_daily_hazard':
|
|
308
|
+
return ['Do not normalize; convert timing only through the verified median-hazard model.'];
|
|
309
|
+
case 'score_only':
|
|
310
|
+
return ['Return scores and ranks without inventing a categorical denominator.'];
|
|
311
|
+
case 'custom_declared':
|
|
312
|
+
return ['Use only the normalization event and selection mode declared by the manifest.'];
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const descriptors = Object.fromEntries(Object.entries(sources).map(([id, source]) => [
|
|
316
|
+
id,
|
|
317
|
+
{
|
|
318
|
+
...source,
|
|
319
|
+
supportedGameVersions: [VERIFIED_GAME_VERSION],
|
|
320
|
+
candidateDiscoveryRules: source.sourceBlockTypes.map((blockType) => `Discover active, unshadowed ${blockType} definitions through the shared index.`),
|
|
321
|
+
eligibilityRules: source.capabilities.eligibility
|
|
322
|
+
? ['Evaluate source eligibility and declared candidate overrides with three-valued logic.']
|
|
323
|
+
: ['The surface is independently evaluated and has no candidate-pool eligibility step.'],
|
|
324
|
+
modifierOrder: [
|
|
325
|
+
'Evaluate the base expression, then source add/factor operations and modifiers in source order.',
|
|
326
|
+
'Apply declared external factors only after source-local operations.',
|
|
327
|
+
],
|
|
328
|
+
poolNormalizationRules: normalizationRule(source.selectionRule),
|
|
329
|
+
timingConversion: source.selectionRule === 'median_daily_hazard'
|
|
330
|
+
? [
|
|
331
|
+
'Treat the effective duration as the median of verified daily checks while active.',
|
|
332
|
+
'Bound activation through the verified 20-day inactive polling phase when its phase is undeclared.',
|
|
333
|
+
]
|
|
334
|
+
: ['No time-distribution conversion is claimed for this adapter.'],
|
|
335
|
+
unsupportedConstructs: [...source.limitations],
|
|
336
|
+
testFixtures: ['fixtures/probability', evidenceRoot],
|
|
337
|
+
},
|
|
338
|
+
]));
|
|
339
|
+
export function probabilityAdapter(id) {
|
|
340
|
+
return descriptors[id];
|
|
341
|
+
}
|
|
342
|
+
export function probabilityAdapters() {
|
|
343
|
+
return Object.values(descriptors);
|
|
344
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
function raw(candidate) {
|
|
2
|
+
const value = candidate?.rawValue;
|
|
3
|
+
if (typeof value === 'object' && value !== null)
|
|
4
|
+
return value.value;
|
|
5
|
+
if (typeof value === 'number')
|
|
6
|
+
return value;
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
function traceSignature(candidate) {
|
|
10
|
+
return new Set((candidate?.trace ?? []).map(({ operation, expression, applied, after }) => `${operation}:${expression}:${applied}:${after?.decimal ?? ''}`));
|
|
11
|
+
}
|
|
12
|
+
function attribution(before, after) {
|
|
13
|
+
if (before === undefined)
|
|
14
|
+
return ['candidate added'];
|
|
15
|
+
if (after === undefined)
|
|
16
|
+
return ['candidate removed'];
|
|
17
|
+
const beforeTrace = traceSignature(before);
|
|
18
|
+
const afterTrace = traceSignature(after);
|
|
19
|
+
const changes = [
|
|
20
|
+
...[...afterTrace].filter((item) => !beforeTrace.has(item)).map((item) => `added ${item}`),
|
|
21
|
+
...[...beforeTrace].filter((item) => !afterTrace.has(item)).map((item) => `removed ${item}`),
|
|
22
|
+
];
|
|
23
|
+
if (before.eligibility !== after.eligibility)
|
|
24
|
+
changes.unshift(`eligibility ${before.eligibility} -> ${after.eligibility}`);
|
|
25
|
+
return changes.length > 0 ? changes : ['source or scenario metadata changed'];
|
|
26
|
+
}
|
|
27
|
+
function changedAstPaths(before, after) {
|
|
28
|
+
const terms = (candidate) => new Map((candidate?.trace ?? []).flatMap((step) => {
|
|
29
|
+
const provenance = step.provenance;
|
|
30
|
+
if (provenance?.astPath === undefined)
|
|
31
|
+
return [];
|
|
32
|
+
const key = JSON.stringify({
|
|
33
|
+
operation: step.operation,
|
|
34
|
+
expression: step.expression,
|
|
35
|
+
applied: step.applied,
|
|
36
|
+
after: step.after?.decimal,
|
|
37
|
+
path: provenance.path,
|
|
38
|
+
astPath: provenance.astPath,
|
|
39
|
+
});
|
|
40
|
+
return [[key, { path: provenance.path, astPath: provenance.astPath }]];
|
|
41
|
+
}));
|
|
42
|
+
const previous = terms(before);
|
|
43
|
+
const current = terms(after);
|
|
44
|
+
return [
|
|
45
|
+
...[...current]
|
|
46
|
+
.filter(([key]) => !previous.has(key))
|
|
47
|
+
.map(([, source]) => ({ change: 'added', ...source })),
|
|
48
|
+
...[...previous]
|
|
49
|
+
.filter(([key]) => !current.has(key))
|
|
50
|
+
.map(([, source]) => ({ change: 'removed', ...source })),
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
function metricValue(candidate, metric) {
|
|
54
|
+
if (candidate === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (metric === 'raw_value')
|
|
57
|
+
return raw(candidate);
|
|
58
|
+
if (metric === 'conditional_probability')
|
|
59
|
+
return candidate.conditionalProbability ?? undefined;
|
|
60
|
+
if (metric === 'cumulative_chance')
|
|
61
|
+
return candidate.cumulativeChance;
|
|
62
|
+
return candidate.effectiveMtthDays;
|
|
63
|
+
}
|
|
64
|
+
function insideBand(value, band) {
|
|
65
|
+
return (value !== undefined &&
|
|
66
|
+
(band.min === undefined || value >= band.min) &&
|
|
67
|
+
(band.max === undefined || value <= band.max));
|
|
68
|
+
}
|
|
69
|
+
export function compareProbabilityResults(before, after, acceptanceBands = []) {
|
|
70
|
+
const scenarioChanges = [];
|
|
71
|
+
const regressions = [];
|
|
72
|
+
const scenarioIds = new Set([
|
|
73
|
+
...before.scenarios.map(({ id }) => id),
|
|
74
|
+
...after.scenarios.map(({ id }) => id),
|
|
75
|
+
]);
|
|
76
|
+
for (const scenarioId of scenarioIds) {
|
|
77
|
+
const beforeScenario = before.scenarios.find(({ id }) => id === scenarioId);
|
|
78
|
+
const afterScenario = after.scenarios.find(({ id }) => id === scenarioId);
|
|
79
|
+
const candidateIds = new Set([
|
|
80
|
+
...(beforeScenario?.candidates.map(({ id }) => id) ?? []),
|
|
81
|
+
...(afterScenario?.candidates.map(({ id }) => id) ?? []),
|
|
82
|
+
]);
|
|
83
|
+
for (const candidateId of candidateIds) {
|
|
84
|
+
const previous = beforeScenario?.candidates.find(({ id }) => id === candidateId);
|
|
85
|
+
const current = afterScenario?.candidates.find(({ id }) => id === candidateId);
|
|
86
|
+
const rawBefore = raw(previous);
|
|
87
|
+
const rawAfter = raw(current);
|
|
88
|
+
const probabilityBefore = previous?.conditionalProbability ?? undefined;
|
|
89
|
+
const probabilityAfter = current?.conditionalProbability ?? undefined;
|
|
90
|
+
const rankBefore = previous?.rank ?? undefined;
|
|
91
|
+
const rankAfter = current?.rank ?? undefined;
|
|
92
|
+
const timingBefore = previous?.effectiveMtthDays;
|
|
93
|
+
const timingAfter = current?.effectiveMtthDays;
|
|
94
|
+
const cumulativeBefore = previous?.cumulativeChance;
|
|
95
|
+
const cumulativeAfter = current?.cumulativeChance;
|
|
96
|
+
const changed = previous === undefined ||
|
|
97
|
+
current === undefined ||
|
|
98
|
+
rawBefore !== rawAfter ||
|
|
99
|
+
probabilityBefore !== probabilityAfter ||
|
|
100
|
+
rankBefore !== rankAfter ||
|
|
101
|
+
previous.eligibility !== current.eligibility ||
|
|
102
|
+
timingBefore !== timingAfter ||
|
|
103
|
+
cumulativeBefore !== cumulativeAfter ||
|
|
104
|
+
previous.unresolved.length !== current.unresolved.length;
|
|
105
|
+
if (!changed)
|
|
106
|
+
continue;
|
|
107
|
+
scenarioChanges.push({
|
|
108
|
+
scenarioId,
|
|
109
|
+
candidateId,
|
|
110
|
+
...(rawBefore === undefined || rawAfter === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { rawDelta: rawAfter - rawBefore }),
|
|
113
|
+
...(probabilityBefore === undefined || probabilityAfter === undefined
|
|
114
|
+
? {}
|
|
115
|
+
: { probabilityDelta: probabilityAfter - probabilityBefore }),
|
|
116
|
+
...(rankBefore === undefined || rankAfter === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: { rankDelta: rankAfter - rankBefore }),
|
|
119
|
+
...(timingBefore === undefined || timingAfter === undefined
|
|
120
|
+
? {}
|
|
121
|
+
: { timingDeltaDays: timingAfter - timingBefore }),
|
|
122
|
+
...(cumulativeBefore === undefined || cumulativeAfter === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { cumulativeChanceDelta: cumulativeAfter - cumulativeBefore }),
|
|
125
|
+
...(previous === undefined ||
|
|
126
|
+
current === undefined ||
|
|
127
|
+
previous.eligibility === current.eligibility
|
|
128
|
+
? {}
|
|
129
|
+
: {
|
|
130
|
+
eligibilityChange: {
|
|
131
|
+
before: previous.eligibility,
|
|
132
|
+
after: current.eligibility,
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
unresolvedDelta: (current?.unresolved.length ?? 0) - (previous?.unresolved.length ?? 0),
|
|
136
|
+
attribution: attribution(previous, current),
|
|
137
|
+
changedAstPaths: changedAstPaths(previous, current),
|
|
138
|
+
});
|
|
139
|
+
if ((previous?.unresolved.length ?? 0) < (current?.unresolved.length ?? 0))
|
|
140
|
+
regressions.push({
|
|
141
|
+
code: 'UNRESOLVED_ANALYSIS_INTRODUCED',
|
|
142
|
+
message: `Patch introduced unresolved analysis for ${candidateId}`,
|
|
143
|
+
scenarioId,
|
|
144
|
+
candidateId,
|
|
145
|
+
});
|
|
146
|
+
if (probabilityBefore !== undefined && probabilityBefore > 0 && probabilityAfter === 0)
|
|
147
|
+
regressions.push({
|
|
148
|
+
code: 'CANDIDATE_STARVED',
|
|
149
|
+
message: `Patch reduces ${candidateId} from a selectable outcome to zero probability`,
|
|
150
|
+
scenarioId,
|
|
151
|
+
candidateId,
|
|
152
|
+
});
|
|
153
|
+
for (const band of acceptanceBands) {
|
|
154
|
+
if (band.candidateId !== candidateId ||
|
|
155
|
+
(band.scenarioId !== undefined && band.scenarioId !== scenarioId))
|
|
156
|
+
continue;
|
|
157
|
+
const previousMetric = metricValue(previous, band.metric);
|
|
158
|
+
const currentMetric = metricValue(current, band.metric);
|
|
159
|
+
if (insideBand(previousMetric, band) && !insideBand(currentMetric, band))
|
|
160
|
+
regressions.push({
|
|
161
|
+
code: 'ACCEPTANCE_BAND_REGRESSION',
|
|
162
|
+
message: `Patch moves ${candidateId} outside acceptance band ${band.id}`,
|
|
163
|
+
scenarioId,
|
|
164
|
+
candidateId,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
beforeAnalysisId: before.analysisId,
|
|
171
|
+
afterAnalysisId: after.analysisId,
|
|
172
|
+
assumptionsChanged: before.metadata.scenarioHash !== after.metadata.scenarioHash,
|
|
173
|
+
adapterChanged: before.metadata.adapterId !== after.metadata.adapterId ||
|
|
174
|
+
before.metadata.adapterVersion !== after.metadata.adapterVersion,
|
|
175
|
+
scenarioChanges,
|
|
176
|
+
regressions,
|
|
177
|
+
};
|
|
178
|
+
}
|