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
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.8.0 - 2026-10-04
6
+
7
+ - Rank documentation with field-weighted BM25 over identifier-aware terms, cite identifier table rows as exact one-line sections, return each result's best-matching `matchLine`, and split sections by CommonMark fences.
8
+ - Locate a source position with `hoi4.source_lookup` `path` and `line`: return its layer, the containing definitions, the enclosing assignment chain, and a `keyPath` that navigates back to it.
9
+ - Suggest indexed identifiers within three edits when `hoi4.source_lookup` finds no definition.
10
+ - Layer installed `dlc/` and `integrated_dlc/` folders between the base game and mods in internal-ID order, so source lookups, renders, and analyses find DLC sprites, interfaces, and portraits; `includeGameDlc: false` opts out.
11
+ - Apply `replace_path` entries from mod and dependency descriptors in addition to configured replace paths.
12
+ - Check province terrain and continents against the active `common/terrain` categories and `continent.txt`, accept any water terrain for seas and lakes, check strategic-region `naval_terrain`, and report adjacency rows after the `-1` terminator or a missing terminator.
13
+ - Search unresolved GUI asset names in grouped wildcard patterns, so windows with hundreds of missing textures render within the wildcard-pattern ceiling.
14
+ - Retry transient Windows sharing violations on job records and capacity leases, release record handles before parsing, bound delete-pending capacity slots by elapsed time, and report worker coordination failures with a fixed stage and allowlisted native code.
15
+ - Keep every production dependency inside its declared range in the published shrinkwrap, so `npm ls` succeeds in consumer installs; the `@hono/node-server` override is no longer needed.
16
+
17
+ ## 3.7.0 - 2026-10-04
18
+
19
+ - Publish an `npm-shrinkwrap.json` so every install reproduces the reviewed dependency closure, including all platforms' optional Sharp binaries (about 300 MB installed).
20
+ - Navigate exact nested source assignments and page through bounded child structure, including repeated keys and same-line blocks.
21
+ - Limit definition-only event/helper lookups to their source family and reuse semantic reference analysis for unchanged snapshots.
22
+ - Check snippet syntax, native command kinds, and declared scopes against cited installed or explicitly selected generated documentation, with unresolved coverage reported separately.
23
+ - Verify source files in bounded parallel batches, reject changing or replaced paths, and bound optional cache persistence without truncating analysis.
24
+ - Load isolated workers' domain modules after authenticating the job, preserving exact operation admission and recorded write policies.
25
+ - Filter reference scans by authority, reuse citation hints, decode supported reference encodings, and preserve byte-bounded continuations and explicit omission counts.
26
+ - Replace the unpatched globbing dependency chain and update vulnerable transitive pins, preserving relative-path, symlink, literal-filename and pattern-complexity checks.
27
+
5
28
  ## 3.6.0 - 2026-09-27
6
29
 
7
30
  - Preview immediate tooltip text from source localisation with declared position, width and padding, using the source tooltip sprite and font.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # HOI4 Agent Tools
2
2
 
3
- Give your coding agent tools for Hearts of Iron IV modding.
4
- Read mod files, find the right documentation, preview trees and interfaces, and make focused changes to focus trees, GUIs and maps.
3
+ Give your coding agent one MCP server for Hearts of Iron IV modding.
4
+ It can find local documentation and vanilla examples, inspect a mod, render its trees and interfaces, compare source behavior, and make focused edits to focus trees, GUIs and maps.
5
+ The pictures below use real Chaos Redux or installed HOI4 files; click any image for the full-size version.
5
6
 
6
7
  ## Get started
7
8
 
@@ -13,40 +14,135 @@ hoi4-agent-tools-setup --print-client-config
13
14
  ```
14
15
 
15
16
  Add the printed configuration to your MCP client and open your mod as its workspace.
17
+ The install is about 280 MiB, because npm fetches the image library's binaries for every platform to keep each install identical.
16
18
  Keep an installed copy of HOI4 available for vanilla examples, artwork and fonts.
17
19
  The [setup guide](docs/setup.md) covers connecting your agent and choosing game or mod paths.
18
20
 
19
- Then try:
21
+ Ask your agent to find a focus tree and render it, locate the installed documentation for a script command, or preview a scripted GUI at different UI scales.
22
+ The examples below show the results.
20
23
 
21
- > Render this mod's focus trees and show which prerequisites are alternatives.
24
+ ## Examples
22
25
 
23
- > Find the installed documentation for `save_event_target_as`, read the relevant section, and show a vanilla example.
26
+ [Focus trees](#focus-trees-and-alternative-prerequisites) · [Technology folders](#technology-folders-and-card-artwork) · [Interfaces](#scripted-interfaces) · [Maps and events](#maps-and-event-chains) · [Local rules and AI](#local-rules-and-ai-weights) · [All tools](#complete-tool-list)
24
27
 
25
- > Preview this scripted GUI at 100% and 125% UI scale and check for clipped text or overlapping controls.
28
+ ### Focus trees and alternative prerequisites
26
29
 
27
- ## What you can do
30
+ | Focus tree | Supplied in-game capture | MCP source preview |
31
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
32
+ | 52 focuses | [![Medium focus tree in game](docs/images/comparisons/fury-ingame.png)](docs/images/comparisons/fury-ingame.png) | [![Medium focus tree preview](docs/images/comparisons/fury-mcp.png)](docs/images/comparisons/fury-mcp.png) |
33
+ | 111 focuses | [![Large focus tree in game](docs/images/comparisons/holy-realm-ingame.png)](docs/images/comparisons/holy-realm-ingame.png) | [![Large focus tree preview](docs/images/comparisons/holy-realm-mcp.png)](docs/images/comparisons/holy-realm-mcp.png) |
34
+ | 124 focuses | [![Branching focus tree in game](docs/images/comparisons/utopia-ingame.png)](docs/images/comparisons/utopia-ingame.png) | [![Branching focus tree preview](docs/images/comparisons/utopia-mcp.png)](docs/images/comparisons/utopia-mcp.png) |
28
35
 
29
- | Task | Guide |
30
- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
31
- | Find the right wiki section, game documentation or source definition | [Local references](docs/reference.md) |
32
- | Inspect, preview, create or reorganize a focus tree | [Focus trees](docs/focus.md) |
33
- | Trace event chains, decisions, costs and dependencies | [Events](docs/events.md) · [Decisions](docs/analysis.md) |
34
- | Preview technology and doctrine trees | [Technology trees](docs/technology.md) |
35
- | Inspect and repair scripted interfaces | [Scripted GUIs](docs/gui.md) |
36
- | Find states and provinces, preview maps and edit connected map data | [Maps](docs/map.md) |
37
- | Compare AI weights and test declared mechanic scenarios | [AI analysis](docs/probability.md) · [Mechanic tests](docs/mechanics.md) |
38
-
39
- ## Real examples
40
-
41
- These previews were rendered from Chaos Redux and installed HOI4 files.
42
- Click an image to open it at full size.
43
-
44
- | Fury focus tree | Vanilla Options interface |
45
- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
46
- | [![Fury focus tree](docs/images/comparisons/fury-mcp.png)](docs/images/comparisons/fury-mcp.png) | [![Options interface](docs/images/comparisons/gui/options-video-mcp.png)](docs/images/comparisons/gui/options-video-mcp.png) |
47
-
48
- Follow the [examples guide](docs/examples.md) for useful prompts and expected results.
49
- See [focus and technology comparisons](docs/visual-comparisons.md) and [interface comparisons](docs/gui-comparisons.md) for supplied in-game captures beside the renders.
36
+ Dotted connectors in the MCP preview mark focuses where either preceding focus satisfies one prerequisite group.
37
+ The tree previews show route structure; the game's focus screen has different framing, labels and icons.
38
+ The [focus guide](docs/focus.md) explains inspection, rendering and edits, and the [comparison details](docs/visual-comparisons.md#focus-trees) retain source revisions.
39
+
40
+ ### Technology folders and card artwork
41
+
42
+ > Render a technology folder. Check card sizes, artwork, links and missing placements.
43
+
44
+ | Folder | Supplied in-game capture | MCP source preview |
45
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | Vanilla infantry | [![Infantry folder in game](docs/images/comparisons/infantry-ingame.png)](docs/images/comparisons/infantry-ingame.png) | [![Infantry technology folder](docs/images/comparisons/infantry-mcp.png)](docs/images/comparisons/infantry-mcp.png) |
47
+ | Mod wide cards | [![Mod technology folder in game](docs/images/comparisons/chemical-recent-ingame.png)](docs/images/comparisons/chemical-recent-ingame.png) | [![Mod technology folder preview](docs/images/comparisons/chemical-historical-mcp.png)](docs/images/comparisons/chemical-historical-mcp.png) |
48
+
49
+ The mod folder preview uses the source revision recorded with its screenshot and includes wide equipment cards.
50
+ Armored-car artwork that installed DLC folders provide appears in the infantry preview.
51
+ The [technology comparison](docs/visual-comparisons.md#technology-folders) records its source revision, and the [technology guide](docs/technology.md) covers inspection and doctrine paths.
52
+
53
+ ### Scripted interfaces
54
+
55
+ > Preview an interface with its game assets and fonts. Check states, text clipping and clickable areas at 100% and 125% UI scale.
56
+
57
+ | Interface | Supplied in-game capture | MCP source preview |
58
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
59
+ | Vanilla video options | [![Options in game](docs/images/comparisons/gui/options-video-ingame.png)](docs/images/comparisons/gui/options-video-ingame.png) | [![Options preview](docs/images/comparisons/gui/options-video-mcp.png)](docs/images/comparisons/gui/options-video-mcp.png) |
60
+ | Vanilla priority panel | [![Priority panel in game](docs/images/comparisons/gui/skoda-priority-ingame.png)](docs/images/comparisons/gui/skoda-priority-ingame.png) | [![Priority panel preview](docs/images/comparisons/gui/skoda-priority-mcp.png)](docs/images/comparisons/gui/skoda-priority-mcp.png) |
61
+ | Mod management window | [![Management window in game](docs/images/comparisons/gui/tag-manager-ingame.png)](docs/images/comparisons/gui/tag-manager-ingame.png) | [![Management window preview](docs/images/comparisons/gui/tag-manager-mcp.png)](docs/images/comparisons/gui/tag-manager-mcp.png) |
62
+
63
+ The management window capture comes from an older layout.
64
+ The video options preview has no observed gamma value, and the priority panel's capture frames its background differently.
65
+
66
+ | Mod event log | Mod status panel | Mod status panel, second tab |
67
+ | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | [![Event Log preview](docs/images/comparisons/gui/event-log-mcp.png)](docs/images/comparisons/gui/event-log-mcp.png) | [![Chaos Meter status](docs/images/comparisons/gui/chaos-meter-status-mcp.png)](docs/images/comparisons/gui/chaos-meter-status-mcp.png) | [![Chaos Meter deaths](docs/images/comparisons/gui/chaos-meter-deaths-mcp.png)](docs/images/comparisons/gui/chaos-meter-deaths-mcp.png) |
69
+
70
+ | Vanilla decisions | Vanilla occupation |
71
+ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72
+ | [![Decision window](docs/images/comparisons/native-ui/countrydecisionview-window.png)](docs/images/comparisons/native-ui/countrydecisionview-window.png) | [![Occupation window](docs/images/comparisons/native-ui/countryoccupationview-window.png)](docs/images/comparisons/native-ui/countryoccupationview-window.png) |
73
+
74
+ The event log, status panel, decision and occupation previews use declared states or positions for layout review.
75
+ They are source previews, not observations of a running campaign.
76
+ See the [interface gallery](docs/gui-comparisons.md) for full images and fidelity notes, and the [GUI guide](docs/gui.md) for scenario inputs.
77
+
78
+ ### Maps and event chains
79
+
80
+ > Find Brandenburg on the map, inspect its provinces and neighbours, and show the affected source files before a change.
81
+
82
+ > Trace an event's options and follow-up events. Point out unresolved calls and link each route to its source.
83
+
84
+ | Source map | Mod event chain |
85
+ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
86
+ | [![Map rendered from installed source](docs/images/comparisons/source/map-mcp.png)](docs/images/comparisons/source/map-mcp.png) | [![Chaos Redux event options](docs/images/comparisons/source/event-chain-mcp.png)](docs/images/comparisons/source/event-chain-mcp.png) |
87
+
88
+ The [map guide](docs/map.md) covers connected province, state, supply and railway data.
89
+ The [event guide](docs/events.md) covers routes, scope, timing and unresolved references.
90
+
91
+ ### Local rules and AI weights
92
+
93
+ > Find the installed documentation for `save_event_target_as`, read the relevant section, and show a vanilla usage with file and line references.
94
+
95
+ > Compare this decision's `ai_will_do` score before and after a change in peace and war. List missing conditions separately from known results.
96
+
97
+ The [reference example](docs/research/reference-benchmark.md) found the installed effect documentation first and read its relevant 12-line section instead of loading whole wiki pages.
98
+ The [AI guide](docs/probability.md) covers supported weights, chance and timing models; a source score alone does not establish an AI click probability.
99
+ The [examples guide](docs/examples.md) has more prompt details.
100
+
101
+ ## Complete tool list
102
+
103
+ The default server exposes 35 tools.
104
+ Your agent can select them from ordinary language; use the live tool schema for call parameters.
105
+
106
+ | Tool | Purpose |
107
+ | --------------------------- | ---------------------------------------------------------------------------------------------------------------- |
108
+ | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
109
+ | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
110
+ | `hoi4.focus_raster` | Produce a PNG preview with decoded source icons. |
111
+ | `hoi4.focus_rewrite` | Create or update a focus tree. |
112
+ | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
113
+ | `hoi4.gui_render` | Render generated and explicit GUI scenarios, states, resolutions, and layout diagnostics. |
114
+ | `hoi4.gui_rewrite` | Create or update a GUI source package. |
115
+ | `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
116
+ | `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
117
+ | `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
118
+ | `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
119
+ | `hoi4.reference_search` | Search local documentation in plain words or script names; each result names its answering line. |
120
+ | `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
121
+ | `hoi4.source_lookup` | Find exact definitions, overrides and usages across game, DLC and mod, or the definition containing a file line. |
122
+ | `hoi4.script_validate` | Check snippet syntax, native command kinds, and declared scopes against local documentation. |
123
+ | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
124
+ | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
125
+ | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
126
+ | `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
127
+ | `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
128
+ | `hoi4.mechanic_test` | Execute bounded source effects on a copied declared scenario and check assertions. |
129
+ | `hoi4.package_check` | Check declarative package definitions, calls, registrations, assets, and case links. |
130
+ | `hoi4.scenario_test` | Run named source and domain cases in resumable, revision-bound batches. |
131
+ | `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
132
+ | `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
133
+ | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
134
+ | `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
135
+ | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
136
+ | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
137
+ | `hoi4.probability_evaluate` | Evaluate supported weights, probability and timing models, and missing inputs. |
138
+ | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
139
+ | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
140
+ | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
141
+ | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
142
+ | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
143
+
144
+ Set `HOI4_AGENT_TOOLS_CHAOSX=1` to expose two optional Chaos Redux workflow tools: `chaosx.focus_country_assets` and `chaosx.visual_revision`.
145
+ The [tool guide](docs/tools.md) explains results, linked artifacts and long operations.
50
146
 
51
147
  ## How it fits your workflow
52
148
 
@@ -81,6 +81,8 @@ export const workspaceRegistrationSchema = z
81
81
  root: z.string().min(1),
82
82
  kind: z.enum(['mod', 'game', 'dependency']).default('mod'),
83
83
  gameRoot: z.string().min(1).optional(),
84
+ /** Layer installed `dlc/` and `integrated_dlc/` folders between the game and mods. */
85
+ includeGameDlc: z.boolean().default(true),
84
86
  dependencyRoots: z.array(z.string().min(1)).max(WORKSPACE_MAX_SOURCE_ROOTS).default([]),
85
87
  dependencies: z.array(dependencyRegistrationSchema).max(WORKSPACE_MAX_SOURCE_ROOTS).default([]),
86
88
  replacePaths: z.array(relativeRootPathSchema).max(WORKSPACE_MAX_PATHS).default([]),
@@ -182,13 +182,6 @@ export class CoreEngine {
182
182
  signal: controller.signal,
183
183
  });
184
184
  controller.signal.throwIfAborted();
185
- const persistentAnalysisCache = await this.persistentAnalysisCache;
186
- const analysisScope = {
187
- workspaceIdentity: workspace.workspaceIdentity,
188
- rootFingerprint: transactionRootFingerprint(workspace),
189
- principal: principal ?? null,
190
- };
191
- await persistentAnalysisCache?.hydrate(analysisScope, files, sourceDocuments, this.indexSegments, controller.signal);
192
185
  const revision = hashCanonical(files.map(({ absolutePath, displayPath, relativePath, rootKind, loadOrder, shadowedBy, sha256, }) => ({
193
186
  absolutePath,
194
187
  displayPath,
@@ -202,6 +195,13 @@ export class CoreEngine {
202
195
  const cached = this.#scanCache.get(cacheKey);
203
196
  if (cached !== undefined)
204
197
  return cached;
198
+ const persistentAnalysisCache = await this.persistentAnalysisCache;
199
+ const analysisScope = {
200
+ workspaceIdentity: workspace.workspaceIdentity,
201
+ rootFingerprint: transactionRootFingerprint(workspace),
202
+ principal: principal ?? null,
203
+ };
204
+ await persistentAnalysisCache?.hydrate(analysisScope, files, sourceDocuments, this.indexSegments, controller.signal);
205
205
  const index = await SymbolIndex.buildAsync(files, controller.signal, this.indexSegments);
206
206
  await persistentAnalysisCache?.persist(analysisScope, files, sourceDocuments, this.indexSegments, controller.signal);
207
207
  const snapshot = {
@@ -0,0 +1,164 @@
1
+ import path from 'node:path';
2
+ import { escape, globIterate, hasMagic, Ignore, unescape } from 'glob';
3
+ import { lstat, readdir } from 'node:fs/promises';
4
+ import { ServiceError } from './result.js';
5
+ function validatePatterns(patterns) {
6
+ if (patterns.length > 8192)
7
+ throw new ServiceError('SCAN_PATTERN_LIMIT', 'Too many source glob patterns');
8
+ let wildcardCount = 0;
9
+ for (const pattern of patterns) {
10
+ if (Buffer.byteLength(pattern, 'utf8') > 4096)
11
+ throw new ServiceError('SCAN_PATTERN_LIMIT', 'Source glob pattern exceeds its byte limit');
12
+ if (pattern.startsWith('/') ||
13
+ /^[A-Za-z]:/u.test(pattern) ||
14
+ pattern.split('/').includes('..') ||
15
+ pattern.includes('\0'))
16
+ throw new ServiceError('SCAN_PATTERN_ESCAPE', 'Source glob patterns must stay relative to the authorized root');
17
+ let depth = 0;
18
+ const groups = [];
19
+ let expansions = 1;
20
+ for (let index = 0; index < pattern.length; index++) {
21
+ const character = pattern[index];
22
+ if (character === '\\') {
23
+ index++;
24
+ continue;
25
+ }
26
+ if (character === '{') {
27
+ if (++depth > 8)
28
+ throw new ServiceError('SCAN_PATTERN_LIMIT', 'Source glob brace nesting exceeds its limit');
29
+ groups.push({ start: index + 1, choices: 1 });
30
+ }
31
+ if (character === ',' && groups.length > 0)
32
+ groups.at(-1).choices++;
33
+ if (character === '}') {
34
+ depth = Math.max(0, depth - 1);
35
+ const group = groups.pop();
36
+ if (group === undefined)
37
+ continue;
38
+ const range = /^(-?\d+|[A-Za-z])\.\.(-?\d+|[A-Za-z])(?:\.\.(-?\d+))?$/u.exec(pattern.slice(group.start, index));
39
+ if (range !== null) {
40
+ const first = /^-?\d+$/u.test(range[1]) ? Number(range[1]) : range[1].charCodeAt(0);
41
+ const last = /^-?\d+$/u.test(range[2]) ? Number(range[2]) : range[2].charCodeAt(0);
42
+ const step = Math.abs(Number(range[3] ?? 1));
43
+ group.choices = Math.floor(Math.abs(last - first) / step) + 1;
44
+ }
45
+ expansions *= group.choices;
46
+ if (!Number.isSafeInteger(expansions) || expansions > 128)
47
+ throw new ServiceError('SCAN_PATTERN_LIMIT', 'Source glob expansion exceeds its limit');
48
+ }
49
+ }
50
+ // Validate brace complexity before invoking the glob parser, which can expand braces.
51
+ if (hasMagic(pattern, { magicalBraces: true }) && ++wildcardCount > 512)
52
+ throw new ServiceError('SCAN_PATTERN_LIMIT', 'Too many wildcard source glob patterns');
53
+ }
54
+ }
55
+ /** Relative, cancellable enumeration without traversing symbolic-link directories. */
56
+ export async function* iterateFiles(patterns, options) {
57
+ validatePatterns(patterns);
58
+ validatePatterns(options.ignore ?? []);
59
+ options.signal?.throwIfAborted();
60
+ const nocase = options.caseSensitive === false;
61
+ const excluded = new Ignore([...(options.ignore ?? [])], { nocase });
62
+ const seen = new Set();
63
+ const allowedPatterns = [];
64
+ for (const pattern of new Set(patterns)) {
65
+ let current = options.cwd;
66
+ let allowed = true;
67
+ let literal = true;
68
+ let finalIsFile = false;
69
+ for (const part of pattern.split('/')) {
70
+ if (hasMagic(part, { magicalBraces: true })) {
71
+ literal = false;
72
+ break;
73
+ }
74
+ let name = unescape(part);
75
+ let candidate = path.resolve(current, name);
76
+ const relative = path.relative(options.cwd, candidate).replaceAll('\\', '/');
77
+ if (relative === '..' || relative.startsWith('../') || path.isAbsolute(relative))
78
+ throw new ServiceError('SCAN_PATTERN_ESCAPE', 'Literal glob prefix escaped its authorized root');
79
+ let metadata;
80
+ try {
81
+ metadata = await lstat(candidate);
82
+ }
83
+ catch (error) {
84
+ if (error.code !== 'ENOENT' &&
85
+ error.code !== 'ENOTDIR')
86
+ throw error;
87
+ if (!nocase) {
88
+ allowed = false;
89
+ break;
90
+ }
91
+ try {
92
+ name =
93
+ (await readdir(current)).find((entry) => entry.toLowerCase() === name.toLowerCase()) ??
94
+ name;
95
+ candidate = path.resolve(current, name);
96
+ metadata = await lstat(candidate);
97
+ }
98
+ catch {
99
+ allowed = false;
100
+ break;
101
+ }
102
+ }
103
+ if (metadata.isSymbolicLink()) {
104
+ allowed = false;
105
+ break;
106
+ }
107
+ finalIsFile = metadata.isFile();
108
+ current = candidate;
109
+ }
110
+ if (!allowed)
111
+ continue;
112
+ if (literal) {
113
+ if (!finalIsFile)
114
+ continue;
115
+ const relative = path.relative(options.cwd, current).replaceAll('\\', '/');
116
+ if (excluded.relative.some((rule) => rule.match(relative)) ||
117
+ excluded.absolute.some((rule) => rule.match(current)))
118
+ continue;
119
+ options.signal?.throwIfAborted();
120
+ if (!seen.has(relative)) {
121
+ seen.add(relative);
122
+ yield relative;
123
+ }
124
+ }
125
+ else
126
+ allowedPatterns.push(pattern);
127
+ }
128
+ if (allowedPatterns.length === 0)
129
+ return;
130
+ const matches = globIterate(allowedPatterns, {
131
+ cwd: options.cwd,
132
+ withFileTypes: true,
133
+ nodir: true,
134
+ dot: false,
135
+ follow: false,
136
+ nocase,
137
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
138
+ ignore: {
139
+ ignored: (entry) => entry.isSymbolicLink() || excluded.ignored(entry),
140
+ childrenIgnored: (entry) => entry.isSymbolicLink() || excluded.childrenIgnored(entry),
141
+ },
142
+ });
143
+ for await (const entry of matches) {
144
+ options.signal?.throwIfAborted();
145
+ if (!entry.isFile() || entry.isSymbolicLink())
146
+ continue;
147
+ const relative = path.relative(options.cwd, entry.fullpath()).replaceAll('\\', '/');
148
+ if (relative === '..' || relative.startsWith('../') || path.isAbsolute(relative))
149
+ throw new ServiceError('SCAN_PATTERN_ESCAPE', 'Glob result escaped its authorized root');
150
+ if (!seen.has(relative)) {
151
+ seen.add(relative);
152
+ yield relative;
153
+ }
154
+ }
155
+ }
156
+ export async function globFiles(patterns, options) {
157
+ const files = [];
158
+ for await (const file of iterateFiles(patterns, options))
159
+ files.push(file);
160
+ return files;
161
+ }
162
+ export function escapeFileGlob(relativePath) {
163
+ return escape(relativePath, { magicalBraces: true });
164
+ }
@@ -38,6 +38,10 @@ export class IndexSegmentCache {
38
38
  this.#entries.set(address, entry);
39
39
  return JSON.parse(entry.encoded);
40
40
  }
41
+ /** Availability check without decoding or cache-hit accounting. */
42
+ has(address) {
43
+ return this.#entries.has(address);
44
+ }
41
45
  /** Import one authenticated canonical entry without counting it as a process-local hit. */
42
46
  importEncoded(address, encoded) {
43
47
  const segment = JSON.parse(encoded);
@@ -0,0 +1,31 @@
1
+ import { JobOperations } from './job-executor.js';
2
+ import { ServiceError } from './result.js';
3
+ /** Load one trusted domain after the authenticated job identifies its operation. */
4
+ export async function registerWorkerOperations(engine, toolName) {
5
+ const operations = new JobOperations();
6
+ if (toolName.startsWith('hoi4.event_'))
7
+ (await import('../event/job-operations.js')).registerEventJobs(operations, engine);
8
+ else if (toolName.startsWith('hoi4.focus_'))
9
+ (await import('../focus/job-operations.js')).registerFocusJobs(operations, engine);
10
+ else if (toolName.startsWith('hoi4.gui_'))
11
+ (await import('../gui/job-operations.js')).registerGuiJobs(operations, engine);
12
+ else if (toolName.startsWith('hoi4.map_'))
13
+ (await import('../map/job-operations.js')).registerMapJobs(operations, engine);
14
+ else if (toolName.startsWith('hoi4.probability_'))
15
+ (await import('../probability/job-operations.js')).registerProbabilityJobs(operations, engine);
16
+ else if (toolName.startsWith('hoi4.tech_'))
17
+ (await import('../technology/job-operations.js')).registerTechnologyJobs(operations, engine);
18
+ else if ([
19
+ 'hoi4.impact_inspect',
20
+ 'hoi4.decision_inspect',
21
+ 'hoi4.mechanic_test',
22
+ 'hoi4.package_check',
23
+ 'hoi4.scenario_test',
24
+ ].includes(toolName))
25
+ (await import('./analysis-job-operations.js')).registerAnalysisJobs(operations, engine);
26
+ else
27
+ throw new ServiceError('JOB_OPERATION_UNAVAILABLE', 'No trusted worker domain matches this job');
28
+ // A matching prefix never authorizes an unknown suffix or a client-selected module.
29
+ operations.get(toolName);
30
+ return operations;
31
+ }
@@ -1,10 +1,10 @@
1
1
  import { lstat, mkdir, open, opendir, rename, unlink } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { hostname } from 'node:os';
4
- import { setTimeout as delay } from 'node:timers/promises';
5
4
  import { z } from 'zod/v4';
6
5
  import { canonicalJson, hashCanonical, secureId } from './canonical.js';
7
6
  import { ServiceError } from './result.js';
7
+ import { retryWindowsSharing } from './windows-sharing.js';
8
8
  import { SharedRequestCapacity } from './shared-request-capacity.js';
9
9
  import { containedGeneratedPath } from './workspace.js';
10
10
  import { PACKAGE_VERSION } from '../version.js';
@@ -248,26 +248,8 @@ async function assertUnlinked(file) {
248
248
  }
249
249
  /** Atomic replacement with a finite retry for Windows sharing violations. */
250
250
  export async function publishJobRecord(temporary, target, options = {}) {
251
- const platform = options.platform ?? process.platform;
252
- const publish = options.publish ?? rename;
253
- const wait = options.wait ?? delay;
254
- const maxAttempts = options.maxAttempts ?? 100;
255
- if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1)
256
- throw new RangeError('Job record publication attempts must be a positive integer');
257
- for (let attempt = 1;; attempt += 1) {
258
- try {
259
- await publish(temporary, target);
260
- return;
261
- }
262
- catch (error) {
263
- const code = error.code ?? '';
264
- if (platform !== 'win32' ||
265
- !['EPERM', 'EBUSY', 'EACCES'].includes(code) ||
266
- attempt >= maxAttempts)
267
- throw error;
268
- await wait(10);
269
- }
270
- }
251
+ const { publish = rename, ...retry } = options;
252
+ await retryWindowsSharing(() => publish(temporary, target), retry);
271
253
  }
272
254
  /** Internal durable job records. Callers must obtain scopes from the authorized workspace resolver. */
273
255
  export class JobStore {
@@ -376,22 +358,14 @@ export class JobStore {
376
358
  if (!['completed', 'cancelled', 'failed'].includes(record.status))
377
359
  throw new ServiceError('JOB_STATE_CONFLICT', 'Only a terminal job record can be removed by retention cleanup');
378
360
  const file = await this.recordPath(scope, id);
379
- for (let attempt = 1;; attempt += 1) {
380
- signal.throwIfAborted();
381
- try {
382
- await unlink(file);
383
- return true;
384
- }
385
- catch (error) {
386
- const code = error.code ?? '';
387
- if (code === 'ENOENT')
388
- return false;
389
- if (process.platform !== 'win32' ||
390
- !['EPERM', 'EBUSY', 'EACCES'].includes(code) ||
391
- attempt >= 100)
392
- throw error;
393
- await delay(10);
394
- }
361
+ try {
362
+ await retryWindowsSharing(() => unlink(file), { signal });
363
+ return true;
364
+ }
365
+ catch (error) {
366
+ if (error.code === 'ENOENT')
367
+ return false;
368
+ throw error;
395
369
  }
396
370
  });
397
371
  }
@@ -573,11 +547,16 @@ export class JobStore {
573
547
  async readEnvelope(file, id, scopeHash) {
574
548
  let handle;
575
549
  try {
576
- handle = await open(file, 'r');
550
+ // A record being replaced is briefly delete-pending on Windows. Release the
551
+ // handle before parsing so a slow reader never blocks a concurrent publication.
552
+ handle = await retryWindowsSharing(() => open(file, 'r'));
577
553
  const metadata = await handle.stat();
578
554
  if (!metadata.isFile() || metadata.size > this.maxRecordBytes)
579
555
  throw new ServiceError('JOB_RECORD_INVALID', 'Persistent job record exceeds its storage budget or is not a file');
580
- const parsed = envelopeSchema.safeParse(JSON.parse(await handle.readFile('utf8')));
556
+ const text = await handle.readFile('utf8');
557
+ await handle.close();
558
+ handle = undefined;
559
+ const parsed = envelopeSchema.safeParse(JSON.parse(text));
581
560
  if (!parsed.success ||
582
561
  parsed.data.record.id !== id ||
583
562
  hashCanonical(parsed.data.record.scope) !== scopeHash ||
@@ -632,7 +611,7 @@ export class JobStore {
632
611
  }
633
612
  finally {
634
613
  await handle?.close();
635
- await unlink(temporary).catch((error) => {
614
+ await retryWindowsSharing(() => unlink(temporary)).catch((error) => {
636
615
  if (error.code !== 'ENOENT')
637
616
  throw error;
638
617
  });