hoi4-agent-tools 1.1.0 → 1.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 +12 -0
- package/README.md +21 -16
- package/dist/hoi4_agent_tools/core/engine.js +12 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +208 -0
- package/dist/hoi4_agent_tools/event/artifact-validation.js +572 -0
- package/dist/hoi4_agent_tools/event/compare.js +413 -0
- package/dist/hoi4_agent_tools/event/graph.js +1586 -0
- package/dist/hoi4_agent_tools/event/index.js +10 -0
- package/dist/hoi4_agent_tools/event/layout.js +218 -0
- package/dist/hoi4_agent_tools/event/limits.js +41 -0
- package/dist/hoi4_agent_tools/event/model.js +2 -0
- package/dist/hoi4_agent_tools/event/queries.js +643 -0
- package/dist/hoi4_agent_tools/event/render.js +443 -0
- package/dist/hoi4_agent_tools/event/scc.js +72 -0
- package/dist/hoi4_agent_tools/event/service.js +1147 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1458 -0
- package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
- package/dist/hoi4_agent_tools/mcp/resources/register.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +4 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +8 -14
- package/dist/hoi4_agent_tools/mcp/tools/event.js +459 -0
- package/dist/hoi4_agent_tools/schemas/event.js +95 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +10 -0
- package/docs/events.md +113 -0
- package/docs/setup.md +3 -3
- package/package.json +7 -3
- package/server.json +3 -3
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { ServiceError } from '../core/result.js';
|
|
2
|
+
import { EVENT_GRAPH_MAX_CONDITION_TEXT, EVENT_GRAPH_MAX_EDGES, EVENT_GRAPH_MAX_HELPER_DEPTH, EVENT_GRAPH_MAX_ISSUES, EVENT_GRAPH_MAX_NODES, EVENT_GRAPH_MAX_STATE_ACCESSES, EVENT_GRAPH_MAX_STATE_LINKS, EVENT_GRAPH_MAX_UNRESOLVED, } from './limits.js';
|
|
3
|
+
import { EVENT_GRAPH_PARSER_VERSION, EVENT_GRAPH_SCHEMA_VERSION, } from './model.js';
|
|
4
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
5
|
+
const MAX_JSON_DEPTH = 32;
|
|
6
|
+
const MAX_JSON_CONTAINER_ENTRIES = 500_000;
|
|
7
|
+
const MAX_JSON_VALUES = 20_000_000;
|
|
8
|
+
const MAX_STRING_LENGTH = 1_048_576;
|
|
9
|
+
const MAX_SOURCE_RECORDS = 100_000;
|
|
10
|
+
const MAX_DIAGNOSTICS = 20_000;
|
|
11
|
+
const MAX_CONDITIONS_PER_EDGE = 10_000;
|
|
12
|
+
const MAX_PROVENANCE_PER_EDGE = 10_000;
|
|
13
|
+
const MAX_RELATED_LOCATIONS = 1_000;
|
|
14
|
+
const nodeKinds = new Set(['event', 'option', 'entry', 'helper', 'unresolved', 'terminal']);
|
|
15
|
+
const edgeReasons = new Set([
|
|
16
|
+
'option_branch',
|
|
17
|
+
'immediate_event_call',
|
|
18
|
+
'option_event_call',
|
|
19
|
+
'hidden_event_call',
|
|
20
|
+
'after_event_call',
|
|
21
|
+
'delayed_event_call',
|
|
22
|
+
'random_event_call',
|
|
23
|
+
'weighted_event_call',
|
|
24
|
+
'scripted_effect_call',
|
|
25
|
+
'scripted_effect_expansion',
|
|
26
|
+
'on_action_entry',
|
|
27
|
+
'focus_entry',
|
|
28
|
+
'decision_entry',
|
|
29
|
+
'mission_entry',
|
|
30
|
+
'country_setup_entry',
|
|
31
|
+
'state_setup_entry',
|
|
32
|
+
'implicit_event_entry',
|
|
33
|
+
'other_entry',
|
|
34
|
+
'unresolved_dynamic_reference',
|
|
35
|
+
'terminal',
|
|
36
|
+
]);
|
|
37
|
+
const confidenceValues = new Set(['confirmed', 'high', 'medium', 'low', 'unresolved']);
|
|
38
|
+
const conditionKinds = new Set([
|
|
39
|
+
'event_trigger',
|
|
40
|
+
'option_trigger',
|
|
41
|
+
'limit',
|
|
42
|
+
'random_chance',
|
|
43
|
+
'weight_modifier',
|
|
44
|
+
'branch_guard',
|
|
45
|
+
'unknown',
|
|
46
|
+
]);
|
|
47
|
+
const provenanceRoles = new Set(['invocation', 'helper_call', 'dispatch', 'condition', 'entry']);
|
|
48
|
+
const timingModes = new Set([
|
|
49
|
+
'immediate',
|
|
50
|
+
'fixed',
|
|
51
|
+
'random',
|
|
52
|
+
'fixed_and_random',
|
|
53
|
+
'mean_time_to_happen',
|
|
54
|
+
'date',
|
|
55
|
+
'unknown',
|
|
56
|
+
]);
|
|
57
|
+
const scopeKinds = new Set([
|
|
58
|
+
'country',
|
|
59
|
+
'state',
|
|
60
|
+
'province',
|
|
61
|
+
'unit_leader',
|
|
62
|
+
'operative',
|
|
63
|
+
'character',
|
|
64
|
+
'global',
|
|
65
|
+
'unknown',
|
|
66
|
+
]);
|
|
67
|
+
const stateKinds = new Set([
|
|
68
|
+
'country_flag',
|
|
69
|
+
'global_flag',
|
|
70
|
+
'state_flag',
|
|
71
|
+
'variable',
|
|
72
|
+
'global_variable',
|
|
73
|
+
'array',
|
|
74
|
+
'event_target',
|
|
75
|
+
'global_event_target',
|
|
76
|
+
'saved_scope',
|
|
77
|
+
]);
|
|
78
|
+
const stateAccessKinds = new Set(['read', 'write', 'read_write', 'replace', 'clear']);
|
|
79
|
+
const defectClasses = new Set([
|
|
80
|
+
'confirmed_error',
|
|
81
|
+
'probable_defect',
|
|
82
|
+
'design_warning',
|
|
83
|
+
'unresolved_analysis',
|
|
84
|
+
]);
|
|
85
|
+
const severities = new Set(['info', 'warning', 'error', 'blocker']);
|
|
86
|
+
const diagnosticCategories = new Set([
|
|
87
|
+
'syntax',
|
|
88
|
+
'reference',
|
|
89
|
+
'layout',
|
|
90
|
+
'design',
|
|
91
|
+
'rendering',
|
|
92
|
+
'security',
|
|
93
|
+
'validation',
|
|
94
|
+
'map',
|
|
95
|
+
'configuration',
|
|
96
|
+
]);
|
|
97
|
+
const unresolvedKinds = new Set([
|
|
98
|
+
'dynamic_event',
|
|
99
|
+
'missing_event',
|
|
100
|
+
'dynamic_helper',
|
|
101
|
+
'missing_helper',
|
|
102
|
+
'scope',
|
|
103
|
+
'state',
|
|
104
|
+
'partial_source',
|
|
105
|
+
]);
|
|
106
|
+
const rootKinds = new Set(['mod', 'game', 'dependency', 'artifact', 'cache', 'fixture']);
|
|
107
|
+
const symbolKinds = new Set([
|
|
108
|
+
'focus_tree',
|
|
109
|
+
'focus',
|
|
110
|
+
'continuous_focus_palette',
|
|
111
|
+
'continuous_focus',
|
|
112
|
+
'decision',
|
|
113
|
+
'decision_category',
|
|
114
|
+
'event',
|
|
115
|
+
'idea',
|
|
116
|
+
'leader',
|
|
117
|
+
'formable',
|
|
118
|
+
'scripted_effect',
|
|
119
|
+
'scripted_trigger',
|
|
120
|
+
'sprite',
|
|
121
|
+
'texture',
|
|
122
|
+
'gui_element',
|
|
123
|
+
'scripted_gui',
|
|
124
|
+
'localisation',
|
|
125
|
+
'state',
|
|
126
|
+
'province',
|
|
127
|
+
'province_color',
|
|
128
|
+
'strategic_region',
|
|
129
|
+
'adjacency',
|
|
130
|
+
'supply_node',
|
|
131
|
+
'railway',
|
|
132
|
+
]);
|
|
133
|
+
function invalid(message, details = {}) {
|
|
134
|
+
throw new ServiceError('EVENT_GRAPH_ARTIFACT_INVALID', message, details);
|
|
135
|
+
}
|
|
136
|
+
function record(value, label) {
|
|
137
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
138
|
+
invalid(`${label} must be an object`);
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
function array(value, label, maximum) {
|
|
143
|
+
if (!Array.isArray(value))
|
|
144
|
+
invalid(`${label} must be an array`);
|
|
145
|
+
if (value.length > maximum) {
|
|
146
|
+
invalid(`${label} exceeds its fixed item ceiling`, {
|
|
147
|
+
count: value.length,
|
|
148
|
+
maximum,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
function string(value, label, maximum = MAX_STRING_LENGTH) {
|
|
154
|
+
if (typeof value !== 'string' || value.length > maximum) {
|
|
155
|
+
invalid(`${label} must be a bounded string`);
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function optionalString(value, label, maximum = MAX_STRING_LENGTH) {
|
|
160
|
+
if (value !== undefined)
|
|
161
|
+
string(value, label, maximum);
|
|
162
|
+
}
|
|
163
|
+
function safeInteger(value, label, minimum = 0) {
|
|
164
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
165
|
+
invalid(`${label} must be a safe integer greater than or equal to ${minimum}`);
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
function enumeration(value, label, allowed) {
|
|
170
|
+
const selected = string(value, label, 128);
|
|
171
|
+
if (!allowed.has(selected))
|
|
172
|
+
invalid(`${label} has an unsupported value`, { value: selected });
|
|
173
|
+
return selected;
|
|
174
|
+
}
|
|
175
|
+
function boolean(value, label) {
|
|
176
|
+
if (typeof value !== 'boolean')
|
|
177
|
+
invalid(`${label} must be a boolean`);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function sha256(value, label) {
|
|
181
|
+
const selected = string(value, label, 64);
|
|
182
|
+
if (!SHA256.test(selected))
|
|
183
|
+
invalid(`${label} must be a lowercase SHA-256 digest`);
|
|
184
|
+
return selected;
|
|
185
|
+
}
|
|
186
|
+
function assertUnique(values, label) {
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
for (const value of values) {
|
|
189
|
+
if (seen.has(value))
|
|
190
|
+
invalid(`${label} contains a duplicate identifier`, { id: value });
|
|
191
|
+
seen.add(value);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function validateJsonShape(value, signal) {
|
|
195
|
+
const stack = [{ value, depth: 0 }];
|
|
196
|
+
let visited = 0;
|
|
197
|
+
while (stack.length > 0) {
|
|
198
|
+
if ((visited & 255) === 0)
|
|
199
|
+
signal?.throwIfAborted();
|
|
200
|
+
const current = stack.pop();
|
|
201
|
+
visited += 1;
|
|
202
|
+
if (visited > MAX_JSON_VALUES) {
|
|
203
|
+
invalid('Event graph artifact exceeds the fixed structural work ceiling', {
|
|
204
|
+
maximumValues: MAX_JSON_VALUES,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (current.depth > MAX_JSON_DEPTH) {
|
|
208
|
+
invalid('Event graph artifact exceeds the fixed JSON nesting ceiling', {
|
|
209
|
+
maximumDepth: MAX_JSON_DEPTH,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (current.value === null || typeof current.value === 'boolean')
|
|
213
|
+
continue;
|
|
214
|
+
if (typeof current.value === 'number') {
|
|
215
|
+
if (!Number.isFinite(current.value))
|
|
216
|
+
invalid('Event graph artifact contains a non-finite number');
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (typeof current.value === 'string') {
|
|
220
|
+
if (current.value.length > MAX_STRING_LENGTH) {
|
|
221
|
+
invalid('Event graph artifact contains an oversized string');
|
|
222
|
+
}
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(current.value)) {
|
|
226
|
+
if (current.value.length > MAX_JSON_CONTAINER_ENTRIES) {
|
|
227
|
+
invalid('Event graph artifact contains an oversized array');
|
|
228
|
+
}
|
|
229
|
+
for (const member of current.value)
|
|
230
|
+
stack.push({ value: member, depth: current.depth + 1 });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (typeof current.value !== 'object')
|
|
234
|
+
invalid('Event graph artifact is not JSON data');
|
|
235
|
+
const entries = Object.entries(current.value);
|
|
236
|
+
if (entries.length > MAX_JSON_CONTAINER_ENTRIES) {
|
|
237
|
+
invalid('Event graph artifact contains an oversized object');
|
|
238
|
+
}
|
|
239
|
+
for (const [, member] of entries) {
|
|
240
|
+
stack.push({ value: member, depth: current.depth + 1 });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
signal?.throwIfAborted();
|
|
244
|
+
}
|
|
245
|
+
function validatePosition(value, label) {
|
|
246
|
+
const selected = record(value, label);
|
|
247
|
+
safeInteger(selected.line, `${label}.line`, 1);
|
|
248
|
+
safeInteger(selected.column, `${label}.column`, 1);
|
|
249
|
+
safeInteger(selected.offset, `${label}.offset`);
|
|
250
|
+
}
|
|
251
|
+
function validateLocation(value, label) {
|
|
252
|
+
const selected = record(value, label);
|
|
253
|
+
string(selected.path, `${label}.path`, 32_768);
|
|
254
|
+
validatePosition(selected.start, `${label}.start`);
|
|
255
|
+
validatePosition(selected.end, `${label}.end`);
|
|
256
|
+
optionalString(selected.symbol, `${label}.symbol`, 32_768);
|
|
257
|
+
const start = selected.start;
|
|
258
|
+
const end = selected.end;
|
|
259
|
+
if (end.offset < start.offset)
|
|
260
|
+
invalid(`${label} ends before it starts`);
|
|
261
|
+
return selected;
|
|
262
|
+
}
|
|
263
|
+
function optionalLocation(value, label) {
|
|
264
|
+
if (value !== undefined)
|
|
265
|
+
validateLocation(value, label);
|
|
266
|
+
}
|
|
267
|
+
function validateCondition(value, label) {
|
|
268
|
+
const selected = record(value, label);
|
|
269
|
+
string(selected.id, `${label}.id`, 32_768);
|
|
270
|
+
enumeration(selected.kind, `${label}.kind`, conditionKinds);
|
|
271
|
+
string(selected.expression, `${label}.expression`, EVENT_GRAPH_MAX_CONDITION_TEXT);
|
|
272
|
+
validateLocation(selected.location, `${label}.location`);
|
|
273
|
+
enumeration(selected.confidence, `${label}.confidence`, confidenceValues);
|
|
274
|
+
}
|
|
275
|
+
function validateConditions(value, label) {
|
|
276
|
+
for (const [index, condition] of array(value, label, MAX_CONDITIONS_PER_EDGE).entries()) {
|
|
277
|
+
validateCondition(condition, `${label}[${index}]`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function validateStringArray(value, label, maximum, itemMaximum = 32_768) {
|
|
281
|
+
return array(value, label, maximum).map((member, index) => string(member, `${label}[${index}]`, itemMaximum));
|
|
282
|
+
}
|
|
283
|
+
function validateTiming(value, label) {
|
|
284
|
+
const selected = record(value, label);
|
|
285
|
+
enumeration(selected.mode, `${label}.mode`, timingModes);
|
|
286
|
+
for (const key of [
|
|
287
|
+
'years',
|
|
288
|
+
'hours',
|
|
289
|
+
'days',
|
|
290
|
+
'months',
|
|
291
|
+
'randomHours',
|
|
292
|
+
'randomDays',
|
|
293
|
+
'randomMonths',
|
|
294
|
+
'date',
|
|
295
|
+
'expression',
|
|
296
|
+
]) {
|
|
297
|
+
optionalString(selected[key], `${label}.${key}`, EVENT_GRAPH_MAX_CONDITION_TEXT);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function validateWeight(value, label) {
|
|
301
|
+
const selected = record(value, label);
|
|
302
|
+
string(selected.value, `${label}.value`, EVENT_GRAPH_MAX_CONDITION_TEXT);
|
|
303
|
+
validateConditions(selected.modifiers, `${label}.modifiers`);
|
|
304
|
+
if (selected.valid !== 'unknown')
|
|
305
|
+
boolean(selected.valid, `${label}.valid`);
|
|
306
|
+
}
|
|
307
|
+
function validateScopeTransition(value, label) {
|
|
308
|
+
const selected = record(value, label);
|
|
309
|
+
enumeration(selected.source, `${label}.source`, scopeKinds);
|
|
310
|
+
enumeration(selected.destination, `${label}.destination`, scopeKinds);
|
|
311
|
+
optionalString(selected.expression, `${label}.expression`, EVENT_GRAPH_MAX_CONDITION_TEXT);
|
|
312
|
+
enumeration(selected.confidence, `${label}.confidence`, confidenceValues);
|
|
313
|
+
}
|
|
314
|
+
function validateBlocker(value, label) {
|
|
315
|
+
const selected = record(value, label);
|
|
316
|
+
string(selected.code, `${label}.code`, 1_024);
|
|
317
|
+
string(selected.message, `${label}.message`);
|
|
318
|
+
optionalLocation(selected.location, `${label}.location`);
|
|
319
|
+
if (selected.details !== undefined)
|
|
320
|
+
record(selected.details, `${label}.details`);
|
|
321
|
+
}
|
|
322
|
+
function validateDiagnostic(value, label) {
|
|
323
|
+
const selected = record(value, label);
|
|
324
|
+
string(selected.code, `${label}.code`, 1_024);
|
|
325
|
+
enumeration(selected.severity, `${label}.severity`, severities);
|
|
326
|
+
enumeration(selected.category, `${label}.category`, diagnosticCategories);
|
|
327
|
+
string(selected.message, `${label}.message`);
|
|
328
|
+
optionalLocation(selected.location, `${label}.location`);
|
|
329
|
+
if (selected.related !== undefined) {
|
|
330
|
+
for (const [index, location] of array(selected.related, `${label}.related`, MAX_RELATED_LOCATIONS).entries()) {
|
|
331
|
+
validateLocation(location, `${label}.related[${index}]`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
optionalString(selected.operationId, `${label}.operationId`, 32_768);
|
|
335
|
+
if (selected.details !== undefined)
|
|
336
|
+
record(selected.details, `${label}.details`);
|
|
337
|
+
return selected;
|
|
338
|
+
}
|
|
339
|
+
function validateSkippedSource(value, label) {
|
|
340
|
+
const selected = record(value, label);
|
|
341
|
+
string(selected.path, `${label}.path`, 32_768);
|
|
342
|
+
string(selected.relativePath, `${label}.relativePath`, 32_768);
|
|
343
|
+
enumeration(selected.rootKind, `${label}.rootKind`, rootKinds);
|
|
344
|
+
safeInteger(selected.loadOrder, `${label}.loadOrder`);
|
|
345
|
+
sha256(selected.sha256, `${label}.sha256`);
|
|
346
|
+
validateStringArray(selected.reasonCodes, `${label}.reasonCodes`, 1_000, 1_024);
|
|
347
|
+
const possible = validateStringArray(selected.possibleSymbolKinds, `${label}.possibleSymbolKinds`, symbolKinds.size, 128);
|
|
348
|
+
for (const kind of possible) {
|
|
349
|
+
if (!symbolKinds.has(kind))
|
|
350
|
+
invalid(`${label}.possibleSymbolKinds has an unsupported value`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Validate an untrusted, parsed Event Chain Viewer graph before comparison or rendering.
|
|
355
|
+
* The validator is intentionally independent of TypeScript's compile-time types.
|
|
356
|
+
*/
|
|
357
|
+
export function validateEventGraphArtifact(value, signal) {
|
|
358
|
+
signal?.throwIfAborted();
|
|
359
|
+
validateJsonShape(value, signal);
|
|
360
|
+
const graph = record(value, 'event graph');
|
|
361
|
+
if (graph.schemaVersion !== EVENT_GRAPH_SCHEMA_VERSION) {
|
|
362
|
+
invalid('Event graph artifact uses an unsupported schema version');
|
|
363
|
+
}
|
|
364
|
+
if (graph.parserVersion !== EVENT_GRAPH_PARSER_VERSION) {
|
|
365
|
+
invalid('Event graph artifact uses an unsupported parser version');
|
|
366
|
+
}
|
|
367
|
+
string(graph.workspaceId, 'event graph.workspaceId', 256);
|
|
368
|
+
sha256(graph.workspaceIdentity, 'event graph.workspaceIdentity');
|
|
369
|
+
sha256(graph.revision, 'event graph.revision');
|
|
370
|
+
boolean(graph.complete, 'event graph.complete');
|
|
371
|
+
const sourceHashes = record(graph.sourceHashes, 'event graph.sourceHashes');
|
|
372
|
+
const sourceHashEntries = Object.entries(sourceHashes);
|
|
373
|
+
if (sourceHashEntries.length > MAX_SOURCE_RECORDS) {
|
|
374
|
+
invalid('event graph.sourceHashes exceeds its fixed item ceiling');
|
|
375
|
+
}
|
|
376
|
+
for (const [sourcePath, sourceHash] of sourceHashEntries) {
|
|
377
|
+
string(sourcePath, 'event graph.sourceHashes key', 32_768);
|
|
378
|
+
sha256(sourceHash, `event graph.sourceHashes[${sourcePath}]`);
|
|
379
|
+
}
|
|
380
|
+
const filesScanned = validateStringArray(graph.filesScanned, 'event graph.filesScanned', MAX_SOURCE_RECORDS, 32_768);
|
|
381
|
+
assertUnique(filesScanned, 'event graph.filesScanned');
|
|
382
|
+
const skippedSourceCount = safeInteger(graph.skippedSourceCount, 'event graph.skippedSourceCount');
|
|
383
|
+
const skippedSources = array(graph.skippedSources, 'event graph.skippedSources', MAX_SOURCE_RECORDS);
|
|
384
|
+
if (skippedSources.length > skippedSourceCount) {
|
|
385
|
+
invalid('event graph.skippedSources exceeds the declared skipped source count');
|
|
386
|
+
}
|
|
387
|
+
for (const [index, skipped] of skippedSources.entries()) {
|
|
388
|
+
validateSkippedSource(skipped, `event graph.skippedSources[${index}]`);
|
|
389
|
+
}
|
|
390
|
+
if (graph.complete === true && skippedSourceCount !== 0) {
|
|
391
|
+
invalid('A complete event graph cannot declare skipped sources');
|
|
392
|
+
}
|
|
393
|
+
const nodes = array(graph.nodes, 'event graph.nodes', EVENT_GRAPH_MAX_NODES);
|
|
394
|
+
const nodeIds = [];
|
|
395
|
+
const nodeKindById = new Map();
|
|
396
|
+
for (const [index, candidate] of nodes.entries()) {
|
|
397
|
+
if ((index & 255) === 0)
|
|
398
|
+
signal?.throwIfAborted();
|
|
399
|
+
const node = record(candidate, `event graph.nodes[${index}]`);
|
|
400
|
+
const id = string(node.id, `event graph.nodes[${index}].id`, 32_768);
|
|
401
|
+
const kind = enumeration(node.kind, `event graph.nodes[${index}].kind`, nodeKinds);
|
|
402
|
+
string(node.label, `event graph.nodes[${index}].label`);
|
|
403
|
+
optionalString(node.eventId, `event graph.nodes[${index}].eventId`, 32_768);
|
|
404
|
+
optionalString(node.namespace, `event graph.nodes[${index}].namespace`, 32_768);
|
|
405
|
+
optionalString(node.sourcePath, `event graph.nodes[${index}].sourcePath`, 32_768);
|
|
406
|
+
optionalLocation(node.location, `event graph.nodes[${index}].location`);
|
|
407
|
+
record(node.metadata, `event graph.nodes[${index}].metadata`);
|
|
408
|
+
nodeIds.push(id);
|
|
409
|
+
nodeKindById.set(id, kind);
|
|
410
|
+
}
|
|
411
|
+
assertUnique(nodeIds, 'event graph.nodes');
|
|
412
|
+
const knownNodeIds = new Set(nodeIds);
|
|
413
|
+
const edges = array(graph.edges, 'event graph.edges', EVENT_GRAPH_MAX_EDGES);
|
|
414
|
+
const edgeIds = [];
|
|
415
|
+
let derivedEdgeCount = 0;
|
|
416
|
+
for (const [index, candidate] of edges.entries()) {
|
|
417
|
+
if ((index & 255) === 0)
|
|
418
|
+
signal?.throwIfAborted();
|
|
419
|
+
const edge = record(candidate, `event graph.edges[${index}]`);
|
|
420
|
+
const id = string(edge.id, `event graph.edges[${index}].id`, 32_768);
|
|
421
|
+
const from = string(edge.from, `event graph.edges[${index}].from`, 32_768);
|
|
422
|
+
const to = string(edge.to, `event graph.edges[${index}].to`, 32_768);
|
|
423
|
+
if (!knownNodeIds.has(from) || !knownNodeIds.has(to)) {
|
|
424
|
+
invalid('Event graph edge references an unknown node', { edgeId: id, from, to });
|
|
425
|
+
}
|
|
426
|
+
enumeration(edge.reason, `event graph.edges[${index}].reason`, edgeReasons);
|
|
427
|
+
validateConditions(edge.conditions, `event graph.edges[${index}].conditions`);
|
|
428
|
+
validateStringArray(edge.helperStack, `event graph.edges[${index}].helperStack`, EVENT_GRAPH_MAX_HELPER_DEPTH, 32_768);
|
|
429
|
+
validateLocation(edge.location, `event graph.edges[${index}].location`);
|
|
430
|
+
for (const [provenanceIndex, candidateProvenance] of array(edge.provenance, `event graph.edges[${index}].provenance`, MAX_PROVENANCE_PER_EDGE).entries()) {
|
|
431
|
+
const provenance = record(candidateProvenance, `event graph.edges[${index}].provenance[${provenanceIndex}]`);
|
|
432
|
+
enumeration(provenance.role, `event graph.edges[${index}].provenance[${provenanceIndex}].role`, provenanceRoles);
|
|
433
|
+
validateLocation(provenance.location, `event graph.edges[${index}].provenance[${provenanceIndex}].location`);
|
|
434
|
+
}
|
|
435
|
+
enumeration(edge.confidence, `event graph.edges[${index}].confidence`, confidenceValues);
|
|
436
|
+
if (boolean(edge.derived, `event graph.edges[${index}].derived`))
|
|
437
|
+
derivedEdgeCount += 1;
|
|
438
|
+
if (edge.timing !== undefined)
|
|
439
|
+
validateTiming(edge.timing, `event graph.edges[${index}].timing`);
|
|
440
|
+
if (edge.weight !== undefined)
|
|
441
|
+
validateWeight(edge.weight, `event graph.edges[${index}].weight`);
|
|
442
|
+
if (edge.scope !== undefined) {
|
|
443
|
+
validateScopeTransition(edge.scope, `event graph.edges[${index}].scope`);
|
|
444
|
+
}
|
|
445
|
+
record(edge.metadata, `event graph.edges[${index}].metadata`);
|
|
446
|
+
edgeIds.push(id);
|
|
447
|
+
}
|
|
448
|
+
assertUnique(edgeIds, 'event graph.edges');
|
|
449
|
+
const stateAccesses = array(graph.stateAccesses, 'event graph.stateAccesses', EVENT_GRAPH_MAX_STATE_ACCESSES);
|
|
450
|
+
const stateAccessIds = [];
|
|
451
|
+
for (const [index, candidate] of stateAccesses.entries()) {
|
|
452
|
+
if ((index & 255) === 0)
|
|
453
|
+
signal?.throwIfAborted();
|
|
454
|
+
const access = record(candidate, `event graph.stateAccesses[${index}]`);
|
|
455
|
+
const id = string(access.id, `event graph.stateAccesses[${index}].id`, 32_768);
|
|
456
|
+
const ownerId = string(access.ownerId, `event graph.stateAccesses[${index}].ownerId`, 32_768);
|
|
457
|
+
if (!knownNodeIds.has(ownerId)) {
|
|
458
|
+
invalid('Event graph state access references an unknown owner', { accessId: id, ownerId });
|
|
459
|
+
}
|
|
460
|
+
enumeration(access.kind, `event graph.stateAccesses[${index}].kind`, stateKinds);
|
|
461
|
+
string(access.name, `event graph.stateAccesses[${index}].name`, 32_768);
|
|
462
|
+
enumeration(access.access, `event graph.stateAccesses[${index}].access`, stateAccessKinds);
|
|
463
|
+
validateLocation(access.location, `event graph.stateAccesses[${index}].location`);
|
|
464
|
+
enumeration(access.confidence, `event graph.stateAccesses[${index}].confidence`, confidenceValues);
|
|
465
|
+
enumeration(access.scope, `event graph.stateAccesses[${index}].scope`, scopeKinds);
|
|
466
|
+
validateStringArray(access.helperStack, `event graph.stateAccesses[${index}].helperStack`, EVENT_GRAPH_MAX_HELPER_DEPTH, 32_768);
|
|
467
|
+
validateConditions(access.conditions, `event graph.stateAccesses[${index}].conditions`);
|
|
468
|
+
boolean(access.dynamic, `event graph.stateAccesses[${index}].dynamic`);
|
|
469
|
+
record(access.metadata, `event graph.stateAccesses[${index}].metadata`);
|
|
470
|
+
stateAccessIds.push(id);
|
|
471
|
+
}
|
|
472
|
+
assertUnique(stateAccessIds, 'event graph.stateAccesses');
|
|
473
|
+
const knownStateAccessIds = new Set(stateAccessIds);
|
|
474
|
+
const stateLinks = array(graph.stateLinks, 'event graph.stateLinks', EVENT_GRAPH_MAX_STATE_LINKS);
|
|
475
|
+
const stateLinkIds = [];
|
|
476
|
+
for (const [index, candidate] of stateLinks.entries()) {
|
|
477
|
+
if ((index & 255) === 0)
|
|
478
|
+
signal?.throwIfAborted();
|
|
479
|
+
const link = record(candidate, `event graph.stateLinks[${index}]`);
|
|
480
|
+
const id = string(link.id, `event graph.stateLinks[${index}].id`, 32_768);
|
|
481
|
+
enumeration(link.stateKind, `event graph.stateLinks[${index}].stateKind`, stateKinds);
|
|
482
|
+
string(link.name, `event graph.stateLinks[${index}].name`, 32_768);
|
|
483
|
+
const producerId = string(link.producerId, `event graph.stateLinks[${index}].producerId`, 32_768);
|
|
484
|
+
const consumerId = string(link.consumerId, `event graph.stateLinks[${index}].consumerId`, 32_768);
|
|
485
|
+
if (!knownStateAccessIds.has(producerId) || !knownStateAccessIds.has(consumerId)) {
|
|
486
|
+
invalid('Event graph state link references an unknown access', {
|
|
487
|
+
linkId: id,
|
|
488
|
+
producerId,
|
|
489
|
+
consumerId,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
enumeration(link.confidence, `event graph.stateLinks[${index}].confidence`, confidenceValues);
|
|
493
|
+
boolean(link.pathConfirmed, `event graph.stateLinks[${index}].pathConfirmed`);
|
|
494
|
+
stateLinkIds.push(id);
|
|
495
|
+
}
|
|
496
|
+
assertUnique(stateLinkIds, 'event graph.stateLinks');
|
|
497
|
+
const issues = array(graph.issues, 'event graph.issues', EVENT_GRAPH_MAX_ISSUES);
|
|
498
|
+
for (const [index, candidate] of issues.entries()) {
|
|
499
|
+
if ((index & 255) === 0)
|
|
500
|
+
signal?.throwIfAborted();
|
|
501
|
+
const issue = record(candidate, `event graph.issues[${index}]`);
|
|
502
|
+
string(issue.code, `event graph.issues[${index}].code`, 1_024);
|
|
503
|
+
enumeration(issue.classification, `event graph.issues[${index}].classification`, defectClasses);
|
|
504
|
+
enumeration(issue.severity, `event graph.issues[${index}].severity`, severities);
|
|
505
|
+
string(issue.message, `event graph.issues[${index}].message`);
|
|
506
|
+
enumeration(issue.confidence, `event graph.issues[${index}].confidence`, confidenceValues);
|
|
507
|
+
optionalLocation(issue.location, `event graph.issues[${index}].location`);
|
|
508
|
+
if (issue.related !== undefined) {
|
|
509
|
+
for (const [relatedIndex, location] of array(issue.related, `event graph.issues[${index}].related`, MAX_RELATED_LOCATIONS).entries()) {
|
|
510
|
+
validateLocation(location, `event graph.issues[${index}].related[${relatedIndex}]`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
for (const [blockerIndex, blocker] of array(issue.blockers, `event graph.issues[${index}].blockers`, 10_000).entries()) {
|
|
514
|
+
validateBlocker(blocker, `event graph.issues[${index}].blockers[${blockerIndex}]`);
|
|
515
|
+
}
|
|
516
|
+
record(issue.details, `event graph.issues[${index}].details`);
|
|
517
|
+
}
|
|
518
|
+
const diagnostics = array(graph.diagnostics, 'event graph.diagnostics', MAX_DIAGNOSTICS);
|
|
519
|
+
for (const [index, diagnostic] of diagnostics.entries()) {
|
|
520
|
+
if ((index & 255) === 0)
|
|
521
|
+
signal?.throwIfAborted();
|
|
522
|
+
validateDiagnostic(diagnostic, `event graph.diagnostics[${index}]`);
|
|
523
|
+
}
|
|
524
|
+
const unresolved = array(graph.unresolved, 'event graph.unresolved', EVENT_GRAPH_MAX_UNRESOLVED);
|
|
525
|
+
const unresolvedIds = [];
|
|
526
|
+
for (const [index, candidate] of unresolved.entries()) {
|
|
527
|
+
if ((index & 255) === 0)
|
|
528
|
+
signal?.throwIfAborted();
|
|
529
|
+
const item = record(candidate, `event graph.unresolved[${index}]`);
|
|
530
|
+
const id = string(item.id, `event graph.unresolved[${index}].id`, 32_768);
|
|
531
|
+
enumeration(item.kind, `event graph.unresolved[${index}].kind`, unresolvedKinds);
|
|
532
|
+
string(item.expression, `event graph.unresolved[${index}].expression`);
|
|
533
|
+
if (item.ownerId !== undefined) {
|
|
534
|
+
const ownerId = string(item.ownerId, `event graph.unresolved[${index}].ownerId`, 32_768);
|
|
535
|
+
if (!knownNodeIds.has(ownerId)) {
|
|
536
|
+
invalid('Event graph unresolved analysis references an unknown owner', { id, ownerId });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
optionalLocation(item.location, `event graph.unresolved[${index}].location`);
|
|
540
|
+
enumeration(item.confidence, `event graph.unresolved[${index}].confidence`, confidenceValues);
|
|
541
|
+
for (const [blockerIndex, blocker] of array(item.blockers, `event graph.unresolved[${index}].blockers`, 10_000).entries()) {
|
|
542
|
+
validateBlocker(blocker, `event graph.unresolved[${index}].blockers[${blockerIndex}]`);
|
|
543
|
+
}
|
|
544
|
+
unresolvedIds.push(id);
|
|
545
|
+
}
|
|
546
|
+
assertUnique(unresolvedIds, 'event graph.unresolved');
|
|
547
|
+
const statistics = record(graph.statistics, 'event graph.statistics');
|
|
548
|
+
const expectedStatistics = {
|
|
549
|
+
eventCount: [...nodeKindById.values()].filter((kind) => kind === 'event').length,
|
|
550
|
+
optionCount: [...nodeKindById.values()].filter((kind) => kind === 'option').length,
|
|
551
|
+
entryCount: [...nodeKindById.values()].filter((kind) => kind === 'entry').length,
|
|
552
|
+
helperCount: [...nodeKindById.values()].filter((kind) => kind === 'helper').length,
|
|
553
|
+
unresolvedNodeCount: [...nodeKindById.values()].filter((kind) => kind === 'unresolved').length,
|
|
554
|
+
terminalCount: [...nodeKindById.values()].filter((kind) => kind === 'terminal').length,
|
|
555
|
+
edgeCount: edges.length,
|
|
556
|
+
derivedEdgeCount,
|
|
557
|
+
stateAccessCount: stateAccesses.length,
|
|
558
|
+
issueCount: issues.length,
|
|
559
|
+
};
|
|
560
|
+
for (const [key, expected] of Object.entries(expectedStatistics)) {
|
|
561
|
+
const actual = safeInteger(statistics[key], `event graph.statistics.${key}`);
|
|
562
|
+
if (actual !== expected) {
|
|
563
|
+
invalid('Event graph statistics do not match the graph structure', {
|
|
564
|
+
field: key,
|
|
565
|
+
expected,
|
|
566
|
+
actual,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
signal?.throwIfAborted();
|
|
571
|
+
return graph;
|
|
572
|
+
}
|