hoi4-agent-tools 4.1.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 (74) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/hoi4_agent_tools/core/canonical.js +42 -0
  3. package/dist/hoi4_agent_tools/core/condition-evaluator.js +12 -8
  4. package/dist/hoi4_agent_tools/core/dependency-walk.js +21 -2
  5. package/dist/hoi4_agent_tools/core/domain-validation.js +6 -1
  6. package/dist/hoi4_agent_tools/core/engine.js +3 -0
  7. package/dist/hoi4_agent_tools/core/game-documentation.js +24 -0
  8. package/dist/hoi4_agent_tools/core/impact-graph.js +6 -2
  9. package/dist/hoi4_agent_tools/core/impact-semantic.js +12 -2
  10. package/dist/hoi4_agent_tools/core/index.js +1 -0
  11. package/dist/hoi4_agent_tools/core/job-worker-host.js +12 -2
  12. package/dist/hoi4_agent_tools/event/algorithms.js +3 -2
  13. package/dist/hoi4_agent_tools/event/graph.js +60 -36
  14. package/dist/hoi4_agent_tools/event/limits.js +4 -0
  15. package/dist/hoi4_agent_tools/event/operations.js +11 -0
  16. package/dist/hoi4_agent_tools/event/queries.js +13 -34
  17. package/dist/hoi4_agent_tools/event/render.js +4 -2
  18. package/dist/hoi4_agent_tools/event/service.js +101 -33
  19. package/dist/hoi4_agent_tools/event/source-analysis.js +101 -7
  20. package/dist/hoi4_agent_tools/event/state-keys.js +63 -0
  21. package/dist/hoi4_agent_tools/event/summary.js +130 -0
  22. package/dist/hoi4_agent_tools/focus/importer.js +8 -6
  23. package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
  24. package/dist/hoi4_agent_tools/focus/tool-support.js +10 -1
  25. package/dist/hoi4_agent_tools/impact/service.js +2 -1
  26. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +1 -0
  27. package/dist/hoi4_agent_tools/mcp/tools/event.js +66 -0
  28. package/dist/hoi4_agent_tools/probability/service.js +18 -18
  29. package/dist/hoi4_agent_tools/reference/source-lookup.js +4 -2
  30. package/dist/hoi4_agent_tools/schemas/probability.js +7 -1
  31. package/dist/hoi4_agent_tools/version.js +1 -1
  32. package/docs/analysis.md +4 -0
  33. package/docs/events.md +12 -0
  34. package/docs/probability.md +1 -1
  35. package/npm-shrinkwrap.json +2 -2
  36. package/package.json +1 -1
  37. package/schemas/configuration.schema.json +1 -1
  38. package/schemas/continuous-focus-palette.schema.json +1 -1
  39. package/schemas/custom-weighted-pool.schema.json +1 -1
  40. package/schemas/decision-inspect-input.schema.json +1 -1
  41. package/schemas/error-log-input.schema.json +1 -1
  42. package/schemas/event-feature-manifest.schema.json +1 -1
  43. package/schemas/focus-plan.schema.json +1 -1
  44. package/schemas/focus-planning-sidecar.schema.json +1 -1
  45. package/schemas/gui-animation-source.schema.json +1 -1
  46. package/schemas/gui-helper.schema.json +1 -1
  47. package/schemas/gui-scenario.schema.json +1 -1
  48. package/schemas/helper-expansion-request.schema.json +1 -1
  49. package/schemas/helper-expansion-summary.schema.json +1 -1
  50. package/schemas/impact-inspect-input.schema.json +1 -1
  51. package/schemas/map-operation.schema.json +1 -1
  52. package/schemas/mechanic-test-input.schema.json +1 -1
  53. package/schemas/mod-index-input.schema.json +1 -1
  54. package/schemas/operation-result.schema.json +1 -1
  55. package/schemas/package-check-input.schema.json +1 -1
  56. package/schemas/package-manifest.schema.json +1 -1
  57. package/schemas/probability-analysis-result.schema.json +7 -1
  58. package/schemas/probability-compare-input.schema.json +1 -1
  59. package/schemas/probability-evaluate-input.schema.json +1 -1
  60. package/schemas/probability-inspect-input.schema.json +1 -1
  61. package/schemas/probability-render-input.schema.json +1 -1
  62. package/schemas/probability-scenario-set.schema.json +1 -1
  63. package/schemas/probability-sequence-input.schema.json +1 -1
  64. package/schemas/probability-simulate-input.schema.json +1 -1
  65. package/schemas/probability-sweep-input.schema.json +1 -1
  66. package/schemas/reference-context-input.schema.json +1 -1
  67. package/schemas/reference-read-input.schema.json +1 -1
  68. package/schemas/reference-search-input.schema.json +1 -1
  69. package/schemas/scenario-suite.schema.json +1 -1
  70. package/schemas/scenario-test-input.schema.json +1 -1
  71. package/schemas/script-validate-input.schema.json +1 -1
  72. package/schemas/shared-scenario.schema.json +1 -1
  73. package/schemas/source-lookup-input.schema.json +1 -1
  74. package/server.json +2 -2
@@ -0,0 +1,130 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ /** Findings returned inline, before an agent opens the linked report. */
3
+ export const EVENT_INLINE_FINDINGS = 10;
4
+ const EVENT_INLINE_CODES = 10;
5
+ const EVENT_INLINE_EVENT_IDS = 20;
6
+ const EVENT_INLINE_MESSAGE_LENGTH = 300;
7
+ const CLASSIFICATION_ORDER = {
8
+ confirmed_error: 0,
9
+ probable_defect: 1,
10
+ design_warning: 2,
11
+ unresolved_analysis: 3,
12
+ };
13
+ const CONFIDENCE_ORDER = {
14
+ confirmed: 0,
15
+ high: 1,
16
+ medium: 2,
17
+ low: 3,
18
+ unresolved: 4,
19
+ };
20
+ function countBy(values, key) {
21
+ const counts = new Map();
22
+ for (const value of values)
23
+ counts.set(key(value), (counts.get(key(value)) ?? 0) + 1);
24
+ return Object.fromEntries([...counts].sort(([left], [right]) => compareCodeUnits(left, right)));
25
+ }
26
+ function layer(issue) {
27
+ const path = issue.location?.path;
28
+ return path === undefined ? 'unknown' : (/^([a-z]+):/u.exec(path)?.[1] ?? 'unknown');
29
+ }
30
+ /** The mod's own files first, then most severe: classification, confidence, source order. */
31
+ function issueOrder(left, right) {
32
+ const leftMod = layer(left) === 'mod' ? 0 : 1;
33
+ const rightMod = layer(right) === 'mod' ? 0 : 1;
34
+ return (leftMod - rightMod ||
35
+ CLASSIFICATION_ORDER[left.classification] - CLASSIFICATION_ORDER[right.classification] ||
36
+ CONFIDENCE_ORDER[left.confidence] - CONFIDENCE_ORDER[right.confidence] ||
37
+ compareCodeUnits(left.location?.path ?? '', right.location?.path ?? '') ||
38
+ (left.location?.start.line ?? 0) - (right.location?.start.line ?? 0) ||
39
+ compareCodeUnits(left.code, right.code) ||
40
+ compareCodeUnits(left.message, right.message));
41
+ }
42
+ /** Counts and the most severe findings of a selection, small enough to return inline. */
43
+ export function summarizeEventIssues(issues) {
44
+ const byCode = Object.entries(countBy(issues, ({ code }) => code))
45
+ .sort(([leftCode, left], [rightCode, right]) => right - left || compareCodeUnits(leftCode, rightCode))
46
+ .slice(0, EVENT_INLINE_CODES);
47
+ return {
48
+ issues: issues.length,
49
+ byClassification: countBy(issues, ({ classification }) => classification),
50
+ // `mod` findings are the workspace's own; `game` and `dependency` ones are context.
51
+ byLayer: countBy(issues, layer),
52
+ topCodes: Object.fromEntries(byCode),
53
+ findings: [...issues]
54
+ .sort(issueOrder)
55
+ .slice(0, EVENT_INLINE_FINDINGS)
56
+ .map((issue) => ({
57
+ code: issue.code,
58
+ classification: issue.classification,
59
+ confidence: issue.confidence,
60
+ message: issue.message.length > EVENT_INLINE_MESSAGE_LENGTH
61
+ ? `${issue.message.slice(0, EVENT_INLINE_MESSAGE_LENGTH - 1)}…`
62
+ : issue.message,
63
+ ...(issue.location === undefined
64
+ ? {}
65
+ : { path: issue.location.path, line: issue.location.start.line }),
66
+ ...(typeof issue.details.name === 'string' ? { subject: issue.details.name } : {}),
67
+ })),
68
+ };
69
+ }
70
+ /** Size of a traced chain and the events it reaches. */
71
+ export function summarizeEventTrace(trace) {
72
+ const events = trace.nodes
73
+ .flatMap(({ kind, eventId }) => (kind === 'event' && eventId !== undefined ? [eventId] : []))
74
+ .sort(compareCodeUnits);
75
+ return {
76
+ nodes: trace.nodes.length,
77
+ edges: trace.edges.length,
78
+ events: events.length,
79
+ eventIds: events.slice(0, EVENT_INLINE_EVENT_IDS),
80
+ boundaryNodes: trace.boundaryNodeIds.length,
81
+ truncated: trace.truncated,
82
+ };
83
+ }
84
+ /** How many chains start where, in a form an agent can read without the report. */
85
+ export function summarizeEventRoots(roots) {
86
+ const nodeLayer = ({ sourcePath }) => sourcePath === undefined ? 'unknown' : (/^([a-z]+):/u.exec(sourcePath)?.[1] ?? 'unknown');
87
+ // The mod's own events first: vanilla callerless events are context, not the mod's defects.
88
+ const ids = (nodes) => [...nodes]
89
+ .sort((left, right) => (nodeLayer(left) === 'mod' ? 0 : 1) - (nodeLayer(right) === 'mod' ? 0 : 1) ||
90
+ compareCodeUnits(left.eventId ?? '', right.eventId ?? ''))
91
+ .flatMap(({ eventId }) => (eventId === undefined ? [] : [eventId]))
92
+ .slice(0, EVENT_INLINE_EVENT_IDS);
93
+ return {
94
+ entryPoints: roots.entryPoints.length,
95
+ entryKinds: countBy(roots.entryPoints, ({ metadata }) => typeof metadata.entryKind === 'string' ? metadata.entryKind : 'unknown'),
96
+ automaticEvents: roots.automaticEvents.length,
97
+ callerlessTriggeredEvents: roots.callerlessTriggeredEvents.length,
98
+ callerlessTriggeredByLayer: countBy(roots.callerlessTriggeredEvents, nodeLayer),
99
+ callerlessTriggeredEventIds: ids(roots.callerlessTriggeredEvents),
100
+ cycles: roots.stronglyConnectedComponents.filter(({ cyclic }) => cyclic).length,
101
+ };
102
+ }
103
+ /** Whether a route exists and, if so, the events it passes through in order. */
104
+ export function summarizeEventPath(path) {
105
+ return {
106
+ found: path.found,
107
+ steps: path.steps.length,
108
+ eventIds: path.steps
109
+ .flatMap(({ node }) => node.kind === 'event' && node.eventId !== undefined ? [node.eventId] : [])
110
+ .slice(0, EVENT_INLINE_EVENT_IDS),
111
+ ...(path.failureReason === undefined ? {} : { failureReason: path.failureReason }),
112
+ ...(path.failureMessage === undefined ? {} : { failureMessage: path.failureMessage }),
113
+ unresolvedAssumptions: path.unresolvedAssumptions.length,
114
+ };
115
+ }
116
+ /** How far a change to the subject reaches, by count. */
117
+ export function summarizeEventImpact(impact) {
118
+ return {
119
+ direct: impact.directNodeIds.length,
120
+ upstream: impact.upstreamNodeIds.length,
121
+ downstream: impact.downstreamNodeIds.length,
122
+ stateAccesses: impact.stateAccessIds.length,
123
+ affectedRoots: impact.affectedRootIds.length,
124
+ affectedTerminals: impact.affectedTerminalIds.length,
125
+ wouldDisconnect: impact.wouldDisconnectNodeIds.length,
126
+ wouldDisconnectTerminals: impact.wouldDisconnectTerminalIds.length,
127
+ unresolved: impact.unresolvedNodeIds.length,
128
+ sampleDownstream: impact.downstreamNodeIds.slice(0, EVENT_INLINE_EVENT_IDS),
129
+ };
130
+ }
@@ -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
@@ -94,6 +94,8 @@ export class ImpactAnalyzer {
94
94
  directConsumers: graph.directConsumers.length,
95
95
  transitiveConsumers: graph.transitiveConsumers.length,
96
96
  affectedFiles: graph.affectedFiles.length,
97
+ // Affected files are evidence, not writes: this read-only analysis never changes a file.
98
+ affectedFilePaths: graph.affectedFiles.slice(0, 64),
97
99
  cycles: graph.cycles.length,
98
100
  scenarioSuites: scenarioSuites.scannedSuites,
99
101
  affectedCases: scenarioSuites.matches.length,
@@ -108,7 +110,6 @@ export class ImpactAnalyzer {
108
110
  result.code = complete ? 'IMPACT_ANALYZED' : 'IMPACT_ANALYZED_PARTIAL';
109
111
  result.artifacts = [publicArtifactLink(artifact)];
110
112
  result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
111
- result.changedFiles = graph.affectedFiles.slice(0, 64);
112
113
  setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
113
114
  result.validation = {
114
115
  passed: complete,
@@ -16,6 +16,7 @@ const impactOutputSchema = strictOperationResultSchema(z
16
16
  directConsumers: nonNegativeIntegerSchema,
17
17
  transitiveConsumers: nonNegativeIntegerSchema,
18
18
  affectedFiles: nonNegativeIntegerSchema,
19
+ affectedFilePaths: z.array(z.string().max(1024)).max(64).optional(),
19
20
  cycles: nonNegativeIntegerSchema,
20
21
  scenarioSuites: nonNegativeIntegerSchema,
21
22
  affectedCases: nonNegativeIntegerSchema,
@@ -70,6 +70,70 @@ const renderHashSchema = z
70
70
  html: sha256Schema.optional(),
71
71
  })
72
72
  .strict();
73
+ const countMapSchema = z.record(z.string().max(256), nonNegativeIntegerSchema);
74
+ const inlineIdsSchema = z.array(z.string().max(256)).max(20);
75
+ /** Bounded inline answer of an inspection; see docs/events.md. */
76
+ const eventInspectSummarySchema = z
77
+ .object({
78
+ issues: nonNegativeIntegerSchema.optional(),
79
+ byClassification: countMapSchema.optional(),
80
+ byLayer: countMapSchema.optional(),
81
+ topCodes: countMapSchema.optional(),
82
+ findings: z
83
+ .array(z
84
+ .object({
85
+ code: z.string().max(256),
86
+ classification: z.enum([
87
+ 'confirmed_error',
88
+ 'probable_defect',
89
+ 'design_warning',
90
+ 'unresolved_analysis',
91
+ ]),
92
+ confidence: z.enum(['confirmed', 'high', 'medium', 'low', 'unresolved']),
93
+ message: z.string().max(300),
94
+ path: z.string().max(1024).optional(),
95
+ line: nonNegativeIntegerSchema.optional(),
96
+ subject: z.string().max(1024).optional(),
97
+ })
98
+ .strict())
99
+ .max(10)
100
+ .optional(),
101
+ trace: z
102
+ .object({
103
+ nodes: nonNegativeIntegerSchema,
104
+ edges: nonNegativeIntegerSchema,
105
+ events: nonNegativeIntegerSchema,
106
+ eventIds: inlineIdsSchema,
107
+ boundaryNodes: nonNegativeIntegerSchema,
108
+ truncated: z.boolean(),
109
+ })
110
+ .strict()
111
+ .optional(),
112
+ entryPoints: nonNegativeIntegerSchema.optional(),
113
+ entryKinds: countMapSchema.optional(),
114
+ automaticEvents: nonNegativeIntegerSchema.optional(),
115
+ callerlessTriggeredEvents: nonNegativeIntegerSchema.optional(),
116
+ callerlessTriggeredByLayer: countMapSchema.optional(),
117
+ callerlessTriggeredEventIds: inlineIdsSchema.optional(),
118
+ cycles: nonNegativeIntegerSchema.optional(),
119
+ found: z.boolean().optional(),
120
+ steps: nonNegativeIntegerSchema.optional(),
121
+ eventIds: inlineIdsSchema.optional(),
122
+ failureReason: z.string().max(64).optional(),
123
+ failureMessage: z.string().max(1024).optional(),
124
+ unresolvedAssumptions: nonNegativeIntegerSchema.optional(),
125
+ direct: nonNegativeIntegerSchema.optional(),
126
+ upstream: nonNegativeIntegerSchema.optional(),
127
+ downstream: nonNegativeIntegerSchema.optional(),
128
+ stateAccesses: nonNegativeIntegerSchema.optional(),
129
+ affectedRoots: nonNegativeIntegerSchema.optional(),
130
+ affectedTerminals: nonNegativeIntegerSchema.optional(),
131
+ wouldDisconnect: nonNegativeIntegerSchema.optional(),
132
+ wouldDisconnectTerminals: nonNegativeIntegerSchema.optional(),
133
+ unresolved: nonNegativeIntegerSchema.optional(),
134
+ sampleDownstream: z.array(z.string().max(1024)).max(20).optional(),
135
+ })
136
+ .strict();
73
137
  const eventInspectDataSchema = z
74
138
  .object({
75
139
  mode: eventInspectModeSchema,
@@ -78,6 +142,7 @@ const eventInspectDataSchema = z
78
142
  graphHash: sha256Schema,
79
143
  counts: eventGraphCountsSchema,
80
144
  helperExpansion: helperExpansionSummarySchema.optional(),
145
+ summary: eventInspectSummarySchema.optional(),
81
146
  boundary: inspectBoundarySchema,
82
147
  })
83
148
  .strict();
@@ -93,6 +158,7 @@ const eventRenderOutputSchema = strictOperationResultSchema(z
93
158
  selectedNodes: nonNegativeIntegerSchema,
94
159
  omittedNodes: nonNegativeIntegerSchema,
95
160
  branchRenders: nonNegativeIntegerSchema,
161
+ omittedBranchRenders: nonNegativeIntegerSchema,
96
162
  }),
97
163
  boundary: z
98
164
  .object({
@@ -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 = {}) {
@@ -1,7 +1,7 @@
1
1
  import { ServiceError } from '../core/result.js';
2
2
  import { decodeSource } from '../core/source/encoding.js';
3
3
  import { readBoundedJsonLines } from './lines.js';
4
- import { scanImpactSemanticReferences } from '../core/impact-semantic.js';
4
+ import { IMPACT_SEMANTIC_MAX_REFERENCES, scanImpactSemanticReferences, } from '../core/impact-semantic.js';
5
5
  import { parseClausewitz } from '../core/source/parser.js';
6
6
  import { navigateSource } from './source-navigation.js';
7
7
  import { locatePatterns, locateSource } from './source-locate.js';
@@ -126,7 +126,9 @@ export async function sourceLookup(engine, workspaceId, input, principal, signal
126
126
  });
127
127
  let semantic = input.includeReferences ? semanticCache.get(snapshot) : undefined;
128
128
  if (input.includeReferences && semantic === undefined) {
129
- semantic = scanImpactSemanticReferences(snapshot, 200_000, { includeOnActions: true });
129
+ semantic = scanImpactSemanticReferences(snapshot, IMPACT_SEMANTIC_MAX_REFERENCES, {
130
+ includeOnActions: true,
131
+ });
130
132
  semanticCache.set(snapshot, semantic);
131
133
  }
132
134
  const referenceIdentities = new Set();
@@ -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.1.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
@@ -58,6 +58,8 @@ Malformed, missing, or oversized suite files produce explicit partial coverage.
58
58
  The graph includes active and overridden definitions, direct and transitive consumers, source locations, state-key reads and writes, and coverage limits.
59
59
  It connects source-backed event, focus, decision, idea, technology, scripted-helper, localisation, GUI, sprite, and texture references where a typed static edge can be established.
60
60
  Unresolved dynamic names, missing definitions, skipped sources, cycles, and traversal limits remain explicit in the report.
61
+ A dynamic reference counts against completeness only when it names a symbol kind the traversal reaches, so `activate_decision = event_target:choice` affects the impact of a decision but not of an idea that no decision consumes.
62
+ `data.affectedFilePaths` lists up to 64 affected files; the result's `changedFiles` stays empty because the analysis never writes.
61
63
  An affected file is a source dependency lead, not a claim that the game executed that path.
62
64
 
63
65
  ## Decisions and missions
@@ -99,6 +101,8 @@ The inventory lists active decisions, category fragments, overridden source defi
99
101
  Category and decision `allowed`, `visible`, and `available` results remain separate; mission visibility is reported as ignored where documented.
100
102
  Target selection covers explicit lists and arrays plus supported state ownership, control, and continent filters.
101
103
  When a target, flag, variable, or helper input is unknown, the corresponding gate remains unresolved.
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 the probability adapters, which run on any installed game version and record the observed build in their evidence.
102
106
 
103
107
  Costs distinguish automatic engine political-power payment from a custom affordability trigger and its separate `complete_effect` payment.
104
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.
package/docs/events.md CHANGED
@@ -26,6 +26,18 @@ For a large helper closure, use [bounded helper expansion](helper-expansion.md)
26
26
 
27
27
  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.
28
28
 
29
+ `data.summary` answers the request inline before the linked report is opened. `scan`, `lint` and `state_flow` return the number of findings in the selection, counts by classification, the ten most frequent codes and the ten most severe findings with their path, line and subject. `trace` adds the traced node, edge and event counts with up to twenty event IDs. `roots` returns entry-point counts by kind, automatic and callerless triggered events, and cycle counts. `data.counts` still describes the whole analyzed graph, including vanilla events.
30
+
31
+ `explain_path` adds whether a route was found, its step count, event IDs and failure reason; `impact` adds direct, upstream, downstream, affected-root and disconnection counts.
32
+
33
+ Workspace-wide modes expand scripted helpers up to a fixed work ceiling. On a very large mod the expansion stops there with an `EVENT_HELPER_PROJECTION_LIMIT` finding that names the completed and total roots; calls from the remaining roots stay structural, and a namespace, event or file selector or `helper_expansion` mode covers them. An unselected overview render returns the overview and as many branch renders as the request's raster budget allows, reporting the rest in `omittedBranchRenders`.
34
+
35
+ A `scan` retains the complete graph as a snapshot artifact only up to 128 MiB of canonical JSON. A larger graph, such as a mod with thousands of events and hundreds of thousands of state accesses, returns the scan report with the info diagnostic `EVENT_GRAPH_SNAPSHOT_OMITTED`; comparisons then use the revisions this server holds in memory.
36
+
37
+ An assignment such as `custom_effect = yes` is reported as a missing helper only where it can execute: in effect blocks of events, decisions, focuses and on-actions, inside scope and loop effects, and in conventionally named effect blocks (`on_*`, `*_effect`, `*_effects`) of other definition files. Arguments of native effects such as `add_building_construction = { instant_build = yes }`, condition blocks such as `visible`, `available`, `allowed`, `ai_will_do`, `NOT` or `any_*`, and data fields of ideas, raids, special projects and scripted GUIs are not calls.
38
+
39
+ A read is linked to a writer of the same name when the writer is a temporary variable, a write inside a scripted helper (whose scope is decided by its caller), a scope-prefixed name such as `GER.value`, or the array behind `entries^num`. `event_target:name` reads targets saved by `save_event_target_as` and `save_global_event_target_as`; `global_event_target:name` reads only global targets. A finding is kept per owner and subject, so different flags or targets read in one event remain separate findings.
40
+
29
41
  Selectors use one of these forms:
30
42
 
31
43
  ```json
@@ -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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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.1.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": {