hoi4-agent-tools 1.1.0 → 2.0.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +21 -18
  3. package/dist/hoi4_agent_tools/core/engine.js +12 -0
  4. package/dist/hoi4_agent_tools/core/workspace.js +31 -3
  5. package/dist/hoi4_agent_tools/event/algorithms.js +208 -0
  6. package/dist/hoi4_agent_tools/event/artifact-validation.js +572 -0
  7. package/dist/hoi4_agent_tools/event/compare.js +413 -0
  8. package/dist/hoi4_agent_tools/event/graph.js +1586 -0
  9. package/dist/hoi4_agent_tools/event/index.js +10 -0
  10. package/dist/hoi4_agent_tools/event/layout.js +218 -0
  11. package/dist/hoi4_agent_tools/event/limits.js +41 -0
  12. package/dist/hoi4_agent_tools/event/model.js +2 -0
  13. package/dist/hoi4_agent_tools/event/queries.js +643 -0
  14. package/dist/hoi4_agent_tools/event/render.js +443 -0
  15. package/dist/hoi4_agent_tools/event/scc.js +72 -0
  16. package/dist/hoi4_agent_tools/event/service.js +1147 -0
  17. package/dist/hoi4_agent_tools/event/source-analysis.js +1458 -0
  18. package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
  19. package/dist/hoi4_agent_tools/gui/layout.js +122 -27
  20. package/dist/hoi4_agent_tools/gui/renderer.js +16 -0
  21. package/dist/hoi4_agent_tools/mcp/resources/register.js +1 -1
  22. package/dist/hoi4_agent_tools/mcp/server/create.js +4 -2
  23. package/dist/hoi4_agent_tools/mcp/server/result.js +8 -14
  24. package/dist/hoi4_agent_tools/mcp/tools/event.js +459 -0
  25. package/dist/hoi4_agent_tools/mcp/tools/focus.js +6 -3
  26. package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -3
  27. package/dist/hoi4_agent_tools/mcp/tools/map.js +6 -3
  28. package/dist/hoi4_agent_tools/runtime.js +112 -2
  29. package/dist/hoi4_agent_tools/schemas/common.js +5 -1
  30. package/dist/hoi4_agent_tools/schemas/event.js +95 -0
  31. package/dist/hoi4_agent_tools/version.js +1 -1
  32. package/docs/README.md +1 -0
  33. package/docs/development.md +10 -0
  34. package/docs/events.md +113 -0
  35. package/docs/focus.md +1 -1
  36. package/docs/gui.md +3 -1
  37. package/docs/map.md +1 -1
  38. package/docs/setup.md +10 -7
  39. package/package.json +7 -3
  40. package/server.json +3 -3
  41. package/dist/hoi4_agent_tools/mcp/tools/mods.js +0 -54
@@ -0,0 +1,10 @@
1
+ export * from './algorithms.js';
2
+ export * from './compare.js';
3
+ export * from './graph.js';
4
+ export * from './layout.js';
5
+ export * from './limits.js';
6
+ export * from './model.js';
7
+ export * from './queries.js';
8
+ export * from './render.js';
9
+ export * from './service.js';
10
+ export * from './source-analysis.js';
@@ -0,0 +1,218 @@
1
+ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
2
+ import { RENDER_MAX_DIMENSION, RENDER_MAX_PIXELS, renderDimensionViolation, } from '../core/render-budget.js';
3
+ import { eventFlowEdges, eventStronglyConnectedComponents } from './algorithms.js';
4
+ const STABLE_LAYER_ROWS = 64;
5
+ function stableAnchor(id) {
6
+ return Number.parseInt(hashCanonical(id).slice(0, 8), 16) >>> 0;
7
+ }
8
+ function dimensionsFor(nodes, padding) {
9
+ return {
10
+ width: Math.max(320, ...nodes.map(({ x, width }) => Math.ceil(x + width + padding))),
11
+ height: Math.max(200, ...nodes.map(({ y, height }) => Math.ceil(y + height + padding))),
12
+ };
13
+ }
14
+ function assignStableSlots(ids, capacity) {
15
+ const assigned = new Map();
16
+ const occupied = new Set();
17
+ const ordered = [...ids].sort((left, right) => stableAnchor(left) - stableAnchor(right) || compareCodeUnits(left, right));
18
+ for (const id of ordered) {
19
+ let slot = stableAnchor(id) % capacity;
20
+ while (occupied.has(slot))
21
+ slot = (slot + 1) % capacity;
22
+ occupied.add(slot);
23
+ assigned.set(id, slot);
24
+ }
25
+ return assigned;
26
+ }
27
+ function shardLayout(nodes, nodeWidth, nodeHeight, horizontalGap, verticalGap, padding) {
28
+ const columns = Math.max(1, Math.ceil(Math.sqrt((nodes.length * (nodeHeight + verticalGap)) / (nodeWidth + horizontalGap))));
29
+ const slots = new Map([...nodes]
30
+ .sort((left, right) => stableAnchor(left.id) - stableAnchor(right.id) || compareCodeUnits(left.id, right.id))
31
+ .map(({ id }, index) => [id, index]));
32
+ return nodes.map((node) => {
33
+ const slot = slots.get(node.id) ?? 0;
34
+ return {
35
+ ...node,
36
+ x: padding + (slot % columns) * (nodeWidth + horizontalGap),
37
+ y: padding + Math.floor(slot / columns) * (nodeHeight + verticalGap),
38
+ };
39
+ });
40
+ }
41
+ function retainedGraph(graph, selectedNodeIds, expandHelpers = false) {
42
+ const nodes = graph.nodes.filter(({ id }) => selectedNodeIds === undefined || selectedNodeIds.has(id));
43
+ const ids = new Set(nodes.map(({ id }) => id));
44
+ return {
45
+ nodes: nodes.sort((left, right) => compareCodeUnits(left.id, right.id)),
46
+ edges: eventFlowEdges(graph, expandHelpers).filter(({ from, to }) => ids.has(from) && ids.has(to)),
47
+ };
48
+ }
49
+ export function layoutEventGraph(graph, selectedNodeIds, options = {}) {
50
+ options.signal?.throwIfAborted();
51
+ const nodeWidth = options.nodeWidth ?? 220;
52
+ const nodeHeight = options.nodeHeight ?? 72;
53
+ const horizontalGap = options.horizontalGap ?? 96;
54
+ const verticalGap = options.verticalGap ?? 36;
55
+ const padding = options.padding ?? 48;
56
+ const retained = retainedGraph(graph, selectedNodeIds, options.expandHelpers ?? false);
57
+ const retainedIds = new Set(retained.nodes.map(({ id }) => id));
58
+ const components = eventStronglyConnectedComponents(graph, options.expandHelpers ?? false, retainedIds, options.signal);
59
+ const componentByNode = new Map();
60
+ const componentMembers = new Map();
61
+ for (const component of components) {
62
+ componentMembers.set(component.id, component.nodeIds);
63
+ for (const nodeId of component.nodeIds)
64
+ componentByNode.set(nodeId, component.id);
65
+ }
66
+ const componentEdges = new Map();
67
+ const incomingCount = new Map();
68
+ for (const edge of retained.edges) {
69
+ const from = componentByNode.get(edge.from);
70
+ const to = componentByNode.get(edge.to);
71
+ if (from === undefined || to === undefined || from === to)
72
+ continue;
73
+ const targets = componentEdges.get(from) ?? new Set();
74
+ if (!targets.has(to))
75
+ incomingCount.set(to, (incomingCount.get(to) ?? 0) + 1);
76
+ targets.add(to);
77
+ componentEdges.set(from, targets);
78
+ }
79
+ const roots = components
80
+ .map(({ id }) => id)
81
+ .filter((id) => (incomingCount.get(id) ?? 0) === 0)
82
+ .sort(compareCodeUnits);
83
+ const layers = new Map(components.map(({ id }) => [id, 0]));
84
+ const queue = roots.length > 0 ? [...roots] : components.map(({ id }) => id).sort(compareCodeUnits);
85
+ let cursor = 0;
86
+ while (cursor < queue.length) {
87
+ if ((cursor & 255) === 0)
88
+ options.signal?.throwIfAborted();
89
+ const component = queue[cursor++];
90
+ const layer = layers.get(component) ?? 0;
91
+ for (const target of [...(componentEdges.get(component) ?? [])].sort(compareCodeUnits)) {
92
+ const next = Math.max(layers.get(target) ?? 0, layer + 1);
93
+ if (next !== layers.get(target))
94
+ layers.set(target, next);
95
+ incomingCount.set(target, (incomingCount.get(target) ?? 1) - 1);
96
+ if (incomingCount.get(target) === 0)
97
+ queue.push(target);
98
+ }
99
+ }
100
+ const byLayer = new Map();
101
+ for (const component of components) {
102
+ const layer = layers.get(component.id) ?? 0;
103
+ const values = byLayer.get(layer) ?? [];
104
+ values.push(component.id);
105
+ byLayer.set(layer, values);
106
+ }
107
+ for (const values of byLayer.values())
108
+ values.sort(compareCodeUnits);
109
+ let layoutNodes = [];
110
+ for (const [layer, componentIds] of [...byLayer.entries()].sort(([a], [b]) => a - b)) {
111
+ const layerMembers = componentIds.flatMap((componentId) => (componentMembers.get(componentId) ?? []).map((nodeId) => ({ nodeId, componentId })));
112
+ if (layerMembers.length > STABLE_LAYER_ROWS) {
113
+ for (const { nodeId, componentId } of layerMembers) {
114
+ layoutNodes.push({
115
+ id: nodeId,
116
+ x: padding + layer * (nodeWidth + horizontalGap),
117
+ y: padding,
118
+ width: nodeWidth,
119
+ height: nodeHeight,
120
+ layer,
121
+ componentId,
122
+ });
123
+ }
124
+ continue;
125
+ }
126
+ const slots = assignStableSlots(layerMembers.map(({ nodeId }) => nodeId), STABLE_LAYER_ROWS);
127
+ for (const { nodeId, componentId } of layerMembers) {
128
+ layoutNodes.push({
129
+ id: nodeId,
130
+ x: padding + layer * (nodeWidth + horizontalGap),
131
+ y: padding + (slots.get(nodeId) ?? 0) * (nodeHeight + verticalGap),
132
+ width: nodeWidth,
133
+ height: nodeHeight,
134
+ layer,
135
+ componentId,
136
+ });
137
+ }
138
+ }
139
+ let dimensions = dimensionsFor(layoutNodes, padding);
140
+ if (options.compact === true ||
141
+ layoutNodes.some((node, index, values) => values.findIndex(({ x, y }) => x === node.x && y === node.y) !== index) ||
142
+ renderDimensionViolation(dimensions.width, dimensions.height, 'event graph layout', {
143
+ maximumDimension: RENDER_MAX_DIMENSION,
144
+ maximumPixels: RENDER_MAX_PIXELS,
145
+ }) !== undefined) {
146
+ layoutNodes = shardLayout(layoutNodes, nodeWidth, nodeHeight, horizontalGap, verticalGap, padding);
147
+ dimensions = dimensionsFor(layoutNodes, padding);
148
+ }
149
+ layoutNodes.sort((left, right) => compareCodeUnits(left.id, right.id));
150
+ const placed = new Map(layoutNodes.map((node) => [node.id, node]));
151
+ const layoutEdges = retained.edges.flatMap((edge) => {
152
+ const from = placed.get(edge.from);
153
+ const to = placed.get(edge.to);
154
+ if (from === undefined || to === undefined)
155
+ return [];
156
+ if (from.id === to.id) {
157
+ const loopRight = from.x + from.width + horizontalGap / 2;
158
+ const loopTop = Math.max(4, from.y - verticalGap / 2);
159
+ return [
160
+ {
161
+ id: edge.id,
162
+ from: edge.from,
163
+ to: edge.to,
164
+ points: [
165
+ { x: from.x + from.width, y: from.y + from.height / 2 },
166
+ { x: loopRight, y: from.y + from.height / 2 },
167
+ { x: loopRight, y: loopTop },
168
+ { x: from.x + from.width / 2, y: loopTop },
169
+ { x: from.x + from.width / 2, y: from.y },
170
+ ],
171
+ },
172
+ ];
173
+ }
174
+ if (to.x <= from.x) {
175
+ const sameColumn = to.x === from.x;
176
+ const outerX = sameColumn
177
+ ? Math.max(from.x + from.width, to.x + to.width) + horizontalGap / 2
178
+ : Math.max(4, Math.min(from.x, to.x) - horizontalGap / 2);
179
+ const fromPoint = sameColumn
180
+ ? { x: from.x + from.width, y: from.y + from.height / 2 }
181
+ : { x: from.x, y: from.y + from.height / 2 };
182
+ const toPoint = sameColumn
183
+ ? { x: to.x + to.width, y: to.y + to.height / 2 }
184
+ : { x: to.x + to.width, y: to.y + to.height / 2 };
185
+ return [
186
+ {
187
+ id: edge.id,
188
+ from: edge.from,
189
+ to: edge.to,
190
+ points: [fromPoint, { x: outerX, y: fromPoint.y }, { x: outerX, y: toPoint.y }, toPoint],
191
+ },
192
+ ];
193
+ }
194
+ const fromPoint = { x: from.x + from.width, y: from.y + from.height / 2 };
195
+ const toPoint = { x: to.x, y: to.y + to.height / 2 };
196
+ const middle = (fromPoint.x + toPoint.x) / 2;
197
+ return [
198
+ {
199
+ id: edge.id,
200
+ from: edge.from,
201
+ to: edge.to,
202
+ points: [fromPoint, { x: middle, y: fromPoint.y }, { x: middle, y: toPoint.y }, toPoint],
203
+ },
204
+ ];
205
+ });
206
+ layoutEdges.sort((left, right) => compareCodeUnits(left.id, right.id));
207
+ const { width, height } = dimensions;
208
+ const withoutHash = {
209
+ schemaVersion: 'event-layout.v1',
210
+ width,
211
+ height,
212
+ nodes: layoutNodes,
213
+ edges: layoutEdges,
214
+ componentCount: components.length,
215
+ cyclicComponentCount: components.filter(({ cyclic }) => cyclic).length,
216
+ };
217
+ return { ...withoutHash, layoutHash: hashCanonical(withoutHash) };
218
+ }
@@ -0,0 +1,41 @@
1
+ import { ServiceError } from '../core/result.js';
2
+ export const EVENT_GRAPH_MAX_NODES = 100_000;
3
+ export const EVENT_GRAPH_MAX_EDGES = 250_000;
4
+ export const EVENT_GRAPH_MAX_STATE_ACCESSES = 250_000;
5
+ export const EVENT_GRAPH_MAX_STATE_LINKS = 250_000;
6
+ export const EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES = 500_000;
7
+ export const EVENT_GRAPH_MAX_ISSUES = 20_000;
8
+ export const EVENT_GRAPH_MAX_UNRESOLVED = 50_000;
9
+ export const EVENT_GRAPH_MAX_HELPER_DEPTH = 64;
10
+ export const EVENT_GRAPH_MAX_HELPER_PROJECTIONS = 200_000;
11
+ export const EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS = 50_000;
12
+ export const EVENT_GRAPH_MAX_CONDITION_TEXT = 16_384;
13
+ export const EVENT_GRAPH_WORK_LIMIT = 5_000_000;
14
+ export const EVENT_FRAGMENT_CACHE_MAX_ENTRIES = 10_000;
15
+ export const EVENT_FRAGMENT_CACHE_MAX_SOURCE_BYTES = 134_217_728;
16
+ export const EVENT_PROPOSED_SOURCE_MAX_FILES = 64;
17
+ export const EVENT_PROPOSED_SOURCE_MAX_BYTES = 67_108_864;
18
+ /** One cooperative, deterministic work ceiling shared by extraction and graph construction. */
19
+ export class EventAnalysisBudget {
20
+ signal;
21
+ maximum;
22
+ #spent = 0;
23
+ constructor(signal, maximum = EVENT_GRAPH_WORK_LIMIT) {
24
+ this.signal = signal;
25
+ this.maximum = maximum;
26
+ }
27
+ spend(label, amount = 1) {
28
+ if (!Number.isSafeInteger(amount) || amount < 0) {
29
+ throw new ServiceError('EVENT_ANALYSIS_BUDGET_INVALID', 'Event work amount is invalid');
30
+ }
31
+ this.#spent += amount;
32
+ if ((this.#spent & 0xff) === 0)
33
+ this.signal?.throwIfAborted();
34
+ if (this.#spent > this.maximum) {
35
+ throw new ServiceError('EVENT_ANALYSIS_WORK_BLOCKED', 'Event-chain analysis exceeds the fixed work ceiling', { label, spent: this.#spent, maximum: this.maximum });
36
+ }
37
+ }
38
+ check() {
39
+ this.signal?.throwIfAborted();
40
+ }
41
+ }
@@ -0,0 +1,2 @@
1
+ export const EVENT_GRAPH_SCHEMA_VERSION = 1;
2
+ export const EVENT_GRAPH_PARSER_VERSION = 'clausewitz-cst.v1';