hoi4-agent-tools 3.2.0 → 3.4.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 (99) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -0
  3. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
  4. package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -9
  5. package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
  6. package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
  7. package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
  8. package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
  9. package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
  10. package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
  11. package/dist/hoi4_agent_tools/event/operations.js +16 -0
  12. package/dist/hoi4_agent_tools/event/service.js +21 -2
  13. package/dist/hoi4_agent_tools/focus/compact.js +241 -27
  14. package/dist/hoi4_agent_tools/focus/layout.js +119 -8
  15. package/dist/hoi4_agent_tools/focus/render.js +62 -0
  16. package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
  17. package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
  18. package/dist/hoi4_agent_tools/gui/assets.js +54 -4
  19. package/dist/hoi4_agent_tools/gui/layout.js +935 -104
  20. package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
  21. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
  22. package/dist/hoi4_agent_tools/gui/shader.js +515 -0
  23. package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
  24. package/dist/hoi4_agent_tools/gui/studio.js +245 -19
  25. package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
  26. package/dist/hoi4_agent_tools/gui/validators.js +38 -12
  27. package/dist/hoi4_agent_tools/map/operations.js +70 -1
  28. package/dist/hoi4_agent_tools/map/render.js +70 -1
  29. package/dist/hoi4_agent_tools/map/service.js +30 -6
  30. package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
  31. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  32. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
  33. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
  34. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
  35. package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
  36. package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
  37. package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
  38. package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
  39. package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
  40. package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
  41. package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
  42. package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
  43. package/dist/hoi4_agent_tools/package-check/service.js +231 -0
  44. package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
  45. package/dist/hoi4_agent_tools/probability/operations.js +8 -0
  46. package/dist/hoi4_agent_tools/probability/service.js +33 -12
  47. package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
  48. package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
  49. package/dist/hoi4_agent_tools/schemas/event.js +8 -0
  50. package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
  51. package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
  52. package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
  53. package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
  54. package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
  55. package/dist/hoi4_agent_tools/technology/graph.js +25 -10
  56. package/dist/hoi4_agent_tools/technology/render.js +286 -31
  57. package/dist/hoi4_agent_tools/technology/service.js +55 -8
  58. package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
  59. package/dist/hoi4_agent_tools/version.js +1 -1
  60. package/docs/README.md +1 -0
  61. package/docs/events.md +3 -1
  62. package/docs/focus.md +10 -0
  63. package/docs/gui.md +49 -3
  64. package/docs/map.md +8 -0
  65. package/docs/mechanics.md +70 -0
  66. package/docs/probability.md +3 -1
  67. package/docs/technology.md +7 -2
  68. package/package.json +2 -1
  69. package/schemas/configuration.schema.json +1 -1
  70. package/schemas/continuous-focus-palette.schema.json +1 -1
  71. package/schemas/custom-weighted-pool.schema.json +1 -1
  72. package/schemas/decision-inspect-input.schema.json +1 -1
  73. package/schemas/event-feature-manifest.schema.json +1 -1
  74. package/schemas/focus-plan.schema.json +1 -1
  75. package/schemas/focus-planning-sidecar.schema.json +1 -1
  76. package/schemas/gui-animation-source.schema.json +1 -1
  77. package/schemas/gui-helper.schema.json +1 -1
  78. package/schemas/gui-scenario.schema.json +1 -1
  79. package/schemas/helper-expansion-request.schema.json +1 -1
  80. package/schemas/helper-expansion-summary.schema.json +1 -1
  81. package/schemas/impact-inspect-input.schema.json +1 -1
  82. package/schemas/map-operation.schema.json +1 -1
  83. package/schemas/mechanic-test-input.schema.json +650 -0
  84. package/schemas/operation-result.schema.json +1 -1
  85. package/schemas/package-check-input.schema.json +164 -0
  86. package/schemas/package-manifest.schema.json +142 -0
  87. package/schemas/probability-analysis-result.schema.json +1 -1
  88. package/schemas/probability-compare-input.schema.json +1 -1
  89. package/schemas/probability-evaluate-input.schema.json +1 -1
  90. package/schemas/probability-inspect-input.schema.json +1 -1
  91. package/schemas/probability-render-input.schema.json +1 -1
  92. package/schemas/probability-scenario-set.schema.json +1 -1
  93. package/schemas/probability-sequence-input.schema.json +1 -1
  94. package/schemas/probability-simulate-input.schema.json +1 -1
  95. package/schemas/probability-sweep-input.schema.json +1 -1
  96. package/schemas/scenario-suite.schema.json +1004 -0
  97. package/schemas/scenario-test-input.schema.json +1057 -0
  98. package/schemas/shared-scenario.schema.json +237 -0
  99. package/server.json +2 -2
@@ -357,7 +357,50 @@ export async function renderMap(index, options = {}) {
357
357
  throw new ServiceError('MAP_RENDER_SCALE_INVALID', 'Map render scale must be an integer from 1 through 16');
358
358
  const budget = options.budget ?? new RenderBudget();
359
359
  assertRenderDimensions(raster.width, raster.height, 'map RGBA source plane');
360
- const outputDimensions = budget.reserve(raster.width * resolved.scale, raster.height * resolved.scale, 'map PNG output');
360
+ let tile = options.tile;
361
+ if (options.area !== undefined) {
362
+ if (tile !== undefined)
363
+ throw new ServiceError('MAP_RENDER_AREA_CONFLICT', 'Choose a map tile or entity area');
364
+ const ids = new Set(options.area.provinceIds ?? []);
365
+ for (const stateId of options.area.stateIds ?? []) {
366
+ const state = index.statesById.get(stateId);
367
+ if (state === undefined)
368
+ throw new ServiceError('MAP_AREA_STATE_MISSING', 'Map area state ID is missing', {
369
+ stateId,
370
+ });
371
+ for (const id of state.provinces)
372
+ ids.add(id);
373
+ }
374
+ for (const regionId of options.area.regionIds ?? []) {
375
+ const region = index.regionsById.get(regionId);
376
+ if (region === undefined)
377
+ throw new ServiceError('MAP_AREA_REGION_MISSING', 'Map area region ID is missing', {
378
+ regionId,
379
+ });
380
+ for (const id of region.provinces)
381
+ ids.add(id);
382
+ }
383
+ if (ids.size === 0)
384
+ throw new ServiceError('MAP_AREA_EMPTY', 'Map area has no provinces');
385
+ const geometry = [...ids].map((id) => {
386
+ const province = raster.geometry.get(id);
387
+ if (province === undefined)
388
+ throw new ServiceError('MAP_AREA_PROVINCE_MISSING', 'Map area province ID has no raster geometry', { provinceId: id });
389
+ return province;
390
+ });
391
+ const padding = options.area.padding ?? 32;
392
+ const x = Math.max(0, Math.min(...geometry.map(({ minX }) => minX)) - padding);
393
+ const y = Math.max(0, Math.min(...geometry.map(({ minY }) => minY)) - padding);
394
+ const right = Math.min(raster.width, Math.max(...geometry.map(({ maxX }) => maxX)) + padding + 1);
395
+ const bottom = Math.min(raster.height, Math.max(...geometry.map(({ maxY }) => maxY)) + padding + 1);
396
+ tile = { x, y, width: right - x, height: bottom - y };
397
+ if (tile.width > 2_048 || tile.height > 2_048)
398
+ throw new ServiceError('MAP_AREA_TOO_LARGE', 'Map area exceeds the tile limit; select fewer entities', { tile });
399
+ }
400
+ if (tile !== undefined &&
401
+ (tile.x + tile.width > raster.width || tile.y + tile.height > raster.height))
402
+ throw new ServiceError('MAP_TILE_OUT_OF_BOUNDS', 'Map tile extends beyond the province raster', { tile, rasterWidth: raster.width, rasterHeight: raster.height });
403
+ const outputDimensions = budget.reserve((tile?.width ?? raster.width) * resolved.scale, (tile?.height ?? raster.height) * resolved.scale, 'map PNG output');
361
404
  const raw = Buffer.alloc(raster.width * raster.height * 4);
362
405
  for (let offset = 0; offset < raster.provinceIds.length; offset += 1) {
363
406
  if (offset % Math.max(raster.width * 32, 1) === 0)
@@ -371,6 +414,32 @@ export async function renderMap(index, options = {}) {
371
414
  }
372
415
  await applyOverlays(index, raw, new Set(resolved.overlays), options.signal);
373
416
  options.signal?.throwIfAborted();
417
+ if (tile !== undefined) {
418
+ const tileRaw = Buffer.alloc(tile.width * tile.height * 4);
419
+ const provinceIds = new Set();
420
+ for (let row = 0; row < tile.height; row += 1) {
421
+ if (row % 32 === 0)
422
+ await renderCheckpoint(options.signal);
423
+ raw.copy(tileRaw, row * tile.width * 4, ((tile.y + row) * raster.width + tile.x) * 4, ((tile.y + row) * raster.width + tile.x + tile.width) * 4);
424
+ for (let column = 0; column < tile.width; column += 1) {
425
+ const id = raster.provinceIds[(tile.y + row) * raster.width + tile.x + column];
426
+ if (id !== undefined && id >= 0)
427
+ provinceIds.add(id);
428
+ }
429
+ }
430
+ const png = await encodePng(tileRaw, tile.width, tile.height, resolved.scale);
431
+ const json = `${canonicalJson({ schemaVersion: 'map-tile.v1', layer: resolved.layer, overlays: resolved.overlays, scale: resolved.scale, tile, raster: { width: raster.width, height: raster.height }, provinceIds: [...provinceIds].sort((left, right) => left - right) })}\n`;
432
+ const html = diffHtmlDocument(`HOI4 map tile - ${resolved.layer}`, png, json);
433
+ return {
434
+ width: outputDimensions.width,
435
+ height: outputDimensions.height,
436
+ tile,
437
+ png,
438
+ json,
439
+ html,
440
+ hashes: { png: sha256Bytes(png), json: sha256Bytes(json), html: sha256Bytes(html) },
441
+ };
442
+ }
374
443
  const png = await encodePng(raw, raster.width, raster.height, resolved.scale);
375
444
  options.signal?.throwIfAborted();
376
445
  const json = `${canonicalJson(await renderMetadata(index, resolved, options.signal))}\n`;
@@ -481,7 +481,7 @@ export class AgentNudger {
481
481
  this.scanner = this.engine.scanner;
482
482
  this.transactions = this.engine.transactions;
483
483
  }
484
- async scan(workspaceId, principal, signal) {
484
+ async scan(workspaceId, principal, signal, includeExternalScriptReferences = false) {
485
485
  signal?.throwIfAborted();
486
486
  const workspace = this.resolver.get(workspaceId, principal);
487
487
  const sourceRoots = {
@@ -493,8 +493,27 @@ export class AgentNudger {
493
493
  const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map);
494
494
  const contentSnapshot = await this.engine.scan(workspaceId, { patterns: [...sourceTextPatterns(sourceRoots), ...bitmapPatterns] }, principal, signal);
495
495
  const locatorSnapshot = await this.engine.scan(workspaceId, { patterns: ['**/*.asset'], rootKinds: ['mod', 'fixture'] }, principal, signal);
496
+ const externalScriptSnapshot = includeExternalScriptReferences
497
+ ? await this.engine.scan(workspaceId, {
498
+ patterns: [
499
+ 'events/**/*.txt',
500
+ 'common/national_focus/**/*.txt',
501
+ 'common/decisions/**/*.txt',
502
+ 'common/scripted_effects/**/*.txt',
503
+ 'common/scripted_triggers/**/*.txt',
504
+ 'common/on_actions/**/*.txt',
505
+ 'common/ideas/**/*.txt',
506
+ 'common/characters/**/*.txt',
507
+ 'history/countries/**/*.txt',
508
+ ],
509
+ }, principal, signal)
510
+ : undefined;
496
511
  signal?.throwIfAborted();
497
- const files = [...contentSnapshot.files, ...locatorSnapshot.files].sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
512
+ const files = [
513
+ ...contentSnapshot.files,
514
+ ...locatorSnapshot.files,
515
+ ...(externalScriptSnapshot?.files ?? []),
516
+ ].sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
498
517
  const revision = hashCanonical(files.map(({ displayPath, loadOrder, sha256 }) => ({ displayPath, loadOrder, sha256 })));
499
518
  return {
500
519
  workspaceId,
@@ -512,7 +531,7 @@ export class AgentNudger {
512
531
  }
513
532
  async prepareOperations(workspaceId, operations, principal, signal) {
514
533
  signal?.throwIfAborted();
515
- const snapshot = await this.scan(workspaceId, principal, signal);
534
+ const snapshot = await this.scan(workspaceId, principal, signal, operations.some(({ kind }) => kind === 'renumber_map_entity'));
516
535
  const plan = await planMapOperationsAsync(snapshot.index, operations, signal);
517
536
  const baselineValidation = await validateMapAsync(snapshot.index, signal === undefined ? {} : { signal });
518
537
  const rawValidation = await validateMapAsync(plan.finalIndex, {
@@ -614,6 +633,9 @@ export class AgentNudger {
614
633
  const bundle = mapBundleWithSourceHashes(rawBundle, completeSourceHashes);
615
634
  const workspace = this.resolver.get(workspaceId, options.principal);
616
635
  const layer = options.layer ?? 'province';
636
+ const tileSuffix = options.tile === undefined && options.area === undefined
637
+ ? ''
638
+ : `-tile-${hashCanonical({ tile: bundle.tile, area: options.area, overlays: [...new Set(options.overlays ?? [])].sort(), scale: options.scale ?? 1 }).slice(0, 12)}`;
617
639
  const provenance = {
618
640
  kind: 'map-render',
619
641
  toolVersion: PACKAGE_VERSION,
@@ -624,24 +646,26 @@ export class AgentNudger {
624
646
  layer,
625
647
  overlays: [...new Set(options.overlays ?? [])].sort(),
626
648
  scale: options.scale ?? 1,
649
+ ...(bundle.tile === undefined ? {} : { tile: bundle.tile }),
650
+ ...(options.area === undefined ? {} : { area: options.area }),
627
651
  },
628
652
  metadata: { sourceHashInventory: sourceEvidence.inventory },
629
653
  };
630
654
  const writes = [
631
655
  {
632
- name: `map-${layer}.png`,
656
+ name: `map-${layer}${tileSuffix}.png`,
633
657
  mimeType: 'image/png',
634
658
  content: bundle.png,
635
659
  provenance,
636
660
  },
637
661
  {
638
- name: `map-${layer}.json`,
662
+ name: `map-${layer}${tileSuffix}.json`,
639
663
  mimeType: 'application/json',
640
664
  content: bundle.json,
641
665
  provenance,
642
666
  },
643
667
  {
644
- name: `map-${layer}.html`,
668
+ name: `map-${layer}${tileSuffix}.html`,
645
669
  mimeType: 'text/html',
646
670
  content: bundle.html,
647
671
  provenance,
@@ -1,7 +1,7 @@
1
1
  import { boundedSourceHashEvidence, publicArtifactLink, } from '../core/artifacts.js';
2
2
  import { canonicalJson, hashCanonical } from '../core/canonical.js';
3
3
  import { autonomousResultArtifacts, } from '../core/transaction-execution.js';
4
- import { emptyServiceResult } from '../core/result.js';
4
+ import { emptyServiceResult, ServiceError } from '../core/result.js';
5
5
  import { setInlineFilesScanned } from '../core/operation-result.js';
6
6
  import { PACKAGE_VERSION } from '../version.js';
7
7
  import { allocateMapIdentifiersAsync, } from './operations.js';
@@ -299,6 +299,69 @@ export function completeMapRewrite(workspaceId, execution, recipe) {
299
299
  }
300
300
  export async function inspectMap(engine, nudger, input, context) {
301
301
  await context.progress(0, 3, 'Inspecting and validating the map');
302
+ if (input.lookupOnly) {
303
+ if (input.allocationRequests.length > 0)
304
+ throw new ServiceError('MAP_LOOKUP_ALLOCATION_UNSUPPORTED', 'Use full map inspection for identifier allocation previews');
305
+ const snapshot = await nudger.scan(context.workspaceId, context.principal, context.signal);
306
+ const catalog = input.query === undefined ? undefined : buildMapCatalog(snapshot.index);
307
+ const queryMatches = catalog === undefined ? [] : searchMapCatalog(catalog, input.query, input.queryLimit);
308
+ const coordinateMatches = input.coordinates.map((coordinate) => lookupMapCoordinate(snapshot.index, coordinate));
309
+ const selected = selectedMapEntities(snapshot, input.provinceIds, input.stateIds, input.regionIds);
310
+ const workspace = engine.resolver.get(context.workspaceId, context.principal);
311
+ const sourceEvidence = mapArtifactSourceEvidence(snapshot.files);
312
+ const selectorHash = hashCanonical({
313
+ provinceIds: input.provinceIds,
314
+ stateIds: input.stateIds,
315
+ regionIds: input.regionIds,
316
+ query: input.query,
317
+ coordinates: input.coordinates,
318
+ }).slice(0, 16);
319
+ const artifact = await engine.artifacts.putChunked(workspace, `map-lookup.${snapshot.revision.slice(0, 16)}.${selectorHash}.json`, 'application/json', `${canonicalJson({ schemaVersion: 1, revision: snapshot.revision, lookupOnly: true, queryMatches, coordinateMatches, selected, sourceHashes: sourceEvidence.complete })}\n`, {
320
+ kind: 'map-lookup',
321
+ toolVersion: PACKAGE_VERSION,
322
+ schemaVersion: 'map-lookup.v1',
323
+ sourceHashes: sourceEvidence.bounded.sourceHashes,
324
+ metadata: { sourceHashInventory: sourceEvidence.bounded.inventory },
325
+ }, 'Bounded map coordinate, catalog, and entity lookup without full validation or overview rendering', context.signal);
326
+ const result = emptyServiceResult(context.workspaceId, {
327
+ revision: snapshot.revision,
328
+ sharedRevision: snapshot.revision,
329
+ width: snapshot.index.raster?.width ?? null,
330
+ height: snapshot.index.raster?.height ?? null,
331
+ definitions: snapshot.index.definitions.length,
332
+ states: snapshot.index.states.length,
333
+ regions: snapshot.index.regions.length,
334
+ ports: snapshot.index.ports.length,
335
+ inspectedProvinceCount: selected.provinces.length,
336
+ inspectedStateCount: selected.states.length,
337
+ inspectedRegionCount: selected.regions.length,
338
+ allocationCount: 0,
339
+ queryMatchCount: queryMatches.length,
340
+ coordinateMatchCount: coordinateMatches.length,
341
+ overviewRendered: false,
342
+ provinceGeometryCount: 0,
343
+ provinceGeometryPixelCount: 0,
344
+ provinceGeometryRowRunCount: 0,
345
+ unknownProvinceIds: [],
346
+ missingGeometryProvinceIds: [],
347
+ lookupOnly: true,
348
+ });
349
+ result.code = 'MAP_LOOKUP_COMPLETED';
350
+ result.validation = {
351
+ passed: true,
352
+ checks: [
353
+ {
354
+ id: 'lookup-only',
355
+ passed: true,
356
+ message: 'Lookup completed; full map validation was not requested',
357
+ },
358
+ ],
359
+ };
360
+ setInlineFilesScanned(result, snapshot.files.map(({ displayPath }) => displayPath));
361
+ result.artifacts = [publicArtifactLink(artifact)];
362
+ await context.progress(3, 3, 'Map lookup complete');
363
+ return result;
364
+ }
302
365
  const { snapshot, validation } = await nudger.validate(context.workspaceId, context.principal, context.signal);
303
366
  const sharedRevision = snapshot.revision;
304
367
  const catalog = buildMapCatalog(snapshot.index);
@@ -474,6 +537,8 @@ export async function renderMapView(nudger, input, context) {
474
537
  ...(input.layer === undefined ? {} : { layer: input.layer }),
475
538
  ...(input.overlays === undefined ? {} : { overlays: input.overlays }),
476
539
  ...(input.scale === undefined ? {} : { scale: input.scale }),
540
+ ...(input.tile === undefined ? {} : { tile: input.tile }),
541
+ ...(input.area === undefined ? {} : { area: input.area }),
477
542
  ...(context.principal === undefined ? {} : { principal: context.principal }),
478
543
  ...(context.signal === undefined ? {} : { signal: context.signal }),
479
544
  });
@@ -483,6 +548,7 @@ export async function renderMapView(nudger, input, context) {
483
548
  height: rendered.bundle.height,
484
549
  hashes: rendered.bundle.hashes,
485
550
  offlineRepresentation: true,
551
+ ...(rendered.bundle.tile === undefined ? {} : { tile: rendered.bundle.tile }),
486
552
  });
487
553
  result.code = 'MAP_RENDERED';
488
554
  setInlineFilesScanned(result, rendered.filesScanned);
@@ -1,2 +1,2 @@
1
1
  /** Shared agent-facing guidance for both MCP protocol generations. */
2
- 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 cross-system source review with hoi4.impact_inspect and decision or mission scenarios with hoi4.decision_inspect. Impact and decision 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. Concurrent calls queue automatically and emit periodic progress when the client requests it. Long operations support negotiated MCP tasks; native background rewrites require a stable requestKey. Large evidence is linked as resources.';
2
+ 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 cross-system source review with hoi4.impact_inspect and decision or mission scenarios with hoi4.decision_inspect. Test declared mechanic steps with hoi4.mechanic_test, package connections with hoi4.package_check, and named suites with hoi4.scenario_test. These analysis 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. Concurrent calls queue automatically and emit periodic progress when the client requests it. Long operations support negotiated MCP tasks; native background rewrites require a stable requestKey. Large evidence is linked as resources.';
@@ -7,6 +7,9 @@ const controlTools = new Set(['hoi4.job_inspect', 'hoi4.job_cancel']);
7
7
  const backgroundTools = new Set([
8
8
  'hoi4.impact_inspect',
9
9
  'hoi4.decision_inspect',
10
+ 'hoi4.mechanic_test',
11
+ 'hoi4.package_check',
12
+ 'hoi4.scenario_test',
10
13
  'hoi4.event_inspect',
11
14
  'hoi4.event_render',
12
15
  'hoi4.event_compare',
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { decisionInspectRequestSchema, impactInspectRequestSchema, } from '../../schemas/analysis.js';
3
+ import { mechanicCaseSchema, mechanicTestRequestSchema, packageManifestSchema, packageCheckRequestSchema, scenarioSuiteSchema, scenarioTestRequestSchema, } from '../../schemas/scenarios.js';
4
+ import { compactValidatedInputSchema } from '../server/context-schemas.js';
3
5
  import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
4
6
  import { strictOperationResultSchema } from '../server/result.js';
5
7
  import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
@@ -36,6 +38,70 @@ const decisionOutputSchema = strictOperationResultSchema(z
36
38
  unresolvedDefinitions: nonNegativeIntegerSchema,
37
39
  })
38
40
  .strict());
41
+ const mechanicOutputSchema = strictOperationResultSchema(z
42
+ .object({
43
+ sourceRevision: sha256Schema,
44
+ reportHash: sha256Schema,
45
+ status: z.enum(['passed', 'failed', 'unresolved']),
46
+ assertions: nonNegativeIntegerSchema,
47
+ passed: nonNegativeIntegerSchema,
48
+ failed: nonNegativeIntegerSchema,
49
+ unresolved: nonNegativeIntegerSchema,
50
+ steps: nonNegativeIntegerSchema,
51
+ })
52
+ .strict());
53
+ const packageOutputSchema = strictOperationResultSchema(z
54
+ .object({
55
+ sourceRevision: sha256Schema,
56
+ reportHash: sha256Schema,
57
+ complete: z.boolean(),
58
+ items: nonNegativeIntegerSchema,
59
+ present: nonNegativeIntegerSchema,
60
+ absent: nonNegativeIntegerSchema,
61
+ shadowed: nonNegativeIntegerSchema,
62
+ unresolved: nonNegativeIntegerSchema,
63
+ })
64
+ .strict());
65
+ const scenarioOutputSchema = strictOperationResultSchema(z
66
+ .object({
67
+ sourceRevision: sha256Schema,
68
+ suiteHash: sha256Schema,
69
+ reportHash: sha256Schema,
70
+ start: nonNegativeIntegerSchema,
71
+ end: nonNegativeIntegerSchema,
72
+ total: nonNegativeIntegerSchema,
73
+ completed: nonNegativeIntegerSchema,
74
+ failed: nonNegativeIntegerSchema,
75
+ unresolved: nonNegativeIntegerSchema,
76
+ pending: nonNegativeIntegerSchema,
77
+ continuation: z.string().optional(),
78
+ })
79
+ .strict());
80
+ const mechanicInputSchema = z
81
+ .object({
82
+ ...mechanicTestRequestSchema.shape,
83
+ test: compactValidatedInputSchema(mechanicCaseSchema, 'Versioned declared scenario, explicit effect steps, and typed assertions. See docs/mechanics.md.'),
84
+ })
85
+ .strict();
86
+ const packageInputSchema = z
87
+ .object({
88
+ ...packageCheckRequestSchema.shape,
89
+ manifest: compactValidatedInputSchema(packageManifestSchema, 'Declarative definitions, calls, registrations, localisation, assets, and required cases. See docs/mechanics.md.'),
90
+ })
91
+ .strict();
92
+ const scenarioInputSchema = z
93
+ .object({
94
+ ...scenarioTestRequestSchema.shape,
95
+ suite: compactValidatedInputSchema(scenarioSuiteSchema, 'Named mechanic, package, or fixed read-only tool cases. See docs/mechanics.md.').optional(),
96
+ })
97
+ .strict()
98
+ .superRefine((input, context) => {
99
+ if ((input.suite === undefined) === (input.suitePath === undefined))
100
+ context.addIssue({
101
+ code: 'custom',
102
+ message: 'Specify exactly one inline suite or suitePath',
103
+ });
104
+ });
39
105
  const readOnlyAnalysisTool = {
40
106
  readOnlyHint: true,
41
107
  destructiveHint: false,
@@ -59,6 +125,30 @@ export const analysisTaskTools = [
59
125
  outputSchema: decisionOutputSchema,
60
126
  annotations: readOnlyAnalysisTool,
61
127
  },
128
+ {
129
+ name: 'hoi4.mechanic_test',
130
+ title: 'Test declared mechanic steps',
131
+ description: 'Interpret bounded, source-backed effect steps on isolated declared state and check typed invariants. Unsupported effects remain unresolved; no game or mod source is changed.',
132
+ inputSchema: mechanicInputSchema,
133
+ outputSchema: mechanicOutputSchema,
134
+ annotations: readOnlyAnalysisTool,
135
+ },
136
+ {
137
+ name: 'hoi4.package_check',
138
+ title: 'Check package connections',
139
+ description: 'Check a declarative package manifest against active definitions, references, registrations, localisation, assets, and required suite cases.',
140
+ inputSchema: packageInputSchema,
141
+ outputSchema: packageOutputSchema,
142
+ annotations: readOnlyAnalysisTool,
143
+ },
144
+ {
145
+ name: 'hoi4.scenario_test',
146
+ title: 'Run scenario suite',
147
+ description: 'Run named inline or workspace-relative cases through bounded typed read-only domain services. Large suites return an authenticated continuation.',
148
+ inputSchema: scenarioInputSchema,
149
+ outputSchema: scenarioOutputSchema,
150
+ annotations: readOnlyAnalysisTool,
151
+ },
62
152
  ];
63
153
  export function registerAnalysisTools(server) {
64
154
  registerLegacyTaskTools(server, analysisTaskTools);
@@ -31,6 +31,7 @@ const eventRenderInputSchema = z
31
31
  const eventCompareInputSchema = z
32
32
  .object({
33
33
  ...eventCompareRequestSchema.shape,
34
+ selector: nestedSelectorSchema.optional(),
34
35
  before: nestedGraphReferenceSchema.optional(),
35
36
  after: nestedGraphReferenceSchema.optional(),
36
37
  proposedSources: z.array(nestedProposedSourceSchema).min(1).max(64).optional(),
@@ -119,6 +120,11 @@ const eventCompareOutputSchema = strictOperationResultSchema(z
119
120
  counts: z
120
121
  .object({
121
122
  changes: nonNegativeIntegerSchema,
123
+ selectedChanges: nonNegativeIntegerSchema.optional(),
124
+ omittedChanges: nonNegativeIntegerSchema.optional(),
125
+ unattributedChanges: nonNegativeIntegerSchema.optional(),
126
+ selectedBeforeNodes: nonNegativeIntegerSchema.optional(),
127
+ selectedAfterNodes: nonNegativeIntegerSchema.optional(),
122
128
  addedNodes: nonNegativeIntegerSchema,
123
129
  removedNodes: nonNegativeIntegerSchema,
124
130
  changedNodes: nonNegativeIntegerSchema,
@@ -146,6 +152,9 @@ const eventCompareOutputSchema = strictOperationResultSchema(z
146
152
  boundary: z
147
153
  .object({
148
154
  proposedSources: nonNegativeIntegerSchema,
155
+ selector: eventSelectorSchema.optional(),
156
+ maxChainNodes: nonNegativeIntegerSchema.optional(),
157
+ selectionTruncated: z.boolean().optional(),
149
158
  render: z.boolean(),
150
159
  maxRenderNodes: nonNegativeIntegerSchema,
151
160
  refresh: z.boolean(),
@@ -179,7 +188,7 @@ export const eventTaskTools = [
179
188
  {
180
189
  name: 'hoi4.event_compare',
181
190
  title: 'Compare event chains',
182
- description: 'Compare cached, artifact-backed, current, or in-memory proposed event graphs without writing source. Full changes are resources.',
191
+ description: 'Compare cached, artifact-backed, current, or in-memory proposed event graphs, optionally selecting one downstream chain. Full changes and explicit selection coverage are resources.',
183
192
  inputSchema: eventCompareInputSchema,
184
193
  outputSchema: eventCompareOutputSchema,
185
194
  annotations: readOnlyEventTool,
@@ -30,6 +30,16 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
30
30
  layoutDecisionCount: nonNegativeIntegerSchema,
31
31
  layoutMetrics: focusLayoutMetricsSchema,
32
32
  diagnosticCount: nonNegativeIntegerSchema,
33
+ scenarioSummary: z
34
+ .object({
35
+ completed: nonNegativeIntegerSchema,
36
+ blocked: nonNegativeIntegerSchema,
37
+ unresolved: nonNegativeIntegerSchema,
38
+ structuralCandidates: nonNegativeIntegerSchema,
39
+ choiceConflictCount: nonNegativeIntegerSchema,
40
+ })
41
+ .strict()
42
+ .optional(),
33
43
  })
34
44
  .strict())
35
45
  .max(100),
@@ -125,7 +135,7 @@ export const focusTaskTools = [
125
135
  {
126
136
  name: 'hoi4.focus_inspect',
127
137
  title: 'Inspect focus trees',
128
- description: 'Inspect national trees or continuous palettes for creation and cleanup, including continuous-focus placement, complete plans, references, diagnostics, and stable layout decisions.',
138
+ description: 'Inspect focus trees and continuous palettes, including source references, diagnostics, and layout.',
129
139
  inputSchema: focusInspectRequestSchema,
130
140
  outputSchema: focusInspectOutputSchema,
131
141
  annotations: artifactProducing,
@@ -133,7 +143,7 @@ export const focusTaskTools = [
133
143
  {
134
144
  name: 'hoi4.focus_render',
135
145
  title: 'Render focus review artifacts',
136
- description: 'Render a national tree or continuous palette as fast deterministic HTML, SVG, JSON, and source-map artifacts. Use hoi4.focus_raster when decoded icons and PNG output are needed.',
146
+ description: 'Render focus HTML, SVG, JSON, and source maps. Use hoi4.focus_raster for decoded icons and PNG.',
137
147
  inputSchema: focusRenderRequestSchema,
138
148
  outputSchema: focusRenderOutputSchema,
139
149
  annotations: artifactProducing,
@@ -141,7 +151,7 @@ export const focusTaskTools = [
141
151
  {
142
152
  name: 'hoi4.focus_raster',
143
153
  title: 'Rasterize focus review artifacts',
144
- description: 'Produce the high-fidelity focus review with decoded source icons and deterministic PNG output. Use focus_render for the faster structural HTML, SVG, and JSON view.',
154
+ description: 'Render focus PNG with decoded source icons; also returns HTML, SVG, and JSON.',
145
155
  inputSchema: focusRenderRequestSchema,
146
156
  outputSchema: focusRenderOutputSchema,
147
157
  annotations: artifactProducing,
@@ -149,7 +159,7 @@ export const focusTaskTools = [
149
159
  {
150
160
  name: 'hoi4.focus_rewrite',
151
161
  title: 'Create or clean up focus content',
152
- description: 'Create or clean up a national tree or continuous palette, validate it, and apply it in one call. Supply a complete plan; set layoutMode compact for automatic arrangement. Existing national trees can omit the plan and use treeId plus layoutMode compact. Set createIfMissing for a new file.',
162
+ description: 'Create or compact a focus tree or palette from a complete plan. Use treeId for existing trees and createIfMissing for new files.',
153
163
  inputSchema: focusRewriteRequestSchema,
154
164
  outputSchema: focusPlanOutputSchema,
155
165
  annotations: {
@@ -43,6 +43,10 @@ const guiRenderOutputSchema = strictOperationResultSchema(z
43
43
  comparison: z
44
44
  .object({ changedPixels: nonNegativeIntegerSchema, changedRatio: z.number().min(0).max(1) })
45
45
  .strict(),
46
+ sourceComparison: z
47
+ .object({ changedPixels: nonNegativeIntegerSchema, changedRatio: z.number().min(0).max(1) })
48
+ .strict()
49
+ .optional(),
46
50
  fidelityCounts: countRecordSchema,
47
51
  offlineRepresentation: z.literal(true),
48
52
  })
@@ -76,7 +80,7 @@ export const guiTaskTools = [
76
80
  {
77
81
  name: 'hoi4.gui_render',
78
82
  title: 'Render scripted GUI artifacts',
79
- description: 'Render GUI states, resolutions, generated or explicit scenarios, dynamic flags and text icons, hierarchy, comparisons, and diagnostics.',
83
+ description: 'Render GUI states, resolutions, scenarios, hierarchy, comparisons, and diagnostics.',
80
84
  inputSchema: guiRenderRequestSchema,
81
85
  outputSchema: guiRenderOutputSchema,
82
86
  annotations: artifactProducing,
@@ -75,6 +75,7 @@ const mapWorkspaceInspectOutputSchema = strictOperationResultSchema(z
75
75
  provinceGeometryCount: nonNegativeIntegerSchema,
76
76
  provinceGeometryPixelCount: nonNegativeIntegerSchema,
77
77
  provinceGeometryRowRunCount: nonNegativeIntegerSchema,
78
+ lookupOnly: z.boolean().optional(),
78
79
  unknownProvinceIds: z.array(nonNegativeIntegerSchema).max(512),
79
80
  missingGeometryProvinceIds: z.array(nonNegativeIntegerSchema).max(512),
80
81
  })
@@ -86,6 +87,15 @@ const mapRenderOutputSchema = strictOperationResultSchema(z
86
87
  height: nonNegativeIntegerSchema,
87
88
  hashes: bitmapRenderHashesSchema,
88
89
  offlineRepresentation: z.literal(true),
90
+ tile: z
91
+ .object({
92
+ x: nonNegativeIntegerSchema,
93
+ y: nonNegativeIntegerSchema,
94
+ width: nonNegativeIntegerSchema,
95
+ height: nonNegativeIntegerSchema,
96
+ })
97
+ .strict()
98
+ .optional(),
89
99
  })
90
100
  .strict());
91
101
  const mapPlanOutputSchema = strictOperationResultSchema(z
@@ -113,7 +123,7 @@ export const mapTaskTools = [
113
123
  {
114
124
  name: 'hoi4.map_inspect',
115
125
  title: 'Inspect HOI4 map',
116
- description: 'Render, validate, search, and click the complete map.',
126
+ description: 'Validate and inspect the complete map, or use lookupOnly for bounded coordinate and entity lookups.',
117
127
  inputSchema: mapInspectRequestSchema,
118
128
  outputSchema: mapWorkspaceInspectOutputSchema,
119
129
  annotations: artifactProducing,
@@ -121,7 +131,7 @@ export const mapTaskTools = [
121
131
  {
122
132
  name: 'hoi4.map_render',
123
133
  title: 'Render map inspection artifacts',
124
- description: 'Render searchable full-map PNG, JSON, and HTML.',
134
+ description: 'Render searchable full-map PNG, JSON, and HTML, or a reusable source-coordinate tile.',
125
135
  inputSchema: mapRenderRequestSchema,
126
136
  outputSchema: mapRenderOutputSchema,
127
137
  annotations: artifactProducing,
@@ -16,6 +16,9 @@ const analysisDataSchema = z
16
16
  cacheKey: sha256Schema,
17
17
  scenarios: nonNegativeIntegerSchema,
18
18
  candidates: nonNegativeIntegerSchema,
19
+ eligibleCandidates: nonNegativeIntegerSchema,
20
+ excludedCandidates: nonNegativeIntegerSchema,
21
+ unresolvedCandidates: nonNegativeIntegerSchema,
19
22
  scopePools: nonNegativeIntegerSchema.optional(),
20
23
  scopePoolCandidates: nonNegativeIntegerSchema.optional(),
21
24
  unresolved: nonNegativeIntegerSchema,
@@ -61,6 +64,8 @@ const inspectDataSchema = z
61
64
  candidateExamples: z.array(z.string().max(512)).max(10),
62
65
  poolComplete: z.boolean().optional(),
63
66
  requiredInputs: nonNegativeIntegerSchema,
67
+ requiredInputPaths: z.array(z.string().max(1024)).max(32),
68
+ requiredInputPathsTruncated: z.boolean(),
64
69
  unresolved: nonNegativeIntegerSchema,
65
70
  })
66
71
  .strict();