hoi4-agent-tools 2.3.3 → 2.3.5

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 (34) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +66 -0
  3. package/dist/hoi4_agent_tools/event/graph.js +14 -0
  4. package/dist/hoi4_agent_tools/event/service.js +30 -13
  5. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +9 -1
  6. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +3 -1
  7. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -3
  8. package/dist/hoi4_agent_tools/mcp/tools/focus.js +5 -4
  9. package/dist/hoi4_agent_tools/mcp/tools/map.js +5 -7
  10. package/dist/hoi4_agent_tools/probability/service.js +8 -4
  11. package/dist/hoi4_agent_tools/version.js +1 -1
  12. package/docs/events.md +2 -2
  13. package/package.json +1 -1
  14. package/schemas/configuration.schema.json +1 -1
  15. package/schemas/continuous-focus-palette.schema.json +1 -1
  16. package/schemas/custom-weighted-pool.schema.json +1 -1
  17. package/schemas/event-feature-manifest.schema.json +1 -1
  18. package/schemas/focus-plan.schema.json +1 -1
  19. package/schemas/focus-planning-sidecar.schema.json +1 -1
  20. package/schemas/gui-animation-source.schema.json +1 -1
  21. package/schemas/gui-helper.schema.json +1 -1
  22. package/schemas/gui-scenario.schema.json +1 -1
  23. package/schemas/map-operation.schema.json +1 -1
  24. package/schemas/operation-result.schema.json +1 -1
  25. package/schemas/probability-analysis-result.schema.json +1 -1
  26. package/schemas/probability-compare-input.schema.json +1 -1
  27. package/schemas/probability-evaluate-input.schema.json +1 -1
  28. package/schemas/probability-inspect-input.schema.json +1 -1
  29. package/schemas/probability-render-input.schema.json +1 -1
  30. package/schemas/probability-scenario-set.schema.json +1 -1
  31. package/schemas/probability-sequence-input.schema.json +1 -1
  32. package/schemas/probability-simulate-input.schema.json +1 -1
  33. package/schemas/probability-sweep-input.schema.json +1 -1
  34. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.5 - 2026-08-02
4
+
5
+ - Replaced all-domain scans in focus, map, probability, rewrite validation, and country-asset discovery with domain-specific source inventories.
6
+ - Prevented large unrelated mod files from producing `SCAN_BYTE_LIMIT` errors in otherwise bounded MCP work and added low-ceiling regression coverage.
7
+
8
+ ## 2.3.4 - 2026-07-28
9
+
10
+ - Bounded large game-backed event scans before workspace-wide lifecycle and helper analysis could exhaust the MCP process and close the stdio transport.
11
+ - Added automatic focused event analysis for large workspaces, explicit deferred-analysis diagnostics, source-profile reporting, and deterministic regression coverage.
12
+
3
13
  ## 2.3.3 - 2026-07-26
4
14
 
5
15
  - Regenerated the npm lockfile with the pinned release npm so clean installs include the patched optional native runtime packages.
@@ -0,0 +1,66 @@
1
+ import { compareCodeUnits } from './canonical.js';
2
+ function under(roots, glob) {
3
+ return roots.map((root) => `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/${glob}`);
4
+ }
5
+ function uniquePatterns(patterns) {
6
+ return [...new Set(patterns)].sort(compareCodeUnits);
7
+ }
8
+ function englishLocalisationPatterns(roots) {
9
+ return [...under(roots, 'english/**/*.{yml,yaml}'), ...under(roots, '*.{yml,yaml}')];
10
+ }
11
+ /** Sources required to import, link, lint, render, and rewrite focus content. */
12
+ export function focusDomainScanPatterns(workspace) {
13
+ const roots = workspace.registration.roots;
14
+ return uniquePatterns([
15
+ ...under(roots.focus, '**/*.txt'),
16
+ ...under(roots.focus, '**/*.focus-plan.json'),
17
+ 'common/continuous_focus/**/*.txt',
18
+ 'common/decisions/**/*.txt',
19
+ 'common/ideas/**/*.txt',
20
+ 'common/characters/**/*.txt',
21
+ 'common/scripted_effects/**/*.txt',
22
+ 'common/scripted_triggers/**/*.txt',
23
+ 'common/script_constants/**/*.txt',
24
+ 'common/mtth/**/*.txt',
25
+ 'events/**/*.txt',
26
+ ...under(roots.interface, '**/*.gfx'),
27
+ ...under(roots.gfx, '**/*.gfx'),
28
+ ...englishLocalisationPatterns(roots.localisation),
29
+ ]);
30
+ }
31
+ /** Weighted source surfaces and shared definitions used by probability analysis. */
32
+ export function probabilityDomainScanPatterns(workspace, sourcePaths = []) {
33
+ const roots = workspace.registration.roots;
34
+ const normalizedSourcePaths = sourcePaths.map((sourcePath) => sourcePath.replace(/^.*?:/u, '').replaceAll('\\', '/').replace(/^\.\//u, ''));
35
+ return uniquePatterns([
36
+ ...normalizedSourcePaths,
37
+ ...under(roots.focus, '**/*.txt'),
38
+ 'common/decisions/**/*.txt',
39
+ 'common/technologies/**/*.txt',
40
+ 'common/doctrines/**/*.txt',
41
+ 'common/ai_strategy/**/*.txt',
42
+ 'common/ai_strategy_plans/**/*.txt',
43
+ 'common/ai_focuses/**/*.txt',
44
+ 'common/on_actions/**/*.txt',
45
+ 'common/scripted_effects/**/*.txt',
46
+ 'common/scripted_triggers/**/*.txt',
47
+ 'common/script_constants/**/*.txt',
48
+ 'common/mtth/**/*.txt',
49
+ 'events/**/*.txt',
50
+ ]);
51
+ }
52
+ /** Text sources needed to discover country flags and leader portraits. */
53
+ export function countryAssetDiscoveryPatterns(workspace) {
54
+ const roots = workspace.registration.roots;
55
+ return uniquePatterns([
56
+ 'history/countries/**/*.txt',
57
+ 'common/characters/**/*.txt',
58
+ 'common/scripted_effects/**/*.txt',
59
+ 'events/**/*.txt',
60
+ ...under(roots.interface, '**/*.gfx'),
61
+ ...under(roots.gfx, '**/*.gfx'),
62
+ ]);
63
+ }
64
+ export function exactChangedFilePatterns(relativePaths) {
65
+ return uniquePatterns(relativePaths.map((relativePath) => relativePath.replaceAll('\\', '/').replace(/^\.\//u, '')));
66
+ }
@@ -1571,6 +1571,20 @@ export function buildEventGraph(snapshot, options = {}) {
1571
1571
  graph.stateAccesses = stableUnique(graph.stateAccesses);
1572
1572
  graph.unresolved = stableUnique(graph.unresolved);
1573
1573
  const focused = options.analysisMode === 'focused';
1574
+ if (focused) {
1575
+ graph.unresolved.push({
1576
+ id: 'event-analysis-focused-boundary',
1577
+ kind: 'partial_source',
1578
+ expression: 'workspace-wide event helper projections and lifecycle diagnostics',
1579
+ confidence: 'unresolved',
1580
+ blockers: [
1581
+ {
1582
+ code: 'EVENT_FOCUSED_ANALYSIS_DEFERRED',
1583
+ message: 'Large event workspaces defer workspace-wide helper projections and lifecycle passes; direct source evidence remains available.',
1584
+ },
1585
+ ],
1586
+ });
1587
+ }
1574
1588
  const stateLinks = focused ? [] : stateLinksFor(graph, budget);
1575
1589
  if (!focused) {
1576
1590
  duplicateAndReferenceIssues(graph);
@@ -45,6 +45,9 @@ class BoundedEventFragmentCache {
45
45
  function eventGraphCacheKey(workspaceId, projectHelpers, analysisMode = 'full') {
46
46
  return `${workspaceId}\0${projectHelpers ? 'expanded' : 'structural'}\0${analysisMode}`;
47
47
  }
48
+ function isGameBackedWorkspace(workspace) {
49
+ return workspace.roots.some(({ kind }) => kind === 'game');
50
+ }
48
51
  const serviceState = new WeakMap();
49
52
  const EVENT_GRAPH_ARTIFACT_MAX_BYTES = 67_108_864;
50
53
  const EVENT_GRAPH_ARTIFACT_MAX_CHUNKS = 1_024;
@@ -77,6 +80,7 @@ export function eventScanReport(graph, fullGraphRecordLimit = EVENT_SCAN_FULL_GR
77
80
  graphSummary: {
78
81
  schemaVersion: graph.schemaVersion,
79
82
  parserVersion: graph.parserVersion,
83
+ analysisMode: graph.analysisMode ?? 'full',
80
84
  workspaceId: graph.workspaceId,
81
85
  workspaceIdentity: graph.workspaceIdentity,
82
86
  revision: graph.revision,
@@ -122,6 +126,7 @@ function cachedEventGraphHash(graph, signal) {
122
126
  ? hashCanonical({
123
127
  schemaVersion: graph.schemaVersion,
124
128
  parserVersion: graph.parserVersion,
129
+ analysisMode: graph.analysisMode ?? 'full',
125
130
  workspaceIdentity: graph.workspaceIdentity,
126
131
  revision: graph.revision,
127
132
  statistics: graph.statistics,
@@ -146,9 +151,12 @@ function sharedState(engine) {
146
151
  function engineGeneration(engine, workspaceId) {
147
152
  return engine.generation(workspaceId);
148
153
  }
149
- function eventScanPatterns(workspace) {
154
+ function eventScanPatterns(workspace, focused = false) {
150
155
  const roots = workspace.registration.roots;
151
156
  const under = (values, glob) => values.map((root) => `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/${glob}`);
157
+ if (focused) {
158
+ return ['events/**/*.txt', 'common/on_actions/**/*.txt'].sort(compareCodeUnits);
159
+ }
152
160
  return [
153
161
  'events/**/*.txt',
154
162
  'history/countries/**/*.txt',
@@ -171,13 +179,16 @@ function eventScanPatterns(workspace) {
171
179
  ...under(roots.localisation, '*.{yml,yaml}'),
172
180
  ].sort(compareCodeUnits);
173
181
  }
174
- async function scanEventSources(engine, workspaceId, workspace, principal, signal) {
175
- const patterns = eventScanPatterns(workspace);
182
+ async function scanEventSources(engine, workspaceId, workspace, focused = false, principal, signal) {
183
+ const patterns = eventScanPatterns(workspace, focused);
184
+ const gamePatterns = eventScanPatterns(workspace, focused);
176
185
  const sourceKinds = ['mod', 'dependency', 'fixture'];
177
186
  const scans = await Promise.all([
178
187
  engine.scan(workspaceId, { patterns, rootKinds: sourceKinds }, principal, signal),
179
188
  ...(workspace.roots.some(({ kind }) => kind === 'game')
180
- ? [engine.scan(workspaceId, { patterns, rootKinds: ['game'] }, principal, signal)]
189
+ ? [
190
+ engine.scan(workspaceId, { patterns: gamePatterns, rootKinds: ['game'] }, principal, signal),
191
+ ]
181
192
  : []),
182
193
  ]);
183
194
  const files = scans.flatMap(({ files }) => files.map((file) => ({ ...file })));
@@ -691,26 +702,32 @@ export class EventChainViewer {
691
702
  // principal bypass the resolver check for a later caller.
692
703
  const workspace = this.engine.resolver.get(workspaceId, options.principal);
693
704
  const generation = engineGeneration(this.engine, workspaceId);
694
- const projectHelpers = options.projectHelpers ?? true;
695
- const analysisMode = options.analysisMode ?? 'full';
696
- const cacheKey = eventGraphCacheKey(workspaceId, projectHelpers, analysisMode);
705
+ const requestedProjectHelpers = options.projectHelpers ?? true;
706
+ const requestedAnalysisMode = options.analysisMode ?? 'full';
707
+ const autoFocused = isGameBackedWorkspace(workspace) && requestedAnalysisMode === 'full';
708
+ const cacheAnalysisMode = autoFocused ? 'focused' : requestedAnalysisMode;
709
+ const cacheProjectHelpers = cacheAnalysisMode === 'focused' ? false : requestedProjectHelpers;
710
+ const cacheKey = eventGraphCacheKey(workspaceId, cacheProjectHelpers, cacheAnalysisMode);
697
711
  const cached = this.#state.current.get(cacheKey);
698
712
  if (options.refresh !== true && cached?.generation === generation) {
699
713
  return cached.graph;
700
714
  }
701
- const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, !projectHelpers, analysisMode)) ??
702
- this.#state.current.get(eventGraphCacheKey(workspaceId, !projectHelpers)) ??
703
- this.#state.current.get(eventGraphCacheKey(workspaceId, projectHelpers));
715
+ const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, !cacheProjectHelpers, cacheAnalysisMode)) ??
716
+ this.#state.current.get(eventGraphCacheKey(workspaceId, requestedProjectHelpers, requestedAnalysisMode)) ??
717
+ this.#state.current.get(eventGraphCacheKey(workspaceId, !requestedProjectHelpers)) ??
718
+ this.#state.current.get(eventGraphCacheKey(workspaceId, requestedProjectHelpers));
719
+ const scanFocused = autoFocused || requestedAnalysisMode === 'focused';
704
720
  const snapshot = options.refresh !== true && sibling?.generation === generation
705
721
  ? sibling.snapshot
706
- : await scanEventSources(this.engine, workspaceId, workspace, options.principal, options.signal);
722
+ : await scanEventSources(this.engine, workspaceId, workspace, scanFocused, options.principal, options.signal);
707
723
  options.signal?.throwIfAborted();
708
724
  if (cached?.snapshot.revision === snapshot.revision && cached.generation === generation) {
709
725
  return cached.graph;
710
726
  }
711
- const buildAnalysisMode = analysisMode === 'focused' && snapshot.files.length > EVENT_FOCUSED_GRAPH_FILE_THRESHOLD
727
+ const buildAnalysisMode = autoFocused || snapshot.files.length > EVENT_FOCUSED_GRAPH_FILE_THRESHOLD
712
728
  ? 'focused'
713
729
  : 'full';
730
+ const projectHelpers = buildAnalysisMode === 'focused' ? false : requestedProjectHelpers;
714
731
  const graph = buildEventGraph(snapshot, {
715
732
  ...(options.signal === undefined ? {} : { signal: options.signal }),
716
733
  cache: this.#state.fragments,
@@ -719,7 +736,7 @@ export class EventChainViewer {
719
736
  analysisMode: buildAnalysisMode,
720
737
  });
721
738
  this.#state.current.set(cacheKey, { generation, snapshot, graph });
722
- if (analysisMode === 'full')
739
+ if (buildAnalysisMode === 'full')
723
740
  rememberGraph(this.#state, workspaceId, graph);
724
741
  return graph;
725
742
  }
@@ -1,5 +1,6 @@
1
1
  import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
2
2
  import { canonicalJson } from '../../core/canonical.js';
3
+ import { exactChangedFilePatterns, focusDomainScanPatterns, } from '../../core/domain-scan-patterns.js';
3
4
  import { ServiceError } from '../../core/result.js';
4
5
  import { parseClausewitz } from '../../core/source/index.js';
5
6
  import { TRANSACTION_MAX_DIAGNOSTICS } from '../../core/transaction-limits.js';
@@ -35,7 +36,14 @@ function focusReferences(engineSnapshot) {
35
36
  }
36
37
  export async function postValidateTransaction(engine, transaction, principal, signal) {
37
38
  engine.invalidate(transaction.workspaceId);
38
- const snapshot = await engine.scan(transaction.workspaceId, {}, principal, signal);
39
+ const workspace = engine.resolver.get(transaction.workspaceId, principal);
40
+ const focusTransaction = transaction.operationKind === 'focus-plan-changes' ||
41
+ transaction.operationKind === 'continuous-focus-plan-changes';
42
+ const snapshot = await engine.scan(transaction.workspaceId, {
43
+ patterns: focusTransaction
44
+ ? focusDomainScanPatterns(workspace)
45
+ : exactChangedFilePatterns(transaction.files.map(({ relativePath }) => relativePath)),
46
+ }, principal, signal);
39
47
  const changed = new Set(transaction.files.map(({ relativePath }) => sourcePathComparisonKey(relativePath)));
40
48
  const diagnostics = snapshot.diagnostics.filter(({ location }) => {
41
49
  if (location === undefined)
@@ -3,6 +3,7 @@ import sharp from 'sharp';
3
3
  import { z } from 'zod/v4';
4
4
  import { publicArtifactLink } from '../../core/artifacts.js';
5
5
  import { compareCodeUnits, hashCanonical } from '../../core/canonical.js';
6
+ import { countryAssetDiscoveryPatterns } from '../../core/domain-scan-patterns.js';
6
7
  import { emptyServiceResult } from '../../core/result.js';
7
8
  import { RenderBudget } from '../../core/render-budget.js';
8
9
  import { workspaceIdSchema } from '../../schemas/common.js';
@@ -382,7 +383,8 @@ export function registerChaosxTools(server, engine, context) {
382
383
  try {
383
384
  const progress = progressReporter(extra);
384
385
  await progress.report(0, 3, 'Resolving ChaosX country assets');
385
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
386
+ const discoveryWorkspace = engine.resolver.get(workspaceId, context.principal);
387
+ const snapshot = await engine.scan(workspaceId, { patterns: countryAssetDiscoveryPatterns(discoveryWorkspace) }, context.principal, progress.signal);
386
388
  const tags = [...new Set(input.countryTags)];
387
389
  const { selections, catalog } = await selectCountryAssets(engine, snapshot, workspaceId, tags, input.eventId, context, progress.signal);
388
390
  await progress.report(1, 3, 'Rasterizing ChaosX country assets');
@@ -122,6 +122,7 @@ const renderHashSchema = z
122
122
  const eventInspectOutputSchema = strictOperationResultSchema(z
123
123
  .object({
124
124
  mode: eventInspectModeSchema,
125
+ analysisMode: z.enum(['full', 'focused']),
125
126
  revision: sha256Schema,
126
127
  graphHash: sha256Schema,
127
128
  counts: eventGraphCountsSchema,
@@ -131,6 +132,7 @@ const eventInspectOutputSchema = strictOperationResultSchema(z
131
132
  const eventRenderOutputSchema = strictOperationResultSchema(z
132
133
  .object({
133
134
  view: eventRenderViewSchema,
135
+ analysisMode: z.enum(['full', 'focused']),
134
136
  revision: sha256Schema,
135
137
  graphHash: sha256Schema,
136
138
  layoutHash: sha256Schema,
@@ -223,15 +225,18 @@ function graphCounts(graph, artifacts) {
223
225
  }
224
226
  function graphValidation(graph) {
225
227
  const counts = graphCounts(graph, 0);
228
+ const focused = graph.analysisMode === 'focused';
226
229
  return {
227
230
  passed: graph.complete && counts.blockingDiagnostics === 0,
228
231
  checks: [
229
232
  {
230
233
  id: 'event-analysis',
231
234
  passed: graph.complete && counts.blockingDiagnostics === 0,
232
- message: graph.complete
233
- ? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
234
- : `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
235
+ message: focused
236
+ ? 'Large workspace analysis deferred workspace-wide helper projections and lifecycle passes; direct evidence is linked'
237
+ : graph.complete
238
+ ? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
239
+ : `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
235
240
  },
236
241
  ],
237
242
  };
@@ -314,6 +319,7 @@ export function registerEventTools(server, engine, context) {
314
319
  await progress.report(2, 3, 'Linking complete event analysis');
315
320
  const result = emptyServiceResult(workspaceId, {
316
321
  mode: inspected.mode,
322
+ analysisMode: inspected.graph.analysisMode ?? 'full',
317
323
  revision: inspected.graph.revision,
318
324
  graphHash: inspected.graphHash,
319
325
  counts: graphCounts(inspected.graph, inspected.artifacts.length),
@@ -352,6 +358,7 @@ export function registerEventTools(server, engine, context) {
352
358
  await progress.report(2, 3, 'Linking complete event renders');
353
359
  const result = emptyServiceResult(workspaceId, {
354
360
  view: rendered.render.view,
361
+ analysisMode: rendered.graph.analysisMode ?? 'full',
355
362
  revision: rendered.graph.revision,
356
363
  graphHash: rendered.graphHash,
357
364
  layoutHash: rendered.render.layout.layoutHash,
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
3
3
  import { compareCodeUnits, canonicalJson, hashCanonical } from '../../core/canonical.js';
4
+ import { focusDomainScanPatterns } from '../../core/domain-scan-patterns.js';
4
5
  import { RenderBudget } from '../../core/render-budget.js';
5
6
  import { emptyServiceResult, ServiceError } from '../../core/result.js';
6
7
  import { parseClausewitz } from '../../core/source/index.js';
@@ -351,9 +352,9 @@ async function executeFocusVisualTool(engine, workbench, context, input, extra,
351
352
  try {
352
353
  const progress = progressReporter(extra);
353
354
  await progress.report(0, 4, 'Importing and indexing focus source');
354
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
355
- await progress.report(1, 4, 'Focus source index complete');
356
355
  const workspace = engine.resolver.get(workspaceId, context.principal);
356
+ const snapshot = await engine.scan(workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, progress.signal);
357
+ await progress.report(1, 4, 'Focus source index complete');
357
358
  const renderBudget = new RenderBudget();
358
359
  if (input.mode === 'continuous') {
359
360
  selectedContinuousFocusFile(snapshot, relativePath);
@@ -610,8 +611,8 @@ export function registerFocusTools(server, engine, context) {
610
611
  try {
611
612
  const progress = progressReporter(extra);
612
613
  await progress.report(0, 3, 'Building shared workspace index');
613
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
614
614
  const workspace = engine.resolver.get(workspaceId, context.principal);
615
+ const snapshot = await engine.scan(workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, progress.signal);
615
616
  if (input.mode === 'continuous') {
616
617
  const selectedFiles = activeContinuousFocusFiles(snapshot).filter((file) => relativePath === undefined || file.relativePath === relativePath);
617
618
  if (selectedFiles.length === 0) {
@@ -849,8 +850,8 @@ export function registerFocusTools(server, engine, context) {
849
850
  requireServerScope(context, 'hoi4:write');
850
851
  const progress = progressReporter(extra);
851
852
  await progress.report(0, 5, 'Validating shared index and current source');
852
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
853
853
  const workspace = engine.resolver.get(workspaceId, context.principal);
854
+ const snapshot = await engine.scan(workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, progress.signal);
854
855
  if (input.mode === 'continuous') {
855
856
  const plan = input.plan;
856
857
  if (createIfMissing &&
@@ -369,10 +369,8 @@ export function registerMapTools(server, engine, context) {
369
369
  try {
370
370
  const progress = progressReporter(extra);
371
371
  await progress.report(0, 3, 'Inspecting and validating the map');
372
- const [shared, { snapshot, validation }] = await Promise.all([
373
- engine.scan(workspaceId, {}, context.principal, progress.signal),
374
- nudger.validate(workspaceId, context.principal, progress.signal),
375
- ]);
372
+ const { snapshot, validation } = await nudger.validate(workspaceId, context.principal, progress.signal);
373
+ const sharedRevision = snapshot.revision;
376
374
  const selected = selectedMapEntities(snapshot, provinceIds, stateIds, regionIds);
377
375
  const provinceGeometry = provinceIds.length === 0
378
376
  ? undefined
@@ -392,7 +390,7 @@ export function registerMapTools(server, engine, context) {
392
390
  schemaVersion: 'map-inspect.v1',
393
391
  sourceHashes: sourceEvidence.bounded.sourceHashes,
394
392
  metadata: {
395
- sharedRevision: shared.revision,
393
+ sharedRevision,
396
394
  sourceHashInventory: sourceEvidence.bounded.inventory,
397
395
  },
398
396
  };
@@ -403,7 +401,7 @@ export function registerMapTools(server, engine, context) {
403
401
  content: `${canonicalJson({
404
402
  schemaVersion: 1,
405
403
  revision: snapshot.revision,
406
- sharedRevision: shared.revision,
404
+ sharedRevision,
407
405
  dimensions: snapshot.index.raster === undefined
408
406
  ? null
409
407
  : {
@@ -471,7 +469,7 @@ export function registerMapTools(server, engine, context) {
471
469
  const artifacts = await engine.artifacts.withAtomicChunkedWrites(workspace, artifactWrites, (stored) => Promise.resolve([...stored]), progress.signal);
472
470
  const result = emptyServiceResult(workspaceId, {
473
471
  revision: snapshot.revision,
474
- sharedRevision: shared.revision,
472
+ sharedRevision,
475
473
  width: snapshot.index.raster?.width ?? null,
476
474
  height: snapshot.index.raster?.height ?? null,
477
475
  definitions: snapshot.index.definitions.length,
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { canonicalJson, hashCanonical } from '../core/canonical.js';
4
4
  import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
5
+ import { probabilityDomainScanPatterns } from '../core/domain-scan-patterns.js';
5
6
  import { publicArtifactLink } from '../core/artifacts.js';
6
7
  import { ServiceError } from '../core/result.js';
7
8
  import { PACKAGE_VERSION } from '../version.js';
@@ -590,10 +591,13 @@ export class ProbabilityAnalyzer {
590
591
  this.engine = engine;
591
592
  this.state = analyzerState(engine);
592
593
  }
593
- async scan(context) {
594
+ async scan(context, sourcePaths = []) {
594
595
  if (context.refresh === true)
595
596
  this.engine.invalidate(context.workspaceId);
596
- return this.engine.scan(context.workspaceId, {}, context.principal, context.signal);
597
+ const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
598
+ return this.engine.scan(context.workspaceId, {
599
+ patterns: probabilityDomainScanPatterns(workspace, sourcePaths.filter((sourcePath) => sourcePath !== undefined)),
600
+ }, context.principal, context.signal);
597
601
  }
598
602
  async verifyGameVersion(context, adapter) {
599
603
  const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
@@ -774,7 +778,7 @@ export class ProbabilityAnalyzer {
774
778
  if (adapter === undefined || source === undefined)
775
779
  return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
776
780
  const gameVersionVerification = await this.verifyGameVersion(context, adapter);
777
- const snapshot = await this.scan(context);
781
+ const snapshot = await this.scan(context, [source.path]);
778
782
  const surface = discoverWeightedSurface(snapshot, adapter, source, candidatePool);
779
783
  const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
780
784
  const inspectedCandidates = evaluateExactCandidates(surface, { id: 'inspection', state: {} }, definitions);
@@ -838,7 +842,7 @@ export class ProbabilityAnalyzer {
838
842
  request.scenarioSet.workspaceId !== request.workspaceId)
839
843
  throw new ServiceError('PROBABILITY_SCENARIO_WORKSPACE_MISMATCH', 'Scenario set belongs to a different workspace');
840
844
  const gameVersionVerification = await this.verifyGameVersion(request, request.adapter);
841
- const snapshot = await this.scan(request);
845
+ const snapshot = await this.scan(request, [request.source.path]);
842
846
  const surface = discoverWeightedSurface(snapshot, request.adapter, request.source, request.candidatePool ?? []);
843
847
  const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
844
848
  const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, request.candidatePool ?? [], gameVersionVerification, {
@@ -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.3.3';
3
+ export const PACKAGE_VERSION = '2.3.5';
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/events.md CHANGED
@@ -16,9 +16,9 @@ Use the three event tools directly from the target mod.
16
16
  - `lint`: find missing targets, unreachable events, suspicious cycles, conflicting definitions, invalid timing, and unresolved dynamic calls.
17
17
  - `impact`: identify callers, descendants, state dependencies, and files affected by a proposed event change.
18
18
 
19
- Use narrow identifiers, direction, and depth limits when the task concerns one chain. Broad scans build the structural event graph without expanding every scripted helper into duplicate paths. Focused trace and path calls traverse helper nodes and collapse only the selected bounded route, while state, impact, and render calls expand helper relationships when their workspace-wide evidence requires it.
19
+ Use narrow identifiers, direction, and depth limits when the task concerns one chain. Broad scans build the structural event graph without expanding every scripted helper into duplicate paths. Game-backed or other large workspaces automatically use a focused source profile containing event definitions and on-actions, then defer workspace-wide helper projections and lifecycle passes. The result is marked partial and records that boundary in the linked evidence instead of waiting until the MCP transport times out. Focused trace and path calls collapse only the selected bounded route.
20
20
 
21
- The compact response summarizes the result. Small scan resources contain the complete graph; very large scan resources contain exact totals, grouped inventories, representative diagnostics, and the revision needed for focused follow-up queries. The viewer retains the complete in-memory graph for those follow-up queries, and the event issue ceiling is high enough for installed-game-scale diagnostic inventories. This keeps an agent's prompt and artifact storage bounded without reducing the results of those focused queries.
21
+ The compact response summarizes the result. Small scan resources contain the complete graph; large scan resources contain exact totals, grouped inventories, representative diagnostics, and the revision needed for focused follow-up queries. Large focused scans keep direct event and on-action evidence, call sites, state accesses, and source locations while leaving helper expansion and workspace-wide lifecycle analysis explicit as deferred boundaries. This keeps an agent's prompt and artifact storage bounded without allowing a broad scan to exhaust the MCP process.
22
22
 
23
23
  Selectors use one of these forms:
24
24
 
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.3.3",
4
+ "version": "2.3.5",
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.3.3/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-render-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-scenario-set.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-sequence-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-simulate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3/schemas/probability-sweep-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.5/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.3.3",
6
+ "version": "2.3.5",
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.3.3",
16
+ "version": "2.3.5",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }