hoi4-agent-tools 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/hoi4_agent_tools/core/domain-validation.js +6 -1
  3. package/dist/hoi4_agent_tools/core/engine.js +3 -0
  4. package/dist/hoi4_agent_tools/core/game-documentation.js +24 -0
  5. package/dist/hoi4_agent_tools/decision/service.js +1 -22
  6. package/dist/hoi4_agent_tools/event/graph.js +8 -1
  7. package/dist/hoi4_agent_tools/event/service.js +8 -0
  8. package/dist/hoi4_agent_tools/event/source-analysis.js +10 -6
  9. package/dist/hoi4_agent_tools/focus/importer.js +8 -6
  10. package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
  11. package/dist/hoi4_agent_tools/focus/tool-support.js +10 -1
  12. package/dist/hoi4_agent_tools/probability/service.js +18 -18
  13. package/dist/hoi4_agent_tools/schemas/probability.js +7 -1
  14. package/dist/hoi4_agent_tools/version.js +1 -1
  15. package/docs/analysis.md +1 -1
  16. package/docs/probability.md +1 -1
  17. package/npm-shrinkwrap.json +2 -2
  18. package/package.json +1 -1
  19. package/schemas/configuration.schema.json +1 -1
  20. package/schemas/continuous-focus-palette.schema.json +1 -1
  21. package/schemas/custom-weighted-pool.schema.json +1 -1
  22. package/schemas/decision-inspect-input.schema.json +1 -1
  23. package/schemas/error-log-input.schema.json +1 -1
  24. package/schemas/event-feature-manifest.schema.json +1 -1
  25. package/schemas/focus-plan.schema.json +1 -1
  26. package/schemas/focus-planning-sidecar.schema.json +1 -1
  27. package/schemas/gui-animation-source.schema.json +1 -1
  28. package/schemas/gui-helper.schema.json +1 -1
  29. package/schemas/gui-scenario.schema.json +1 -1
  30. package/schemas/helper-expansion-request.schema.json +1 -1
  31. package/schemas/helper-expansion-summary.schema.json +1 -1
  32. package/schemas/impact-inspect-input.schema.json +1 -1
  33. package/schemas/map-operation.schema.json +1 -1
  34. package/schemas/mechanic-test-input.schema.json +1 -1
  35. package/schemas/mod-index-input.schema.json +1 -1
  36. package/schemas/operation-result.schema.json +1 -1
  37. package/schemas/package-check-input.schema.json +1 -1
  38. package/schemas/package-manifest.schema.json +1 -1
  39. package/schemas/probability-analysis-result.schema.json +7 -1
  40. package/schemas/probability-compare-input.schema.json +1 -1
  41. package/schemas/probability-evaluate-input.schema.json +1 -1
  42. package/schemas/probability-inspect-input.schema.json +1 -1
  43. package/schemas/probability-render-input.schema.json +1 -1
  44. package/schemas/probability-scenario-set.schema.json +1 -1
  45. package/schemas/probability-sequence-input.schema.json +1 -1
  46. package/schemas/probability-simulate-input.schema.json +1 -1
  47. package/schemas/probability-sweep-input.schema.json +1 -1
  48. package/schemas/reference-context-input.schema.json +1 -1
  49. package/schemas/reference-read-input.schema.json +1 -1
  50. package/schemas/reference-search-input.schema.json +1 -1
  51. package/schemas/scenario-suite.schema.json +1 -1
  52. package/schemas/scenario-test-input.schema.json +1 -1
  53. package/schemas/script-validate-input.schema.json +1 -1
  54. package/schemas/shared-scenario.schema.json +1 -1
  55. package/schemas/source-lookup-input.schema.json +1 -1
  56. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 4.3.0 - 2026-10-19
6
+
7
+ - Run probability analysis on every installed HOI4 version. `metadata.gameVersionVerification` records the observed build as `workspace_verified`, `workspace_observed` or `version_unknown` instead of refusing with `PROBABILITY_GAME_VERSION_UNSUPPORTED` or `PROBABILITY_GAME_VERSION_UNVERIFIED`; decision inspection evaluates AI weights on any version.
8
+ - Recognize every effect the installed game documents in `documentation/effects_documentation.md` as native in event and focus analysis, so effects added by later patches are not reported as missing scripted helpers.
9
+
5
10
  ## 4.2.0 - 2026-10-19
6
11
 
7
12
  - Keep job workers running when a discovered mod folder is removed after server start. Workers now resolve only workspaces whose roots still exist, while a job for the removed workspace still fails with `WORKSPACE_ROOT_MISSING`.
@@ -54,7 +54,12 @@ export async function postValidateTransaction(engine, transaction, principal, si
54
54
  relativePath.toLowerCase().endsWith('.txt') &&
55
55
  changed.has(sourcePathComparisonKey(relativePath)))) {
56
56
  const document = parseClausewitz(file.bytes, file.displayPath);
57
- const imported = importFocusTrees(document, { references: catalog });
57
+ const imported = importFocusTrees(document, {
58
+ references: catalog,
59
+ ...(snapshot.documentedEffects === undefined
60
+ ? {}
61
+ : { nativeEffects: snapshot.documentedEffects.names }),
62
+ });
58
63
  diagnostics.push(...imported.diagnostics);
59
64
  for (const plan of imported.plans) {
60
65
  const layout = await layoutFocusTreeAsync(plan, signal === undefined ? {} : { signal });
@@ -1,4 +1,5 @@
1
1
  import { compareCodeUnits, hashCanonical } from './canonical.js';
2
+ import { installedEffectCommands } from './game-documentation.js';
2
3
  import { SymbolIndex } from './index.js';
3
4
  import { IndexSegmentCache } from './index-segments.js';
4
5
  import { PersistentAnalysisCache, } from './persistent-analysis-cache.js';
@@ -231,6 +232,7 @@ export class CoreEngine {
231
232
  .at(-1)?.[1].index;
232
233
  const index = await SymbolIndex.buildAsync(files, controller.signal, this.indexSegments, previous);
233
234
  await persistentAnalysisCache?.persist(analysisScope, files, sourceDocuments, this.indexSegments, controller.signal);
235
+ const documentedEffects = await installedEffectCommands(workspace.gameRoot);
234
236
  const snapshot = {
235
237
  workspaceId,
236
238
  revision,
@@ -240,6 +242,7 @@ export class CoreEngine {
240
242
  skippedSourceCount: index.skippedSourceCount,
241
243
  skippedSources: index.skippedSources,
242
244
  diagnostics: index.diagnostics,
245
+ ...(documentedEffects === undefined ? {} : { documentedEffects }),
243
246
  };
244
247
  if ((this.#scanGenerations.get(workspaceId) ?? 0) === generation) {
245
248
  // Replace this request's earlier revision and drop earlier generations; keep the
@@ -0,0 +1,24 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { sha256Bytes } from './canonical.js';
4
+ /** `## name` headings of an installed `*_documentation.md` file. */
5
+ export function documentedCommandNames(markdown) {
6
+ return new Set([...markdown.matchAll(/^## ([a-z_][a-z0-9_]*)[ \t]*\r?$/gmu)].map((match) => match[1]));
7
+ }
8
+ /**
9
+ * Read the effect names the installed game documents. Each game version ships this file, so
10
+ * effects added by a later patch are recognized without a tool update. Absent or unreadable
11
+ * documentation returns `undefined` and callers fall back to their built-in list.
12
+ */
13
+ export async function installedEffectCommands(gameRoot) {
14
+ if (gameRoot === undefined)
15
+ return undefined;
16
+ try {
17
+ const bytes = await readFile(path.join(gameRoot, 'documentation', 'effects_documentation.md'));
18
+ const names = documentedCommandNames(bytes.toString('utf8'));
19
+ return names.size === 0 ? undefined : { names, sha256: sha256Bytes(bytes) };
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ }
@@ -99,20 +99,7 @@ export class DecisionAnalyzer {
99
99
  }));
100
100
  const ai = input.mode === 'inventory'
101
101
  ? undefined
102
- : await inspectDecisionAi(this.engine, before, input.id, scenarios, overlay, input.principal, input.signal).catch((error) => {
103
- // AI weights depend on adapters verified for one game version. A game patch leaves
104
- // the AI score unresolved; visibility, availability and cost evidence still stand.
105
- if (error instanceof ServiceError &&
106
- (error.code === 'PROBABILITY_GAME_VERSION_UNSUPPORTED' ||
107
- error.code === 'PROBABILITY_GAME_VERSION_UNVERIFIED'))
108
- return {
109
- status: 'unresolved',
110
- code: error.code,
111
- message: error.message,
112
- details: error.details,
113
- };
114
- throw error;
115
- });
102
+ : await inspectDecisionAi(this.engine, before, input.id, scenarios, overlay, input.principal, input.signal);
116
103
  const sourceComplete = input.mode === 'inventory'
117
104
  ? inventory.complete
118
105
  : input.mode === 'compare'
@@ -193,14 +180,6 @@ export class DecisionAnalyzer {
193
180
  : {}),
194
181
  });
195
182
  result.code = complete ? 'DECISION_ANALYZED' : 'DECISION_ANALYZED_PARTIAL';
196
- if (ai !== undefined && 'code' in ai)
197
- result.diagnostics.push({
198
- category: 'configuration',
199
- code: ai.code,
200
- severity: 'warning',
201
- message: `${ai.message}; AI weights are unresolved, the remaining evidence is unaffected`,
202
- details: ai.details,
203
- });
204
183
  result.artifacts = [publicArtifactLink(artifact)];
205
184
  result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
206
185
  setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
@@ -1383,14 +1383,21 @@ function analyzeFragments(snapshot, files, catalog, options, budget) {
1383
1383
  activeDecisionPaths: reads.bind('active-decisions', 'decision', catalog.activeDecisionPaths),
1384
1384
  inventoryComplete: snapshot.complete,
1385
1385
  catalogFingerprint: catalog.fingerprint,
1386
+ ...(options.documentedEffects === undefined
1387
+ ? {}
1388
+ : { documentedEffectKeys: options.documentedEffects.names }),
1386
1389
  };
1390
+ // A game update that documents new effects changes how fragments read, so it keys the cache.
1391
+ const fragmentFingerprint = options.documentedEffects === undefined
1392
+ ? catalog.fingerprint
1393
+ : `${catalog.fingerprint}:${options.documentedEffects.sha256}`;
1387
1394
  for (const file of files) {
1388
1395
  budget.spend('source_fragment');
1389
1396
  if (!shouldAnalyzeEventFile(file))
1390
1397
  continue;
1391
1398
  if (snapshot.index.isSourceSkipped(file.displayPath))
1392
1399
  continue;
1393
- const cacheKey = eventSemanticFragmentCacheKey(file, catalog.fingerprint);
1400
+ const cacheKey = eventSemanticFragmentCacheKey(file, fragmentFingerprint);
1394
1401
  const fragment = semanticFragment(dependencies, options.cache, file, cacheKey, reads, () => analyzeEventSource(file, context));
1395
1402
  fragments.push(fragment);
1396
1403
  }
@@ -3,6 +3,7 @@ import { inspectHelperExpansion } from '../core/helper-expansion.js';
3
3
  import { eventHelperInventory } from './helper-expansion.js';
4
4
  import { clearSemanticDependencies } from '../core/semantic-dependencies.js';
5
5
  import { canonicalJson, canonicalJsonByteLength, compareCodeUnits, hashCanonical, sha256Bytes, } from '../core/canonical.js';
6
+ import { installedEffectCommands } from '../core/game-documentation.js';
6
7
  import { boundedSourceHashEvidence, publicArtifactLink, } from '../core/artifacts.js';
7
8
  import { ServiceError } from '../core/result.js';
8
9
  import { RenderBudget } from '../core/render-budget.js';
@@ -714,6 +715,8 @@ function coherentBranchPartitions(graph, nodeLimit, maximumGroups, signal) {
714
715
  export class EventChainViewer {
715
716
  engine;
716
717
  #state;
718
+ /** Effect names the installed game documented at each workspace's last scan. */
719
+ #documentedEffects = new Map();
717
720
  constructor(engine) {
718
721
  this.engine = engine;
719
722
  this.#state = sharedState(engine);
@@ -757,8 +760,11 @@ export class EventChainViewer {
757
760
  return cached.graph;
758
761
  const buildAnalysisMode = requestedAnalysisMode;
759
762
  const projectHelpers = buildAnalysisMode === 'focused' ? false : requestedProjectHelpers;
763
+ const documentedEffects = await installedEffectCommands(workspace.gameRoot);
764
+ this.#documentedEffects.set(workspaceId, documentedEffects);
760
765
  const graph = await buildEventGraphAsync(snapshot, {
761
766
  ...(options.signal === undefined ? {} : { signal: options.signal }),
767
+ ...(documentedEffects === undefined ? {} : { documentedEffects }),
762
768
  cache: this.#state.fragments,
763
769
  workspaceIdentity: workspace.workspaceIdentity,
764
770
  projectHelpers,
@@ -1321,8 +1327,10 @@ export class EventChainViewer {
1321
1327
  skippedSources: index.skippedSources,
1322
1328
  diagnostics: index.diagnostics,
1323
1329
  };
1330
+ const documentedEffects = this.#documentedEffects.get(workspaceId);
1324
1331
  return buildEventGraph(snapshot, {
1325
1332
  ...(signal === undefined ? {} : { signal }),
1333
+ ...(documentedEffects === undefined ? {} : { documentedEffects }),
1326
1334
  cache: this.#state.fragments,
1327
1335
  workspaceIdentity: workspace.workspaceIdentity,
1328
1336
  projectHelpers: false,
@@ -216,8 +216,11 @@ const NATIVE_EFFECT_CONTAINERS = new Set([
216
216
  'while_loop_effect',
217
217
  ]);
218
218
  /** Inside a native effect such as `add_building_construction`, `key = yes` is an argument. */
219
- function isNativeArgumentBlock(normalizedKey) {
220
- return (nativeEffectKeys.has(normalizedKey) &&
219
+ function isNativeEffect(normalizedKey, documented) {
220
+ return nativeEffectKeys.has(normalizedKey) || documented?.has(normalizedKey) === true;
221
+ }
222
+ function isNativeArgumentBlock(normalizedKey, documented) {
223
+ return (isNativeEffect(normalizedKey, documented) &&
221
224
  !NATIVE_EFFECT_CONTAINERS.has(normalizedKey) &&
222
225
  !CONTROL_BLOCKS.has(normalizedKey) &&
223
226
  SCOPE_BLOCKS[normalizedKey] === undefined &&
@@ -285,10 +288,10 @@ function localisationKeysFromValue(value) {
285
288
  }
286
289
  return [...new Set(keys)].sort(compareCodeUnits);
287
290
  }
288
- function missingHelperCandidate(key, value, effectContext) {
291
+ function missingHelperCandidate(key, value, effectContext, documented) {
289
292
  const normalizedKey = key.toLowerCase();
290
293
  if (!effectContext ||
291
- nativeEffectKeys.has(normalizedKey) ||
294
+ isNativeEffect(normalizedKey, documented) ||
292
295
  NON_HELPER_BLOCKS.has(normalizedKey) ||
293
296
  SCOPE_BLOCKS[normalizedKey] !== undefined ||
294
297
  STATE_RULES[normalizedKey] !== undefined ||
@@ -993,7 +996,7 @@ class FragmentAnalyzer {
993
996
  if (this.context.inventoryComplete !== false &&
994
997
  context.nativeArguments !== true &&
995
998
  context.inferMissingHelpers !== false &&
996
- missingHelperCandidate(key, entry.value, effectContext)) {
999
+ missingHelperCandidate(key, entry.value, effectContext, this.context.documentedEffectKeys)) {
997
1000
  const target = this.unresolvedNode('missing_helper', key, entry, context.ownerId, semanticPath);
998
1001
  this.addEdge(context.ownerId, target, 'scripted_effect_call', entry, context.conditions, {
999
1002
  phase: context.phase,
@@ -1122,7 +1125,8 @@ class FragmentAnalyzer {
1122
1125
  effectContext: NON_EXECUTING_BLOCKS.has(normalizedKey) || isTriggerIterator(normalizedKey)
1123
1126
  ? false
1124
1127
  : effectContext,
1125
- nativeArguments: context.nativeArguments === true || isNativeArgumentBlock(normalizedKey),
1128
+ nativeArguments: context.nativeArguments === true ||
1129
+ isNativeArgumentBlock(normalizedKey, this.context.documentedEffectKeys),
1126
1130
  ...(context.inferMissingHelpers === false && isConventionalEffectBlock(normalizedKey)
1127
1131
  ? { inferMissingHelpers: true }
1128
1132
  : {}),
@@ -135,9 +135,9 @@ function isFormableDecision(target, options) {
135
135
  return false;
136
136
  return /(?:^form_|formable|form_country)/iu.test(target);
137
137
  }
138
- function isEffectContainer(key) {
138
+ function isEffectContainer(key, nativeEffects) {
139
139
  return (effectContainerKeys.has(key) ||
140
- (nativeFocusEffectKeys.has(key) &&
140
+ ((nativeFocusEffectKeys.has(key) || nativeEffects?.has(key) === true) &&
141
141
  /^(?:(?:global_)?every|ordered|random)_[a-z0-9_]+$/u.test(key)));
142
142
  }
143
143
  function isScopeKey(key, knownLeaders) {
@@ -342,6 +342,7 @@ function referenceLinks(document, block, options) {
342
342
  const links = [];
343
343
  const knownHelpers = catalogValues(options, 'helper');
344
344
  const knownLeaders = catalogValues(options, 'leader');
345
+ const isNative = (key) => nativeFocusEffectKeys.has(key) || options.nativeEffects?.has(key) === true;
345
346
  function recordReference(assignment, specification) {
346
347
  for (const target of referenceTargets(assignment, specification)) {
347
348
  const kind = specification.kind === 'decision' && isFormableDecision(target, options)
@@ -364,7 +365,8 @@ function referenceLinks(document, block, options) {
364
365
  recordReference(assignment, reference);
365
366
  if (assignment.value.type === 'block') {
366
367
  for (const nested of assignments(assignment.value)) {
367
- if (nested.value.type === 'block' && isEffectContainer(nested.key.value.toLowerCase())) {
368
+ if (nested.value.type === 'block' &&
369
+ isEffectContainer(nested.key.value.toLowerCase(), options.nativeEffects)) {
368
370
  effectAssignment(nested, new Set());
369
371
  }
370
372
  }
@@ -373,7 +375,7 @@ function referenceLinks(document, block, options) {
373
375
  }
374
376
  if (knownHelpers.has(key) &&
375
377
  !effectContainerKeys.has(normalizedKey) &&
376
- !nativeFocusEffectKeys.has(normalizedKey)) {
378
+ !isNative(normalizedKey)) {
377
379
  links.push({
378
380
  kind: 'helper',
379
381
  target: key,
@@ -381,7 +383,7 @@ function referenceLinks(document, block, options) {
381
383
  });
382
384
  return;
383
385
  }
384
- if (isEffectContainer(normalizedKey)) {
386
+ if (isEffectContainer(normalizedKey, options.nativeEffects)) {
385
387
  if (assignment.value.type !== 'block')
386
388
  return;
387
389
  if (normalizedKey === 'random_list') {
@@ -397,7 +399,7 @@ function referenceLinks(document, block, options) {
397
399
  }
398
400
  return;
399
401
  }
400
- if (nativeFocusEffectKeys.has(normalizedKey))
402
+ if (isNative(normalizedKey))
401
403
  return;
402
404
  if (assignment.value.type === 'block' && isScopeKey(key, knownLeaders)) {
403
405
  effectWalk(assignment.value, new Set());
@@ -2,7 +2,9 @@
2
2
  * Native effect identifiers used to distinguish direct scripted-effect calls
3
3
  * from engine effects while importing focus completion rewards.
4
4
  *
5
- * Source: the official HOI4 1.19.2 effects documentation reviewed 2026-07-10.
5
+ * Source: the official HOI4 1.19.2 effects documentation reviewed 2026-07-10. This is the
6
+ * fallback list; analysis also accepts every effect the installed game documents in
7
+ * documentation/effects_documentation.md, so effects added by later patches are recognized.
6
8
  * Keep this list synchronized during compatibility reviews.
7
9
  */
8
10
  export const nativeEffectKeys = new Set([
@@ -40,7 +40,7 @@ export function activeSidecar(snapshot, focusFile) {
40
40
  }
41
41
  export function importFocusFile(snapshot, file, palettes) {
42
42
  const document = parseClausewitz(file.bytes, file.displayPath);
43
- const imported = importFocusTrees(document, { references: referenceCatalog(snapshot) });
43
+ const imported = importFocusTrees(document, focusImportOptions(snapshot));
44
44
  const sidecar = activeSidecar(snapshot, file);
45
45
  imported.plans = imported.plans.map((plan) => {
46
46
  const enriched = sidecar === undefined
@@ -89,6 +89,15 @@ export function selectContinuousPlan(result, paletteId) {
89
89
  throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', `Continuous focus palette was not found: ${paletteId}`);
90
90
  return plan;
91
91
  }
92
+ /** Import options from a scan: its reference catalog and the installed game's documented effects. */
93
+ export function focusImportOptions(snapshot) {
94
+ return {
95
+ references: referenceCatalog(snapshot),
96
+ ...(snapshot.documentedEffects === undefined
97
+ ? {}
98
+ : { nativeEffects: snapshot.documentedEffects.names }),
99
+ };
100
+ }
92
101
  export function referenceCatalog(snapshot) {
93
102
  const identifiers = (kind) => [
94
103
  ...new Set(snapshot.index
@@ -775,39 +775,39 @@ export class ProbabilityAnalyzer {
775
775
  if (adapter === 'custom_weighted_pool' || workspace.gameRoot === undefined)
776
776
  return { status: 'adapter_target_only', adapterTarget: VERIFIED_GAME_VERSION };
777
777
  const sourcePath = path.join(workspace.gameRoot, 'launcher-settings.json');
778
+ const unknown = (reason) => ({
779
+ status: 'version_unknown',
780
+ adapterTarget: VERIFIED_GAME_VERSION,
781
+ sourcePath,
782
+ reason,
783
+ });
778
784
  let parsed;
779
785
  try {
780
786
  parsed = JSON.parse(await readFile(sourcePath, 'utf8'));
781
787
  }
782
788
  catch (error) {
783
- throw new ServiceError('PROBABILITY_GAME_VERSION_UNVERIFIED', 'The configured game version could not be verified from launcher-settings.json', { sourcePath, reason: error instanceof Error ? error.message : String(error) });
789
+ return unknown(error instanceof Error ? error.message : String(error));
784
790
  }
785
791
  if (typeof parsed !== 'object' || parsed === null)
786
- throw new ServiceError('PROBABILITY_GAME_VERSION_UNVERIFIED', 'The configured launcher-settings.json does not contain a version object', { sourcePath });
792
+ return unknown('launcher-settings.json does not contain a version object');
787
793
  const settings = parsed;
788
794
  const observedVersion = typeof settings.version === 'string' ? settings.version : undefined;
789
795
  const observedRawVersion = typeof settings.rawVersion === 'string' ? settings.rawVersion : undefined;
790
- const observedChecksum = observedVersion?.match(/\(([a-z0-9]+)\)\s*$/iu)?.[1];
791
- if (observedVersion === undefined ||
792
- observedRawVersion === undefined ||
793
- observedChecksum === undefined)
794
- throw new ServiceError('PROBABILITY_GAME_VERSION_UNVERIFIED', 'The configured launcher-settings.json is missing version, rawVersion, or checksum data', { sourcePath, observedVersion, observedRawVersion, observedChecksum });
795
- if (observedRawVersion !== VERIFIED_GAME_RAW_VERSION ||
796
- observedChecksum.toLowerCase() !== VERIFIED_GAME_CHECKSUM)
797
- throw new ServiceError('PROBABILITY_GAME_VERSION_UNSUPPORTED', `Probability adapters are verified for ${VERIFIED_GAME_VERSION}, not the configured game version`, {
798
- sourcePath,
799
- observedVersion,
800
- observedRawVersion,
801
- observedChecksum,
802
- adapterTarget: VERIFIED_GAME_VERSION,
803
- });
796
+ const observedChecksum = observedVersion?.match(/\(([a-z0-9]+)\)\s*$/iu)?.[1]?.toLowerCase();
797
+ if (observedVersion === undefined || observedRawVersion === undefined)
798
+ return unknown('launcher-settings.json is missing version or rawVersion');
799
+ // Any installed version is analyzed with the documented rules; the observed identity is
800
+ // recorded so a result can be traced to the game it describes.
804
801
  return {
805
- status: 'workspace_verified',
802
+ status: observedRawVersion === VERIFIED_GAME_RAW_VERSION &&
803
+ observedChecksum === VERIFIED_GAME_CHECKSUM
804
+ ? 'workspace_verified'
805
+ : 'workspace_observed',
806
806
  adapterTarget: VERIFIED_GAME_VERSION,
807
807
  sourcePath,
808
808
  observedVersion,
809
809
  observedRawVersion,
810
- observedChecksum: observedChecksum.toLowerCase(),
810
+ ...(observedChecksum === undefined ? {} : { observedChecksum }),
811
811
  };
812
812
  }
813
813
  metadata(context, snapshot, surface, scenarioSet, candidatePool, gameVersionVerification, extra = {}) {
@@ -770,7 +770,13 @@ const probabilityMetadataSchema = z
770
770
  gameVersion: z.string(),
771
771
  gameVersionVerification: z
772
772
  .object({
773
- status: z.enum(['workspace_verified', 'adapter_target_only']),
773
+ status: z.enum([
774
+ 'workspace_verified',
775
+ 'workspace_observed',
776
+ 'version_unknown',
777
+ 'adapter_target_only',
778
+ ]),
779
+ reason: z.string().max(1024).optional(),
774
780
  adapterTarget: z.string(),
775
781
  sourcePath: z.string().optional(),
776
782
  observedVersion: z.string().optional(),
@@ -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 = '4.2.0';
3
+ export const PACKAGE_VERSION = '4.3.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 2;
package/docs/analysis.md CHANGED
@@ -102,7 +102,7 @@ Category and decision `allowed`, `visible`, and `available` results remain separ
102
102
  Target selection covers explicit lists and arrays plus supported state ownership, control, and continent filters.
103
103
  When a target, flag, variable, or helper input is unknown, the corresponding gate remains unresolved.
104
104
  This includes `has_variable` and `has_event_target`: an undeclared variable or event target is unknown, not absent, unless the scenario sets `closedFlags: true`.
105
- AI weights use probability adapters verified for one game version. On another installed version the report keeps the gate, cost and lifecycle evidence, marks the AI part unresolved and returns a `PROBABILITY_GAME_VERSION_UNSUPPORTED` warning.
105
+ AI weights use the probability adapters, which run on any installed game version and record the observed build in their evidence.
106
106
 
107
107
  Costs distinguish automatic engine political-power payment from a custom affordability trigger and its separate `complete_effect` payment.
108
108
  The report identifies direct deductions, possible double payment, missing direct payment, conditional or helper-dependent paths, repeatability, cooldowns, removal, activation, cancellation, and mission timeout paths.
@@ -12,7 +12,7 @@ Use them for event timing and option weights, decision and mission scores, focus
12
12
 
13
13
  ## Game version
14
14
 
15
- When `gameRoot` is configured, `launcher-settings.json` must identify the supported HOI4 build and checksum for evaluation to run. Without a game root, results identify the adapter version. Proposed source is evaluated in memory.
15
+ The adapters apply the documented weighting rules of `ai_will_do`, `ai_chance`, mean time to happen, `random_list` and AI strategies, which are the same in every game version, so evaluation runs on whatever HOI4 version is installed. When `gameRoot` is configured, `metadata.gameVersionVerification` records the build read from `launcher-settings.json`: `workspace_verified` for the build the adapters were last reviewed against, `workspace_observed` for any other build, and `version_unknown` with a `reason` when the file cannot be read. Without a game root, results identify the adapter version (`adapter_target_only`). Proposed source is evaluated in memory.
16
16
 
17
17
  ## One call for current weights
18
18
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "hoi4-agent-tools",
9
- "version": "4.2.0",
9
+ "version": "4.3.0",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@fontsource-variable/roboto": "5.3.0",
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": "4.2.0",
4
+ "version": "4.3.0",
5
5
  "description": "Build HOI4 mods: focus trees, events, decisions, worlds, GUI previews, docs and AI balance.",
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/v4.2.0/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/decision-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/decision-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/v4.2.0/schemas/error-log-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/error-log-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/v4.2.0/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/helper-expansion-request.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/helper-expansion-request.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/v4.2.0/schemas/helper-expansion-summary.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/helper-expansion-summary.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/v4.2.0/schemas/impact-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/impact-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/v4.2.0/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/mechanic-test-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/mechanic-test-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/v4.2.0/schemas/mod-index-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/mod-index-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/v4.2.0/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/package-check-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/package-check-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/v4.2.0/schemas/package-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/package-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/v4.2.0/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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": {
@@ -79,9 +79,15 @@
79
79
  "type": "string",
80
80
  "enum": [
81
81
  "workspace_verified",
82
+ "workspace_observed",
83
+ "version_unknown",
82
84
  "adapter_target_only"
83
85
  ]
84
86
  },
87
+ "reason": {
88
+ "type": "string",
89
+ "maxLength": 1024
90
+ },
85
91
  "adapterTarget": {
86
92
  "type": "string"
87
93
  },
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.2.0/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-render-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-scenario-set.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-sequence-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-simulate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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/v4.2.0/schemas/probability-sweep-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.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": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.2.0/schemas/reference-context-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/reference-context-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/v4.2.0/schemas/reference-read-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/reference-read-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/v4.2.0/schemas/reference-search-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/reference-search-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/v4.2.0/schemas/scenario-suite.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/scenario-suite.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/v4.2.0/schemas/scenario-test-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/scenario-test-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/v4.2.0/schemas/script-validate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/script-validate-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/v4.2.0/schemas/shared-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/shared-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/v4.2.0/schemas/source-lookup-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/source-lookup-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": "Build HOI4 mods: focus trees, events, decisions, worlds, GUI previews, docs and AI balance.",
6
- "version": "4.2.0",
6
+ "version": "4.3.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": "4.2.0",
16
+ "version": "4.3.0",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }