hoi4-agent-tools 2.1.1 → 2.3.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 (76) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +32 -18
  3. package/dist/bin/stdio.js +2 -1
  4. package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
  5. package/dist/hoi4_agent_tools/core/engine.js +14 -0
  6. package/dist/hoi4_agent_tools/core/index.js +349 -2
  7. package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
  8. package/dist/hoi4_agent_tools/core/source/index.js +1 -0
  9. package/dist/hoi4_agent_tools/event/limits.js +1 -1
  10. package/dist/hoi4_agent_tools/focus/render.js +37 -13
  11. package/dist/hoi4_agent_tools/mcp/server/create.js +5 -1
  12. package/dist/hoi4_agent_tools/mcp/server/result.js +4 -4
  13. package/dist/hoi4_agent_tools/mcp/tools/focus.js +39 -2
  14. package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
  15. package/dist/hoi4_agent_tools/mcp/tools/technology.js +368 -0
  16. package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
  17. package/dist/hoi4_agent_tools/probability/compare.js +178 -0
  18. package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
  19. package/dist/hoi4_agent_tools/probability/index.js +13 -0
  20. package/dist/hoi4_agent_tools/probability/model.js +5 -0
  21. package/dist/hoi4_agent_tools/probability/rational.js +145 -0
  22. package/dist/hoi4_agent_tools/probability/render.js +307 -0
  23. package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
  24. package/dist/hoi4_agent_tools/probability/service.js +1168 -0
  25. package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
  26. package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
  27. package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
  28. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
  29. package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
  30. package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
  31. package/dist/hoi4_agent_tools/schemas/technology.js +77 -0
  32. package/dist/hoi4_agent_tools/technology/compare.js +240 -0
  33. package/dist/hoi4_agent_tools/technology/graph.js +1196 -0
  34. package/dist/hoi4_agent_tools/technology/index.js +7 -0
  35. package/dist/hoi4_agent_tools/technology/model.js +2 -0
  36. package/dist/hoi4_agent_tools/technology/queries.js +300 -0
  37. package/dist/hoi4_agent_tools/technology/render.js +641 -0
  38. package/dist/hoi4_agent_tools/technology/service.js +686 -0
  39. package/dist/hoi4_agent_tools/technology/source-analysis.js +967 -0
  40. package/dist/hoi4_agent_tools/version.js +1 -1
  41. package/docs/README.md +2 -0
  42. package/docs/development.md +7 -1
  43. package/docs/probability.md +80 -0
  44. package/docs/research/probability-adapter-evidence.md +64 -0
  45. package/docs/setup.md +1 -1
  46. package/docs/technology.md +90 -0
  47. package/examples/clients/codex-config.toml +3 -0
  48. package/examples/clients/generic.json +8 -0
  49. package/examples/probability/adaptive-event-pool.json +92 -0
  50. package/examples/probability/adaptive-event-sequence-input.json +57 -0
  51. package/examples/probability/event-mtth-scenarios.json +57 -0
  52. package/examples/probability/event-mtth-simulate-input.json +37 -0
  53. package/examples/probability/focus-route-scenarios.json +61 -0
  54. package/examples/probability/focus-route-sweep-input.json +36 -0
  55. package/package.json +26 -8
  56. package/schemas/configuration.schema.json +747 -0
  57. package/schemas/continuous-focus-palette.schema.json +325 -0
  58. package/schemas/custom-weighted-pool.schema.json +298 -0
  59. package/schemas/event-feature-manifest.schema.json +63 -0
  60. package/schemas/focus-plan.schema.json +927 -0
  61. package/schemas/focus-planning-sidecar.schema.json +407 -0
  62. package/schemas/gui-animation-source.schema.json +176 -0
  63. package/schemas/gui-helper.schema.json +301 -0
  64. package/schemas/gui-scenario.schema.json +298 -0
  65. package/schemas/map-operation.schema.json +2284 -0
  66. package/schemas/operation-result.schema.json +327 -0
  67. package/schemas/probability-analysis-result.schema.json +2211 -0
  68. package/schemas/probability-compare-input.schema.json +664 -0
  69. package/schemas/probability-evaluate-input.schema.json +665 -0
  70. package/schemas/probability-inspect-input.schema.json +97 -0
  71. package/schemas/probability-render-input.schema.json +141 -0
  72. package/schemas/probability-scenario-set.schema.json +430 -0
  73. package/schemas/probability-sequence-input.schema.json +886 -0
  74. package/schemas/probability-simulate-input.schema.json +691 -0
  75. package/schemas/probability-sweep-input.schema.json +702 -0
  76. package/server.json +3 -3
@@ -0,0 +1,7 @@
1
+ export * from './model.js';
2
+ export * from './compare.js';
3
+ export * from './graph.js';
4
+ export * from './queries.js';
5
+ export * from './render.js';
6
+ export * from './service.js';
7
+ export * from './source-analysis.js';
@@ -0,0 +1,2 @@
1
+ export const TECHNOLOGY_GRAPH_SCHEMA_VERSION = 1;
2
+ export const TECHNOLOGY_PARSER_VERSION = 'clausewitz-cst.v1';
@@ -0,0 +1,300 @@
1
+ import { compareCodeUnits, deterministicId } from '../core/canonical.js';
2
+ import { ServiceError } from '../core/result.js';
3
+ const MAX_TRACE_DEPTH = 256;
4
+ const MAX_TRACE_NODES = 25_000;
5
+ function requireTechnology(graph, technologyId) {
6
+ if (!graph.technologies.some(({ id }) => id === technologyId))
7
+ throw new ServiceError('TECHNOLOGY_NOT_FOUND', `Technology ${technologyId} was not found`, {
8
+ technologyId,
9
+ });
10
+ }
11
+ export function technologyScanReport(graph) {
12
+ const classifications = Object.fromEntries(['confirmed_error', 'probable_defect', 'design_warning', 'unresolved_analysis'].map((classification) => [
13
+ classification,
14
+ graph.issues.filter((issue) => issue.classification === classification).length,
15
+ ]));
16
+ return {
17
+ schemaVersion: graph.schemaVersion,
18
+ parserVersion: graph.parserVersion,
19
+ workspaceId: graph.workspaceId,
20
+ workspaceIdentity: graph.workspaceIdentity,
21
+ revision: graph.revision,
22
+ complete: graph.complete,
23
+ statistics: graph.statistics,
24
+ issueClassifications: classifications,
25
+ folderIds: graph.folders.map(({ id }) => id),
26
+ modernDoctrineFolders: graph.doctrineDefinitions
27
+ .filter(({ kind }) => kind === 'folder')
28
+ .map(({ id }) => id),
29
+ sourceHashCount: Object.keys(graph.sourceHashes).length,
30
+ skippedSourceCount: graph.skippedSourceCount,
31
+ analysisBoundary: graph.analysisBoundary,
32
+ issueSamples: graph.issues.slice(0, 100),
33
+ unresolvedSamples: graph.unresolved.slice(0, 100),
34
+ authoritativeGraphIncluded: true,
35
+ };
36
+ }
37
+ export function discoverTechnologyFolders(graph, folderId) {
38
+ const incoming = new Set(graph.edges.filter(({ kind }) => kind === 'prerequisite').map(({ to }) => to));
39
+ const selected = graph.folders.filter((folder) => folderId === undefined || folder.id === folderId);
40
+ if (folderId !== undefined && selected.length === 0)
41
+ throw new ServiceError('TECH_FOLDER_NOT_FOUND', `Technology folder ${folderId} was not found`, {
42
+ folderId,
43
+ });
44
+ return {
45
+ folders: selected.map((folder) => {
46
+ const placements = graph.placements.filter(({ folderId: candidate }) => candidate === folder.id);
47
+ const technologyIds = [...new Set(placements.map(({ technologyId }) => technologyId))].sort(compareCodeUnits);
48
+ const roots = technologyIds.filter((technologyId) => !incoming.has(technologyId));
49
+ return {
50
+ folder,
51
+ placementCount: placements.length,
52
+ technologyCount: technologyIds.length,
53
+ roots,
54
+ placements,
55
+ issues: graph.issues.filter(({ details }) => details.folderId === folder.id),
56
+ };
57
+ }),
58
+ modernDoctrines: graph.doctrineDefinitions
59
+ .filter(({ kind, id }) => kind === 'folder' && (folderId === undefined || id === folderId))
60
+ .map((folder) => ({
61
+ folder,
62
+ grandDoctrines: graph.doctrineDefinitions.filter(({ kind, folderId: candidate }) => kind === 'grand_doctrine' && candidate === folder.id),
63
+ })),
64
+ };
65
+ }
66
+ export function traceTechnology(graph, input) {
67
+ requireTechnology(graph, input.technologyId);
68
+ const direction = input.direction ?? 'both';
69
+ const maxDepth = input.maxDepth ?? 32;
70
+ const maxNodes = input.maxNodes ?? 2_000;
71
+ if (maxDepth < 0 || maxDepth > MAX_TRACE_DEPTH || maxNodes < 1 || maxNodes > MAX_TRACE_NODES)
72
+ throw new ServiceError('TECH_TRACE_LIMIT_INVALID', 'Technology trace limits are outside supported bounds', {
73
+ maxDepth,
74
+ maxNodes,
75
+ maximumDepth: MAX_TRACE_DEPTH,
76
+ maximumNodes: MAX_TRACE_NODES,
77
+ });
78
+ const allowedKinds = new Set([
79
+ 'prerequisite',
80
+ ...(input.includeSubTechnologies === true ? ['sub_technology'] : []),
81
+ ]);
82
+ const relevant = graph.edges.filter(({ kind }) => allowedKinds.has(kind));
83
+ const incoming = new Map();
84
+ const outgoing = new Map();
85
+ for (const edge of relevant) {
86
+ const upstream = incoming.get(edge.to) ?? [];
87
+ upstream.push(edge);
88
+ incoming.set(edge.to, upstream);
89
+ const downstream = outgoing.get(edge.from) ?? [];
90
+ downstream.push(edge);
91
+ outgoing.set(edge.from, downstream);
92
+ }
93
+ const selected = new Set([input.technologyId]);
94
+ const selectedEdges = new Map();
95
+ const depthById = new Map([[input.technologyId, 0]]);
96
+ const queue = [input.technologyId];
97
+ const boundary = new Set();
98
+ let truncated = false;
99
+ while (queue.length > 0) {
100
+ input.signal?.throwIfAborted();
101
+ const current = queue.shift();
102
+ const depth = depthById.get(current) ?? 0;
103
+ const candidates = [
104
+ ...(direction === 'descendants'
105
+ ? []
106
+ : (incoming.get(current) ?? []).map((edge) => ({ edge, next: edge.from }))),
107
+ ...(direction === 'prerequisites'
108
+ ? []
109
+ : (outgoing.get(current) ?? []).map((edge) => ({ edge, next: edge.to }))),
110
+ ].sort((left, right) => compareCodeUnits(left.edge.id, right.edge.id));
111
+ for (const { edge, next } of candidates) {
112
+ selectedEdges.set(edge.id, edge);
113
+ if (selected.has(next))
114
+ continue;
115
+ if (depth >= maxDepth || selected.size >= maxNodes) {
116
+ truncated = true;
117
+ boundary.add(next);
118
+ continue;
119
+ }
120
+ selected.add(next);
121
+ depthById.set(next, depth + 1);
122
+ queue.push(next);
123
+ }
124
+ }
125
+ const layers = [...new Set(depthById.values())]
126
+ .sort((left, right) => left - right)
127
+ .map((depth) => ({
128
+ depth,
129
+ technologyIds: [...depthById]
130
+ .filter(([, candidate]) => candidate === depth)
131
+ .map(([id]) => id)
132
+ .sort(compareCodeUnits),
133
+ }));
134
+ return {
135
+ technologyId: input.technologyId,
136
+ direction,
137
+ nodes: [...selected].sort(compareCodeUnits),
138
+ edges: [...selectedEdges.values()].sort((left, right) => compareCodeUnits(left.id, right.id)),
139
+ layers,
140
+ truncated,
141
+ boundary: [...boundary].sort(compareCodeUnits),
142
+ };
143
+ }
144
+ export function explainTechnology(graph, technologyId, signal) {
145
+ requireTechnology(graph, technologyId);
146
+ const technology = graph.technologies.find(({ id }) => id === technologyId);
147
+ const prerequisites = traceTechnology(graph, {
148
+ technologyId,
149
+ direction: 'prerequisites',
150
+ maxDepth: 256,
151
+ maxNodes: 25_000,
152
+ ...(signal === undefined ? {} : { signal }),
153
+ });
154
+ const descendants = traceTechnology(graph, {
155
+ technologyId,
156
+ direction: 'descendants',
157
+ maxDepth: 256,
158
+ maxNodes: 25_000,
159
+ ...(signal === undefined ? {} : { signal }),
160
+ });
161
+ return {
162
+ technology,
163
+ placements: graph.placements.filter(({ technologyId: candidate }) => candidate === technologyId),
164
+ directPrerequisites: graph.edges.filter(({ kind, to }) => kind === 'prerequisite' && to === technologyId),
165
+ transitivePrerequisites: prerequisites,
166
+ directDescendants: graph.edges.filter(({ kind, from }) => kind === 'prerequisite' && from === technologyId),
167
+ descendants,
168
+ exclusiveChoices: graph.edges.filter(({ kind, from, to }) => kind === 'exclusive' && (from === technologyId || to === technologyId)),
169
+ unlocks: graph.unlocks.filter(({ technologyId: candidate }) => candidate === technologyId),
170
+ externalGrants: graph.externalReferences.filter(({ technologyId: candidate, kind }) => candidate === technologyId && (kind === 'grant' || kind === 'starting_technology')),
171
+ matchingBonuses: graph.externalReferences.filter(({ technologyId: candidate, categoryId, kind }) => kind === 'research_bonus' &&
172
+ (candidate === technologyId ||
173
+ (categoryId !== undefined && technology.categories.includes(categoryId)))),
174
+ issues: graph.issues.filter(({ details }) => details.technologyId === technologyId ||
175
+ (Array.isArray(details.technologyIds) && details.technologyIds.includes(technologyId))),
176
+ unresolved: graph.unresolved.filter(({ ownerId }) => ownerId === technologyId),
177
+ confidenceLimits: graph.analysisBoundary,
178
+ };
179
+ }
180
+ export function inspectTechnologyUnlocks(graph, selector) {
181
+ if (selector.technologyId !== undefined)
182
+ requireTechnology(graph, selector.technologyId);
183
+ const unlocks = graph.unlocks.filter(({ technologyId, kind, targetId }) => (selector.technologyId === undefined || technologyId === selector.technologyId) &&
184
+ (selector.targetKind === undefined || kind === selector.targetKind) &&
185
+ (selector.targetId === undefined || targetId === selector.targetId));
186
+ return {
187
+ selector,
188
+ unlocks,
189
+ targets: graph.unlockTargets.filter(({ kind, targetId }) => unlocks.some((unlock) => unlock.kind === kind && unlock.targetId === targetId)),
190
+ externalGrants: graph.externalReferences.filter(({ technologyId, kind }) => technologyId !== undefined &&
191
+ unlocks.some((unlock) => unlock.technologyId === technologyId) &&
192
+ (kind === 'grant' || kind === 'starting_technology')),
193
+ issues: graph.issues.filter(({ code, details }) => code === 'TECH_UNLOCK_TARGET_MISSING' &&
194
+ unlocks.some(({ technologyId, kind, targetId }) => details.technologyId === technologyId &&
195
+ details.targetKind === kind &&
196
+ details.targetId === targetId)),
197
+ };
198
+ }
199
+ export function technologyBonusCoverage(graph, selector = {}) {
200
+ if (selector.technologyId !== undefined)
201
+ requireTechnology(graph, selector.technologyId);
202
+ const categories = graph.categories.filter(({ id }) => selector.categoryId === undefined || id === selector.categoryId);
203
+ if (selector.categoryId !== undefined && categories.length === 0)
204
+ throw new ServiceError('TECH_CATEGORY_NOT_FOUND', `Technology category ${selector.categoryId} was not found`, { categoryId: selector.categoryId });
205
+ const rows = categories.map((category) => {
206
+ const technologyIds = graph.technologies
207
+ .filter(({ id, categories: memberships }) => memberships.includes(category.id) &&
208
+ (selector.technologyId === undefined || id === selector.technologyId))
209
+ .map(({ id }) => id);
210
+ const bonusSources = graph.externalReferences.filter(({ kind, categoryId, technologyId }) => kind === 'research_bonus' &&
211
+ (categoryId === category.id ||
212
+ (technologyId !== undefined && technologyIds.includes(technologyId))));
213
+ const sharingSources = graph.externalReferences.filter(({ kind, categoryId }) => kind === 'technology_sharing' && categoryId === category.id);
214
+ return {
215
+ category,
216
+ technologyIds,
217
+ bonusSources,
218
+ sharingSources,
219
+ covered: bonusSources.length > 0 || sharingSources.length > 0,
220
+ };
221
+ });
222
+ return {
223
+ selector,
224
+ rows,
225
+ uncoveredCategoryIds: rows.filter(({ covered }) => !covered).map(({ category }) => category.id),
226
+ technologySpecificBonuses: graph.externalReferences.filter(({ kind, technologyId }) => kind === 'research_bonus' &&
227
+ technologyId !== undefined &&
228
+ (selector.technologyId === undefined || technologyId === selector.technologyId)),
229
+ invalidBonusSources: graph.issues.filter(({ code }) => code === 'TECH_EXTERNAL_CATEGORY_MISSING' || code === 'TECH_BONUS_TARGET_MISSING'),
230
+ };
231
+ }
232
+ export function lintTechnologyGraph(graph, filter = {}) {
233
+ const issues = graph.issues.filter((issue) => (filter.classifications === undefined ||
234
+ filter.classifications.includes(issue.classification)) &&
235
+ (filter.codes === undefined || filter.codes.includes(issue.code)) &&
236
+ (filter.folderId === undefined || issue.details.folderId === filter.folderId) &&
237
+ (filter.technologyId === undefined ||
238
+ issue.details.technologyId === filter.technologyId ||
239
+ (Array.isArray(issue.details.technologyIds) &&
240
+ issue.details.technologyIds.includes(filter.technologyId))));
241
+ const grouped = (key) => {
242
+ const counts = new Map();
243
+ for (const issue of issues)
244
+ counts.set(key(issue), (counts.get(key(issue)) ?? 0) + 1);
245
+ return Object.fromEntries([...counts].sort(([left], [right]) => compareCodeUnits(left, right)));
246
+ };
247
+ return {
248
+ filter,
249
+ issueCount: issues.length,
250
+ byClassification: grouped(({ classification }) => classification),
251
+ byCode: grouped(({ code }) => code),
252
+ issues,
253
+ unresolved: graph.unresolved.filter((item) => filter.technologyId === undefined ? true : item.ownerId === filter.technologyId),
254
+ };
255
+ }
256
+ export function analyzeTechnologyImpact(graph, input) {
257
+ if (input.operation === 'rename' && input.replacementId === undefined)
258
+ throw new ServiceError('TECH_IMPACT_REPLACEMENT_REQUIRED', 'Rename impact requires replacementId');
259
+ const references = [];
260
+ if (input.kind === 'technology') {
261
+ requireTechnology(graph, input.id);
262
+ references.push(...graph.edges.filter(({ from, to }) => from === input.id || to === input.id), ...graph.placements.filter(({ technologyId }) => technologyId === input.id), ...graph.unlocks.filter(({ technologyId }) => technologyId === input.id), ...graph.externalReferences.filter(({ technologyId }) => technologyId === input.id));
263
+ }
264
+ else if (input.kind === 'category') {
265
+ references.push(...graph.technologies.filter(({ categories }) => categories.includes(input.id)), ...graph.externalReferences.filter(({ categoryId }) => categoryId === input.id));
266
+ }
267
+ else if (input.kind === 'folder') {
268
+ references.push(...graph.placements.filter(({ folderId }) => folderId === input.id), ...graph.technologies.filter(({ folders }) => folders.includes(input.id)));
269
+ }
270
+ else {
271
+ references.push(...graph.unlocks.filter(({ targetId }) => targetId === input.id));
272
+ }
273
+ const locations = references.flatMap((reference) => {
274
+ if (reference !== null && typeof reference === 'object') {
275
+ if ('location' in reference && reference.location !== undefined)
276
+ return [reference.location];
277
+ if ('source' in reference &&
278
+ reference.source !== undefined &&
279
+ reference.source !== null &&
280
+ typeof reference.source === 'object' &&
281
+ 'location' in reference.source)
282
+ return [reference.source.location];
283
+ }
284
+ return [];
285
+ });
286
+ return {
287
+ id: deterministicId('tech-impact', input),
288
+ subject: input,
289
+ referenceCount: references.length,
290
+ references,
291
+ sourceLocations: locations,
292
+ wouldBreakReferences: references.length > 0,
293
+ replacementCollision: input.replacementId === undefined
294
+ ? false
295
+ : graph.technologies.some(({ id }) => id === input.replacementId) ||
296
+ graph.categories.some(({ id }) => id === input.replacementId) ||
297
+ graph.folders.some(({ id }) => id === input.replacementId),
298
+ limitations: graph.analysisBoundary.unsupportedConstructs,
299
+ };
300
+ }