hoi4-agent-tools 2.4.0 → 2.5.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 (31) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +25 -25
  3. package/dist/hoi4_agent_tools/mcp/tools/focus.js +8 -1
  4. package/dist/hoi4_agent_tools/mcp/tools/probability.js +52 -6
  5. package/dist/hoi4_agent_tools/probability/service.js +81 -7
  6. package/dist/hoi4_agent_tools/probability/source-analysis.js +83 -32
  7. package/dist/hoi4_agent_tools/version.js +1 -1
  8. package/docs/focus.md +1 -1
  9. package/docs/probability.md +3 -1
  10. package/package.json +1 -1
  11. package/schemas/configuration.schema.json +1 -1
  12. package/schemas/continuous-focus-palette.schema.json +1 -1
  13. package/schemas/custom-weighted-pool.schema.json +1 -1
  14. package/schemas/event-feature-manifest.schema.json +1 -1
  15. package/schemas/focus-plan.schema.json +1 -1
  16. package/schemas/focus-planning-sidecar.schema.json +1 -1
  17. package/schemas/gui-animation-source.schema.json +1 -1
  18. package/schemas/gui-helper.schema.json +1 -1
  19. package/schemas/gui-scenario.schema.json +1 -1
  20. package/schemas/map-operation.schema.json +1 -1
  21. package/schemas/operation-result.schema.json +1 -1
  22. package/schemas/probability-analysis-result.schema.json +1 -1
  23. package/schemas/probability-compare-input.schema.json +1 -1
  24. package/schemas/probability-evaluate-input.schema.json +1 -1
  25. package/schemas/probability-inspect-input.schema.json +1 -1
  26. package/schemas/probability-render-input.schema.json +1 -1
  27. package/schemas/probability-scenario-set.schema.json +1 -1
  28. package/schemas/probability-sequence-input.schema.json +1 -1
  29. package/schemas/probability-simulate-input.schema.json +1 -1
  30. package/schemas/probability-sweep-input.schema.json +1 -1
  31. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0 - 2026-08-09
4
+
5
+ - Added exact continuous-focus coordinates and linked palette IDs to compact national focus inspection results.
6
+ - Added source-first probability inspection that discovers compatible adapters, candidate counts, and selector examples without requiring the caller to guess a surface type.
7
+ - Changed empty or mismatched probability inspection into an actionable discovery result with a reason and suggested adapter while keeping evaluation, sweeps, and simulation strict.
8
+ - Prevented focus-tree blocks from being misclassified as decision or mission weighted surfaces.
9
+
3
10
  ## 2.4.0 - 2026-08-09
4
11
 
5
12
  - Added named value-driven scripted-GUI scenario matrices with deterministic SVG, PNG, JSON, and per-element visibility, movement, size, text, and frame-change evidence.
package/README.md CHANGED
@@ -31,31 +31,31 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
31
31
 
32
32
  ## Tools
33
33
 
34
- | Tool | Purpose |
35
- | --------------------------- | --------------------------------------------------------------------------------------- |
36
- | `hoi4.focus_inspect` | Read focus trees and report structural or reference problems. |
37
- | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
38
- | `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
39
- | `hoi4.focus_rewrite` | Create or update a focus tree. |
40
- | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
41
- | `hoi4.gui_render` | Render GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics. |
42
- | `hoi4.gui_rewrite` | Create or update a GUI source package. |
43
- | `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
44
- | `hoi4.map_render` | Render map layers and overlays. |
45
- | `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes. |
46
- | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
47
- | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
48
- | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
49
- | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
50
- | `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
51
- | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
52
- | `hoi4.probability_inspect` | Locate weighted logic, candidates, required inputs, and supported analysis. |
53
- | `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
54
- | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
55
- | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
56
- | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
57
- | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
58
- | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
34
+ | Tool | Purpose |
35
+ | --------------------------- | ---------------------------------------------------------------------------------------- |
36
+ | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
37
+ | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
38
+ | `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
39
+ | `hoi4.focus_rewrite` | Create or update a focus tree. |
40
+ | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
41
+ | `hoi4.gui_render` | Render GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics. |
42
+ | `hoi4.gui_rewrite` | Create or update a GUI source package. |
43
+ | `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
44
+ | `hoi4.map_render` | Render map layers and overlays. |
45
+ | `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes. |
46
+ | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
47
+ | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
48
+ | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
49
+ | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
50
+ | `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
51
+ | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
52
+ | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
53
+ | `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
54
+ | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
55
+ | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
56
+ | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
57
+ | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
58
+ | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
59
59
 
60
60
  Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
61
61
 
@@ -31,6 +31,11 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
31
31
  branchCount: nonNegativeIntegerSchema,
32
32
  continuousPaletteCount: nonNegativeIntegerSchema,
33
33
  continuousFocusCount: nonNegativeIntegerSchema,
34
+ continuousFocusPosition: z
35
+ .object({ x: z.number().int(), y: z.number().int() })
36
+ .strict()
37
+ .nullable(),
38
+ continuousFocusPaletteIds: z.array(z.string().max(256)).max(100),
34
39
  resolvedTitleCount: nonNegativeIntegerSchema,
35
40
  layoutHash: sha256Schema,
36
41
  layoutDecisionCount: nonNegativeIntegerSchema,
@@ -601,7 +606,7 @@ export function registerFocusTools(server, engine, context) {
601
606
  const workbench = new FocusWorkbench(engine.resolver, engine.transactions, engine.artifacts);
602
607
  server.registerTool('hoi4.focus_inspect', {
603
608
  title: 'Inspect focus trees',
604
- description: 'Inspect national trees or continuous palettes for creation and cleanup, including complete plans, references, diagnostics, and stable layout decisions.',
609
+ description: 'Inspect national trees or continuous palettes for creation and cleanup, including continuous-focus placement, complete plans, references, diagnostics, and stable layout decisions.',
605
610
  inputSchema: focusInspectInput,
606
611
  outputSchema: focusInspectOutputSchema,
607
612
  annotations: artifactProducing,
@@ -791,6 +796,8 @@ export function registerFocusTools(server, engine, context) {
791
796
  branchCount: plan.branchGroups.length,
792
797
  continuousPaletteCount: plan.continuousFocusPaletteIds.length,
793
798
  continuousFocusCount: plan.continuousFocusIds.length,
799
+ continuousFocusPosition: plan.continuousFocusPosition ?? null,
800
+ continuousFocusPaletteIds: plan.continuousFocusPaletteIds.slice(0, 100),
794
801
  resolvedTitleCount: plan.focuses.filter(({ id }) => presentation.entries[id]?.titleSourceLocation !== undefined).length,
795
802
  layoutHash: layout.layoutHash,
796
803
  layoutDecisionCount: layout.decisions.length,
@@ -39,10 +39,10 @@ const inspectInput = z
39
39
  })
40
40
  .strict()
41
41
  .superRefine((value, context) => {
42
- if ((value.adapter === undefined) !== (value.source === undefined))
42
+ if (value.adapter !== undefined && value.source === undefined)
43
43
  context.addIssue({
44
44
  code: 'custom',
45
- message: 'Provide adapter and source together, or omit both to list adapters',
45
+ message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
46
46
  });
47
47
  });
48
48
  const evaluateInput = z.object(commonShape).strict();
@@ -135,9 +135,32 @@ const inspectDataSchema = z
135
135
  .object({
136
136
  adapters: nonNegativeIntegerSchema,
137
137
  adapterId: z.string().max(256).optional(),
138
+ requestedAdapter: z.string().max(256).optional(),
139
+ suggestedAdapter: z.string().max(256).optional(),
140
+ discoveryReason: z
141
+ .enum([
142
+ 'source_inventory',
143
+ 'requested_adapter_empty',
144
+ 'identifier_not_found',
145
+ 'candidate_pool_not_found',
146
+ 'no_weighted_surfaces',
147
+ ])
148
+ .optional(),
138
149
  sourceRevision: sha256Schema.optional(),
139
150
  sourceHash: sha256Schema.optional(),
140
151
  candidates: nonNegativeIntegerSchema,
152
+ availableCandidates: nonNegativeIntegerSchema,
153
+ availableAdapters: z
154
+ .array(z
155
+ .object({
156
+ adapterId: z.string().max(256),
157
+ candidates: nonNegativeIntegerSchema,
158
+ identifierMatches: nonNegativeIntegerSchema,
159
+ candidatePoolMatches: nonNegativeIntegerSchema,
160
+ })
161
+ .strict())
162
+ .max(10),
163
+ candidateExamples: z.array(z.string().max(512)).max(10),
141
164
  poolComplete: z.boolean().optional(),
142
165
  requiredInputs: nonNegativeIntegerSchema,
143
166
  unresolved: nonNegativeIntegerSchema,
@@ -211,7 +234,7 @@ export function registerProbabilityTools(server, engine, context) {
211
234
  const analyzer = new ProbabilityAnalyzer(engine);
212
235
  server.registerTool('hoi4.probability_inspect', {
213
236
  title: 'Inspect AI and MTTH weighted logic',
214
- description: 'List versioned adapters or discover weighted blocks, candidate pools, capabilities, provenance, and unsupported constructs.',
237
+ description: 'List versioned adapters, or provide a source alone to discover compatible weighted adapters, candidate pools, capabilities, provenance, and unsupported constructs before narrowing the request.',
215
238
  inputSchema: inspectInput,
216
239
  outputSchema: inspectOutput,
217
240
  annotations: readOnly,
@@ -231,14 +254,37 @@ export function registerProbabilityTools(server, engine, context) {
231
254
  sourceHash: inspected.surface.sourceHash,
232
255
  poolComplete: inspected.surface.poolComplete,
233
256
  }),
257
+ ...(inspected.discovery === undefined
258
+ ? {}
259
+ : {
260
+ ...(inspected.discovery.requestedAdapter === undefined
261
+ ? {}
262
+ : { requestedAdapter: inspected.discovery.requestedAdapter }),
263
+ ...(inspected.discovery.suggestedAdapter === undefined
264
+ ? {}
265
+ : { suggestedAdapter: inspected.discovery.suggestedAdapter }),
266
+ discoveryReason: inspected.discovery.reason,
267
+ sourceRevision: inspected.discovery.sourceRevision,
268
+ sourceHash: inspected.discovery.sourceHash,
269
+ }),
234
270
  candidates: inspected.surface?.candidateCount ?? 0,
271
+ availableCandidates: inspected.discovery?.availableAdapters.reduce((sum, { candidateCount }) => sum + candidateCount, 0) ?? 0,
272
+ availableAdapters: inspected.discovery?.availableAdapters.map((available) => ({
273
+ adapterId: available.adapterId,
274
+ candidates: available.candidateCount,
275
+ identifierMatches: available.identifierMatchCount,
276
+ candidatePoolMatches: available.candidatePoolMatchCount,
277
+ })) ?? [],
278
+ candidateExamples: inspected.discovery?.exampleCandidateIds ?? [],
235
279
  requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
236
280
  unresolved: inspected.surface?.unsupported.length ?? 0,
237
281
  });
238
282
  result.code =
239
- inspected.surface === undefined
240
- ? 'PROBABILITY_ADAPTERS_LISTED'
241
- : 'PROBABILITY_SOURCE_INSPECTED';
283
+ inspected.surface !== undefined
284
+ ? 'PROBABILITY_SOURCE_INSPECTED'
285
+ : inspected.discovery !== undefined
286
+ ? 'PROBABILITY_SOURCE_DISCOVERED'
287
+ : 'PROBABILITY_ADAPTERS_LISTED';
242
288
  result.artifacts = inspected.artifacts;
243
289
  setInlineFilesScanned(result, inspected.filesScanned);
244
290
  await progress.report(2, 2, 'Weighted source inspection complete');
@@ -1,6 +1,6 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { canonicalJson, hashCanonical } from '../core/canonical.js';
3
+ import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
4
4
  import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
5
5
  import { probabilityDomainScanPatterns } from '../core/domain-scan-patterns.js';
6
6
  import { publicArtifactLink } from '../core/artifacts.js';
@@ -13,7 +13,7 @@ import { VERIFIED_GAME_CHECKSUM, VERIFIED_GAME_RAW_VERSION, VERIFIED_GAME_VERSIO
13
13
  import { renderProbabilityResult } from './render.js';
14
14
  import { analyzeSequence } from './sequence.js';
15
15
  import { simulateSurface } from './simulation.js';
16
- import { discoverWeightedSurface } from './source-analysis.js';
16
+ import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
17
17
  import { sweepSurface } from './sweep.js';
18
18
  const states = new WeakMap();
19
19
  function analyzerState(engine) {
@@ -775,18 +775,92 @@ export class ProbabilityAnalyzer {
775
775
  return complete;
776
776
  }
777
777
  async inspect(context, adapter, source, candidatePool = []) {
778
- if (adapter === undefined || source === undefined)
778
+ if (source === undefined)
779
779
  return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
780
- const gameVersionVerification = await this.verifyGameVersion(context, adapter);
781
780
  const snapshot = await this.scan(context, [source.path]);
782
- const surface = discoverWeightedSurface(snapshot, adapter, source, candidatePool);
781
+ const storeDiscovery = async (inventory, reason) => {
782
+ const availableAdapters = inventory.adapters.filter(({ candidateCount }) => candidateCount > 0);
783
+ const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) => source.identifier !== undefined && identifierMatchCount > 0);
784
+ const matchingPool = availableAdapters.filter(({ candidatePoolMatchCount }) => candidatePool.length > 0 && candidatePoolMatchCount > 0);
785
+ const requested = availableAdapters.find(({ adapterId }) => adapterId === adapter);
786
+ const ranked = [
787
+ ...(matchingIdentifier.length > 0
788
+ ? matchingIdentifier
789
+ : matchingPool.length > 0
790
+ ? matchingPool
791
+ : requested === undefined
792
+ ? availableAdapters
793
+ : [requested]),
794
+ ].sort((left, right) => right.identifierMatchCount - left.identifierMatchCount ||
795
+ right.candidatePoolMatchCount - left.candidatePoolMatchCount ||
796
+ right.candidateCount - left.candidateCount ||
797
+ compareCodeUnits(left.adapterId, right.adapterId));
798
+ const suggested = ranked[0];
799
+ const discovery = {
800
+ ...(adapter === undefined ? {} : { requestedAdapter: adapter }),
801
+ reason,
802
+ sourceRevision: inventory.sourceRevision,
803
+ sourceHash: inventory.sourceHash,
804
+ availableAdapters,
805
+ ...(suggested === undefined ? {} : { suggestedAdapter: suggested.adapterId }),
806
+ exampleCandidateIds: suggested?.exampleCandidateIds ?? [],
807
+ };
808
+ const report = {
809
+ schemaVersion: 'probability-inspection.v2',
810
+ workspaceId: context.workspaceId,
811
+ discovery,
812
+ };
813
+ const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
814
+ const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${inventory.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
815
+ kind: 'probability-inspection',
816
+ toolVersion: PACKAGE_VERSION,
817
+ schemaVersion: 'probability-inspection.v2',
818
+ sourceHashes: { aggregate: inventory.sourceHash },
819
+ }, 'Weighted-source adapter inventory and candidate examples', context.signal);
820
+ return {
821
+ adapters: probabilityAdapters(),
822
+ discovery,
823
+ artifacts: [publicArtifactLink(artifact)],
824
+ filesScanned: inventory.filesScanned,
825
+ };
826
+ };
827
+ if (adapter === undefined) {
828
+ const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
829
+ return storeDiscovery(inventory, inventory.adapters.some(({ candidateCount }) => candidateCount > 0)
830
+ ? 'source_inventory'
831
+ : 'no_weighted_surfaces');
832
+ }
833
+ const gameVersionVerification = await this.verifyGameVersion(context, adapter);
834
+ let surface;
835
+ try {
836
+ surface = discoverWeightedSurface(snapshot, adapter, source, candidatePool);
837
+ }
838
+ catch (error) {
839
+ if (!(error instanceof ServiceError) ||
840
+ (error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
841
+ error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
842
+ throw error;
843
+ const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
844
+ const requested = inventory.adapters.find(({ adapterId }) => adapterId === adapter);
845
+ const reason = error.code === 'PROBABILITY_IDENTIFIER_NOT_FOUND'
846
+ ? 'identifier_not_found'
847
+ : requested !== undefined &&
848
+ requested.candidateCount > 0 &&
849
+ candidatePool.length > 0 &&
850
+ requested.candidatePoolMatchCount === 0
851
+ ? 'candidate_pool_not_found'
852
+ : inventory.adapters.every(({ candidateCount }) => candidateCount === 0)
853
+ ? 'no_weighted_surfaces'
854
+ : 'requested_adapter_empty';
855
+ return storeDiscovery(inventory, reason);
856
+ }
783
857
  const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
784
858
  const inspectedCandidates = evaluateExactCandidates(surface, { id: 'inspection', state: {} }, definitions);
785
859
  const requiredInputs = [
786
860
  ...new Set(inspectedCandidates.flatMap(({ unresolved }) => unresolved.flatMap(({ path }) => (path === undefined ? [] : [path])))),
787
861
  ].sort();
788
862
  const report = {
789
- schemaVersion: 'probability-inspection.v1',
863
+ schemaVersion: 'probability-inspection.v2',
790
864
  workspaceId: context.workspaceId,
791
865
  surface: {
792
866
  id: surface.id,
@@ -826,7 +900,7 @@ export class ProbabilityAnalyzer {
826
900
  const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${surface.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
827
901
  kind: 'probability-inspection',
828
902
  toolVersion: PACKAGE_VERSION,
829
- schemaVersion: 'probability-inspection.v1',
903
+ schemaVersion: 'probability-inspection.v2',
830
904
  sourceHashes: { aggregate: surface.sourceHash },
831
905
  }, 'Discovered weighted surfaces and adapter capabilities', context.signal);
832
906
  return {
@@ -2,6 +2,18 @@ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
2
2
  import { ServiceError } from '../core/result.js';
3
3
  import { assignments, astPathFor, childBlocks, firstScalar, nodeLocation, parseClausewitz, } from '../core/source/index.js';
4
4
  import { probabilityAdapter } from './adapters.js';
5
+ const sourceBackedAdapterIds = [
6
+ 'event_mean_time_to_happen',
7
+ 'event_option_ai_chance',
8
+ 'decision_ai_will_do',
9
+ 'mission_ai_will_do',
10
+ 'national_focus_ai_will_do',
11
+ 'technology_ai_will_do',
12
+ 'doctrine_ai_will_do',
13
+ 'direct_random',
14
+ 'random_list',
15
+ 'ai_strategy_factor',
16
+ ];
5
17
  const eventKeys = new Set([
6
18
  'country_event',
7
19
  'news_event',
@@ -116,6 +128,9 @@ function focusCandidates(context) {
116
128
  return output;
117
129
  }
118
130
  function decisionCandidates(context, adapterId) {
131
+ const sourcePath = normalized(context.file.relativePath);
132
+ if (context.file.absolutePath !== '<inline>' && !sourcePath.startsWith('common/decisions/'))
133
+ return [];
119
134
  const output = [];
120
135
  const ignored = new Set([
121
136
  'allowed',
@@ -131,6 +146,10 @@ function decisionCandidates(context, adapterId) {
131
146
  for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
132
147
  if (assignment.value.type !== 'block' ||
133
148
  ancestors.length !== 1 ||
149
+ ancestors[0].key === 'focus_tree' ||
150
+ ancestors[0].key === 'continuous_focus_palette' ||
151
+ ancestors[0].key === 'technologies' ||
152
+ eventKeys.has(ancestors[0].key) ||
134
153
  ignored.has(assignment.key.value))
135
154
  continue;
136
155
  const mission = firstScalar(assignment.value, 'is_mission')?.value === 'yes' ||
@@ -353,42 +372,74 @@ function samePool(candidate, selected) {
353
372
  return candidate.metadata.randomListId === selected.metadata.randomListId;
354
373
  return candidate.id === selected.id;
355
374
  }
375
+ function candidatesAtSourceLine(candidates, adapterId, line) {
376
+ if (line === undefined)
377
+ return candidates;
378
+ let selected = candidates.filter(({ metadata, provenance: [first] }) => {
379
+ const location = first?.location;
380
+ const poolStartLine = metadata.poolStartLine;
381
+ const poolEndLine = metadata.poolEndLine;
382
+ return ((location !== undefined && line >= location.start.line && line <= location.end.line) ||
383
+ (typeof poolStartLine === 'number' &&
384
+ typeof poolEndLine === 'number' &&
385
+ line >= poolStartLine &&
386
+ line <= poolEndLine));
387
+ });
388
+ if (adapterId !== 'event_option_ai_chance' && adapterId !== 'random_list')
389
+ return selected;
390
+ const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
391
+ const pools = new Map();
392
+ for (const candidate of selected) {
393
+ const start = candidate.metadata.poolStartLine;
394
+ const end = candidate.metadata.poolEndLine;
395
+ const span = typeof start === 'number' && typeof end === 'number' ? end - start : Number.MAX_SAFE_INTEGER;
396
+ const key = candidate.metadata[poolProperty];
397
+ const existing = pools.get(key);
398
+ if (existing === undefined || span < existing.span)
399
+ pools.set(key, { span });
400
+ }
401
+ const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
402
+ if (selectedPool !== undefined)
403
+ selected = selected.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
404
+ return selected;
405
+ }
406
+ export function inventoryWeightedSurfaces(snapshot, source, candidatePool = [], adapterIds = sourceBackedAdapterIds) {
407
+ const contexts = sourceContexts(snapshot, source);
408
+ const wanted = new Set(candidatePool);
409
+ const adapters = adapterIds
410
+ .filter((adapterId) => adapterId !== 'custom_weighted_pool')
411
+ .map((adapterId) => {
412
+ const candidates = candidatesAtSourceLine(contexts.flatMap((context) => candidatesFor(context, adapterId)), adapterId, source.line).sort((left, right) => compareCodeUnits(left.id, right.id));
413
+ return {
414
+ adapterId,
415
+ candidateCount: candidates.length,
416
+ identifierMatchCount: source.identifier === undefined
417
+ ? candidates.length
418
+ : candidates.filter((candidate) => identifierMatches(candidate, source.identifier))
419
+ .length,
420
+ candidatePoolMatchCount: candidatePool.length === 0
421
+ ? candidates.length
422
+ : candidates.filter(({ id }) => wanted.has(id)).length,
423
+ exampleCandidateIds: [...new Set(candidates.map(({ id }) => id))].slice(0, 10),
424
+ sourcePaths: [
425
+ ...new Set(candidates.flatMap(({ provenance }) => provenance.map(({ path }) => path))),
426
+ ].sort(compareCodeUnits),
427
+ };
428
+ })
429
+ .sort((left, right) => compareCodeUnits(left.adapterId, right.adapterId));
430
+ const hashes = Object.fromEntries(contexts.map(({ file }) => [file.displayPath, file.sha256]));
431
+ return {
432
+ sourceRevision: snapshot.revision,
433
+ sourceHash: hashCanonical(hashes),
434
+ filesScanned: contexts.map(({ file }) => file.displayPath),
435
+ adapters,
436
+ };
437
+ }
356
438
  export function discoverWeightedSurface(snapshot, adapterId, source, candidatePool = []) {
357
439
  const contexts = sourceContexts(snapshot, source);
358
440
  let candidates = contexts.flatMap((context) => candidatesFor(context, adapterId));
359
441
  candidates.sort((left, right) => compareCodeUnits(left.id, right.id));
360
- if (source.line !== undefined) {
361
- candidates = candidates.filter(({ metadata, provenance: [first] }) => {
362
- const location = first?.location;
363
- const poolStartLine = metadata.poolStartLine;
364
- const poolEndLine = metadata.poolEndLine;
365
- return ((location !== undefined &&
366
- source.line >= location.start.line &&
367
- source.line <= location.end.line) ||
368
- (typeof poolStartLine === 'number' &&
369
- typeof poolEndLine === 'number' &&
370
- source.line >= poolStartLine &&
371
- source.line <= poolEndLine));
372
- });
373
- if (adapterId === 'event_option_ai_chance' || adapterId === 'random_list') {
374
- const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
375
- const pools = new Map();
376
- for (const candidate of candidates) {
377
- const start = candidate.metadata.poolStartLine;
378
- const end = candidate.metadata.poolEndLine;
379
- const span = typeof start === 'number' && typeof end === 'number'
380
- ? end - start
381
- : Number.MAX_SAFE_INTEGER;
382
- const key = candidate.metadata[poolProperty];
383
- const existing = pools.get(key);
384
- if (existing === undefined || span < existing.span)
385
- pools.set(key, { span });
386
- }
387
- const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
388
- if (selectedPool !== undefined)
389
- candidates = candidates.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
390
- }
391
- }
442
+ candidates = candidatesAtSourceLine(candidates, adapterId, source.line);
392
443
  const localCategoricalPool = adapterId === 'event_option_ai_chance' || adapterId === 'random_list';
393
444
  let completeLocalPoolIds;
394
445
  if (source.identifier !== undefined) {
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '2.4.0';
3
+ export const PACKAGE_VERSION = '2.5.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/focus.md CHANGED
@@ -44,7 +44,7 @@ If malformed or unsupported script makes a requested change ambiguous, the rewri
44
44
 
45
45
  ## What inspection checks
46
46
 
47
- Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. Renders are offline review artifacts, not game screenshots.
47
+ Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. National-tree inspection also returns the exact `continuous_focus_position` coordinates and linked palette IDs in its compact result, so an agent can identify and move the continuous-focus area without opening the full artifact. Renders are offline review artifacts, not game screenshots.
48
48
 
49
49
  `hoi4.focus_render` is the normal structural view and writes complete HTML, SVG, JSON, and source-map artifacts without decoding every icon or creating a PNG. `hoi4.focus_raster` adds decoded icons and the high-fidelity PNG. National trees with 200 or more focuses raster at half scale by default; pass `reviewScale` from `0.25` through `1` when a different PNG size is needed.
50
50
 
@@ -6,7 +6,7 @@ Use them for event timing and option weights, decision and mission scores, focus
6
6
 
7
7
  | Tool | Use |
8
8
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
9
- | `hoi4.probability_inspect` | Discover weighted blocks, adapters, candidates, provenance, capabilities, and unsupported constructs. |
9
+ | `hoi4.probability_inspect` | Discover weighted blocks, compatible adapters, candidates, provenance, capabilities, and unsupported constructs. |
10
10
  | `hoi4.probability_evaluate` | Evaluate eligibility, modifier traces, raw values, proven probabilities, and MTTH horizon chances across scenarios. |
11
11
  | `hoi4.probability_sweep` | Sweep declared ranges and locate sensitivity changes, breakpoints, and rank reversals. |
12
12
  | `hoi4.probability_simulate` | Sample declared distributions with a deterministic seed and confidence intervals. |
@@ -16,6 +16,8 @@ Use them for event timing and option weights, decision and mission scores, focus
16
16
 
17
17
  All seven tools are read-only. Proposed source is parsed in memory and never written. When an installed game root is configured, evaluation fails closed unless `launcher-settings.json` identifies the supported HOI4 build and checksum. Results otherwise state that they target the adapter version without claiming local-game verification.
18
18
 
19
+ Pass a source without an adapter when the surface type is not yet known. Inspection returns every compatible adapter, candidate counts, and example candidate IDs. If a requested adapter, identifier, or candidate pool does not match the source, inspection returns the same discovery result with an explanation and suggested adapter instead of `PROBABILITY_SURFACE_EMPTY`; evaluation, sweeps, and simulation remain strict and still reject an empty requested surface.
20
+
19
21
  ## Scenarios
20
22
 
21
23
  A scenario contains only state the caller is willing to declare. Missing values stay unresolved. Alternatives and ranges produce exact branches or bounds; probability distributions require `hoi4.probability_simulate`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
3
  "mcpName": "io.github.klimPaskov/hoi4-agent-tools",
4
- "version": "2.4.0",
4
+ "version": "2.5.0",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/configuration.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/continuous-focus-palette.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/custom-weighted-pool.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/event-feature-manifest.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/focus-plan.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/focus-planning-sidecar.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/gui-animation-source.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/gui-helper.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/gui-scenario.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/map-operation.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "oneOf": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/operation-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-analysis-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-compare-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-evaluate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-inspect-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-render-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-render-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-scenario-set.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-scenario-set.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-sequence-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-sequence-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-simulate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-simulate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/probability-sweep-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/probability-sweep-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.klimPaskov/hoi4-agent-tools",
4
4
  "title": "HOI4 Agent Tools",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
- "version": "2.4.0",
6
+ "version": "2.5.0",
7
7
  "repository": {
8
8
  "url": "https://github.com/klimPaskov/hoi4-agent-tools",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "hoi4-agent-tools",
16
- "version": "2.4.0",
16
+ "version": "2.5.0",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }