hoi4-agent-tools 2.2.0 → 2.3.3
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 +28 -0
- package/README.md +30 -21
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +5 -0
- package/dist/hoi4_agent_tools/core/index.js +57 -1
- 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/algorithms.js +178 -1
- package/dist/hoi4_agent_tools/event/graph.js +24 -9
- package/dist/hoi4_agent_tools/event/limits.js +5 -2
- package/dist/hoi4_agent_tools/event/service.js +182 -45
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +13 -63
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
- 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/runtime.js +109 -7
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/technology/graph.js +30 -3
- package/dist/hoi4_agent_tools/technology/queries.js +70 -3
- package/dist/hoi4_agent_tools/technology/service.js +38 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +2 -0
- package/docs/events.md +2 -2
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +2 -0
- package/docs/technology.md +4 -2
- 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 +19 -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,458 @@
|
|
|
1
|
+
import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
2
|
+
import { ServiceError } from '../core/result.js';
|
|
3
|
+
import { assignments, astPathFor, childBlocks, firstScalar, nodeLocation, parseClausewitz, } from '../core/source/index.js';
|
|
4
|
+
import { probabilityAdapter } from './adapters.js';
|
|
5
|
+
const eventKeys = new Set([
|
|
6
|
+
'country_event',
|
|
7
|
+
'news_event',
|
|
8
|
+
'state_event',
|
|
9
|
+
'unit_leader_event',
|
|
10
|
+
'operative_leader_event',
|
|
11
|
+
]);
|
|
12
|
+
function normalized(value) {
|
|
13
|
+
return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function walkAssignments(block, ancestors = [], output = []) {
|
|
16
|
+
for (const assignment of assignments(block)) {
|
|
17
|
+
output.push({ assignment, ancestors });
|
|
18
|
+
if (assignment.value.type === 'block') {
|
|
19
|
+
walkAssignments(assignment.value, [...ancestors, { key: assignment.key.value, block, assignment }], output);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return output;
|
|
23
|
+
}
|
|
24
|
+
function provenance(context, node, symbol) {
|
|
25
|
+
return {
|
|
26
|
+
path: context.file.displayPath,
|
|
27
|
+
rootKind: context.file.rootKind,
|
|
28
|
+
loadOrder: context.file.loadOrder,
|
|
29
|
+
sourceHash: context.file.sha256,
|
|
30
|
+
location: nodeLocation(context.document, node, symbol),
|
|
31
|
+
...(astPathFor(context.document, node) === undefined
|
|
32
|
+
? {}
|
|
33
|
+
: { astPath: astPathFor(context.document, node) }),
|
|
34
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function weightCandidate(adapterId, id, sourceKind, defaultValue, context, definition, options = {}) {
|
|
38
|
+
return {
|
|
39
|
+
id,
|
|
40
|
+
adapterId,
|
|
41
|
+
sourceKind,
|
|
42
|
+
defaultValue,
|
|
43
|
+
...(options.valueExpression === undefined ? {} : { valueExpression: options.valueExpression }),
|
|
44
|
+
...(options.weightBlock === undefined ? {} : { weightBlock: options.weightBlock }),
|
|
45
|
+
...(options.eligibilityBlock === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: { eligibilityBlock: options.eligibilityBlock }),
|
|
48
|
+
document: context.document,
|
|
49
|
+
provenance: [provenance(context, definition, id)],
|
|
50
|
+
metadata: options.metadata ?? {},
|
|
51
|
+
...(options.parentRandomPools === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { parentRandomPools: options.parentRandomPools }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function eventCandidates(context, adapterId) {
|
|
57
|
+
const output = [];
|
|
58
|
+
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
59
|
+
if (assignment.value.type !== 'block' ||
|
|
60
|
+
ancestors.length !== 0 ||
|
|
61
|
+
!eventKeys.has(assignment.key.value) ||
|
|
62
|
+
firstScalar(assignment.value, 'id') === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
const event = assignment.value;
|
|
65
|
+
const eventId = firstScalar(event, 'id').value;
|
|
66
|
+
if (adapterId === 'event_mean_time_to_happen') {
|
|
67
|
+
const mtth = childBlocks(event, 'mean_time_to_happen')[0];
|
|
68
|
+
if (mtth === undefined)
|
|
69
|
+
continue;
|
|
70
|
+
output.push(weightCandidate(adapterId, eventId, assignment.key.value, '0', context, assignment, {
|
|
71
|
+
weightBlock: mtth,
|
|
72
|
+
...(childBlocks(event, 'trigger')[0] === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: { eligibilityBlock: childBlocks(event, 'trigger')[0] }),
|
|
75
|
+
metadata: { eventId },
|
|
76
|
+
}));
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const options = childBlocks(event, 'option');
|
|
80
|
+
for (const [index, option] of options.entries()) {
|
|
81
|
+
const chance = childBlocks(option, 'ai_chance')[0];
|
|
82
|
+
const name = firstScalar(option, 'name')?.value ?? `${eventId}.option.${index + 1}`;
|
|
83
|
+
output.push(weightCandidate(adapterId, name, 'event_option', '1', context, option, {
|
|
84
|
+
...(chance === undefined ? {} : { weightBlock: chance }),
|
|
85
|
+
...(childBlocks(option, 'trigger')[0] === undefined
|
|
86
|
+
? {}
|
|
87
|
+
: { eligibilityBlock: childBlocks(option, 'trigger')[0] }),
|
|
88
|
+
metadata: { eventId, optionIndex: index },
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
function focusCandidates(context) {
|
|
95
|
+
const output = [];
|
|
96
|
+
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
97
|
+
if (assignment.key.value !== 'focus' ||
|
|
98
|
+
assignment.value.type !== 'block' ||
|
|
99
|
+
ancestors.at(-1)?.key !== 'focus_tree')
|
|
100
|
+
continue;
|
|
101
|
+
const id = firstScalar(assignment.value, 'id')?.value;
|
|
102
|
+
if (id === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
const weight = childBlocks(assignment.value, 'ai_will_do')[0];
|
|
105
|
+
output.push(weightCandidate('national_focus_ai_will_do', id, 'focus', '1', context, assignment, {
|
|
106
|
+
...(weight === undefined ? {} : { weightBlock: weight }),
|
|
107
|
+
...(childBlocks(assignment.value, 'available')[0] === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { eligibilityBlock: childBlocks(assignment.value, 'available')[0] }),
|
|
110
|
+
metadata: {
|
|
111
|
+
prerequisiteBlocks: childBlocks(assignment.value, 'prerequisite').length,
|
|
112
|
+
hasBypass: childBlocks(assignment.value, 'bypass').length > 0,
|
|
113
|
+
},
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
return output;
|
|
117
|
+
}
|
|
118
|
+
function decisionCandidates(context, adapterId) {
|
|
119
|
+
const output = [];
|
|
120
|
+
const ignored = new Set([
|
|
121
|
+
'allowed',
|
|
122
|
+
'available',
|
|
123
|
+
'icon',
|
|
124
|
+
'picture',
|
|
125
|
+
'priority',
|
|
126
|
+
'scripted_gui',
|
|
127
|
+
'target_root_trigger',
|
|
128
|
+
'visible',
|
|
129
|
+
'visible_when_empty',
|
|
130
|
+
]);
|
|
131
|
+
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
132
|
+
if (assignment.value.type !== 'block' ||
|
|
133
|
+
ancestors.length !== 1 ||
|
|
134
|
+
ignored.has(assignment.key.value))
|
|
135
|
+
continue;
|
|
136
|
+
const mission = firstScalar(assignment.value, 'is_mission')?.value === 'yes' ||
|
|
137
|
+
firstScalar(assignment.value, 'days_mission_timeout') !== undefined ||
|
|
138
|
+
firstScalar(assignment.value, 'days_remove') !== undefined;
|
|
139
|
+
if ((adapterId === 'mission_ai_will_do') !== mission)
|
|
140
|
+
continue;
|
|
141
|
+
const weight = childBlocks(assignment.value, 'ai_will_do')[0];
|
|
142
|
+
if (weight === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
output.push(weightCandidate(adapterId, assignment.key.value, mission ? 'mission' : 'decision', '1', context, assignment, {
|
|
145
|
+
weightBlock: weight,
|
|
146
|
+
...(childBlocks(assignment.value, 'available')[0] === undefined
|
|
147
|
+
? {}
|
|
148
|
+
: { eligibilityBlock: childBlocks(assignment.value, 'available')[0] }),
|
|
149
|
+
metadata: { category: ancestors[0].key },
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
function technologyCandidates(context, adapterId) {
|
|
155
|
+
const output = [];
|
|
156
|
+
const sourcePath = normalized(context.file.relativePath);
|
|
157
|
+
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
158
|
+
if (assignment.value.type !== 'block')
|
|
159
|
+
continue;
|
|
160
|
+
const classic = adapterId === 'technology_ai_will_do' &&
|
|
161
|
+
sourcePath.startsWith('common/technologies/') &&
|
|
162
|
+
ancestors.length === 1 &&
|
|
163
|
+
ancestors[0]?.key === 'technologies' &&
|
|
164
|
+
!assignment.key.value.startsWith('@');
|
|
165
|
+
const doctrine = adapterId === 'doctrine_ai_will_do' &&
|
|
166
|
+
sourcePath.startsWith('common/doctrines/') &&
|
|
167
|
+
ancestors.length === 0 &&
|
|
168
|
+
!assignment.key.value.startsWith('@');
|
|
169
|
+
if (!classic && !doctrine)
|
|
170
|
+
continue;
|
|
171
|
+
const weight = childBlocks(assignment.value, 'ai_will_do')[0];
|
|
172
|
+
if (weight === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
output.push(weightCandidate(adapterId, assignment.key.value, doctrine ? 'doctrine' : 'technology', '1', context, assignment, {
|
|
175
|
+
weightBlock: weight,
|
|
176
|
+
...((childBlocks(assignment.value, 'allow')[0] ??
|
|
177
|
+
childBlocks(assignment.value, 'available')[0]) === undefined
|
|
178
|
+
? {}
|
|
179
|
+
: {
|
|
180
|
+
eligibilityBlock: childBlocks(assignment.value, 'allow')[0] ??
|
|
181
|
+
childBlocks(assignment.value, 'available')[0],
|
|
182
|
+
}),
|
|
183
|
+
metadata: { sourcePath },
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
return output;
|
|
187
|
+
}
|
|
188
|
+
function randomListEntryCandidates(context, list) {
|
|
189
|
+
if (list.value.type !== 'block')
|
|
190
|
+
return [];
|
|
191
|
+
const location = nodeLocation(context.document, list);
|
|
192
|
+
const blockId = `${context.file.relativePath}:${location.start.line}`;
|
|
193
|
+
return assignments(list.value).flatMap((entry, index) => {
|
|
194
|
+
if (entry.value.type !== 'block')
|
|
195
|
+
return [];
|
|
196
|
+
const trigger = childBlocks(entry.value, 'trigger')[0] ?? childBlocks(entry.value, 'limit')[0];
|
|
197
|
+
return [
|
|
198
|
+
weightCandidate('random_list', `${blockId}.entry.${index + 1}`, 'random_list_entry', entry.key.value, context, entry, {
|
|
199
|
+
weightBlock: entry.value,
|
|
200
|
+
...(trigger === undefined ? {} : { eligibilityBlock: trigger }),
|
|
201
|
+
metadata: {
|
|
202
|
+
randomListId: blockId,
|
|
203
|
+
entryIndex: index,
|
|
204
|
+
poolStartLine: location.start.line,
|
|
205
|
+
poolEndLine: location.end.line,
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
];
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function parentRandomPools(context, ancestors) {
|
|
212
|
+
const output = [];
|
|
213
|
+
for (let index = 0; index + 1 < ancestors.length; index += 1) {
|
|
214
|
+
const list = ancestors[index].assignment;
|
|
215
|
+
const selectedEntry = ancestors[index + 1].assignment;
|
|
216
|
+
if (list.key.value !== 'random_list' || list.value.type !== 'block')
|
|
217
|
+
continue;
|
|
218
|
+
const entries = assignments(list.value).filter(({ value }) => value.type === 'block');
|
|
219
|
+
const selectedEntryIndex = entries.indexOf(selectedEntry);
|
|
220
|
+
if (selectedEntryIndex < 0)
|
|
221
|
+
continue;
|
|
222
|
+
const candidates = randomListEntryCandidates(context, list);
|
|
223
|
+
const randomListId = candidates[0]?.metadata.randomListId;
|
|
224
|
+
output.push({
|
|
225
|
+
id: typeof randomListId === 'string' ? randomListId : `random-list-${index}`,
|
|
226
|
+
selectedEntryIndex,
|
|
227
|
+
candidates,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return output;
|
|
231
|
+
}
|
|
232
|
+
function randomCandidates(context, adapterId) {
|
|
233
|
+
const output = [];
|
|
234
|
+
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
235
|
+
if (assignment.key.value !== (adapterId === 'direct_random' ? 'random' : 'random_list'))
|
|
236
|
+
continue;
|
|
237
|
+
if (assignment.value.type !== 'block')
|
|
238
|
+
continue;
|
|
239
|
+
const location = nodeLocation(context.document, assignment);
|
|
240
|
+
const blockId = `${context.file.relativePath}:${location.start.line}`;
|
|
241
|
+
if (adapterId === 'direct_random') {
|
|
242
|
+
const chance = firstScalar(assignment.value, 'chance');
|
|
243
|
+
if (chance === undefined)
|
|
244
|
+
continue;
|
|
245
|
+
output.push(weightCandidate(adapterId, blockId, 'random', '0', context, assignment, {
|
|
246
|
+
valueExpression: chance.value,
|
|
247
|
+
metadata: { randomBlockId: blockId },
|
|
248
|
+
}));
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const parents = parentRandomPools(context, ancestors);
|
|
252
|
+
output.push(...randomListEntryCandidates(context, assignment).map((candidate) => ({
|
|
253
|
+
...candidate,
|
|
254
|
+
...(parents.length === 0 ? {} : { parentRandomPools: parents }),
|
|
255
|
+
})));
|
|
256
|
+
}
|
|
257
|
+
return output;
|
|
258
|
+
}
|
|
259
|
+
function aiStrategyCandidates(context) {
|
|
260
|
+
const output = [];
|
|
261
|
+
for (const { assignment } of walkAssignments(context.document.root)) {
|
|
262
|
+
if (assignment.value.type !== 'block')
|
|
263
|
+
continue;
|
|
264
|
+
const type = firstScalar(assignment.value, 'type')?.value;
|
|
265
|
+
if (type !== 'research_weight_factor' && type !== 'research_tech' && type !== 'focus')
|
|
266
|
+
continue;
|
|
267
|
+
const value = firstScalar(assignment.value, 'value');
|
|
268
|
+
if (value === undefined)
|
|
269
|
+
continue;
|
|
270
|
+
const target = firstScalar(assignment.value, 'id')?.value ?? 'unspecified';
|
|
271
|
+
const location = nodeLocation(context.document, assignment);
|
|
272
|
+
const id = `${type}:${target}:${context.file.relativePath}:${location.start.line}`;
|
|
273
|
+
output.push(weightCandidate('ai_strategy_factor', id, 'ai_strategy', '0', context, assignment, {
|
|
274
|
+
valueExpression: value.value,
|
|
275
|
+
metadata: { strategyType: type, target },
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
return output;
|
|
279
|
+
}
|
|
280
|
+
function candidatesFor(context, adapterId) {
|
|
281
|
+
switch (adapterId) {
|
|
282
|
+
case 'event_mean_time_to_happen':
|
|
283
|
+
case 'event_option_ai_chance':
|
|
284
|
+
return eventCandidates(context, adapterId);
|
|
285
|
+
case 'decision_ai_will_do':
|
|
286
|
+
case 'mission_ai_will_do':
|
|
287
|
+
return decisionCandidates(context, adapterId);
|
|
288
|
+
case 'national_focus_ai_will_do':
|
|
289
|
+
return focusCandidates(context);
|
|
290
|
+
case 'technology_ai_will_do':
|
|
291
|
+
case 'doctrine_ai_will_do':
|
|
292
|
+
return technologyCandidates(context, adapterId);
|
|
293
|
+
case 'direct_random':
|
|
294
|
+
case 'random_list':
|
|
295
|
+
return randomCandidates(context, adapterId);
|
|
296
|
+
case 'ai_strategy_factor':
|
|
297
|
+
return aiStrategyCandidates(context);
|
|
298
|
+
case 'custom_weighted_pool':
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function sourceContexts(snapshot, source) {
|
|
303
|
+
if (source.inlineClausewitz !== undefined || source.virtualPatch !== undefined) {
|
|
304
|
+
const text = source.virtualPatch ?? source.inlineClausewitz ?? '';
|
|
305
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
306
|
+
const sourceHash = hashCanonical({ text });
|
|
307
|
+
const file = {
|
|
308
|
+
absolutePath: '<inline>',
|
|
309
|
+
displayPath: 'proposed:inline-probability-source.txt',
|
|
310
|
+
relativePath: 'proposed/inline-probability-source.txt',
|
|
311
|
+
rootKind: 'fixture',
|
|
312
|
+
loadOrder: Number.MAX_SAFE_INTEGER,
|
|
313
|
+
size: bytes.length,
|
|
314
|
+
modifiedMs: 0,
|
|
315
|
+
sha256: sourceHash,
|
|
316
|
+
bytes,
|
|
317
|
+
};
|
|
318
|
+
return [{ file, document: parseClausewitz(bytes, file.displayPath) }];
|
|
319
|
+
}
|
|
320
|
+
const requestedPath = source.path === undefined ? undefined : normalized(source.path);
|
|
321
|
+
const selected = snapshot.files.filter(({ relativePath, shadowedBy }) => {
|
|
322
|
+
if (shadowedBy !== undefined || !relativePath.toLowerCase().endsWith('.txt'))
|
|
323
|
+
return false;
|
|
324
|
+
if (requestedPath === undefined)
|
|
325
|
+
return true;
|
|
326
|
+
const relative = normalized(relativePath);
|
|
327
|
+
return relative === requestedPath || relative.endsWith(`/${requestedPath}`);
|
|
328
|
+
});
|
|
329
|
+
if (requestedPath !== undefined && selected.length === 0)
|
|
330
|
+
throw new ServiceError('PROBABILITY_SOURCE_NOT_FOUND', 'Probability source path was not found', {
|
|
331
|
+
path: source.path,
|
|
332
|
+
});
|
|
333
|
+
if (source.expectedSourceHash !== undefined &&
|
|
334
|
+
selected.some(({ sha256 }) => sha256 !== source.expectedSourceHash))
|
|
335
|
+
throw new ServiceError('PROBABILITY_SOURCE_STALE', 'Probability source hash is stale', {
|
|
336
|
+
expectedSourceHash: source.expectedSourceHash,
|
|
337
|
+
actualSourceHashes: selected.map(({ sha256 }) => sha256),
|
|
338
|
+
});
|
|
339
|
+
return selected.map((file) => ({
|
|
340
|
+
file,
|
|
341
|
+
document: parseClausewitz(file.bytes, file.displayPath),
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
function identifierMatches(candidate, identifier) {
|
|
345
|
+
if (candidate.id === identifier)
|
|
346
|
+
return true;
|
|
347
|
+
return Object.values(candidate.metadata).some((value) => value === identifier);
|
|
348
|
+
}
|
|
349
|
+
function samePool(candidate, selected) {
|
|
350
|
+
if (candidate.adapterId === 'event_option_ai_chance')
|
|
351
|
+
return candidate.metadata.eventId === selected.metadata.eventId;
|
|
352
|
+
if (candidate.adapterId === 'random_list')
|
|
353
|
+
return candidate.metadata.randomListId === selected.metadata.randomListId;
|
|
354
|
+
return candidate.id === selected.id;
|
|
355
|
+
}
|
|
356
|
+
export function discoverWeightedSurface(snapshot, adapterId, source, candidatePool = []) {
|
|
357
|
+
const contexts = sourceContexts(snapshot, source);
|
|
358
|
+
let candidates = contexts.flatMap((context) => candidatesFor(context, adapterId));
|
|
359
|
+
candidates.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
360
|
+
if (source.line !== undefined) {
|
|
361
|
+
candidates = candidates.filter(({ metadata, provenance: [first] }) => {
|
|
362
|
+
const location = first?.location;
|
|
363
|
+
const poolStartLine = metadata.poolStartLine;
|
|
364
|
+
const poolEndLine = metadata.poolEndLine;
|
|
365
|
+
return ((location !== undefined &&
|
|
366
|
+
source.line >= location.start.line &&
|
|
367
|
+
source.line <= location.end.line) ||
|
|
368
|
+
(typeof poolStartLine === 'number' &&
|
|
369
|
+
typeof poolEndLine === 'number' &&
|
|
370
|
+
source.line >= poolStartLine &&
|
|
371
|
+
source.line <= poolEndLine));
|
|
372
|
+
});
|
|
373
|
+
if (adapterId === 'event_option_ai_chance' || adapterId === 'random_list') {
|
|
374
|
+
const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
|
|
375
|
+
const pools = new Map();
|
|
376
|
+
for (const candidate of candidates) {
|
|
377
|
+
const start = candidate.metadata.poolStartLine;
|
|
378
|
+
const end = candidate.metadata.poolEndLine;
|
|
379
|
+
const span = typeof start === 'number' && typeof end === 'number'
|
|
380
|
+
? end - start
|
|
381
|
+
: Number.MAX_SAFE_INTEGER;
|
|
382
|
+
const key = candidate.metadata[poolProperty];
|
|
383
|
+
const existing = pools.get(key);
|
|
384
|
+
if (existing === undefined || span < existing.span)
|
|
385
|
+
pools.set(key, { span });
|
|
386
|
+
}
|
|
387
|
+
const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
|
|
388
|
+
if (selectedPool !== undefined)
|
|
389
|
+
candidates = candidates.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const localCategoricalPool = adapterId === 'event_option_ai_chance' || adapterId === 'random_list';
|
|
393
|
+
let completeLocalPoolIds;
|
|
394
|
+
if (source.identifier !== undefined) {
|
|
395
|
+
const selected = candidates.find((candidate) => identifierMatches(candidate, source.identifier));
|
|
396
|
+
if (selected === undefined)
|
|
397
|
+
throw new ServiceError('PROBABILITY_IDENTIFIER_NOT_FOUND', 'No weighted source matched the requested identifier', { identifier: source.identifier, adapterId });
|
|
398
|
+
if (localCategoricalPool) {
|
|
399
|
+
candidates = candidates.filter((candidate) => samePool(candidate, selected));
|
|
400
|
+
completeLocalPoolIds = new Set(candidates.map(({ id }) => id));
|
|
401
|
+
}
|
|
402
|
+
else if (candidatePool.length === 0)
|
|
403
|
+
candidates = [selected];
|
|
404
|
+
}
|
|
405
|
+
const candidatesBeforeExplicitPool = candidates;
|
|
406
|
+
if (candidatePool.length > 0) {
|
|
407
|
+
const wanted = new Set(candidatePool);
|
|
408
|
+
candidates = candidates.filter(({ id }) => wanted.has(id));
|
|
409
|
+
}
|
|
410
|
+
if (candidates.length === 0 && adapterId !== 'custom_weighted_pool')
|
|
411
|
+
throw new ServiceError('PROBABILITY_SURFACE_EMPTY', 'No weighted blocks matched this request', {
|
|
412
|
+
adapterId,
|
|
413
|
+
source,
|
|
414
|
+
});
|
|
415
|
+
const missingCandidates = candidatePool.filter((id) => !candidates.some((candidate) => candidate.id === id));
|
|
416
|
+
const unsupported = missingCandidates.map((candidateId) => ({
|
|
417
|
+
code: 'CANDIDATE_NOT_FOUND',
|
|
418
|
+
message: 'Declared candidate was not found in active source',
|
|
419
|
+
candidateId,
|
|
420
|
+
}));
|
|
421
|
+
const inherentPool = adapterId === 'direct_random' || adapterId === 'event_mean_time_to_happen';
|
|
422
|
+
if (localCategoricalPool && completeLocalPoolIds === undefined) {
|
|
423
|
+
const poolKey = (candidate) => String(adapterId === 'event_option_ai_chance'
|
|
424
|
+
? candidate.metadata.eventId
|
|
425
|
+
: candidate.metadata.randomListId);
|
|
426
|
+
const selectedKeys = new Set(candidates.map(poolKey));
|
|
427
|
+
if (selectedKeys.size === 1) {
|
|
428
|
+
const [selectedKey] = selectedKeys;
|
|
429
|
+
completeLocalPoolIds = new Set(candidatesBeforeExplicitPool
|
|
430
|
+
.filter((candidate) => poolKey(candidate) === selectedKey)
|
|
431
|
+
.map(({ id }) => id));
|
|
432
|
+
}
|
|
433
|
+
else
|
|
434
|
+
unsupported.push({
|
|
435
|
+
code: 'MULTIPLE_CATEGORICAL_POOLS',
|
|
436
|
+
message: 'Select one event-option or random-list pool before requesting normalized probabilities',
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const localPoolIds = completeLocalPoolIds;
|
|
440
|
+
const localPoolComplete = localPoolIds?.size === candidates.length && candidates.every(({ id }) => localPoolIds.has(id));
|
|
441
|
+
const poolComplete = missingCandidates.length === 0 &&
|
|
442
|
+
(inherentPool ||
|
|
443
|
+
(localCategoricalPool && localPoolComplete) ||
|
|
444
|
+
(!localCategoricalPool &&
|
|
445
|
+
candidatePool.length > 0 &&
|
|
446
|
+
candidates.length === candidatePool.length));
|
|
447
|
+
const hashes = Object.fromEntries(contexts.map(({ file }) => [file.displayPath, file.sha256]));
|
|
448
|
+
return {
|
|
449
|
+
id: source.identifier ?? source.path ?? `${adapterId}-workspace-scan`,
|
|
450
|
+
adapter: probabilityAdapter(adapterId),
|
|
451
|
+
candidates,
|
|
452
|
+
poolComplete,
|
|
453
|
+
sourceRevision: snapshot.revision,
|
|
454
|
+
sourceHash: hashCanonical(hashes),
|
|
455
|
+
filesScanned: contexts.map(({ file }) => file.displayPath),
|
|
456
|
+
unsupported,
|
|
457
|
+
};
|
|
458
|
+
}
|