hoi4-agent-tools 3.3.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 (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/hoi4_agent_tools/core/condition-evaluator.js +13 -8
  3. package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
  4. package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
  5. package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
  6. package/dist/hoi4_agent_tools/event/operations.js +16 -0
  7. package/dist/hoi4_agent_tools/event/service.js +21 -2
  8. package/dist/hoi4_agent_tools/focus/compact.js +241 -27
  9. package/dist/hoi4_agent_tools/focus/layout.js +119 -8
  10. package/dist/hoi4_agent_tools/focus/render.js +62 -0
  11. package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
  12. package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
  13. package/dist/hoi4_agent_tools/gui/assets.js +54 -4
  14. package/dist/hoi4_agent_tools/gui/layout.js +935 -104
  15. package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
  16. package/dist/hoi4_agent_tools/gui/shader.js +515 -0
  17. package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
  18. package/dist/hoi4_agent_tools/gui/studio.js +245 -19
  19. package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
  20. package/dist/hoi4_agent_tools/gui/validators.js +38 -12
  21. package/dist/hoi4_agent_tools/map/operations.js +70 -1
  22. package/dist/hoi4_agent_tools/map/render.js +70 -1
  23. package/dist/hoi4_agent_tools/map/service.js +30 -6
  24. package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
  25. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
  26. package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
  27. package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
  28. package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
  29. package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
  30. package/dist/hoi4_agent_tools/probability/evaluation.js +3 -0
  31. package/dist/hoi4_agent_tools/probability/operations.js +8 -0
  32. package/dist/hoi4_agent_tools/probability/service.js +33 -12
  33. package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
  34. package/dist/hoi4_agent_tools/schemas/event.js +8 -0
  35. package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
  36. package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
  37. package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
  38. package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
  39. package/dist/hoi4_agent_tools/technology/graph.js +25 -10
  40. package/dist/hoi4_agent_tools/technology/render.js +286 -31
  41. package/dist/hoi4_agent_tools/technology/service.js +55 -8
  42. package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
  43. package/dist/hoi4_agent_tools/version.js +1 -1
  44. package/docs/events.md +3 -1
  45. package/docs/focus.md +10 -0
  46. package/docs/gui.md +49 -3
  47. package/docs/map.md +8 -0
  48. package/docs/probability.md +3 -1
  49. package/docs/technology.md +7 -2
  50. package/package.json +1 -1
  51. package/schemas/configuration.schema.json +1 -1
  52. package/schemas/continuous-focus-palette.schema.json +1 -1
  53. package/schemas/custom-weighted-pool.schema.json +1 -1
  54. package/schemas/decision-inspect-input.schema.json +1 -1
  55. package/schemas/event-feature-manifest.schema.json +1 -1
  56. package/schemas/focus-plan.schema.json +1 -1
  57. package/schemas/focus-planning-sidecar.schema.json +1 -1
  58. package/schemas/gui-animation-source.schema.json +1 -1
  59. package/schemas/gui-helper.schema.json +1 -1
  60. package/schemas/gui-scenario.schema.json +1 -1
  61. package/schemas/helper-expansion-request.schema.json +1 -1
  62. package/schemas/helper-expansion-summary.schema.json +1 -1
  63. package/schemas/impact-inspect-input.schema.json +1 -1
  64. package/schemas/map-operation.schema.json +1 -1
  65. package/schemas/mechanic-test-input.schema.json +1 -1
  66. package/schemas/operation-result.schema.json +1 -1
  67. package/schemas/package-check-input.schema.json +1 -1
  68. package/schemas/package-manifest.schema.json +1 -1
  69. package/schemas/probability-analysis-result.schema.json +1 -1
  70. package/schemas/probability-compare-input.schema.json +1 -1
  71. package/schemas/probability-evaluate-input.schema.json +1 -1
  72. package/schemas/probability-inspect-input.schema.json +1 -1
  73. package/schemas/probability-render-input.schema.json +1 -1
  74. package/schemas/probability-scenario-set.schema.json +1 -1
  75. package/schemas/probability-sequence-input.schema.json +1 -1
  76. package/schemas/probability-simulate-input.schema.json +1 -1
  77. package/schemas/probability-sweep-input.schema.json +1 -1
  78. package/schemas/scenario-suite.schema.json +1 -1
  79. package/schemas/scenario-test-input.schema.json +1 -1
  80. package/schemas/shared-scenario.schema.json +1 -1
  81. 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);
@@ -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();
@@ -114,6 +114,9 @@ export function evaluateExactCandidates(surface, scenario, definitions) {
114
114
  ...external.trace,
115
115
  ],
116
116
  unresolved: [...eligibility.unresolved, ...value.unresolved, ...external.unresolved],
117
+ eligibilityUnresolved: eligibility.unresolved,
118
+ valueUnresolved: value.unresolved,
119
+ externalUnresolved: external.unresolved,
117
120
  provenance: [
118
121
  ...candidate.provenance,
119
122
  ...(eligibility.helperProvenance ?? []),
@@ -21,6 +21,9 @@ export function probabilityAnalysisData(result) {
21
21
  cacheKey: result.metadata.cacheKey,
22
22
  scenarios: result.scenarios.length,
23
23
  candidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.length, 0),
24
+ eligibleCandidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.filter(({ eligibility }) => eligibility === 'true').length, 0),
25
+ excludedCandidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.filter(({ eligibility }) => eligibility === 'false').length, 0),
26
+ unresolvedCandidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.filter(({ eligibility }) => eligibility === 'unresolved').length, 0),
24
27
  ...(result.scenarios.some(({ scopePools }) => (scopePools?.length ?? 0) > 0)
25
28
  ? {
26
29
  scopePools: result.scenarios.reduce((sum, scenario) => sum + (scenario.scopePools?.length ?? 0), 0),
@@ -87,6 +90,8 @@ export function normalizeProbabilityRenderRequest(input) {
87
90
  }
88
91
  export async function inspectProbabilities(analyzer, input, context) {
89
92
  const inspected = await analyzer.inspect(runtime(context, input.refresh), input.adapter, input.source, input.candidatePool, input.customPoolManifest);
93
+ const sourceRequiredInputPaths = (inspected.surface?.requiredInputs ?? []).filter((path) => path !== 'focus.external_factors_complete' && path !== 'technology.external_factors_complete');
94
+ const adapterRequiredInputPaths = (inspected.surface?.requiredInputs ?? []).filter((path) => path === 'focus.external_factors_complete' || path === 'technology.external_factors_complete');
90
95
  const result = emptyServiceResult(context.workspaceId, {
91
96
  adapters: inspected.adapters.length,
92
97
  ...(inspected.surface === undefined
@@ -120,6 +125,9 @@ export async function inspectProbabilities(analyzer, input, context) {
120
125
  })) ?? [],
121
126
  candidateExamples: inspected.discovery?.exampleCandidateIds ?? [],
122
127
  requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
128
+ requiredInputPaths: sourceRequiredInputPaths.slice(0, 32),
129
+ requiredInputPathsTruncated: sourceRequiredInputPaths.length > 32,
130
+ adapterRequiredInputPaths,
123
131
  unresolved: inspected.surface?.unsupported.length ?? 0,
124
132
  });
125
133
  result.code =
@@ -9,7 +9,7 @@ import { PACKAGE_VERSION } from '../version.js';
9
9
  import { probabilityAnalysisResultSchema } from '../schemas/probability.js';
10
10
  import { probabilityAdapter, probabilityAdapters } from './adapters.js';
11
11
  import { compareProbabilityResults } from './compare.js';
12
- import { evaluateExactCandidates, evaluateSurfaceScenarios } from './evaluation.js';
12
+ import { evaluateExactCandidates, evaluateSurfaceScenarios, } from './evaluation.js';
13
13
  import { VERIFIED_GAME_CHECKSUM, VERIFIED_GAME_RAW_VERSION, VERIFIED_GAME_VERSION, } from './model.js';
14
14
  import { renderProbabilityResult } from './render.js';
15
15
  import { analyzeSequence, evaluateCustomPoolPoint } from './sequence.js';
@@ -17,6 +17,28 @@ import { simulateSurface } from './simulation.js';
17
17
  import { Rational } from './rational.js';
18
18
  import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
19
19
  import { sweepSurface } from './sweep.js';
20
+ function missingScenarioInputs(unresolved) {
21
+ return [
22
+ ...new Set(unresolved.flatMap(({ code, path, details }) => {
23
+ if (typeof details?.scenarioInput === 'string')
24
+ return [details.scenarioInput];
25
+ if (code === 'VALUE_UNRESOLVED' && path !== undefined && !/^(?:@|constant:)/u.test(path))
26
+ return [path];
27
+ if (path !== undefined &&
28
+ (code === 'FOCUS_EXTERNAL_FACTORS_UNDECLARED' ||
29
+ code === 'TECHNOLOGY_EXTERNAL_FACTORS_UNDECLARED'))
30
+ return [path];
31
+ return [];
32
+ })),
33
+ ].sort(compareCodeUnits);
34
+ }
35
+ function blockingInspectionUnresolved(candidate) {
36
+ if (candidate.eligibility === 'false')
37
+ return [];
38
+ if (candidate.eligibility === 'unresolved')
39
+ return [...candidate.eligibilityUnresolved, ...candidate.externalUnresolved];
40
+ return [...candidate.valueUnresolved, ...candidate.externalUnresolved];
41
+ }
20
42
  const states = new WeakMap();
21
43
  const ANALYSIS_CACHE_ENTRIES = 16;
22
44
  function resolveSourceBackedAdapter(snapshot, source, candidatePool, requested) {
@@ -1063,9 +1085,7 @@ export class ProbabilityAnalyzer {
1063
1085
  }
1064
1086
  const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
1065
1087
  const inspectedCandidates = evaluateExactCandidates(surface, { id: 'inspection', state: {} }, definitions);
1066
- const requiredInputs = [
1067
- ...new Set(inspectedCandidates.flatMap(({ unresolved }) => unresolved.flatMap(({ path }) => (path === undefined ? [] : [path])))),
1068
- ].sort();
1088
+ const requiredInputs = missingScenarioInputs(inspectedCandidates.flatMap(blockingInspectionUnresolved));
1069
1089
  const report = {
1070
1090
  schemaVersion: 'probability-inspection.v2',
1071
1091
  workspaceId: context.workspaceId,
@@ -1079,9 +1099,8 @@ export class ProbabilityAnalyzer {
1079
1099
  candidateCount: surface.candidates.length,
1080
1100
  candidates: surface.candidates.map((candidate, index) => {
1081
1101
  const evaluated = inspectedCandidates[index];
1082
- const candidateRequiredInputs = [
1083
- ...new Set(evaluated.unresolved.flatMap(({ path }) => (path === undefined ? [] : [path]))),
1084
- ].sort();
1102
+ const blockingUnresolved = blockingInspectionUnresolved(evaluated);
1103
+ const candidateRequiredInputs = missingScenarioInputs(blockingUnresolved);
1085
1104
  return {
1086
1105
  id: candidate.id,
1087
1106
  sourceKind: candidate.sourceKind,
@@ -1092,11 +1111,13 @@ export class ProbabilityAnalyzer {
1092
1111
  referencedProvenance: evaluated.provenance.filter(({ path, symbol }) => !candidate.provenance.some((source) => source.path === path && source.symbol === symbol)),
1093
1112
  analysisSupport: candidateRequiredInputs.length > 0
1094
1113
  ? 'requires_scenario'
1095
- : surface.adapter.selectionRule === 'score_only'
1096
- ? 'score_only'
1097
- : surface.adapter.completePoolRequired && !surface.poolComplete
1098
- ? 'requires_complete_pool'
1099
- : 'exact',
1114
+ : blockingUnresolved.length > 0
1115
+ ? 'unresolved_source'
1116
+ : surface.adapter.selectionRule === 'score_only'
1117
+ ? 'score_only'
1118
+ : surface.adapter.completePoolRequired && !surface.poolComplete
1119
+ ? 'requires_complete_pool'
1120
+ : 'exact',
1100
1121
  };
1101
1122
  }),
1102
1123
  requiredInputs,
@@ -421,6 +421,7 @@ export function inventoryWeightedSurfaces(snapshot, source, candidatePool = [],
421
421
  ? candidates.length
422
422
  : candidates.filter(({ id }) => wanted.has(id)).length,
423
423
  exampleCandidateIds: [...new Set(candidates.map(({ id }) => id))].slice(0, 10),
424
+ candidateCatalog: candidates.map(({ id, provenance }) => ({ id, provenance })),
424
425
  sourcePaths: [
425
426
  ...new Set(candidates.flatMap(({ provenance }) => provenance.map(({ path }) => path))),
426
427
  ].sort(compareCodeUnits),
@@ -151,6 +151,12 @@ export function validateEventCompareRequest(value, context) {
151
151
  path: ['after'],
152
152
  message: 'after and proposedSources are mutually exclusive',
153
153
  });
154
+ if (value.maxChainNodes !== undefined && value.selector === undefined)
155
+ context.addIssue({
156
+ code: 'custom',
157
+ path: ['maxChainNodes'],
158
+ message: 'maxChainNodes requires selector',
159
+ });
154
160
  }
155
161
  export const eventCompareRequestSchema = z
156
162
  .object({
@@ -158,6 +164,8 @@ export const eventCompareRequestSchema = z
158
164
  before: eventGraphReferenceSchema.optional(),
159
165
  after: eventGraphReferenceSchema.optional(),
160
166
  proposedSources: z.array(eventProposedSourceSchema).min(1).max(64).optional(),
167
+ selector: eventSelectorSchema.optional(),
168
+ maxChainNodes: z.number().int().min(1).max(5_000).optional(),
161
169
  render: z.boolean().optional(),
162
170
  maxRenderNodes: z.number().int().min(1).max(240).optional(),
163
171
  refresh: z.boolean().optional(),