hoi4-agent-tools 3.6.0 → 3.8.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 (87) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +124 -28
  3. package/dist/hoi4_agent_tools/core/configuration.js +2 -0
  4. package/dist/hoi4_agent_tools/core/engine.js +7 -7
  5. package/dist/hoi4_agent_tools/core/file-glob.js +164 -0
  6. package/dist/hoi4_agent_tools/core/index-segments.js +4 -0
  7. package/dist/hoi4_agent_tools/core/job-operation-registration.js +31 -0
  8. package/dist/hoi4_agent_tools/core/job-store.js +19 -40
  9. package/dist/hoi4_agent_tools/core/job-worker-host.js +84 -19
  10. package/dist/hoi4_agent_tools/core/job-worker.js +8 -16
  11. package/dist/hoi4_agent_tools/core/operation-tasks.js +2 -7
  12. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +37 -6
  13. package/dist/hoi4_agent_tools/core/scanner.js +136 -47
  14. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +27 -13
  15. package/dist/hoi4_agent_tools/core/source/cache.js +4 -0
  16. package/dist/hoi4_agent_tools/core/windows-sharing.js +42 -0
  17. package/dist/hoi4_agent_tools/core/workspace.js +114 -8
  18. package/dist/hoi4_agent_tools/event/artifact-validation.js +1 -1
  19. package/dist/hoi4_agent_tools/event/service.js +1 -1
  20. package/dist/hoi4_agent_tools/focus/service.js +2 -2
  21. package/dist/hoi4_agent_tools/gui/studio.js +16 -3
  22. package/dist/hoi4_agent_tools/map/model.js +81 -2
  23. package/dist/hoi4_agent_tools/map/service.js +3 -2
  24. package/dist/hoi4_agent_tools/map/validation.js +60 -4
  25. package/dist/hoi4_agent_tools/mcp/tools/reference.js +10 -1
  26. package/dist/hoi4_agent_tools/package-check/service.js +1 -1
  27. package/dist/hoi4_agent_tools/reference/lines.js +12 -0
  28. package/dist/hoi4_agent_tools/reference/operations.js +45 -2
  29. package/dist/hoi4_agent_tools/reference/ranking.js +309 -0
  30. package/dist/hoi4_agent_tools/reference/script-validation.js +267 -0
  31. package/dist/hoi4_agent_tools/reference/service.js +285 -177
  32. package/dist/hoi4_agent_tools/reference/source-locate.js +156 -0
  33. package/dist/hoi4_agent_tools/reference/source-lookup.js +125 -18
  34. package/dist/hoi4_agent_tools/reference/source-navigation.js +130 -0
  35. package/dist/hoi4_agent_tools/reference/suggestions.js +38 -0
  36. package/dist/hoi4_agent_tools/schemas/reference.js +135 -2
  37. package/dist/hoi4_agent_tools/schemas/script-validation.js +69 -0
  38. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -1
  39. package/dist/hoi4_agent_tools/technology/service.js +1 -1
  40. package/dist/hoi4_agent_tools/version.js +1 -1
  41. package/docs/README.md +1 -0
  42. package/docs/development.md +6 -0
  43. package/docs/examples.md +3 -13
  44. package/docs/gui.md +1 -15
  45. package/docs/map.md +10 -0
  46. package/docs/reference.md +47 -4
  47. package/docs/research/reference-benchmark.md +80 -0
  48. package/docs/setup.md +15 -0
  49. package/docs/tools.md +37 -36
  50. package/npm-shrinkwrap.json +7101 -0
  51. package/package.json +8 -7
  52. package/schemas/configuration.schema.json +95 -88
  53. package/schemas/continuous-focus-palette.schema.json +1 -1
  54. package/schemas/custom-weighted-pool.schema.json +1 -1
  55. package/schemas/decision-inspect-input.schema.json +1 -1
  56. package/schemas/event-feature-manifest.schema.json +1 -1
  57. package/schemas/focus-plan.schema.json +1 -1
  58. package/schemas/focus-planning-sidecar.schema.json +1 -1
  59. package/schemas/gui-animation-source.schema.json +1 -1
  60. package/schemas/gui-helper.schema.json +1 -1
  61. package/schemas/gui-scenario.schema.json +1 -1
  62. package/schemas/helper-expansion-request.schema.json +1 -1
  63. package/schemas/helper-expansion-summary.schema.json +1 -1
  64. package/schemas/impact-inspect-input.schema.json +1 -1
  65. package/schemas/map-operation.schema.json +1 -1
  66. package/schemas/mechanic-test-input.schema.json +1 -1
  67. package/schemas/operation-result.schema.json +1 -1
  68. package/schemas/package-check-input.schema.json +1 -1
  69. package/schemas/package-manifest.schema.json +1 -1
  70. package/schemas/probability-analysis-result.schema.json +1 -1
  71. package/schemas/probability-compare-input.schema.json +1 -1
  72. package/schemas/probability-evaluate-input.schema.json +1 -1
  73. package/schemas/probability-inspect-input.schema.json +1 -1
  74. package/schemas/probability-render-input.schema.json +1 -1
  75. package/schemas/probability-scenario-set.schema.json +1 -1
  76. package/schemas/probability-sequence-input.schema.json +1 -1
  77. package/schemas/probability-simulate-input.schema.json +1 -1
  78. package/schemas/probability-sweep-input.schema.json +1 -1
  79. package/schemas/reference-context-input.schema.json +1 -1
  80. package/schemas/reference-read-input.schema.json +9 -1
  81. package/schemas/reference-search-input.schema.json +1 -1
  82. package/schemas/scenario-suite.schema.json +1 -1
  83. package/schemas/scenario-test-input.schema.json +1 -1
  84. package/schemas/script-validate-input.schema.json +92 -0
  85. package/schemas/shared-scenario.schema.json +1 -1
  86. package/schemas/source-lookup-input.schema.json +144 -15
  87. package/server.json +2 -2
@@ -26,6 +26,7 @@ export const referenceReadRequestSchema = z
26
26
  workspaceId: workspaceIdSchema,
27
27
  id: z.string().regex(/^[a-f0-9]{64}$/u),
28
28
  revision: z.string().regex(/^[a-f0-9]{64}$/u),
29
+ source: referenceSourceSchema.optional(),
29
30
  startLine: z.number().int().min(1).optional(),
30
31
  startColumn: z.number().int().min(1).optional(),
31
32
  maxLines: z.number().int().min(1).max(80).default(40),
@@ -39,10 +40,25 @@ export const referenceContextRequestSchema = z
39
40
  limit: z.number().int().min(1).max(24).default(16),
40
41
  })
41
42
  .strict();
43
+ export const sourceKeyPathSchema = z
44
+ .array(z.union([
45
+ z
46
+ .object({
47
+ key: z.string().min(1).max(1024),
48
+ occurrence: z.number().int().min(0).max(100_000).optional(),
49
+ })
50
+ .strict(),
51
+ z.object({ index: z.number().int().min(0).max(100_000) }).strict(),
52
+ ]))
53
+ .max(24);
42
54
  export const sourceLookupRequestSchema = z
43
55
  .object({
44
56
  workspaceId: workspaceIdSchema,
45
- symbol: z.string().trim().min(1).max(256),
57
+ symbol: z.string().trim().min(1).max(256).optional(),
58
+ /** Locate mode: a scanned source path (`mod:events/a.txt`, `events/a.txt`, or absolute). */
59
+ path: z.string().trim().min(1).max(4096).optional(),
60
+ line: z.number().int().min(1).optional(),
61
+ column: z.number().int().min(1).optional(),
46
62
  kind: z
47
63
  .string()
48
64
  .regex(/^[a-z][a-z0-9_]{0,63}$/u)
@@ -51,6 +67,10 @@ export const sourceLookupRequestSchema = z
51
67
  maxDefinitions: z.number().int().min(1).max(3).default(3),
52
68
  maxReferences: z.number().int().min(0).max(20).default(10),
53
69
  maxLines: z.number().int().min(1).max(80).default(30),
70
+ keyPath: sourceKeyPathSchema.optional(),
71
+ view: z.enum(['source', 'structure']).default('source'),
72
+ maxChildren: z.number().int().min(1).max(40).default(20),
73
+ childOffset: z.number().int().min(0).max(100_000).default(0),
54
74
  fromLine: z.number().int().min(1).optional(),
55
75
  fromColumn: z.number().int().min(1).optional(),
56
76
  expectedRevision: z
@@ -58,7 +78,39 @@ export const sourceLookupRequestSchema = z
58
78
  .regex(/^[a-f0-9]{64}$/u)
59
79
  .optional(),
60
80
  })
61
- .strict();
81
+ .strict()
82
+ .superRefine((value, context) => {
83
+ const locating = value.path !== undefined || value.line !== undefined;
84
+ if (locating === (value.symbol !== undefined))
85
+ context.addIssue({
86
+ code: 'custom',
87
+ path: ['symbol'],
88
+ message: 'Provide either symbol, or path and line to locate a source position',
89
+ });
90
+ if (locating && (value.path === undefined || value.line === undefined))
91
+ context.addIssue({
92
+ code: 'custom',
93
+ path: ['line'],
94
+ message: 'Locating a source position requires both path and line',
95
+ });
96
+ if (!locating && value.column !== undefined)
97
+ context.addIssue({
98
+ code: 'custom',
99
+ path: ['column'],
100
+ message: 'column applies only with path and line',
101
+ });
102
+ if (locating &&
103
+ (value.keyPath !== undefined ||
104
+ value.view === 'structure' ||
105
+ value.fromLine !== undefined ||
106
+ value.fromColumn !== undefined ||
107
+ value.expectedRevision !== undefined))
108
+ context.addIssue({
109
+ code: 'custom',
110
+ path: ['path'],
111
+ message: 'Navigation options apply to a symbol lookup; locate first, then pass its keyPath',
112
+ });
113
+ });
62
114
  const hash = z.string().regex(/^[a-f0-9]{64}$/u);
63
115
  const line = z.number().int().min(1);
64
116
  const count = z.number().int().min(0);
@@ -67,6 +119,7 @@ const coverage = z
67
119
  files: count,
68
120
  sections: count,
69
121
  unavailable: z.boolean(),
122
+ included: z.boolean(),
70
123
  })
71
124
  .strict();
72
125
  const coverageBySource = z
@@ -84,12 +137,16 @@ export const referenceSectionSchema = z
84
137
  endLine: line,
85
138
  excerpt: z.string().max(300),
86
139
  authority: z.string().max(80),
140
+ /** The cited line that best matches the query; start a bounded read here. */
141
+ matchLine: line.optional(),
87
142
  })
88
143
  .strict();
89
144
  export const referenceSearchDataSchema = z
90
145
  .object({
91
146
  results: z.array(referenceSectionSchema.extend({ score: z.number() })).max(12),
92
147
  total: count,
148
+ omitted: count,
149
+ limitedByBytes: z.boolean(),
93
150
  coverage: coverageBySource,
94
151
  skipped: count,
95
152
  })
@@ -109,6 +166,7 @@ export const referenceContextDataSchema = z
109
166
  surface: referenceSurfaceSchema,
110
167
  sections: z.array(referenceSectionSchema).max(24),
111
168
  omitted: count,
169
+ limitedByBytes: z.boolean(),
112
170
  omittedSources: z.array(z.string().max(256)).max(32),
113
171
  missing: z.array(z.string().max(256)).max(32),
114
172
  coverage: coverageBySource,
@@ -117,15 +175,62 @@ export const referenceContextDataSchema = z
117
175
  .strict();
118
176
  export const sourceLookupDataSchema = z
119
177
  .object({
178
+ sourceScope: z.enum([
179
+ 'workspace_index',
180
+ 'event_definitions',
181
+ 'event_consumers',
182
+ 'helper_definitions',
183
+ 'source_location',
184
+ ]),
120
185
  revision: hash,
121
186
  complete: z.boolean(),
122
187
  skippedSourceCount: count,
123
188
  definitionCount: count,
189
+ definitionsTruncated: z.boolean(),
124
190
  referencesIncluded: z.boolean(),
125
191
  referenceCount: count,
126
192
  referencesTruncated: z.boolean(),
127
193
  referencesComplete: z.boolean(),
128
194
  unresolvedReferenceCount: count,
195
+ /** Indexed identifiers within three edits of a symbol that has no definition. */
196
+ suggestions: z.array(z.string().max(1024)).max(5).optional(),
197
+ location: z
198
+ .object({
199
+ path: z.string().max(4096),
200
+ rootKind: z.string().max(32),
201
+ loadOrder: count,
202
+ shadowed: z.boolean(),
203
+ line,
204
+ column: line,
205
+ text: z.string().max(300),
206
+ owners: z
207
+ .array(z
208
+ .object({
209
+ kind: z.string().max(64),
210
+ id: z.string().max(1024),
211
+ startLine: line,
212
+ endLine: line,
213
+ overridden: z.boolean(),
214
+ })
215
+ .strict())
216
+ .max(8),
217
+ chain: z
218
+ .array(z
219
+ .object({
220
+ key: z.string().max(128).nullable(),
221
+ occurrence: count,
222
+ index: count,
223
+ startLine: line,
224
+ endLine: line,
225
+ })
226
+ .strict())
227
+ .max(24),
228
+ chainTruncated: z.boolean(),
229
+ keyPath: sourceKeyPathSchema.nullable(),
230
+ structureAvailable: z.boolean(),
231
+ })
232
+ .strict()
233
+ .optional(),
129
234
  definitions: z
130
235
  .array(z
131
236
  .object({
@@ -143,6 +248,34 @@ export const sourceLookupDataSchema = z
143
248
  nextLine: line.nullable(),
144
249
  nextColumn: line.nullable(),
145
250
  text: z.string().max(2000),
251
+ navigation: z
252
+ .object({
253
+ keyPath: sourceKeyPathSchema,
254
+ startLine: line,
255
+ startColumn: line,
256
+ endLine: line,
257
+ endColumn: line,
258
+ children: z
259
+ .array(z
260
+ .object({
261
+ key: z.string().max(1024).nullable(),
262
+ index: count,
263
+ keyTruncated: z.boolean(),
264
+ occurrence: count,
265
+ kind: z.enum(['scalar', 'block']),
266
+ value: z.string().max(120).nullable(),
267
+ valueTruncated: z.boolean(),
268
+ startLine: line,
269
+ endLine: line,
270
+ childCount: count,
271
+ })
272
+ .strict())
273
+ .max(40),
274
+ omittedChildren: count,
275
+ nextChildOffset: count.nullable(),
276
+ })
277
+ .strict()
278
+ .optional(),
146
279
  })
147
280
  .strict())
148
281
  .max(3),
@@ -0,0 +1,69 @@
1
+ import { z } from 'zod/v4';
2
+ import { workspaceIdSchema } from './common.js';
3
+ import { referenceSectionSchema } from './reference.js';
4
+ const scope = z.enum([
5
+ 'country',
6
+ 'state',
7
+ 'character',
8
+ 'combatant',
9
+ 'ace',
10
+ 'strategic_region',
11
+ 'operation',
12
+ 'industrial_org',
13
+ 'purchase_contract',
14
+ 'raid_instance',
15
+ 'special_project',
16
+ 'faction',
17
+ ]);
18
+ export const scriptValidateRequestSchema = z
19
+ .object({
20
+ workspaceId: workspaceIdSchema,
21
+ source: z.string().min(1).max(64_000),
22
+ kind: z.enum(['effect', 'trigger']),
23
+ scope,
24
+ documentation: z.enum(['game_doc', 'script_doc']).default('game_doc'),
25
+ bindings: z
26
+ .array(z.object({ name: z.string().min(1).max(256), scope }).strict())
27
+ .max(16)
28
+ .default([]),
29
+ limit: z.number().int().min(1).max(32).default(12),
30
+ })
31
+ .strict();
32
+ export const scriptValidateDataSchema = z
33
+ .object({
34
+ valid: z.boolean().nullable(),
35
+ checksPerformed: z.array(z.enum(['syntax', 'command_kind', 'declared_scope'])),
36
+ parametersChecked: z.literal(false),
37
+ documentation: z
38
+ .object({
39
+ source: z.enum(['game_doc', 'script_doc']),
40
+ revision: z.string().regex(/^[a-f0-9]{64}$/u),
41
+ effectCount: z.number().int().min(0),
42
+ triggerCount: z.number().int().min(0),
43
+ skippedSources: z.number().int().min(0),
44
+ })
45
+ .strict(),
46
+ findings: z
47
+ .array(z
48
+ .object({
49
+ code: z.string().max(80),
50
+ status: z.enum(['supported', 'error', 'unresolved']),
51
+ command: z.string().max(256),
52
+ kind: z.enum(['effect', 'trigger']),
53
+ scope: z.string().max(64),
54
+ line: z.number().int().min(1),
55
+ column: z.number().int().min(1),
56
+ message: z.string().max(500),
57
+ suggestions: z.array(z.string().max(256)).max(3),
58
+ reference: referenceSectionSchema.optional(),
59
+ })
60
+ .strict())
61
+ .max(32),
62
+ totalFindings: z.number().int().min(0),
63
+ omittedFindings: z.number().int().min(0),
64
+ checkedCommands: z.number().int().min(0),
65
+ unresolvedCount: z.number().int().min(0),
66
+ truncated: z.boolean(),
67
+ argumentBlocksUnchecked: z.number().int().min(0),
68
+ })
69
+ .strict();
@@ -34,7 +34,7 @@ const transactionOperationSchema = z
34
34
  .strict();
35
35
  const transactionReadDependencySchema = z
36
36
  .object({
37
- rootKind: z.enum(['mod', 'game', 'dependency', 'artifact', 'cache', 'fixture']),
37
+ rootKind: z.enum(['mod', 'game', 'dlc', 'dependency', 'artifact', 'cache', 'fixture']),
38
38
  loadOrder: z.number().int().min(0),
39
39
  relativePath: workspaceRelativePathSchema,
40
40
  sha256: sha256Schema,
@@ -117,7 +117,7 @@ async function scanTechnologySources(engine, workspaceId, workspace, principal,
117
117
  const scans = await Promise.all([
118
118
  engine.scan(workspaceId, { patterns, rootKinds: ['mod', 'dependency', 'fixture'] }, principal, signal),
119
119
  ...(workspace.roots.some(({ kind }) => kind === 'game')
120
- ? [engine.scan(workspaceId, { patterns, rootKinds: ['game'] }, principal, signal)]
120
+ ? [engine.scan(workspaceId, { patterns, rootKinds: ['game', 'dlc'] }, principal, signal)]
121
121
  : []),
122
122
  ]);
123
123
  const files = scans.flatMap(({ files }) => files.map((file) => ({ ...file })));
@@ -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 = '3.6.0';
3
+ export const PACKAGE_VERSION = '3.8.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/README.md CHANGED
@@ -20,6 +20,7 @@ You can ask your coding agent for these tasks in ordinary language; you do not n
20
20
  ## See the results
21
21
 
22
22
  - [Examples](examples.md): practical prompts with real renders.
23
+ - [Latest release](https://github.com/klimPaskov/hoi4-agent-tools/releases/latest): a short introduction to the published server.
23
24
  - [Focus and technology comparisons](visual-comparisons.md): supplied game captures beside source renders.
24
25
  - [Interface comparisons](gui-comparisons.md): vanilla and Chaos Redux windows.
25
26
 
@@ -42,3 +42,9 @@ Add `HOI4_DEPENDENCY_ROOTS` as the platform path-delimited list when the externa
42
42
  The installed-game qualification also parses every shipped bitmap-font descriptor, resolves every atlas page, shapes native and enlarged glyphs, verifies deterministic raster output, and checks HOI4 localisation colours across base fonts and language overrides.
43
43
 
44
44
  See the package [Security Policy](../SECURITY.md) for private vulnerability reporting.
45
+
46
+ ## Dependencies
47
+
48
+ `npm-shrinkwrap.json` is published and fixes the complete production dependency tree for every consumer, who no longer receive newer transitive releases on their own.
49
+ Run `npm audit --omit=dev` and `npm audit signatures` before each release, and publish a new version with a refreshed shrinkwrap when a production advisory is fixed upstream.
50
+ Keep `overrides` inside every dependent's declared range; consumers do not apply this package's overrides, and `tests/package/metadata.test.ts` checks each production dependency edge in the shrinkwrap against its range.
package/docs/examples.md CHANGED
@@ -14,21 +14,11 @@ No wiki download or game launch was needed.
14
14
 
15
15
  Use [Local references](reference.md) to configure an offline wiki snapshot or narrow a search.
16
16
 
17
- ## Check alternative focus prerequisites
18
-
19
- > Find Fury's focus tree. Render the branch leading to “The Last Neighbor Has Fallen” and show whether both preceding focuses are required or either one is enough.
20
-
21
- ![Fury branch with dotted alternative prerequisites](images/comparisons/fury-or-mcp.png)
22
-
23
- Dotted connectors mark alternatives within one prerequisite group; solid connectors mark separate required prerequisites.
24
- The agent can link each finding to the source block before you change the route.
25
- The [in-game comparison](visual-comparisons.md#focus-trees) shows the same branch and explains the presentation differences.
26
-
27
17
  ## Review a large tree
28
18
 
29
- > Render the Utopia Manifesto focus tree. Check its branch connections, alternative prerequisites, missing references and crowded areas before suggesting edits.
19
+ > Render the mod's largest branching focus tree. Check its branch connections, alternative prerequisites, missing references and crowded areas before suggesting edits.
30
20
 
31
- [![Utopia Manifesto focus tree](images/comparisons/utopia-mcp.png)](images/comparisons/utopia-mcp.png)
21
+ [![Branching focus tree preview](images/comparisons/utopia-mcp.png)](images/comparisons/utopia-mcp.png)
32
22
 
33
23
  Open the image at full size for the complete layout.
34
24
  This example contains 124 focuses in the recorded source version.
@@ -41,7 +31,7 @@ The [focus guide](focus.md) covers inspecting, rendering and reorganizing trees.
41
31
  [![Infantry technology folder](images/comparisons/infantry-mcp.png)](images/comparisons/infantry-mcp.png)
42
32
 
43
33
  The preview follows the loaded technology and GUI definitions, including wide equipment cards.
44
- The [technology comparisons](visual-comparisons.md#technology-folders) also show chemical and biological folders and explain why different source versions can produce different layouts.
34
+ The [technology comparisons](visual-comparisons.md#technology-folders) also show a mod folder with wide equipment cards rendered from a matching recorded source revision.
45
35
  See the [technology guide](technology.md) for grants, bonuses and doctrine paths.
46
36
 
47
37
  ## Inspect an interface before editing it
package/docs/gui.md CHANGED
@@ -14,22 +14,8 @@ These are source-model results, not proof of native click execution.
14
14
 
15
15
  ## Preview immediate tooltip text
16
16
 
17
- An explicit tooltip request uses a visible element's `pdx_tooltip` localisation key:
17
+ An explicit tooltip request uses a visible element's `pdx_tooltip` localisation key. Supply `tooltip.target` with its inspected instance ID or a unique element name, plus viewport `x`, `y`, `width` and `padding` values.
18
18
 
19
- ```json
20
- {
21
- "id": "close-tooltip",
22
- "tooltip": {
23
- "target": "close_button",
24
- "x": 980,
25
- "y": 100,
26
- "width": 160,
27
- "padding": 16
28
- }
29
- }
30
- ```
31
-
32
- The target can be an instance ID from inspection or a unique element name.
33
19
  Position, width and padding are declared viewport pixels; height follows the measured text.
34
20
  The preview uses the loaded `ToolTip` sprite and `cg_16b` font.
35
21
  Missing or ambiguous targets, hidden or unresolved visibility, missing text/assets, and bounds errors produce diagnostics without a guessed popup.
package/docs/map.md CHANGED
@@ -18,6 +18,16 @@ Pass `tile: { x, y, width, height }` to `hoi4.map_render` for a reusable bounded
18
18
 
19
19
  Pass `area: { provinceIds, stateIds, regionIds, padding }` to render the exact bounding area of those map entities with optional pixel padding. The result reports the resolved tile coordinates. An unknown ID or an area larger than 2,048 pixels in either direction is explicit, so a narrower selection can be requested.
20
20
 
21
+ ## Catalog consistency
22
+
23
+ Validation reads the active `common/terrain/*.txt` categories and `map/continent.txt` (or the continent file `default.map` names) along with the map files.
24
+ A province whose terrain no category defines reports `MAP_TERRAIN_UNKNOWN`, and a land province with a continent number beyond the continents listed reports `MAP_CONTINENT_UNKNOWN`; continent IDs follow their order in the file.
25
+ Seas and lakes must use a category with `is_water = yes` (`MAP_SEA_TERRAIN_INVALID`, `MAP_LAKE_TERRAIN_INVALID`), land provinces must not (`MAP_LAND_TERRAIN_WATER`), and a strategic region's `naval_terrain` must name a category with `naval_terrain = yes` (`MAP_REGION_NAVAL_TERRAIN_INVALID`).
26
+ When no terrain category file is available, seas and lakes are checked against the base game's `ocean` and `lakes` names instead, and unknown terrain is not reported.
27
+
28
+ The engine stops reading `adjacencies.csv` at its first `-1` row.
29
+ Rows after it are not read and report `MAP_ADJACENCY_AFTER_TERMINATOR`; a file without that row reports `MAP_ADJACENCY_TERMINATOR_MISSING` as a warning, because the offline wiki describes it as required although at least one published mod omits it.
30
+
21
31
  ## Create states
22
32
 
23
33
  The compact `create_state` form needs selected provinces and a display name:
package/docs/reference.md CHANGED
@@ -10,14 +10,57 @@ The server discovers these directories for a workspace:
10
10
  | Offline wiki | `modRoot/paradox_wiki/**/*.md` | Set absolute `wikiRoot`. |
11
11
  | Generated script documentation | `modRoot/script_docs/**/*.md` and `**/*.log` | Set absolute `scriptDocsRoot` to a user-supplied dump. |
12
12
 
13
- Only Markdown headings and recognizable standalone names in generated `.log` files form sections. A dump that yields one section should be treated as insufficiently parsed; inspect its source format before relying on it. Source bytes are hashed on each request, while unchanged section indexes are reused. Missing roots are reported as unavailable. The public package contains none of these local source files.
13
+ Markdown headings and recognizable standalone names in generated `.log` files form sections. A Markdown table row whose first cell is an identifier, such as a wiki effect, trigger, modifier, define, or on action, is also its own one-line section, so a citation can name the exact entry inside a long table. A dump that yields one section should be treated as insufficiently parsed; inspect its source format before relying on it. Source bytes are hashed on each request, while unchanged section indexes are reused. Missing roots are reported as unavailable. The public package contains none of these local source files.
14
14
 
15
15
  ## Tools
16
16
 
17
17
  - `hoi4.reference_context`: request a surface such as `event`, `focus`, `technology`, `gui`, or `map`. It reserves citations for available installed documentation and offline wiki sources before adding further question matches. Supply `question` to select the best matching section within each required source. At a limit of two or more, both source kinds are represented when available; a one-result limit prioritizes installed documentation. `omittedSources` names required sources excluded by the result limit, while `missing` names sources absent from the configured roots. Increase the limit or search an omitted source directly before treating the bundle as sufficient. Citations are pointers and excerpts; read the pertinent sections before changing code.
18
- - `hoi4.reference_search`: search page names, headings, exact script tokens, and matching body lines. The default limit is six results and the maximum is twelve. Filter `sources` to `game_doc`, `wiki`, or `script_doc` when appropriate.
19
- - `hoi4.reference_read`: provide a search/context result's `id` and `revision`. Read up to 80 lines and 8,000 UTF-8 bytes. Continue with the returned `nextLine`, `nextColumn` as `startColumn`, and the same revision. Long source lines continue without losing characters. A changed source returns `REFERENCE_REVISION_STALE`.
18
+ - `hoi4.reference_search`: search with plain words or exact script tokens. Ranking weighs headings above body text and rare words above common ones, and it matches identifiers both whole and by their words, so "check if a state is controlled by a country" can find `is_controlled_by`. A query that names an identifier only matches sections containing it, an exact heading ranks first, and installed documentation precedes the wiki when they rank equally. Each result's `matchLine` is the cited line that best matches the query; pass it as `startLine` to `hoi4.reference_read` to land on the answer inside a long section. When several sources document the same name, the first copy keeps its place and later copies follow other distinct answers. The default limit is six results and the maximum is twelve. Filter `sources` to `game_doc`, `wiki`, or `script_doc` when appropriate. Only selected authorities are scanned; `coverage.included = false` means that an authority was excluded from this query, not that it is missing.
19
+ - `hoi4.reference_read`: provide a search/context result's `id` and `revision`. Optionally pass its `source` for a focused authority read; resident citations provide this hint automatically. Read up to 80 lines and 8,000 UTF-8 bytes, with JSON escaping also bounded. Continue with the returned `nextLine`, `nextColumn` as `startColumn`, and the same revision. Long source lines continue without losing characters. A changed source returns `REFERENCE_REVISION_STALE`.
20
20
  - `hoi4.source_lookup`: find exact definitions and indexed usages in the installed game and configured mod source. The response includes load order, override status, up to three source blocks bounded to 2,000 bytes each, a scan revision, and explicit reference counts and truncation. Follow `nextLine` and `nextColumn` as `fromLine` and `fromColumn`, with `expectedRevision`, to read more of the active definition.
21
+ - When `hoi4.source_lookup` finds no definition, `suggestions` lists up to five indexed identifiers within three edits of the requested symbol, nearest first, which catches typos and near-miss names.
22
+ - `hoi4.source_lookup` with `path` and `line` instead of `symbol` locates a source position, such as a diagnostic or a changed line in a diff. `path` may be a result path such as `mod:events/example.txt`, a path relative to a source root, or an absolute path inside one. The result names the file's layer and whether a later layer shadows it, the indexed definitions that contain the line, innermost first, and the chain of enclosing assignments with zero-based occurrences for repeated keys. Its `keyPath` starts inside the innermost definition: pass that definition's `id` as `symbol`, its `kind`, and the `keyPath` to read or list that exact block. Without `column`, the first non-blank character of the line is used. A file with parse errors returns its definitions but no structural chain. Only the requested file's folder is scanned, in every layer.
23
+ - `hoi4.script_validate`: check a bounded effect or trigger body against installed native-command documentation. Supply the body in `source`, its `kind`, and its current `scope`. Results cover syntax, command kind, and declared scope, with exact documentation citations. They do not validate command parameters or prove game execution.
24
+
25
+ ## Navigate a definition
26
+
27
+ Use `view: "structure"` to list the immediate children of the active definition, then pass `keyPath` to select a nested block or scalar assignment.
28
+ Keyed path steps have a `key` and an optional zero-based `occurrence`; repeated keys require an explicit occurrence.
29
+ Use an `{ "index": 1 }` step to select an unkeyed array entry or a child with an unusually long key.
30
+ For example, `[{"key":"option","occurrence":1},{"key":"hidden_effect"}]` selects the second option's hidden effects.
31
+ Navigation reads exact parser boundaries, including when several assignments share a line.
32
+ If an identifier has multiple symbol kinds, supply `kind` before navigating it.
33
+ Override counts remain visible, while navigation selects the active definition.
34
+
35
+ Structure replies have at most 40 children.
36
+ They also have a byte ceiling, with `keyTruncated` and `valueTruncated` identifying shortened previews.
37
+ Continue with `navigation.nextChildOffset` as `childOffset`, retaining the same `keyPath`, query settings, and `expectedRevision`.
38
+ Source replies continue with `nextLine` and `nextColumn` as before.
39
+ Use `includeReferences: false` with an exact `event`, `scripted_effect`, or `scripted_trigger` kind when only the definition is needed; this limits scanning to that source family.
40
+ Enable references when looking for consumers across the workspace.
41
+ Event-consumer queries scan the source families supported by event reference analysis rather than unrelated presentation files.
42
+ `sourceScope` labels the scanned surface, and `definitionsTruncated` and reference omission fields identify bounded output.
43
+ Revisions describe the selected scan, so keep the scan settings unchanged during continuation.
44
+
45
+ ## Check native commands
46
+
47
+ `hoi4.script_validate` accepts a standalone effect or trigger body rather than an entire event or focus definition.
48
+ It recognizes documented control wrappers, conditional limits, country/state iterators, fixed country/state scopes, and the `ROOT`, `THIS`, and `PREV` traversal context.
49
+ Use `bindings` to declare external scope names such as `event_target:recipient` when their type is known.
50
+ Those bindings are supplied facts, not observed game state.
51
+
52
+ The default documentation authority is `game_doc`.
53
+ Use `documentation: "script_doc"` explicitly for a configured generated dump; the checker does not silently mix authorities or substitute wiki text for native signatures.
54
+ It reads supported scopes from `effects_documentation.md`, `triggers_documentation.md`, and corresponding `effect_docs.log`/`trigger_docs.log` files.
55
+
56
+ `valid: false` identifies a syntax, kind, or scope error.
57
+ `valid: null` identifies missing or conflicting documentation, an unresolved command/helper, an unchecked native argument block, or exhausted coverage.
58
+ `valid: true` applies only to `checksPerformed`; `parametersChecked` remains false.
59
+ Inspect the cited parameter syntax and use the relevant domain tools for broader validation.
60
+ Replies prioritize problems, bound `findings`, and report `omittedFindings`; execution is bounded to 256 visited entries and 64,000 UTF-8 source bytes.
61
+ Search and context replies also report `limitedByBytes` when metadata exceeds their reply budget.
62
+ Read omitted required sources with targeted calls rather than repeatedly requesting the same oversized bundle.
63
+ Reference text supports UTF-8, Windows-1252, and explicitly BOM-marked UTF-16; native engine source encoding rules remain separate.
21
64
 
22
65
  Example:
23
66
 
@@ -29,4 +72,4 @@ Call `hoi4.reference_context` with that input, then search `save_event_target_as
29
72
 
30
73
  The local index admits at most 512 files per source kind, 2 MB per file, 32 MB in total, and 20,000 sections. Search and read responses are bounded by the MCP result budget. `skipped` and per-source coverage expose incomplete ingestion instead of implying that an omitted source was checked. Registered external reference roots are operator configuration and remain private to the authorized workspace.
31
74
 
32
- See [the measured Chaos Redux reference retrieval](research/reference-benchmark.md) for actual source coverage and response sizes.
75
+ See [the measured Chaos Redux reference retrieval](research/reference-benchmark.md) for actual source coverage, held-out discovery results, and response sizes. Plain-word questions still miss when the documentation uses different vocabulary, for example "national spirit" for `ideas`; search the script name directly when it is known.
@@ -27,6 +27,45 @@ The retrieval benchmark used the local service directly. A separate real-workspa
27
27
 
28
28
  The four additional public tool definitions add approximately 6.5 KB to the one-time MCP tool discovery response. The complete list is about 72 KB in the discovery fixture and is tested against a 72 KiB ceiling. The context reduction above applies to reference retrieval after discovery.
29
29
 
30
+ ## Held-out discovery questions
31
+
32
+ Measured on 2026-10-04 against the same local Chaos Redux wiki snapshot and installed documentation.
33
+ The nine questions above were assembled alongside the previous ranking, so they cannot show whether a ranking change generalizes.
34
+ Two further sets in `scripts/reference-discovery-cases.ts` phrase tasks the way an agent asks them, mostly without the script name.
35
+ Every expected answer was checked by hand in the local files, and a case may list several acceptable sections.
36
+ A result answers a question when its cited section contains an expected answer line; it lands when its cited line is within eight lines of that answer.
37
+
38
+ The discovery set has 32 plain-word questions, five exact identifiers, and two unrelated negative queries.
39
+ The validation set has 24 plain-word questions written before any ranking adjustment and was never used to tune it.
40
+
41
+ | Set and ranking | Answer first | Within three | Within five | Lands within three | Mean reciprocal rank |
42
+ | ------------------------------ | -----------: | -----------: | ----------: | -----------------: | -------------------: |
43
+ | Discovery, previous ranking | 7/37 | 19/37 | 22/37 | 18/37 | 0.342 |
44
+ | Discovery, field-weighted BM25 | 19/37 | 28/37 | 29/37 | 26/37 | 0.628 |
45
+ | Validation, previous ranking | 4/24 | 7/24 | 12/24 | 7/24 | 0.272 |
46
+ | Validation, field-weighted | 11/24 | 16/24 | 20/24 | 16/24 | 0.588 |
47
+
48
+ The previous ranking placed four of the five exact identifiers first and did not return the `on_startup` wiki entry at all; the current one places each first, with installed documentation ahead of the wiki.
49
+ Both returned no results for the two negative queries.
50
+ On the original nine questions, all nine stay within five results, three rank first rather than five, and one moves from fourth to fifth.
51
+
52
+ In three repeated runs, the first query after a source change took 370–386 ms to index about twelve thousand sections, compared with 147–159 ms with the previous ranking.
53
+ Later queries took a median of 37 ms, compared with 49–63 ms, and the median ranked reply was about 3.7 KB in both.
54
+
55
+ Twelve plain-word questions across both sets still miss the top five.
56
+ Most use vocabulary the documentation does not: "national spirit" where script says `ideas`, "give a state" for `transfer_state`, "repeat effects" for `for_loop_effect`, and "build a factory" for `add_building_construction`.
57
+ Search the script name when it is known; the ranking does not add synonyms for individual questions.
58
+ These are 61 task questions over one documentation snapshot, not a general precision guarantee.
59
+
60
+ Run both sets with:
61
+
62
+ ```bash
63
+ npx tsx scripts/evaluate-reference-discovery.ts MOD_ROOT GAME_ROOT
64
+ npx tsx scripts/evaluate-reference-discovery.ts MOD_ROOT GAME_ROOT --validation
65
+ ```
66
+
67
+ The script exits with a failure when a case has no expected answer in the supplied sources.
68
+
30
69
  ## Compact context budgets
31
70
 
32
71
  A 2026-09-27 check of the installed 3.5.0 server found that eight GUI results could contain only wiki sections even though the installed GUI documentation was available. The 3.5.1 source qualifies limits of one, two, and eight citations across the same six work surfaces. All eighteen cases retained installed documentation; limits of two or more also retained wiki citations. Required sources excluded by the limit appeared in `omittedSources`, separately from files absent under `missing`. The nine search queries still found all expected answer-bearing sections.
@@ -40,3 +79,44 @@ For the GUI question “Where are scripted GUI click effects and triggers define
40
79
  | 8 | Installed documentation and wiki | 0 | 5,926 | 339 |
41
80
 
42
81
  The default six-surface context bundles used 3,545–5,556 bytes including omission metadata. A smaller response is not sufficient evidence when `omittedSources` is nonempty: increase the limit or search and read those sources directly. The evaluation script fails when a compact budget drops a required source kind or silently omits its required-source pointers.
82
+
83
+ ## Source navigation and scan measurements
84
+
85
+ On 2026-10-03, `scripts/evaluate-source-tools.ts --game-only` selected the installed `political.1` definition, listed its options, read exactly `add_political_power = -100` from its first option, and checked that command against installed documentation.
86
+ The country-scope check succeeded and the state-scope check reported the documented mismatch.
87
+ The evaluation retained source and documentation revisions and compared repeated reference results for equality.
88
+
89
+ | Operation | Initial cache-enabled run | Scoped-cache run | Reply bytes |
90
+ | ------------------------------ | ------------------------: | ---------------: | ----------: |
91
+ | Cold event structure | 55,550 ms | 24,723 ms | 2,258 |
92
+ | Repeated nested read | 20,936 ms | 3,565 ms | 862 |
93
+ | Installed command check | 1,471 ms | 10,804 ms | 1,152 |
94
+ | Event-consumer lookup | Timed out at 180,002 ms | 145,532 ms | 1,363 |
95
+ | Repeated event-consumer lookup | Not completed in that run | 16,233 ms | 1,363 |
96
+
97
+ A separate run with persistent caching disabled completed the broader lookup in 102,437 ms and its repeated lookup in 122,023 ms.
98
+ This helped identify filesystem and cache costs; it is not a causal comparison under identical machine load.
99
+ The candidate bounds optional cache writes, skips resident hydration, overlaps source verification, and scopes event queries to the analyser's supported owner sources.
100
+ Its persistent cache recorded 32 writes across two bounded batches, with no invalid entries or evictions.
101
+ The process recorded 616–625 MiB resident memory during the consumer lookups; these are sampled values, not peak-memory guarantees.
102
+
103
+ Other work was running on the host, and latency varied materially.
104
+ The command-document check was slower in the scoped-cache run, so these measurements must not be presented as an improvement for every operation or as a latency guarantee.
105
+ The selected source index was complete, while reference completeness remained false because 2,379 dynamic references were unresolved.
106
+ Static retrieval does not resolve those engine-dependent dispatches or establish runtime behavior.
107
+
108
+ These source timings preceded the subsequent batched reference reader and glob-library replacement.
109
+ A separate 2026-10-03 reference evaluation found all nine expected pages and answer-bearing sections and passed all eighteen compact-context cases, with no missing required sources or budget failures.
110
+ Its six context replies used 3,616–5,627 JSON bytes.
111
+ The candidate's complete MCP discovery list contains 35 tools and is tested against a 75 KiB ceiling.
112
+
113
+ ## Worker startup
114
+
115
+ Five isolated readiness handshakes on 2026-10-03 averaged 2,163 ms with eager imports and 1,408 ms with authenticated domain loading.
116
+ The individual samples were 2,056/2,570/2,349/1,857/1,985 ms and 3,213/930/995/960/942 ms respectively.
117
+ The first changed-worker sample includes a cold TypeScript loader cache.
118
+ These samples measure the readiness handshake without running a job, and do not establish complete request latency or throughput.
119
+ Concurrent HTTP qualification keeps its original four-minute total request ceiling.
120
+ Two earlier 64-request runs exceeded that ceiling for 23 and five requests respectively.
121
+ The isolated run after domain loading passed all 64 requests in a 124.49-second test, including fixture setup and cleanup.
122
+ Shared-host load varied between runs, so these are qualification receipts rather than a controlled throughput comparison.
package/docs/setup.md CHANGED
@@ -9,6 +9,9 @@ Keep an installed copy of Hearts of Iron IV available for vanilla references, ar
9
9
  npm install --global hoi4-agent-tools
10
10
  ```
11
11
 
12
+ The package publishes an `npm-shrinkwrap.json`, so every registry or `npx` install receives the same reviewed dependency versions.
13
+ npm installs the image library's prebuilt binaries for every supported platform from it, about 280 MiB in total; the server loads only the one for your system.
14
+
12
15
  Source inspection can work without an installed game.
13
16
  Previews need the assets and fonts referenced by the mod; missing files are reported in the result.
14
17
 
@@ -53,6 +56,18 @@ For a custom configuration location, add `--config PATH` to both setup commands.
53
56
  `--workspace-storage-root PATH` chooses where generated indexes and images are saved.
54
57
  Linked mod directories are not followed; configure their real parent directory instead.
55
58
 
59
+ ### How sources are layered
60
+
61
+ Sources load in the engine's order: the base game, then each installed folder under `dlc/` and `integrated_dlc/` in order of its internal DLC ID, then dependencies, then the mod.
62
+ A later layer replaces a file with the same path from an earlier one, so a DLC's version of an interface file is the active one and the base game's copy is reported as shadowed.
63
+ Results name a DLC layer by its folder, for example `dlc023_man_the_guns:interface/mtg_leaders.gfx`.
64
+ Only folders that contain a `dlcNNN.dlc` descriptor and resolve inside the game root are layers; archives inside a DLC folder are not unpacked.
65
+ An installed DLC folder is not proof that the DLC is enabled in a particular playset; check `has_dlc` where content depends on it.
66
+ Set `"includeGameDlc": false` on a configured workspace to analyze the base game alone.
67
+
68
+ `replace_path` entries in a mod's or dependency's `descriptor.mod` unload earlier-loaded files under that folder, as the engine does, and join any `replacePaths` configured for the workspace.
69
+ Entries that are not plain relative folders, such as `../outside` or wildcard paths, are ignored.
70
+
56
71
  ## Add local reference material
57
72
 
58
73
  Place an offline wiki snapshot in `paradox_wiki/` inside the mod.