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.
@@ -0,0 +1,413 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
3
+ import { reachableEventNodeIds } from './algorithms.js';
4
+ import { discoverEventRoots } from './queries.js';
5
+ function semanticNode(node) {
6
+ return {
7
+ ...node,
8
+ location: undefined,
9
+ sourcePath: node.sourcePath,
10
+ };
11
+ }
12
+ function semanticCondition(condition) {
13
+ return { ...condition, location: { path: condition.location.path } };
14
+ }
15
+ function semanticEdge(edge) {
16
+ return {
17
+ ...edge,
18
+ location: { path: edge.location.path },
19
+ conditions: edge.conditions.map(semanticCondition),
20
+ provenance: edge.provenance.map(({ role, location }) => ({
21
+ role,
22
+ location: { path: location.path },
23
+ })),
24
+ weight: edge.weight === undefined
25
+ ? undefined
26
+ : { ...edge.weight, modifiers: edge.weight.modifiers.map(semanticCondition) },
27
+ };
28
+ }
29
+ function semanticStateAccess(access) {
30
+ return {
31
+ ...access,
32
+ location: { path: access.location.path },
33
+ conditions: access.conditions.map(semanticCondition),
34
+ };
35
+ }
36
+ function semanticIssue(issue) {
37
+ return {
38
+ ...issue,
39
+ location: issue.location === undefined ? undefined : { path: issue.location.path },
40
+ related: issue.related?.map(({ path }) => ({ path })),
41
+ blockers: issue.blockers.map((blocker) => ({
42
+ ...blocker,
43
+ location: blocker.location === undefined ? undefined : { path: blocker.location.path },
44
+ })),
45
+ };
46
+ }
47
+ function issueId(issue) {
48
+ return `issue:${issue.code}:${hashCanonical(semanticIssue(issue)).slice(0, 24)}`;
49
+ }
50
+ function semanticUnresolved(item) {
51
+ return {
52
+ ...item,
53
+ location: item.location === undefined ? undefined : { path: item.location.path },
54
+ blockers: item.blockers.map((blocker) => ({
55
+ ...blocker,
56
+ location: blocker.location === undefined ? undefined : { path: blocker.location.path },
57
+ })),
58
+ };
59
+ }
60
+ function semanticCollectionHash(domain, values, identity, semantic, signal) {
61
+ const records = values.map((value, index) => {
62
+ if ((index & 255) === 0)
63
+ signal?.throwIfAborted();
64
+ return { id: identity(value), hash: hashCanonical(semantic(value)) };
65
+ });
66
+ records.sort((left, right) => compareCodeUnits(left.id, right.id) || compareCodeUnits(left.hash, right.hash));
67
+ const digest = createHash('sha256');
68
+ const append = (value) => {
69
+ digest.update(String(Buffer.byteLength(value, 'utf8')));
70
+ digest.update(':');
71
+ digest.update(value);
72
+ };
73
+ append(domain);
74
+ for (const [index, record] of records.entries()) {
75
+ if ((index & 255) === 0)
76
+ signal?.throwIfAborted();
77
+ append(record.id);
78
+ append(record.hash);
79
+ }
80
+ signal?.throwIfAborted();
81
+ return digest.digest('hex');
82
+ }
83
+ function graphHash(graph, signal) {
84
+ signal?.throwIfAborted();
85
+ return hashCanonical({
86
+ schema: 'event-semantic-graph-hash.v2',
87
+ nodes: semanticCollectionHash('nodes', graph.nodes, ({ id }) => id, semanticNode, signal),
88
+ edges: semanticCollectionHash('edges', graph.edges, ({ id }) => id, semanticEdge, signal),
89
+ stateAccesses: semanticCollectionHash('state-accesses', graph.stateAccesses, ({ id }) => id, semanticStateAccess, signal),
90
+ stateLinks: semanticCollectionHash('state-links', graph.stateLinks, ({ id }) => id, (value) => value, signal),
91
+ issues: semanticCollectionHash('issues', graph.issues, issueId, semanticIssue, signal),
92
+ unresolved: semanticCollectionHash('unresolved', graph.unresolved, ({ id }) => id, semanticUnresolved, signal),
93
+ });
94
+ }
95
+ function mapById(values, signal) {
96
+ const result = new Map();
97
+ for (const [index, value] of values.entries()) {
98
+ if ((index & 255) === 0)
99
+ signal?.throwIfAborted();
100
+ result.set(value.id, value);
101
+ }
102
+ return result;
103
+ }
104
+ function incomingCallers(graph, signal) {
105
+ const callers = new Map();
106
+ for (const [index, edge] of graph.edges.entries()) {
107
+ if ((index & 255) === 0)
108
+ signal?.throwIfAborted();
109
+ if (edge.derived)
110
+ continue;
111
+ const values = callers.get(edge.to) ?? [];
112
+ values.push(edge.from);
113
+ callers.set(edge.to, values);
114
+ }
115
+ for (const values of callers.values())
116
+ values.sort(compareCodeUnits);
117
+ return callers;
118
+ }
119
+ function rootEventIds(roots) {
120
+ return new Set(roots.knownRootEventIds);
121
+ }
122
+ function disconnectedByKind(before, after, kind, signal) {
123
+ const rootNodeIds = (graph) => {
124
+ const roots = discoverEventRoots(graph, signal);
125
+ return [...roots.entryPoints, ...roots.automaticEvents].map(({ id }) => id);
126
+ };
127
+ const beforeReachable = new Set(reachableEventNodeIds(before, rootNodeIds(before), 'downstream', false, signal));
128
+ const afterReachable = new Set(reachableEventNodeIds(after, rootNodeIds(after), 'downstream', false, signal));
129
+ const afterNodeIds = new Set(after.nodes.map(({ id }) => id));
130
+ return before.nodes
131
+ .filter((node, index) => {
132
+ if ((index & 255) === 0)
133
+ signal?.throwIfAborted();
134
+ return node.kind === kind && afterNodeIds.has(node.id);
135
+ })
136
+ .filter(({ id }) => beforeReachable.has(id) && !afterReachable.has(id))
137
+ .map(({ id }) => id)
138
+ .sort(compareCodeUnits);
139
+ }
140
+ export function compareEventGraphs(before, after, signal) {
141
+ signal?.throwIfAborted();
142
+ let work = 0;
143
+ const check = () => {
144
+ if ((work++ & 255) === 0)
145
+ signal?.throwIfAborted();
146
+ };
147
+ const beforeNodes = mapById(before.nodes, signal);
148
+ const afterNodes = mapById(after.nodes, signal);
149
+ const beforeEdges = mapById(before.edges, signal);
150
+ const afterEdges = mapById(after.edges, signal);
151
+ const beforeStateAccesses = mapById(before.stateAccesses, signal);
152
+ const afterStateAccesses = mapById(after.stateAccesses, signal);
153
+ const beforeStateLinks = mapById(before.stateLinks, signal);
154
+ const afterStateLinks = mapById(after.stateLinks, signal);
155
+ const beforeIssues = new Map(before.issues.map((issue) => {
156
+ check();
157
+ return [issueId(issue), issue];
158
+ }));
159
+ const afterIssues = new Map(after.issues.map((issue) => {
160
+ check();
161
+ return [issueId(issue), issue];
162
+ }));
163
+ const beforeUnresolved = mapById(before.unresolved, signal);
164
+ const afterUnresolved = mapById(after.unresolved, signal);
165
+ const addedNodeIds = [...afterNodes.keys()]
166
+ .filter((id) => {
167
+ check();
168
+ return !beforeNodes.has(id);
169
+ })
170
+ .sort(compareCodeUnits);
171
+ const removedNodeIds = [...beforeNodes.keys()]
172
+ .filter((id) => {
173
+ check();
174
+ return !afterNodes.has(id);
175
+ })
176
+ .sort(compareCodeUnits);
177
+ const changedNodeIds = [...beforeNodes.keys()]
178
+ .filter((id) => {
179
+ check();
180
+ const current = afterNodes.get(id);
181
+ return (current !== undefined &&
182
+ hashCanonical(semanticNode(beforeNodes.get(id))) !== hashCanonical(semanticNode(current)));
183
+ })
184
+ .sort(compareCodeUnits);
185
+ const addedEdgeIds = [...afterEdges.keys()]
186
+ .filter((id) => {
187
+ check();
188
+ return !beforeEdges.has(id);
189
+ })
190
+ .sort(compareCodeUnits);
191
+ const removedEdgeIds = [...beforeEdges.keys()]
192
+ .filter((id) => {
193
+ check();
194
+ return !afterEdges.has(id);
195
+ })
196
+ .sort(compareCodeUnits);
197
+ const changedEdgeIds = [...beforeEdges.keys()]
198
+ .filter((id) => {
199
+ check();
200
+ const current = afterEdges.get(id);
201
+ return (current !== undefined &&
202
+ hashCanonical(semanticEdge(beforeEdges.get(id))) !== hashCanonical(semanticEdge(current)));
203
+ })
204
+ .sort(compareCodeUnits);
205
+ const addedStateAccessIds = [...afterStateAccesses.keys()]
206
+ .filter((id) => {
207
+ check();
208
+ return !beforeStateAccesses.has(id);
209
+ })
210
+ .sort(compareCodeUnits);
211
+ const removedStateAccessIds = [...beforeStateAccesses.keys()]
212
+ .filter((id) => {
213
+ check();
214
+ return !afterStateAccesses.has(id);
215
+ })
216
+ .sort(compareCodeUnits);
217
+ const changedStateAccessIds = [...beforeStateAccesses.keys()]
218
+ .filter((id) => {
219
+ check();
220
+ const current = afterStateAccesses.get(id);
221
+ return (current !== undefined &&
222
+ hashCanonical(semanticStateAccess(beforeStateAccesses.get(id))) !==
223
+ hashCanonical(semanticStateAccess(current)));
224
+ })
225
+ .sort(compareCodeUnits);
226
+ const addedStateLinkIds = [...afterStateLinks.keys()]
227
+ .filter((id) => {
228
+ check();
229
+ return !beforeStateLinks.has(id);
230
+ })
231
+ .sort(compareCodeUnits);
232
+ const removedStateLinkIds = [...beforeStateLinks.keys()]
233
+ .filter((id) => {
234
+ check();
235
+ return !afterStateLinks.has(id);
236
+ })
237
+ .sort(compareCodeUnits);
238
+ const changedStateLinkIds = [...beforeStateLinks.keys()]
239
+ .filter((id) => {
240
+ check();
241
+ const current = afterStateLinks.get(id);
242
+ return (current !== undefined && hashCanonical(beforeStateLinks.get(id)) !== hashCanonical(current));
243
+ })
244
+ .sort(compareCodeUnits);
245
+ const addedIssueIds = [...afterIssues.keys()]
246
+ .filter((id) => {
247
+ check();
248
+ return !beforeIssues.has(id);
249
+ })
250
+ .sort(compareCodeUnits);
251
+ const resolvedIssueIds = [...beforeIssues.keys()]
252
+ .filter((id) => {
253
+ check();
254
+ return !afterIssues.has(id);
255
+ })
256
+ .sort(compareCodeUnits);
257
+ const addedUnresolvedIds = [...afterUnresolved.keys()]
258
+ .filter((id) => {
259
+ check();
260
+ return !beforeUnresolved.has(id);
261
+ })
262
+ .sort(compareCodeUnits);
263
+ const resolvedUnresolvedIds = [...beforeUnresolved.keys()]
264
+ .filter((id) => {
265
+ check();
266
+ return !afterUnresolved.has(id);
267
+ })
268
+ .sort(compareCodeUnits);
269
+ const beforeCallers = incomingCallers(before, signal);
270
+ const afterCallers = incomingCallers(after, signal);
271
+ const callerRemoved = [...afterNodes.keys()]
272
+ .filter((id) => {
273
+ check();
274
+ return (beforeCallers.get(id)?.length ?? 0) > (afterCallers.get(id)?.length ?? 0);
275
+ })
276
+ .sort(compareCodeUnits);
277
+ const beforeRoots = rootEventIds(discoverEventRoots(before, signal));
278
+ const afterRoots = rootEventIds(discoverEventRoots(after, signal));
279
+ const newlyDisconnectedRootIds = before.nodes
280
+ .filter(({ kind, eventId }) => kind === 'event' &&
281
+ eventId !== undefined &&
282
+ beforeRoots.has(eventId) &&
283
+ !afterRoots.has(eventId))
284
+ .map(({ id }) => id)
285
+ .sort(compareCodeUnits);
286
+ const newlyDisconnectedBranchIds = disconnectedByKind(before, after, 'option', signal);
287
+ const newlyDisconnectedTerminalIds = disconnectedByKind(before, after, 'terminal', signal);
288
+ const changes = [];
289
+ const add = (kind, subjectId, beforeValue, afterValue, relatedIds = []) => {
290
+ changes.push({
291
+ id: `${kind}:${subjectId}`,
292
+ kind,
293
+ subjectId,
294
+ ...(beforeValue === undefined ? {} : { beforeHash: hashCanonical(beforeValue) }),
295
+ ...(afterValue === undefined ? {} : { afterHash: hashCanonical(afterValue) }),
296
+ relatedIds: [...relatedIds].sort(compareCodeUnits),
297
+ });
298
+ };
299
+ for (const id of addedNodeIds) {
300
+ check();
301
+ add('node_added', id, undefined, semanticNode(afterNodes.get(id)));
302
+ }
303
+ for (const id of removedNodeIds) {
304
+ check();
305
+ add('node_removed', id, semanticNode(beforeNodes.get(id)));
306
+ }
307
+ for (const id of changedNodeIds) {
308
+ check();
309
+ add('node_changed', id, semanticNode(beforeNodes.get(id)), semanticNode(afterNodes.get(id)));
310
+ }
311
+ for (const id of addedEdgeIds) {
312
+ check();
313
+ add('edge_added', id, undefined, semanticEdge(afterEdges.get(id)));
314
+ }
315
+ for (const id of removedEdgeIds) {
316
+ check();
317
+ add('edge_removed', id, semanticEdge(beforeEdges.get(id)));
318
+ }
319
+ for (const id of changedEdgeIds) {
320
+ check();
321
+ add('edge_changed', id, semanticEdge(beforeEdges.get(id)), semanticEdge(afterEdges.get(id)));
322
+ }
323
+ for (const id of addedStateAccessIds) {
324
+ check();
325
+ add('state_access_added', id, undefined, semanticStateAccess(afterStateAccesses.get(id)));
326
+ }
327
+ for (const id of removedStateAccessIds) {
328
+ check();
329
+ add('state_access_removed', id, semanticStateAccess(beforeStateAccesses.get(id)));
330
+ }
331
+ for (const id of changedStateAccessIds) {
332
+ check();
333
+ add('state_access_changed', id, semanticStateAccess(beforeStateAccesses.get(id)), semanticStateAccess(afterStateAccesses.get(id)));
334
+ }
335
+ for (const id of addedStateLinkIds) {
336
+ check();
337
+ add('state_link_added', id, undefined, afterStateLinks.get(id));
338
+ }
339
+ for (const id of removedStateLinkIds) {
340
+ check();
341
+ add('state_link_removed', id, beforeStateLinks.get(id));
342
+ }
343
+ for (const id of changedStateLinkIds) {
344
+ check();
345
+ add('state_link_changed', id, beforeStateLinks.get(id), afterStateLinks.get(id));
346
+ }
347
+ for (const id of addedIssueIds) {
348
+ check();
349
+ add('diagnostic_added', id, undefined, semanticIssue(afterIssues.get(id)));
350
+ }
351
+ for (const id of resolvedIssueIds) {
352
+ check();
353
+ add('diagnostic_resolved', id, semanticIssue(beforeIssues.get(id)));
354
+ }
355
+ for (const id of addedUnresolvedIds) {
356
+ check();
357
+ add('unresolved_added', id, undefined, semanticUnresolved(afterUnresolved.get(id)));
358
+ }
359
+ for (const id of resolvedUnresolvedIds) {
360
+ check();
361
+ add('unresolved_resolved', id, semanticUnresolved(beforeUnresolved.get(id)));
362
+ }
363
+ for (const id of callerRemoved) {
364
+ check();
365
+ add('caller_removed', id, beforeCallers.get(id), afterCallers.get(id), [
366
+ ...(beforeCallers.get(id) ?? []),
367
+ ...(afterCallers.get(id) ?? []),
368
+ ]);
369
+ }
370
+ for (const id of newlyDisconnectedRootIds) {
371
+ check();
372
+ add('root_disconnected', id);
373
+ }
374
+ for (const id of newlyDisconnectedBranchIds) {
375
+ check();
376
+ add('branch_disconnected', id);
377
+ }
378
+ for (const id of newlyDisconnectedTerminalIds) {
379
+ check();
380
+ add('terminal_disconnected', id);
381
+ }
382
+ changes.sort((left, right) => compareCodeUnits(left.kind, right.kind) || compareCodeUnits(left.subjectId, right.subjectId));
383
+ return {
384
+ schemaVersion: 'event-comparison.v1',
385
+ beforeRevision: before.revision,
386
+ afterRevision: after.revision,
387
+ beforeGraphHash: graphHash(before, signal),
388
+ afterGraphHash: graphHash(after, signal),
389
+ changes,
390
+ addedNodeIds,
391
+ removedNodeIds,
392
+ changedNodeIds,
393
+ addedEdgeIds,
394
+ removedEdgeIds,
395
+ changedEdgeIds,
396
+ addedStateAccessIds,
397
+ removedStateAccessIds,
398
+ changedStateAccessIds,
399
+ addedStateLinkIds,
400
+ removedStateLinkIds,
401
+ changedStateLinkIds,
402
+ addedIssueIds,
403
+ resolvedIssueIds,
404
+ addedUnresolvedIds,
405
+ resolvedUnresolvedIds,
406
+ newlyDisconnectedRootIds,
407
+ newlyDisconnectedBranchIds,
408
+ newlyDisconnectedTerminalIds,
409
+ };
410
+ }
411
+ export function eventGraphHash(graph, signal) {
412
+ return graphHash(graph, signal);
413
+ }