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,41 @@
1
+ const cachedPaths = new WeakMap();
2
+ function indexBlock(block, path, output) {
3
+ output.set(block, path);
4
+ const occurrences = new Map();
5
+ for (const [index, entry] of block.entries.entries()) {
6
+ if (entry.type === 'assignment') {
7
+ const key = entry.key.value;
8
+ const occurrence = occurrences.get(key) ?? 0;
9
+ occurrences.set(key, occurrence + 1);
10
+ const entryPath = [...path, `${key}[${occurrence}]`];
11
+ output.set(entry, entryPath);
12
+ output.set(entry.key, [...entryPath, '$key']);
13
+ output.set(entry.value, entryPath);
14
+ if (entry.value.type === 'block')
15
+ indexBlock(entry.value, entryPath, output);
16
+ continue;
17
+ }
18
+ const entryPath = [...path, `#${entry.type}[${index}]`];
19
+ output.set(entry, entryPath);
20
+ if (entry.type === 'block')
21
+ indexBlock(entry, entryPath, output);
22
+ }
23
+ }
24
+ function pathsFor(document) {
25
+ const existing = cachedPaths.get(document);
26
+ if (existing !== undefined)
27
+ return existing;
28
+ const indexed = new WeakMap();
29
+ indexBlock(document.root, [], indexed);
30
+ cachedPaths.set(document, indexed);
31
+ return indexed;
32
+ }
33
+ /** Stable key-and-occurrence path for a node inside one parsed Clausewitz document. */
34
+ export function astPathFor(document, node) {
35
+ const path = pathsFor(document).get(node);
36
+ return path === undefined ? undefined : [...path];
37
+ }
38
+ /** Test and diagnostic helper for callers that already hold a generic source entry. */
39
+ export function sourceEntryAstPath(document, entry) {
40
+ return astPathFor(document, entry);
41
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './encoding.js';
2
+ export * from './ast-path.js';
2
3
  export * from './lexer.js';
3
4
  export * from './limits.js';
4
5
  export * from './localisation.js';
@@ -1,5 +1,5 @@
1
1
  import { ServiceError } from '../core/result.js';
2
- export const EVENT_GRAPH_MAX_NODES = 100_000;
2
+ export const EVENT_GRAPH_MAX_NODES = 250_000;
3
3
  export const EVENT_GRAPH_MAX_EDGES = 500_000;
4
4
  export const EVENT_GRAPH_MAX_STATE_ACCESSES = 250_000;
5
5
  export const EVENT_GRAPH_MAX_STATE_LINKS = 250_000;
@@ -143,6 +143,22 @@ function focusIconDataUri(focus, options) {
143
143
  ? undefined
144
144
  : (options.presentation?.icons[icon.sprite]?.dataUri ?? options.iconDataUris?.[icon.sprite]);
145
145
  }
146
+ function focusTitleLayout(renderer, value, maximumWidth) {
147
+ const title = value.trim().replace(/\s+/gu, ' ');
148
+ if (renderer.measure(title, 11) <= maximumWidth)
149
+ return { lines: [title], fontSize: 11 };
150
+ const words = title.split(' ');
151
+ if (words.length < 2)
152
+ return { lines: [title], fontSize: 10 };
153
+ let best = { lines: [title], width: renderer.measure(title, 10) };
154
+ for (let index = 1; index < words.length; index += 1) {
155
+ const lines = [words.slice(0, index).join(' '), words.slice(index).join(' ')];
156
+ const width = Math.max(...lines.map((line) => renderer.measure(line, 10)));
157
+ if (width < best.width)
158
+ best = { lines, width };
159
+ }
160
+ return { lines: best.lines, fontSize: 10 };
161
+ }
146
162
  function svgDocument(plan, layout, diagnostics, options) {
147
163
  const horizontal = options.horizontalSpacing ?? FOCUS_HORIZONTAL_GRID_PIXELS;
148
164
  const vertical = options.verticalSpacing ?? FOCUS_VERTICAL_GRID_PIXELS;
@@ -160,10 +176,10 @@ function svgDocument(plan, layout, diagnostics, options) {
160
176
  const nodeWidth = FOCUS_NODE_WIDTH_PIXELS;
161
177
  const nodeHeight = FOCUS_NODE_HEIGHT_PIXELS;
162
178
  const nodes = [...layout.nodes].sort((left, right) => compareCodeUnits(left.id, right.id));
163
- const minimumX = Math.min(0, ...nodes.map(({ x }) => x));
164
- const minimumY = Math.min(0, ...nodes.map(({ y }) => y));
165
- const maximumX = Math.max(0, ...nodes.map(({ x }) => x));
166
- const maximumY = Math.max(0, ...nodes.map(({ y }) => y));
179
+ const minimumX = nodes.length === 0 ? 0 : Math.min(...nodes.map(({ x }) => x));
180
+ const minimumY = nodes.length === 0 ? 0 : Math.min(...nodes.map(({ y }) => y));
181
+ const maximumX = nodes.length === 0 ? 0 : Math.max(...nodes.map(({ x }) => x));
182
+ const maximumY = nodes.length === 0 ? 0 : Math.max(...nodes.map(({ y }) => y));
167
183
  const logicalWidth = Math.max(320, (maximumX - minimumX) * horizontal + nodeWidth + padding * 2);
168
184
  const logicalHeight = Math.max(240, (maximumY - minimumY) * vertical + nodeHeight + padding * 2);
169
185
  const width = Math.max(1, Math.round(logicalWidth * outputScale));
@@ -268,6 +284,8 @@ function svgDocument(plan, layout, diagnostics, options) {
268
284
  .map(({ code }) => code)
269
285
  .sort()
270
286
  .join(' ');
287
+ const titleWidth = nodeWidth - 72;
288
+ const titleLayout = focusTitleLayout(toolText, title, titleWidth);
271
289
  parts.push(`<g class="focus-node ${escapeXml(focus.visibility)} ${severity}" id="focus-${escapeXml(node.id)}" data-focus-id="${escapeXml(node.id)}" data-branch="${escapeXml(focus.branchId ?? '')}" data-lane="${escapeXml(node.laneId)}" data-diagnostics="${escapeXml(diagnosticCodes)}" transform="translate(${point.x} ${point.y})" filter="url(#shadow)">`, `<title>${escapeXml(`${title} (${node.id}) at ${node.x},${node.y}`)}</title>`, `<rect width="${nodeWidth}" height="${nodeHeight}" rx="8" fill="${fill}" stroke="${stroke}" stroke-width="${severity === 'clean' ? 1.5 : 3}"/>`);
272
290
  if (iconUri !== undefined) {
273
291
  parts.push(`<image href="${escapeXml(iconUri)}" x="8" y="10" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>`);
@@ -281,20 +299,26 @@ function svgDocument(plan, layout, diagnostics, options) {
281
299
  fill: '#91a0b4',
282
300
  }));
283
301
  }
284
- parts.push(toolText.render(title.slice(0, 18), {
285
- x: 64,
286
- y: 25,
287
- fontSize: 12,
288
- weight: 700,
289
- fill: '#f2f5fa',
290
- }), toolText.render(node.id.slice(0, 22), {
302
+ const titleY = titleLayout.lines.length === 1 ? 25 : 20;
303
+ for (const [index, line] of titleLayout.lines.entries()) {
304
+ const measuredWidth = toolText.measure(line, titleLayout.fontSize);
305
+ parts.push(toolText.render(line, {
306
+ x: 64,
307
+ y: titleY + index * 13,
308
+ fontSize: titleLayout.fontSize,
309
+ weight: 700,
310
+ fill: '#f2f5fa',
311
+ ...(measuredWidth <= titleWidth ? {} : { targetWidth: titleWidth }),
312
+ }));
313
+ }
314
+ parts.push(toolText.render(node.id.slice(0, 22), {
291
315
  x: 64,
292
- y: 43,
316
+ y: 49,
293
317
  fontSize: 9,
294
318
  fill: '#aeb9c8',
295
319
  }), toolText.render(`(${node.x}, ${node.y}) · ${node.laneId}`, {
296
320
  x: 64,
297
- y: 59,
321
+ y: 64,
298
322
  fontSize: 9,
299
323
  fill: '#8391a5',
300
324
  }), '</g>');
@@ -4,8 +4,10 @@ import { registerFocusTools } from '../tools/focus.js';
4
4
  import { registerGuiTools } from '../tools/gui.js';
5
5
  import { registerMapTools } from '../tools/map.js';
6
6
  import { registerEventTools } from '../tools/event.js';
7
+ import { registerTechnologyTools } from '../tools/technology.js';
8
+ import { registerProbabilityTools } from '../tools/probability.js';
7
9
  import { registerMcpResources } from '../resources/register.js';
8
- export const SERVER_INSTRUCTIONS = 'Use hoi4.focus_inspect and hoi4.focus_render for fast structural work; hoi4.focus_raster adds decoded icons and PNG output. Use hoi4.event_inspect to analyze event chains. Event tools are read-only. Focus, GUI, and map rewrite tools edit the current mod. Large evidence is returned through linked resources.';
10
+ export const SERVER_INSTRUCTIONS = 'Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Start unfamiliar event chains with hoi4.event_inspect. Event tools are read-only. Start technology and doctrine work with hoi4.tech_inspect. Technology tools are read-only. Start weighted AI, MTTH, random, and declared-pool analysis with hoi4.probability_inspect. Probability tools are read-only. Large evidence is linked as resources.';
9
11
  export function createMcpServer(engine, context = {}) {
10
12
  const server = new McpServer({
11
13
  name: PACKAGE_NAME,
@@ -19,6 +21,8 @@ export function createMcpServer(engine, context = {}) {
19
21
  registerGuiTools(server, engine, context);
20
22
  registerMapTools(server, engine, context);
21
23
  registerEventTools(server, engine, context);
24
+ registerTechnologyTools(server, engine, context);
25
+ registerProbabilityTools(server, engine, context);
22
26
  registerMcpResources(server, engine, context);
23
27
  return server;
24
28
  }
@@ -44,13 +44,13 @@ const compactPublicOperationResultSchema = z
44
44
  status: z.enum(['ok', 'blocked', 'error']),
45
45
  code: z.string(),
46
46
  workspaceId: z.string(),
47
- filesScanned: z.array(z.unknown()).max(MAX_INLINE_FILES_SCANNED),
48
- proposedFiles: z.array(z.unknown()).max(MAX_INLINE_PROPOSED_FILES),
49
- changedFiles: z.array(z.unknown()).max(MAX_INLINE_CHANGED_FILES),
47
+ filesScanned: z.array(z.unknown()),
48
+ proposedFiles: z.array(z.unknown()),
49
+ changedFiles: z.array(z.unknown()),
50
50
  diagnostics: z.array(z.unknown()).max(MAX_INLINE_DIAGNOSTICS),
51
51
  artifacts: z.array(z.unknown()).max(MAX_INLINE_ARTIFACT_LINKS),
52
52
  validation: z.unknown(),
53
- blockers: z.array(z.unknown()).max(MAX_INLINE_BLOCKERS),
53
+ blockers: z.array(z.unknown()),
54
54
  data: z.object({}).catchall(z.unknown()),
55
55
  })
56
56
  .strict();
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
3
- import { compareCodeUnits, canonicalJson } from '../../core/canonical.js';
3
+ import { compareCodeUnits, canonicalJson, hashCanonical } from '../../core/canonical.js';
4
4
  import { RenderBudget } from '../../core/render-budget.js';
5
5
  import { emptyServiceResult, ServiceError } from '../../core/result.js';
6
6
  import { parseClausewitz } from '../../core/source/index.js';
@@ -116,6 +116,7 @@ const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('
116
116
  .strict(),
117
117
  ]));
118
118
  const compactFocusPlanSchema = compactValidatedInputSchema(z.union([focusTreePlanSchema, continuousFocusPaletteSchema]), `Complete focus plan: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/focus.md`);
119
+ const compactFocusLayoutSchema = compactValidatedInputSchema(focusLayoutSchema, 'Prior deterministic focus layout used to stabilize unchanged positions.');
119
120
  const focusInspectInput = z
120
121
  .object({
121
122
  mode: z.enum(['national', 'continuous']).optional(),
@@ -123,7 +124,7 @@ const focusInspectInput = z
123
124
  relativePath: workspaceRelativePathSchema.optional(),
124
125
  treeId: z.string().min(1).max(256).optional(),
125
126
  paletteId: z.string().min(1).max(256).optional(),
126
- previous: focusLayoutSchema.optional(),
127
+ previous: compactFocusLayoutSchema.optional(),
127
128
  laneSpacing: z.number().int().min(1).max(100).optional(),
128
129
  nodeSpacing: z.number().int().min(1).max(100).optional(),
129
130
  })
@@ -307,6 +308,42 @@ const LARGE_FOCUS_RENDER_SCALE = 0.5;
307
308
  function automaticFocusRenderScale(focusCount) {
308
309
  return focusCount >= LARGE_FOCUS_RENDER_NODE_THRESHOLD ? LARGE_FOCUS_RENDER_SCALE : 1;
309
310
  }
311
+ export async function computeFocusVisualRevisions(engine, context, workspaceId, snapshot, selectors, signal) {
312
+ const workspace = engine.resolver.get(workspaceId, context.principal);
313
+ const paletteImport = continuousPalettes(snapshot);
314
+ const revisions = [];
315
+ for (const selector of selectors) {
316
+ signal?.throwIfAborted();
317
+ const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, selector.relativePath), paletteImport.palettes);
318
+ const plan = selectPlan(imported, selector.treeId);
319
+ const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
320
+ const presentation = await resolveFocusPresentation({
321
+ plans: [plan],
322
+ palettes: linkedPalettes,
323
+ files: snapshot.files,
324
+ index: snapshot.index,
325
+ scanner: engine.scanner,
326
+ workspace,
327
+ decodeIcons: false,
328
+ budget: new RenderBudget(),
329
+ ...(signal === undefined ? {} : { signal }),
330
+ });
331
+ const filesScanned = [
332
+ ...new Set([plan.provenance.sourcePath, ...presentation.filesScanned]),
333
+ ].sort(compareCodeUnits);
334
+ revisions.push({
335
+ relativePath: selector.relativePath,
336
+ treeId: plan.id,
337
+ revision: hashCanonical({
338
+ planHash: focusPlanHash(plan),
339
+ linkedPalettes,
340
+ presentationSourceHashes: presentation.sourceHashes,
341
+ }),
342
+ filesScanned,
343
+ });
344
+ }
345
+ return revisions;
346
+ }
310
347
  async function executeFocusVisualTool(engine, workbench, context, input, extra, rasterize) {
311
348
  const { workspaceId: requestedWorkspaceId, relativePath } = input;
312
349
  const workspaceId = engine.resolver.resolveWorkspaceId(requestedWorkspaceId, context.principal);
@@ -0,0 +1,413 @@
1
+ import { z } from 'zod/v4';
2
+ import { emptyServiceResult } from '../../core/result.js';
3
+ import { ProbabilityAnalyzer, } from '../../probability/service.js';
4
+ import { customWeightedPoolManifestSchema, probabilityAcceptanceBandSchema, probabilityAdapterIdSchema, probabilityDiagnosticThresholdsSchema, probabilityMetricSchema, probabilityOutputSchema, probabilityRenderFilterSchema, probabilityScenarioSetSchema, probabilitySamplingMethodSchema, probabilitySourceSchema, } from '../../schemas/probability.js';
5
+ import { workspaceIdSchema } from '../../schemas/common.js';
6
+ import { compactValidatedInputSchema } from '../server/context-schemas.js';
7
+ import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
8
+ import { progressReporter } from '../server/progress.js';
9
+ import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
10
+ const nestedSource = compactValidatedInputSchema(probabilitySourceSchema, 'Source selector or proposed source.');
11
+ const nestedScenarios = compactValidatedInputSchema(probabilityScenarioSetSchema, 'Explicit world-state scenarios.');
12
+ const nestedManifest = compactValidatedInputSchema(customWeightedPoolManifestSchema, 'Declared custom weighted pool and state transitions.');
13
+ const nestedRenderFilter = compactValidatedInputSchema(probabilityRenderFilterSchema, 'Optional scenario, candidate, metric, diagnostic, source, and page filter.');
14
+ const nestedAcceptanceBands = compactValidatedInputSchema(z.array(probabilityAcceptanceBandSchema).max(100_000), 'Optional named metric acceptance bands.');
15
+ const nestedDiagnosticThresholds = compactValidatedInputSchema(probabilityDiagnosticThresholdsSchema, 'Optional diagnostic thresholds.');
16
+ const candidatePool = z.array(z.string().min(1).max(512)).max(100_000).optional();
17
+ const outputs = z.array(probabilityOutputSchema).max(10).optional();
18
+ const commonShape = {
19
+ workspaceId: workspaceIdSchema,
20
+ adapter: probabilityAdapterIdSchema,
21
+ source: nestedSource,
22
+ scenarioSet: nestedScenarios,
23
+ candidatePool,
24
+ horizonDays: z.number().positive().max(1_000_000).optional(),
25
+ metrics: z.array(probabilityMetricSchema).min(1).max(4).optional(),
26
+ acceptanceBands: nestedAcceptanceBands.optional(),
27
+ diagnosticThresholds: nestedDiagnosticThresholds.optional(),
28
+ outputs,
29
+ refresh: z.boolean().optional(),
30
+ };
31
+ const inspectInput = z
32
+ .object({
33
+ workspaceId: workspaceIdSchema,
34
+ adapter: probabilityAdapterIdSchema.optional(),
35
+ source: nestedSource.optional(),
36
+ candidatePool,
37
+ refresh: z.boolean().optional(),
38
+ })
39
+ .strict()
40
+ .superRefine((value, context) => {
41
+ if ((value.adapter === undefined) !== (value.source === undefined))
42
+ context.addIssue({
43
+ code: 'custom',
44
+ message: 'Provide adapter and source together, or omit both to list adapters',
45
+ });
46
+ });
47
+ const evaluateInput = z.object(commonShape).strict();
48
+ const sweepInput = z
49
+ .object({
50
+ ...commonShape,
51
+ sweep: z
52
+ .object({
53
+ paths: z.array(z.string().min(1).max(1024)).min(1).max(32),
54
+ steps: z.number().int().min(2).max(10_000).default(25),
55
+ pairwise: z.boolean().default(false),
56
+ findRankReversals: z.boolean().default(true),
57
+ })
58
+ .strict(),
59
+ })
60
+ .strict();
61
+ const simulateInput = z
62
+ .object({
63
+ ...commonShape,
64
+ samples: z.number().int().min(100).max(10_000_000).default(100_000),
65
+ seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
66
+ confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
67
+ samplingMethod: probabilitySamplingMethodSchema.default('latin_hypercube'),
68
+ })
69
+ .strict();
70
+ const sequenceInput = z
71
+ .object({
72
+ workspaceId: workspaceIdSchema,
73
+ scenarioSet: nestedScenarios,
74
+ customPoolManifest: nestedManifest,
75
+ horizonDays: z.number().positive().max(1_000_000),
76
+ maxSteps: z.number().int().min(1).max(100_000).default(1_000),
77
+ samples: z.number().int().min(100).max(10_000_000).default(100_000),
78
+ seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
79
+ confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
80
+ acceptanceBands: nestedAcceptanceBands.optional(),
81
+ diagnosticThresholds: nestedDiagnosticThresholds.optional(),
82
+ outputs,
83
+ })
84
+ .strict();
85
+ const compareInput = z
86
+ .object({
87
+ workspaceId: workspaceIdSchema,
88
+ adapter: probabilityAdapterIdSchema,
89
+ before: nestedSource,
90
+ after: nestedSource,
91
+ scenarioSet: nestedScenarios,
92
+ candidatePool,
93
+ horizonDays: z.number().positive().max(1_000_000).optional(),
94
+ acceptanceBands: nestedAcceptanceBands.optional(),
95
+ diagnosticThresholds: nestedDiagnosticThresholds.optional(),
96
+ outputs,
97
+ refresh: z.boolean().optional(),
98
+ })
99
+ .strict();
100
+ const renderInput = z
101
+ .object({
102
+ workspaceId: workspaceIdSchema,
103
+ analysisId: z.string().min(1).max(256),
104
+ expectedScenarioHash: sha256Schema.optional(),
105
+ outputs: z.array(probabilityOutputSchema).min(1).max(10),
106
+ includeHtml: z.boolean().default(false),
107
+ filter: nestedRenderFilter.optional(),
108
+ })
109
+ .strict();
110
+ const analysisDataSchema = z
111
+ .object({
112
+ operation: z.enum(['evaluate', 'sweep', 'simulate', 'sequence', 'compare', 'render']),
113
+ analysisId: z.string().max(256),
114
+ analysisStatus: z.enum(['complete', 'partial', 'blocked', 'cancelled', 'stale']),
115
+ adapterId: z.string().max(256),
116
+ sourceRevision: sha256Schema,
117
+ sourceHash: sha256Schema,
118
+ scenarioHash: sha256Schema,
119
+ cacheKey: sha256Schema,
120
+ scenarios: nonNegativeIntegerSchema,
121
+ candidates: nonNegativeIntegerSchema,
122
+ unresolved: nonNegativeIntegerSchema,
123
+ diagnostics: nonNegativeIntegerSchema,
124
+ sweepPoints: nonNegativeIntegerSchema.optional(),
125
+ samples: nonNegativeIntegerSchema.optional(),
126
+ sequenceMethod: z
127
+ .enum(['exact_state_distribution', 'bounded_beam', 'seeded_monte_carlo'])
128
+ .optional(),
129
+ comparisonChanges: nonNegativeIntegerSchema.optional(),
130
+ visualResources: nonNegativeIntegerSchema.optional(),
131
+ })
132
+ .strict();
133
+ const inspectDataSchema = z
134
+ .object({
135
+ adapters: nonNegativeIntegerSchema,
136
+ adapterId: z.string().max(256).optional(),
137
+ sourceRevision: sha256Schema.optional(),
138
+ sourceHash: sha256Schema.optional(),
139
+ candidates: nonNegativeIntegerSchema,
140
+ poolComplete: z.boolean().optional(),
141
+ requiredInputs: nonNegativeIntegerSchema,
142
+ unresolved: nonNegativeIntegerSchema,
143
+ })
144
+ .strict();
145
+ const analysisOutput = strictOperationResultSchema(analysisDataSchema);
146
+ const inspectOutput = strictOperationResultSchema(inspectDataSchema);
147
+ const readOnly = {
148
+ readOnlyHint: true,
149
+ destructiveHint: false,
150
+ idempotentHint: true,
151
+ openWorldHint: false,
152
+ };
153
+ function requestContext(engine, context, workspaceId, refresh, signal) {
154
+ return {
155
+ workspaceId: engine.resolver.resolveWorkspaceId(workspaceId, context.principal),
156
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
157
+ ...(refresh === undefined ? {} : { refresh }),
158
+ signal,
159
+ };
160
+ }
161
+ function analysisData(result) {
162
+ return {
163
+ operation: result.operation,
164
+ analysisId: result.analysisId,
165
+ analysisStatus: result.status,
166
+ adapterId: result.adapter.id,
167
+ sourceRevision: result.metadata.sourceRevision,
168
+ sourceHash: result.metadata.sourceHash,
169
+ scenarioHash: result.metadata.scenarioHash,
170
+ cacheKey: result.metadata.cacheKey,
171
+ scenarios: result.scenarios.length,
172
+ candidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.length, 0),
173
+ unresolved: result.unresolved.length,
174
+ diagnostics: result.diagnostics.length,
175
+ ...(result.sweep === undefined ? {} : { sweepPoints: result.sweep.points.length }),
176
+ ...(result.metadata.samples === undefined ? {} : { samples: result.metadata.samples }),
177
+ ...(result.sequence === undefined ? {} : { sequenceMethod: result.sequence.method }),
178
+ ...(result.comparison === undefined
179
+ ? {}
180
+ : { comparisonChanges: result.comparison.scenarioChanges.length }),
181
+ visualResources: result.resources.filter(({ mimeType }) => mimeType.startsWith('image/'))
182
+ .length,
183
+ };
184
+ }
185
+ function analysisServiceResult(workspaceId, result) {
186
+ const output = emptyServiceResult(workspaceId, analysisData(result));
187
+ output.code =
188
+ result.status === 'complete'
189
+ ? 'PROBABILITY_ANALYZED'
190
+ : result.status === 'stale'
191
+ ? result.diagnostics.some(({ code }) => code === 'PROBABILITY_SCENARIO_STALE')
192
+ ? 'PROBABILITY_SCENARIO_STALE'
193
+ : 'PROBABILITY_ANALYSIS_STALE'
194
+ : 'PROBABILITY_ANALYZED_PARTIAL';
195
+ output.artifacts = result.resources;
196
+ output.diagnostics = result.diagnostics;
197
+ output.validation = {
198
+ passed: result.status !== 'blocked' && result.status !== 'cancelled',
199
+ checks: [
200
+ {
201
+ id: 'uncertainty-visible',
202
+ passed: true,
203
+ message: `${result.unresolved.length} unresolved or bounded analysis item(s) are explicit`,
204
+ },
205
+ ],
206
+ };
207
+ return output;
208
+ }
209
+ export function registerProbabilityTools(server, engine, context) {
210
+ const analyzer = new ProbabilityAnalyzer(engine);
211
+ server.registerTool('hoi4.probability_inspect', {
212
+ title: 'Inspect AI and MTTH weighted logic',
213
+ description: 'List versioned adapters or discover weighted blocks, candidate pools, capabilities, provenance, and unsupported constructs.',
214
+ inputSchema: inspectInput,
215
+ outputSchema: inspectOutput,
216
+ annotations: readOnly,
217
+ }, async (input, extra) => {
218
+ const progress = progressReporter(extra);
219
+ const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
220
+ try {
221
+ await progress.report(0, 2, 'Inspecting weighted source');
222
+ const inspected = await analyzer.inspect(base, input.adapter, input.source, input.candidatePool);
223
+ const result = emptyServiceResult(base.workspaceId, {
224
+ adapters: inspected.adapters.length,
225
+ ...(inspected.surface === undefined
226
+ ? {}
227
+ : {
228
+ adapterId: inspected.surface.adapter.id,
229
+ sourceRevision: inspected.surface.sourceRevision,
230
+ sourceHash: inspected.surface.sourceHash,
231
+ poolComplete: inspected.surface.poolComplete,
232
+ }),
233
+ candidates: inspected.surface?.candidateCount ?? 0,
234
+ requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
235
+ unresolved: inspected.surface?.unsupported.length ?? 0,
236
+ });
237
+ result.code =
238
+ inspected.surface === undefined
239
+ ? 'PROBABILITY_ADAPTERS_LISTED'
240
+ : 'PROBABILITY_SOURCE_INSPECTED';
241
+ result.artifacts = inspected.artifacts;
242
+ setInlineFilesScanned(result, inspected.filesScanned);
243
+ await progress.report(2, 2, 'Weighted source inspection complete');
244
+ return toolResult(result);
245
+ }
246
+ catch (error) {
247
+ return errorResult(error, base.workspaceId);
248
+ }
249
+ });
250
+ server.registerTool('hoi4.probability_evaluate', {
251
+ title: 'Evaluate AI and MTTH scenarios',
252
+ description: 'Evaluate eligibility, modifier traces, exact values, proven probabilities, MTTH timing, bounds, and unresolved inputs.',
253
+ inputSchema: evaluateInput,
254
+ outputSchema: analysisOutput,
255
+ annotations: readOnly,
256
+ }, async (input, extra) => {
257
+ const progress = progressReporter(extra);
258
+ const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
259
+ try {
260
+ await progress.report(0, 3, 'Scanning and evaluating weighted source');
261
+ const request = {
262
+ ...base,
263
+ ...input,
264
+ workspaceId: base.workspaceId,
265
+ };
266
+ const analyzed = await analyzer.evaluate(request);
267
+ await progress.report(3, 3, 'Scenario evaluation complete');
268
+ return toolResult(analysisServiceResult(base.workspaceId, analyzed));
269
+ }
270
+ catch (error) {
271
+ return errorResult(error, base.workspaceId);
272
+ }
273
+ });
274
+ server.registerTool('hoi4.probability_sweep', {
275
+ title: 'Sweep AI and MTTH parameters',
276
+ description: 'Evaluate declared ranges, sensitivity, breakpoints, and rank reversals without inventing world state.',
277
+ inputSchema: sweepInput,
278
+ outputSchema: analysisOutput,
279
+ annotations: readOnly,
280
+ }, async (input, extra) => {
281
+ const progress = progressReporter(extra);
282
+ const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
283
+ try {
284
+ await progress.report(0, 3, 'Evaluating parameter sweep');
285
+ const request = {
286
+ ...base,
287
+ ...input,
288
+ workspaceId: base.workspaceId,
289
+ };
290
+ const analyzed = await analyzer.sweep(request);
291
+ await progress.report(3, 3, 'Parameter sweep complete');
292
+ return toolResult(analysisServiceResult(base.workspaceId, analyzed));
293
+ }
294
+ catch (error) {
295
+ return errorResult(error, base.workspaceId);
296
+ }
297
+ });
298
+ server.registerTool('hoi4.probability_simulate', {
299
+ title: 'Simulate uncertain weighted scenarios',
300
+ description: 'Run reproducible seeded Monte Carlo analysis with distributions, supported correlations, and confidence intervals.',
301
+ inputSchema: simulateInput,
302
+ outputSchema: analysisOutput,
303
+ annotations: readOnly,
304
+ }, async (input, extra) => {
305
+ const progress = progressReporter(extra);
306
+ const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
307
+ try {
308
+ await progress.report(0, input.samples, 'Sampling uncertain scenarios');
309
+ const request = {
310
+ ...base,
311
+ ...input,
312
+ workspaceId: base.workspaceId,
313
+ };
314
+ const analyzed = await analyzer.simulate(request);
315
+ await progress.report(input.samples, input.samples, 'Seeded simulation complete');
316
+ return toolResult(analysisServiceResult(base.workspaceId, analyzed));
317
+ }
318
+ catch (error) {
319
+ return errorResult(error, base.workspaceId);
320
+ }
321
+ });
322
+ server.registerTool('hoi4.probability_sequence', {
323
+ title: 'Analyze a declared weighted sequence',
324
+ description: 'Analyze only manifest-declared recovery, caps, cooldowns, removals, resets, timers, and terminal states.',
325
+ inputSchema: sequenceInput,
326
+ outputSchema: analysisOutput,
327
+ annotations: readOnly,
328
+ }, async (input, extra) => {
329
+ const progress = progressReporter(extra);
330
+ const base = requestContext(engine, context, input.workspaceId, undefined, progress.signal);
331
+ try {
332
+ await progress.report(0, input.maxSteps, 'Analyzing declared sequence');
333
+ const request = {
334
+ ...base,
335
+ ...input,
336
+ workspaceId: base.workspaceId,
337
+ };
338
+ const analyzed = await analyzer.sequence(request);
339
+ await progress.report(input.maxSteps, input.maxSteps, 'Sequence analysis complete');
340
+ return toolResult(analysisServiceResult(base.workspaceId, analyzed));
341
+ }
342
+ catch (error) {
343
+ return errorResult(error, base.workspaceId);
344
+ }
345
+ });
346
+ server.registerTool('hoi4.probability_compare', {
347
+ title: 'Compare weighted source patches',
348
+ description: 'Compare real or proposed source under identical scenarios with modifier, score, probability, timing, and rank attribution.',
349
+ inputSchema: compareInput,
350
+ outputSchema: analysisOutput,
351
+ annotations: readOnly,
352
+ }, async (input, extra) => {
353
+ const progress = progressReporter(extra);
354
+ const base = requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
355
+ try {
356
+ await progress.report(0, 4, 'Comparing weighted source');
357
+ const request = {
358
+ ...base,
359
+ ...input,
360
+ workspaceId: base.workspaceId,
361
+ };
362
+ const analyzed = await analyzer.compare(request);
363
+ await progress.report(4, 4, 'Weighted source comparison complete');
364
+ return toolResult(analysisServiceResult(base.workspaceId, analyzed));
365
+ }
366
+ catch (error) {
367
+ return errorResult(error, base.workspaceId);
368
+ }
369
+ });
370
+ server.registerTool('hoi4.probability_render', {
371
+ title: 'Render AI and MTTH analysis',
372
+ description: 'Render cached analysis data as deterministic ranking, matrix, waterfall, timing, sensitivity, sequence, comparison, or unresolved resources.',
373
+ inputSchema: renderInput,
374
+ outputSchema: analysisOutput,
375
+ annotations: readOnly,
376
+ }, async (input, extra) => {
377
+ const progress = progressReporter(extra);
378
+ const base = requestContext(engine, context, input.workspaceId, undefined, progress.signal);
379
+ try {
380
+ await progress.report(0, input.outputs.length, 'Rendering analysis resources');
381
+ const request = {
382
+ ...base,
383
+ ...input,
384
+ workspaceId: base.workspaceId,
385
+ outputs: input.outputs.filter((output) => output !== 'json'),
386
+ };
387
+ const analyzed = await analyzer.render(request);
388
+ await progress.report(input.outputs.length, input.outputs.length, 'Analysis rendering complete');
389
+ return toolResult(analysisServiceResult(base.workspaceId, analyzed));
390
+ }
391
+ catch (error) {
392
+ return errorResult(error, base.workspaceId);
393
+ }
394
+ });
395
+ server.registerPrompt('hoi4.probability_analysis', {
396
+ title: 'Analyze HOI4 weighted logic',
397
+ description: 'Plan one source-linked AI, MTTH, random, or declared-pool analysis and return to the owning modding workflow.',
398
+ argsSchema: {
399
+ objective: z.string().min(1).max(4096),
400
+ sourceHint: z.string().max(1024).optional(),
401
+ },
402
+ }, ({ objective, sourceHint }) => ({
403
+ messages: [
404
+ {
405
+ role: 'user',
406
+ content: {
407
+ type: 'text',
408
+ text: `Analyze this HOI4 weighted-logic task: ${objective}${sourceHint === undefined ? '' : `\nSource hint: ${sourceHint}`}\nIdentify the exact weighted surface and adapter, declare representative world-state scenarios and every required candidate or external factor, inspect first, run the narrowest useful evaluate/sweep/simulate/sequence/compare operation, review linked uncertainty and provenance, then return the findings to the normal owning modding workflow. Do not infer missing state, execute effects, or edit source through probability tools.`,
409
+ },
410
+ },
411
+ ],
412
+ }));
413
+ }