hoi4-agent-tools 3.0.9 → 3.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +10 -2
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
  6. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  7. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  8. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  9. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
  16. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  17. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  18. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  19. package/dist/hoi4_agent_tools/core/index.js +47 -4
  20. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  21. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  22. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  23. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  24. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  25. package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
  26. package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
  27. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  28. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  29. package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
  30. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  31. package/dist/hoi4_agent_tools/core/result.js +1 -0
  32. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  33. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  34. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  35. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  36. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  37. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  38. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  39. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  40. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  41. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  42. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  43. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  44. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  45. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  46. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  47. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  48. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  49. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  50. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  51. package/dist/hoi4_agent_tools/event/service.js +67 -15
  52. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  53. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  54. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  55. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  56. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  57. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  58. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  59. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  60. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  61. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  62. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  63. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  64. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  66. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  67. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  68. package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
  69. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  70. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  71. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  72. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  73. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  74. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
  75. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  76. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  77. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
  78. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  79. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  80. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  81. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  82. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  83. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  84. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  85. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  86. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  87. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  88. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  89. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  90. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  91. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  92. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  93. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  94. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  95. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  96. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  97. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  98. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  99. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  100. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  101. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  102. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  103. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  104. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  105. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  106. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  107. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  108. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  109. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  110. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  111. package/dist/hoi4_agent_tools/version.js +1 -1
  112. package/docs/README.md +3 -0
  113. package/docs/analysis.md +100 -0
  114. package/docs/development.md +2 -0
  115. package/docs/events.md +7 -2
  116. package/docs/helper-expansion.md +78 -0
  117. package/docs/http.md +2 -0
  118. package/docs/jobs.md +86 -0
  119. package/docs/technology.md +6 -2
  120. package/package.json +15 -6
  121. package/schemas/configuration.schema.json +1 -1
  122. package/schemas/continuous-focus-palette.schema.json +1 -1
  123. package/schemas/custom-weighted-pool.schema.json +1 -1
  124. package/schemas/decision-inspect-input.schema.json +290 -0
  125. package/schemas/event-feature-manifest.schema.json +1 -1
  126. package/schemas/focus-plan.schema.json +1 -1
  127. package/schemas/focus-planning-sidecar.schema.json +1 -1
  128. package/schemas/gui-animation-source.schema.json +1 -1
  129. package/schemas/gui-helper.schema.json +1 -1
  130. package/schemas/gui-scenario.schema.json +1 -1
  131. package/schemas/helper-expansion-request.schema.json +44 -0
  132. package/schemas/helper-expansion-summary.schema.json +70 -0
  133. package/schemas/impact-inspect-input.schema.json +203 -0
  134. package/schemas/map-operation.schema.json +1 -1
  135. package/schemas/operation-result.schema.json +1 -1
  136. package/schemas/probability-analysis-result.schema.json +1 -1
  137. package/schemas/probability-compare-input.schema.json +1 -1
  138. package/schemas/probability-evaluate-input.schema.json +1 -1
  139. package/schemas/probability-inspect-input.schema.json +1 -1
  140. package/schemas/probability-render-input.schema.json +1 -1
  141. package/schemas/probability-scenario-set.schema.json +1 -1
  142. package/schemas/probability-sequence-input.schema.json +1 -1
  143. package/schemas/probability-simulate-input.schema.json +1 -1
  144. package/schemas/probability-sweep-input.schema.json +1 -1
  145. package/server.json +3 -3
@@ -0,0 +1,256 @@
1
+ import { compareCodeUnits } from './canonical.js';
2
+ import { assignments, firstScalar, nodeLocation, parseClausewitz, } from './source/index.js';
3
+ const eventKeys = new Set([
4
+ 'country_event',
5
+ 'news_event',
6
+ 'state_event',
7
+ 'unit_leader_event',
8
+ 'operative_leader_event',
9
+ ]);
10
+ const scalarTargets = new Map([
11
+ ['activate_decision', 'decision'],
12
+ ['remove_decision', 'decision'],
13
+ ['complete_decision', 'decision'],
14
+ ['add_ideas', 'idea'],
15
+ ['remove_ideas', 'idea'],
16
+ ['has_idea', 'idea'],
17
+ ['has_completed_focus', 'focus'],
18
+ ['load_focus_tree', 'focus_tree'],
19
+ ['has_tech', 'technology'],
20
+ ['country_event', 'event'],
21
+ ['news_event', 'event'],
22
+ ['state_event', 'event'],
23
+ ]);
24
+ const stateKeyOperations = new Map([
25
+ ['set_variable', { kind: 'variable', role: 'write' }],
26
+ ['add_to_variable', { kind: 'variable', role: 'write' }],
27
+ ['subtract_from_variable', { kind: 'variable', role: 'write' }],
28
+ ['multiply_variable', { kind: 'variable', role: 'write' }],
29
+ ['divide_variable', { kind: 'variable', role: 'write' }],
30
+ ['check_variable', { kind: 'variable', role: 'read' }],
31
+ ['has_variable', { kind: 'variable', role: 'read' }],
32
+ ['set_country_flag', { kind: 'flag', role: 'write', scope: 'country' }],
33
+ ['clr_country_flag', { kind: 'flag', role: 'write', scope: 'country' }],
34
+ ['has_country_flag', { kind: 'flag', role: 'read', scope: 'country' }],
35
+ ['set_state_flag', { kind: 'flag', role: 'write', scope: 'state' }],
36
+ ['clr_state_flag', { kind: 'flag', role: 'write', scope: 'state' }],
37
+ ['has_state_flag', { kind: 'flag', role: 'read', scope: 'state' }],
38
+ ['set_global_flag', { kind: 'flag', role: 'write', scope: 'global' }],
39
+ ['clr_global_flag', { kind: 'flag', role: 'write', scope: 'global' }],
40
+ ['has_global_flag', { kind: 'flag', role: 'read', scope: 'global' }],
41
+ ['save_event_target_as', { kind: 'event_target', role: 'write' }],
42
+ ['save_global_event_target_as', { kind: 'event_target', role: 'write' }],
43
+ ['clear_global_event_target', { kind: 'event_target', role: 'write' }],
44
+ ['has_event_target', { kind: 'event_target', role: 'read' }],
45
+ ]);
46
+ function stateKeyTarget(assignment, kind) {
47
+ if (assignment.value.type === 'scalar')
48
+ return assignment.value.value;
49
+ if (kind !== 'variable')
50
+ return undefined;
51
+ const explicit = firstScalar(assignment.value, 'var');
52
+ if (explicit !== undefined)
53
+ return explicit.value;
54
+ return assignments(assignment.value).find(({ key }) => !['compare', 'value', 'tooltip', 'var', 'value_name'].includes(key.value))?.key.value;
55
+ }
56
+ function normalized(file) {
57
+ return file.relativePath.replaceAll('\\', '/').toLowerCase();
58
+ }
59
+ function ownerFor(snapshot, file, assignment, ancestors) {
60
+ if (assignment.value.type !== 'block')
61
+ return undefined;
62
+ const key = assignment.key.value;
63
+ const sourcePath = normalized(file);
64
+ let owner;
65
+ if (sourcePath.startsWith('events/') && ancestors.length === 0 && eventKeys.has(key)) {
66
+ const id = firstScalar(assignment.value, 'id')?.value;
67
+ if (id !== undefined)
68
+ owner = { kind: 'event', id };
69
+ }
70
+ else if (key === 'focus') {
71
+ const id = firstScalar(assignment.value, 'id')?.value;
72
+ if (id !== undefined)
73
+ owner = {
74
+ kind: ancestors.includes('continuous_focus_palette') ? 'continuous_focus' : 'focus',
75
+ id,
76
+ };
77
+ }
78
+ else if (sourcePath.startsWith('common/decisions/') &&
79
+ !sourcePath.startsWith('common/decisions/categories/') &&
80
+ ancestors.length === 1) {
81
+ owner = { kind: 'decision', id: key };
82
+ }
83
+ else if (sourcePath.startsWith('common/decisions/categories/') && ancestors.length === 0) {
84
+ owner = { kind: 'decision_category', id: key };
85
+ }
86
+ else if (sourcePath.startsWith('common/ideas/') &&
87
+ ancestors.length === 2 &&
88
+ ancestors[0] === 'ideas') {
89
+ owner = { kind: 'idea', id: key };
90
+ }
91
+ else if (sourcePath.startsWith('common/technologies/') &&
92
+ ancestors.length === 1 &&
93
+ ancestors[0] === 'technologies') {
94
+ owner = { kind: 'technology', id: key };
95
+ }
96
+ else if (sourcePath.startsWith('common/scripted_effects/') && ancestors.length === 0) {
97
+ owner = { kind: 'scripted_effect', id: key };
98
+ }
99
+ else if (sourcePath.startsWith('common/scripted_triggers/') && ancestors.length === 0) {
100
+ owner = { kind: 'scripted_trigger', id: key };
101
+ }
102
+ else if (sourcePath.startsWith('common/scripted_guis/') &&
103
+ ancestors.length === 1 &&
104
+ ancestors[0] === 'scripted_gui') {
105
+ owner = { kind: 'scripted_gui', id: key };
106
+ }
107
+ return owner === undefined || snapshot.index.find(owner.kind, owner.id)?.path !== file.displayPath
108
+ ? undefined
109
+ : owner;
110
+ }
111
+ function staticTarget(value) {
112
+ return (/^[A-Za-z0-9_.:-]+$/u.test(value) &&
113
+ !/^(?:event_target|scope|var|constant):/iu.test(value) &&
114
+ !/^(?:ROOT|FROM|PREV|THIS)$/iu.test(value));
115
+ }
116
+ /** Extracts typed script edges while preserving unknown dynamic references. */
117
+ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000) {
118
+ if (!Number.isSafeInteger(maxReferences) || maxReferences < 1 || maxReferences > 1_000_000)
119
+ throw new RangeError('Semantic reference limit is outside the supported range');
120
+ const references = [];
121
+ const unresolved = [];
122
+ let scannedFiles = 0;
123
+ let complete = true;
124
+ const emit = (file, document, node, owner, toKind, target, referenceKind) => {
125
+ if (!staticTarget(target)) {
126
+ if (unresolved.length < maxReferences)
127
+ unresolved.push({
128
+ path: file.displayPath,
129
+ location: nodeLocation(document, node, owner.id),
130
+ reason: `Dynamic ${referenceKind} target ${target} cannot be resolved statically`,
131
+ });
132
+ else
133
+ complete = false;
134
+ return;
135
+ }
136
+ if (references.length >= maxReferences) {
137
+ complete = false;
138
+ return;
139
+ }
140
+ references.push({
141
+ kind: referenceKind,
142
+ from: owner.id,
143
+ toKind,
144
+ to: target,
145
+ path: file.displayPath,
146
+ location: nodeLocation(document, node, owner.id),
147
+ });
148
+ };
149
+ const walk = (file, document, block, ancestors, owner) => {
150
+ for (const assignment of assignments(block)) {
151
+ const identified = ownerFor(snapshot, file, assignment, ancestors);
152
+ const current = identified ?? owner;
153
+ if (identified?.kind === 'decision' && ancestors.length === 1)
154
+ emit(file, document, assignment, identified, 'decision_category', ancestors[0], 'decision_category_membership');
155
+ if (current !== undefined && identified === undefined) {
156
+ const key = assignment.key.value;
157
+ if (assignment.value.type === 'scalar') {
158
+ const value = assignment.value.value;
159
+ const localisation = (current.kind === 'event' &&
160
+ (key === 'title' ||
161
+ key === 'desc' ||
162
+ (key === 'name' && ancestors.includes('option')) ||
163
+ (key === 'text' && ancestors.includes('desc')))) ||
164
+ key === 'custom_effect_tooltip';
165
+ if (localisation)
166
+ emit(file, document, assignment, current, 'localisation', value, 'localisation_key');
167
+ if ((key === 'picture' ||
168
+ (key === 'icon' && ['decision', 'decision_category'].includes(current.kind))) &&
169
+ value.startsWith('GFX_'))
170
+ emit(file, document, assignment, current, 'sprite', value, 'display_sprite');
171
+ if (key === 'scripted_gui' && ['decision', 'decision_category'].includes(current.kind))
172
+ emit(file, document, assignment, current, 'scripted_gui', value, 'scripted_gui_binding');
173
+ if (current.kind === 'scripted_gui' && key === 'parent_window_window')
174
+ emit(file, document, assignment, current, 'gui_element', value, key);
175
+ if (current.kind === 'scripted_gui' && key === 'parent_scripted_gui')
176
+ emit(file, document, assignment, current, 'scripted_gui', value, key);
177
+ }
178
+ if (key.startsWith('event_target:'))
179
+ emit(file, document, assignment, current, 'event_target', key.slice('event_target:'.length), 'event_target_read');
180
+ const stateOperation = stateKeyOperations.get(key);
181
+ if (stateOperation !== undefined) {
182
+ const target = stateKeyTarget(assignment, stateOperation.kind);
183
+ if (target === undefined) {
184
+ if (unresolved.length < maxReferences)
185
+ unresolved.push({
186
+ path: file.displayPath,
187
+ location: nodeLocation(document, assignment, current.id),
188
+ reason: `Cannot identify ${key} state key`,
189
+ });
190
+ else
191
+ complete = false;
192
+ }
193
+ else
194
+ emit(file, document, assignment, current, stateOperation.kind, stateOperation.scope === undefined ? target : `${stateOperation.scope}:${target}`, `${stateOperation.kind}_${stateOperation.role}`);
195
+ }
196
+ const targetKind = scalarTargets.get(key);
197
+ if (key === 'set_technology' && assignment.value.type === 'block')
198
+ for (const technology of assignments(assignment.value))
199
+ if (technology.key.value !== 'popup')
200
+ emit(file, document, technology, current, 'technology', technology.key.value, 'set_technology');
201
+ if (targetKind !== undefined) {
202
+ if (assignment.value.type === 'scalar') {
203
+ emit(file, document, assignment, current, targetKind, assignment.value.value, key);
204
+ }
205
+ else {
206
+ const id = firstScalar(assignment.value, 'id') ?? firstScalar(assignment.value, 'decision');
207
+ if (id !== undefined)
208
+ emit(file, document, assignment, current, targetKind, id.value, key);
209
+ else
210
+ for (const entry of assignment.value.entries)
211
+ if (entry.type === 'scalar')
212
+ emit(file, document, assignment, current, targetKind, entry.value, key);
213
+ }
214
+ }
215
+ const helperKind = snapshot.index.find('scripted_effect', key) !== undefined
216
+ ? 'scripted_effect'
217
+ : snapshot.index.find('scripted_trigger', key) !== undefined
218
+ ? 'scripted_trigger'
219
+ : undefined;
220
+ if (helperKind !== undefined)
221
+ emit(file, document, assignment, current, helperKind, key, 'scripted_helper_call');
222
+ }
223
+ if (assignment.value.type === 'block')
224
+ walk(file, document, assignment.value, [...ancestors, assignment.key.value], current);
225
+ }
226
+ };
227
+ for (const file of snapshot.files) {
228
+ if (file.shadowedBy !== undefined)
229
+ continue;
230
+ const path = normalized(file);
231
+ if (!path.endsWith('.txt') ||
232
+ !(path.startsWith('events/') ||
233
+ path.startsWith('common/national_focus/') ||
234
+ path.startsWith('common/continuous_focus/') ||
235
+ path.startsWith('common/decisions/') ||
236
+ path.startsWith('common/ideas/') ||
237
+ path.startsWith('common/technologies/') ||
238
+ path.startsWith('common/scripted_effects/') ||
239
+ path.startsWith('common/scripted_triggers/') ||
240
+ path.startsWith('common/scripted_guis/')))
241
+ continue;
242
+ if (snapshot.index.isSourceSkipped(file.displayPath)) {
243
+ complete = false;
244
+ continue;
245
+ }
246
+ scannedFiles += 1;
247
+ const document = parseClausewitz(file.bytes, file.displayPath);
248
+ walk(file, document, document.root, []);
249
+ }
250
+ references.sort((left, right) => compareCodeUnits(left.path, right.path) ||
251
+ compareCodeUnits(left.from, right.from) ||
252
+ compareCodeUnits(left.toKind, right.toKind) ||
253
+ compareCodeUnits(left.to, right.to) ||
254
+ compareCodeUnits(left.kind, right.kind));
255
+ return { references, unresolved, scannedFiles, complete };
256
+ }
@@ -0,0 +1,137 @@
1
+ import { canonicalJson, compareCodeUnits, hashCanonical } from './canonical.js';
2
+ import { PACKAGE_VERSION } from '../version.js';
3
+ export function indexSegmentAddress(file, isProvinceDefinition) {
4
+ return hashCanonical({
5
+ version: 'file-index-segment.v1',
6
+ toolVersion: PACKAGE_VERSION,
7
+ sourceHash: file.sha256,
8
+ path: file.displayPath,
9
+ relativePath: file.relativePath,
10
+ rootKind: file.rootKind,
11
+ loadOrder: file.loadOrder,
12
+ shadowedBy: file.shadowedBy ?? null,
13
+ isProvinceDefinition,
14
+ });
15
+ }
16
+ /** Encoded entries keep the aggregate index's mutable records out of the shared cache. */
17
+ export class IndexSegmentCache {
18
+ maxBytes;
19
+ #entries = new Map();
20
+ #retainedBytes = 0;
21
+ #hits = 0;
22
+ #misses = 0;
23
+ #evictions = 0;
24
+ #oversized = 0;
25
+ constructor(maxBytes = 64 * 1024 * 1024) {
26
+ this.maxBytes = maxBytes;
27
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
28
+ throw new RangeError('Index segment cache size must be a non-negative integer');
29
+ }
30
+ get(address) {
31
+ const entry = this.#entries.get(address);
32
+ if (entry === undefined) {
33
+ this.#misses += 1;
34
+ return undefined;
35
+ }
36
+ this.#hits += 1;
37
+ this.#entries.delete(address);
38
+ this.#entries.set(address, entry);
39
+ return JSON.parse(entry.encoded);
40
+ }
41
+ /** Import one authenticated canonical entry without counting it as a process-local hit. */
42
+ importEncoded(address, encoded) {
43
+ const segment = JSON.parse(encoded);
44
+ if (typeof segment !== 'object' ||
45
+ segment === null ||
46
+ !('symbols' in segment) ||
47
+ !Array.isArray(segment.symbols) ||
48
+ !('references' in segment) ||
49
+ !Array.isArray(segment.references) ||
50
+ !('diagnostics' in segment) ||
51
+ !Array.isArray(segment.diagnostics) ||
52
+ !('complete' in segment) ||
53
+ typeof segment.complete !== 'boolean')
54
+ throw new Error('Persistent index segment is structurally invalid');
55
+ this.put(address, segment);
56
+ }
57
+ exportEncoded(address) {
58
+ return this.#entries.get(address)?.encoded;
59
+ }
60
+ put(address, segment) {
61
+ const encoded = canonicalJson(segment);
62
+ // Account for the UTF-16 string retained by V8, including conservative key overhead.
63
+ const bytes = encoded.length * 2 + address.length * 2 + 128;
64
+ if (bytes > this.maxBytes) {
65
+ this.#oversized += 1;
66
+ return;
67
+ }
68
+ const previous = this.#entries.get(address);
69
+ if (previous !== undefined) {
70
+ if (previous.encoded !== encoded)
71
+ throw new Error('An immutable index segment address produced different file-local facts');
72
+ this.#entries.delete(address);
73
+ this.#entries.set(address, previous);
74
+ return;
75
+ }
76
+ while (this.#retainedBytes + bytes > this.maxBytes) {
77
+ const oldest = this.#entries.entries().next().value;
78
+ if (oldest === undefined)
79
+ break;
80
+ this.#entries.delete(oldest[0]);
81
+ this.#retainedBytes -= oldest[1].bytes;
82
+ this.#evictions += 1;
83
+ }
84
+ this.#entries.set(address, { encoded, bytes });
85
+ this.#retainedBytes += bytes;
86
+ }
87
+ clear() {
88
+ this.#entries.clear();
89
+ this.#retainedBytes = 0;
90
+ }
91
+ statistics() {
92
+ return {
93
+ hits: this.#hits,
94
+ misses: this.#misses,
95
+ evictions: this.#evictions,
96
+ oversized: this.#oversized,
97
+ retainedBytes: this.#retainedBytes,
98
+ retainedSegments: this.#entries.size,
99
+ };
100
+ }
101
+ }
102
+ /** Reverse edges are derived from typed source references, never matching arbitrary strings. */
103
+ export class ReverseSourceDependencies {
104
+ #consumers = new Map();
105
+ #definitions = new Map();
106
+ constructor(symbols, references) {
107
+ for (const symbol of symbols) {
108
+ const definitions = this.#definitions.get(symbol.path) ?? new Set();
109
+ definitions.add(`${symbol.kind}:${symbol.id}`);
110
+ this.#definitions.set(symbol.path, definitions);
111
+ }
112
+ for (const reference of references) {
113
+ const key = `${reference.toKind}:${reference.to}`;
114
+ const consumers = this.#consumers.get(key) ?? new Set();
115
+ consumers.add(reference.path);
116
+ this.#consumers.set(key, consumers);
117
+ }
118
+ }
119
+ affectedFiles(changedFiles) {
120
+ const affected = new Set(changedFiles);
121
+ const queue = [...affected];
122
+ for (const source of queue) {
123
+ for (const definition of this.#definitions.get(source) ?? []) {
124
+ for (const consumer of this.#consumers.get(definition) ?? []) {
125
+ if (affected.has(consumer))
126
+ continue;
127
+ affected.add(consumer);
128
+ queue.push(consumer);
129
+ }
130
+ }
131
+ }
132
+ return [...affected].sort(compareCodeUnits);
133
+ }
134
+ consumers(kind, id) {
135
+ return [...(this.#consumers.get(`${kind}:${id}`) ?? [])].sort(compareCodeUnits);
136
+ }
137
+ }
@@ -1,6 +1,7 @@
1
1
  import { compareCodeUnits } from './canonical.js';
2
2
  import path from 'node:path';
3
3
  import { sortDiagnostics } from './diagnostics.js';
4
+ import { indexSegmentAddress } from './index-segments.js';
4
5
  import { parseClausewitz, parseLocalisation, assignments, childBlocks, firstScalar, locationFor, sourcePartialLimitDiagnostics, SOURCE_MAX_BYTES, SOURCE_MAX_NESTING, nodeLocation, } from './source/index.js';
5
6
  const spriteDefinitionKeys = new Set([
6
7
  'spritetype',
@@ -269,7 +270,8 @@ export class SymbolIndex {
269
270
  #symbolLimitReported = false;
270
271
  #referenceLimitReported = false;
271
272
  #diagnosticsTruncated = false;
272
- static build(files) {
273
+ #indexTableBlocked = new Set();
274
+ static build(files, segments) {
273
275
  const index = new SymbolIndex();
274
276
  const definitionSelection = selectedDefinitionFiles(files);
275
277
  for (const displayPath of definitionSelection.selected)
@@ -279,13 +281,13 @@ export class SymbolIndex {
279
281
  }
280
282
  for (const file of [...files].sort((a, b) => a.loadOrder - b.loadOrder || compareCodeUnits(a.displayPath, b.displayPath))) {
281
283
  index.files.set(file.displayPath, file);
282
- index.indexFile(file);
284
+ index.indexFileWithSegments(file, segments);
283
285
  }
284
286
  index.finalize();
285
287
  return index;
286
288
  }
287
289
  /** Yield between source files so protocol cancellation and progress remain responsive. */
288
- static async buildAsync(files, signal) {
290
+ static async buildAsync(files, signal, segments) {
289
291
  signal?.throwIfAborted();
290
292
  const index = new SymbolIndex();
291
293
  const definitionSelection = selectedDefinitionFiles(files);
@@ -298,7 +300,7 @@ export class SymbolIndex {
298
300
  for (const file of [...files].sort((a, b) => a.loadOrder - b.loadOrder || compareCodeUnits(a.displayPath, b.displayPath))) {
299
301
  signal?.throwIfAborted();
300
302
  index.files.set(file.displayPath, file);
301
- index.indexFile(file);
303
+ index.indexFileWithSegments(file, segments);
302
304
  if (performance.now() - yieldedAt >= 20) {
303
305
  await new Promise((resolve) => setImmediate(resolve));
304
306
  signal?.throwIfAborted();
@@ -435,6 +437,45 @@ export class SymbolIndex {
435
437
  for (const diagnostic of diagnostics)
436
438
  this.addDiagnostic(diagnostic);
437
439
  }
440
+ indexFileWithSegments(file, cache) {
441
+ if (cache === undefined) {
442
+ this.indexFile(file);
443
+ return;
444
+ }
445
+ const address = indexSegmentAddress(file, this.#definitionFiles.has(file.displayPath));
446
+ const cached = cache.get(address);
447
+ if (cached?.complete === true &&
448
+ this.symbols.length + cached.symbols.length <= INDEX_RECORD_LIMIT &&
449
+ this.references.length + cached.references.length <= INDEX_RECORD_LIMIT &&
450
+ this.diagnostics.length + cached.diagnostics.length <= INDEX_DIAGNOSTIC_LIMIT) {
451
+ this.#currentFileShadowed = file.shadowedBy !== undefined;
452
+ for (const symbol of cached.symbols)
453
+ this.addSymbol(symbol);
454
+ for (const reference of cached.references)
455
+ this.addReference(reference);
456
+ this.addDiagnostics(cached.diagnostics);
457
+ return;
458
+ }
459
+ const symbols = this.symbols.length;
460
+ const references = this.references.length;
461
+ const diagnostics = this.diagnostics.length;
462
+ this.indexFile(file);
463
+ // Incomplete files and aggregate ceilings retain the full builder's exact diagnostic
464
+ // ordering and coverage bookkeeping. They are never cached as complete segments.
465
+ if (this.#indexTableBlocked.has(file.displayPath) ||
466
+ this.#skippedSourcePaths.has(file.displayPath) ||
467
+ this.#indexNestingBlocked.has(file.displayPath) ||
468
+ this.#symbolLimitReported ||
469
+ this.#referenceLimitReported ||
470
+ this.#diagnosticsTruncated)
471
+ return;
472
+ cache.put(address, {
473
+ symbols: this.symbols.slice(symbols),
474
+ references: this.references.slice(references),
475
+ diagnostics: this.diagnostics.slice(diagnostics),
476
+ complete: true,
477
+ });
478
+ }
438
479
  indexFile(file) {
439
480
  this.#currentFileShadowed = file.shadowedBy !== undefined;
440
481
  const diagnosticStart = this.diagnostics.length;
@@ -1163,6 +1204,7 @@ export class SymbolIndex {
1163
1204
  let line = 1;
1164
1205
  while (start <= text.length) {
1165
1206
  if (line > INDEX_TABLE_RECORD_LIMIT) {
1207
+ this.#indexTableBlocked.add(file.displayPath);
1166
1208
  this.#complete = false;
1167
1209
  this.addDiagnostic({
1168
1210
  code: 'INDEX_TABLE_RECORD_LIMIT',
@@ -1189,6 +1231,7 @@ export class SymbolIndex {
1189
1231
  let match;
1190
1232
  while ((match = matcher.exec(value)) !== null) {
1191
1233
  if (fields.length >= INDEX_TABLE_FIELD_LIMIT) {
1234
+ this.#indexTableBlocked.add(file.displayPath);
1192
1235
  this.#complete = false;
1193
1236
  this.addDiagnostic({
1194
1237
  code: 'INDEX_TABLE_FIELD_LIMIT',
@@ -0,0 +1,109 @@
1
+ import { z } from 'zod/v4';
2
+ import { JobService } from './job-service.js';
3
+ import { emptyServiceResult, ServiceError } from './result.js';
4
+ import { errorResult, strictOperationResultSchema, toolResult } from './operation-result.js';
5
+ import { requireOperationScope, resolveOperationWorkspaceId, } from './operation-context.js';
6
+ import { workspaceIdSchema } from '../schemas/common.js';
7
+ const jobIdSchema = z.string().regex(/^job_[a-f0-9]{64}$/u);
8
+ export const jobReferenceSchema = z
9
+ .object({ workspaceId: workspaceIdSchema, jobId: jobIdSchema })
10
+ .strict();
11
+ const jobStatusDataSchema = z
12
+ .object({
13
+ jobId: jobIdSchema,
14
+ toolName: z.string().max(256),
15
+ status: z.enum(['queued', 'running', 'reconciling', 'completed', 'cancelled', 'failed']),
16
+ revision: z.number().int().min(0),
17
+ createdAt: z.iso.datetime(),
18
+ updatedAt: z.iso.datetime(),
19
+ cancelRequested: z.boolean(),
20
+ resultReady: z.boolean(),
21
+ progress: z
22
+ .object({
23
+ completed: z.number().nonnegative(),
24
+ total: z.number().nonnegative().optional(),
25
+ message: z.string().max(4096),
26
+ })
27
+ .strict()
28
+ .optional(),
29
+ failure: z
30
+ .object({ code: z.string().max(256), message: z.string().max(4096) })
31
+ .strict()
32
+ .optional(),
33
+ })
34
+ .strict();
35
+ export const jobStatusOutputSchema = strictOperationResultSchema(jobStatusDataSchema);
36
+ export function isJobControlTool(name) {
37
+ return name === 'hoi4.job_inspect' || name === 'hoi4.job_cancel';
38
+ }
39
+ function statusResult(record) {
40
+ const result = emptyServiceResult(record.scope.workspaceId, {
41
+ jobId: record.id,
42
+ toolName: record.request.toolName,
43
+ status: record.status,
44
+ revision: record.revision,
45
+ createdAt: record.createdAt,
46
+ updatedAt: record.updatedAt,
47
+ cancelRequested: record.cancelRequested,
48
+ resultReady: record.result !== undefined,
49
+ ...(record.progress === undefined ? {} : { progress: record.progress }),
50
+ ...(record.failure === undefined ? {} : { failure: record.failure }),
51
+ });
52
+ result.code = record.status === 'failed' ? 'JOB_FAILED' : 'JOB_STATUS';
53
+ if (record.status === 'failed') {
54
+ result.status = 'error';
55
+ result.validation.passed = false;
56
+ result.blockers = [record.failure];
57
+ }
58
+ else if (record.status === 'cancelled') {
59
+ result.status = 'blocked';
60
+ result.code = 'JOB_CANCELLED';
61
+ result.validation.passed = false;
62
+ }
63
+ return result;
64
+ }
65
+ /** Bounded control traffic never acquires execution capacity or starts a worker. */
66
+ export class JobControlService {
67
+ engine;
68
+ jobsPromise;
69
+ constructor(engine, jobsPromise) {
70
+ this.engine = engine;
71
+ this.jobsPromise = jobsPromise;
72
+ }
73
+ async call(name, input, context, signal) {
74
+ const parsed = jobReferenceSchema.parse(input);
75
+ let workspaceId = parsed.workspaceId;
76
+ try {
77
+ signal?.throwIfAborted();
78
+ workspaceId = await resolveOperationWorkspaceId(this.engine, context, parsed.workspaceId, signal);
79
+ this.jobsPromise ??= JobService.create(this.engine.resolver);
80
+ const jobs = await this.jobsPromise;
81
+ const record = await jobs.get(workspaceId, parsed.jobId, context.principal, signal);
82
+ if (name === 'hoi4.job_cancel') {
83
+ if (record.request.mutation)
84
+ requireOperationScope(context, 'hoi4:write');
85
+ return toolResult(statusResult(await jobs.cancel(workspaceId, record.id, context.principal, signal)));
86
+ }
87
+ if (record.status !== 'completed')
88
+ return toolResult(statusResult(record));
89
+ if (record.result === undefined)
90
+ throw new ServiceError('JOB_RESULT_MISSING', 'Completed job has no authenticated result');
91
+ return {
92
+ ...record.result,
93
+ _meta: {
94
+ ...z.record(z.string(), z.json()).parse(record.result._meta ?? {}),
95
+ 'io.github.klimPaskov/hoi4-agent-tools/job': {
96
+ jobId: record.id,
97
+ status: record.status,
98
+ revision: record.revision,
99
+ },
100
+ },
101
+ };
102
+ }
103
+ catch (error) {
104
+ if (signal?.aborted)
105
+ throw error;
106
+ return errorResult(error, workspaceId);
107
+ }
108
+ }
109
+ }