hoi4-agent-tools 3.4.0 → 3.5.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.
- package/CHANGELOG.md +27 -0
- package/README.md +26 -18
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +48 -2
- package/dist/hoi4_agent_tools/core/condition-schema.js +18 -0
- package/dist/hoi4_agent_tools/core/configuration.js +10 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +9 -1
- package/dist/hoi4_agent_tools/core/impact-semantic.js +11 -4
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
- package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
- package/dist/hoi4_agent_tools/core/scanner.js +1 -1
- package/dist/hoi4_agent_tools/core/scenario-model.js +6 -0
- package/dist/hoi4_agent_tools/core/scenario-state.js +3 -0
- package/dist/hoi4_agent_tools/core/workspace.js +13 -1
- package/dist/hoi4_agent_tools/event/graph.js +8 -1
- package/dist/hoi4_agent_tools/event/operations.js +16 -10
- package/dist/hoi4_agent_tools/event/service.js +1 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +10 -2
- package/dist/hoi4_agent_tools/gui/layout.js +58 -11
- package/dist/hoi4_agent_tools/gui/scenario.js +3 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +4 -1
- package/dist/hoi4_agent_tools/gui/studio.js +32 -10
- package/dist/hoi4_agent_tools/gui/validators.js +3 -0
- package/dist/hoi4_agent_tools/map/model.js +121 -12
- package/dist/hoi4_agent_tools/map/operations.js +80 -7
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/map/validation.js +38 -1
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +18 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +60 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
- package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
- package/dist/hoi4_agent_tools/probability/service.js +2 -2
- package/dist/hoi4_agent_tools/probability/source-analysis.js +16 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +2 -1
- package/dist/hoi4_agent_tools/reference/lines.js +52 -0
- package/dist/hoi4_agent_tools/reference/operations.js +41 -0
- package/dist/hoi4_agent_tools/reference/service.js +427 -0
- package/dist/hoi4_agent_tools/reference/source-lookup.js +90 -0
- package/dist/hoi4_agent_tools/schemas/analysis.js +2 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability.js +26 -2
- package/dist/hoi4_agent_tools/schemas/reference.js +160 -0
- package/dist/hoi4_agent_tools/technology/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +11 -5
- package/dist/hoi4_agent_tools/technology/service.js +1 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/events.md +1 -1
- package/docs/gui.md +7 -0
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- package/docs/probability.md +7 -0
- package/docs/reference.md +32 -0
- package/docs/research/reference-benchmark.md +28 -0
- package/docs/setup.md +6 -0
- package/docs/technology.md +5 -1
- package/examples/clients/codex-config.toml +2 -0
- package/package.json +4 -2
- package/schemas/configuration.schema.json +72 -58
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +43 -26
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +102 -75
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +49 -32
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +160 -131
- package/schemas/probability-evaluate-input.schema.json +161 -132
- package/schemas/probability-inspect-input.schema.json +20 -11
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +110 -90
- package/schemas/probability-sequence-input.schema.json +154 -134
- package/schemas/probability-simulate-input.schema.json +161 -132
- package/schemas/probability-sweep-input.schema.json +163 -134
- package/schemas/reference-context-input.schema.json +42 -0
- package/schemas/reference-read-input.schema.json +41 -0
- package/schemas/reference-search-input.schema.json +45 -0
- package/schemas/scenario-suite.schema.json +92 -75
- package/schemas/scenario-test-input.schema.json +103 -86
- package/schemas/shared-scenario.schema.json +39 -22
- package/schemas/source-lookup-input.schema.json +61 -0
- package/server.json +2 -2
- package/docs/images/readme/chaos-meter-ingame.png +0 -0
- package/docs/images/readme/chaos-meter-mcp.png +0 -0
- package/docs/images/readme/communist-insurgency-ingame.png +0 -0
- package/docs/images/readme/communist-insurgency-mcp.png +0 -0
- package/docs/images/readme/event-chain-mcp.png +0 -0
- package/docs/images/readme/focus-tree-mcp.png +0 -0
- package/docs/images/readme/map-mcp.png +0 -0
- package/docs/images/readme/probability-mcp.png +0 -0
- package/docs/images/readme/settings-advanced-mcp.png +0 -0
- package/docs/images/readme/settings-events-mcp.png +0 -0
- package/docs/images/readme/technology-tree-mcp.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.5.0 - 2026-09-26
|
|
6
|
+
|
|
7
|
+
- Search and read bounded, revision-bound local offline-wiki, installed-game, and optional generated-script documentation sections; retrieve task citation bundles and exact indexed source definitions and usages through four read-only MCP tools.
|
|
8
|
+
- Compare hash-bound frozen probability source files at their logical gameplay paths while preserving physical source provenance and leaving current files untouched.
|
|
9
|
+
- Keep candidate eligibility overrides separate from weight-modifier conditions so an eligible scenario does not force every modifier to apply.
|
|
10
|
+
- Evaluate hidden and custom-tooltip trigger wrappers with their enclosed conditions while excluding presentation metadata from eligibility logic.
|
|
11
|
+
- Accept explicit GUI scenario dates and state-controller facts, evaluate their source conditions, render requested native windows at their declared settled show position, and resolve native list templates and scenario-only localisation from the correct source.
|
|
12
|
+
- Honor single-line native labels and preserve the first inferred font line, with explicit fidelity reporting for undersized native text-height conventions.
|
|
13
|
+
- Preserve dotted event namespaces so a namespace query selects its own events rather than unrelated systems sharing the first name segment.
|
|
14
|
+
- Compact selected event diagrams without retaining empty row gaps from the rest of the workspace graph.
|
|
15
|
+
- Resolve supported dated state-history values at the earliest active bookmark for map inspection, and refuse state rewrites that would silently conflict with or discard dated commands.
|
|
16
|
+
- Interpret floating-harbor placements as sea-side markers with land targets, preserving vanilla-compatible anomalies as source-linked review warnings instead of thousands of false map errors.
|
|
17
|
+
- Show alternatives within a national focus prerequisite group with dotted connectors, leaving individually required prerequisites solid.
|
|
18
|
+
- Draw wide technology-folder icons at their decoded sprite dimensions and the source GUI's icon anchor, preserving 131×52 chemical cylinders instead of shrinking them to 70×70.
|
|
19
|
+
- Include user-approved in-game focus and technology captures beside current-source and source-matched historical MCP renders, with source revisions and explicit whole-image comparison limits.
|
|
20
|
+
- Include native-size vanilla and Chaos Redux interface references with declared scenarios, full images, diagnostics, and source hashes; keep game imagery outside the npm package.
|
|
21
|
+
|
|
22
|
+
## 3.4.1 - 2026-09-25
|
|
23
|
+
|
|
24
|
+
- Validate technology renders against the requested view's source placement, node coverage, and sprite resolution without inheriting unrelated graph-wide diagnostics, while reporting deferred whole-workspace helper projection as a separate analysis boundary; resolve subtechnology art from the source GUI slot before using a technology-id fallback and include every referenced slot texture in the asset scan.
|
|
25
|
+
- Accept focused event inspection and render evidence when every event source was scanned and no blocking diagnostics remain, while retaining deferred workspace-wide helper and lifecycle projections as an explicit boundary.
|
|
26
|
+
- Keep repeated window-local GUI control names additive in the shared index, exclude engine-owned compound-control roles from scripted click-effect requirements, and preserve compact operation summaries when linked evidence exceeds the MCP wire budget.
|
|
27
|
+
- Parse each active scripted-effect source document once when building mechanic helper inventories so bounded real-workspace scenarios do not multiply parser state by the number of helpers in a file.
|
|
28
|
+
- Interpret declared temporary-variable assignment, arithmetic, rounding, clamping, and scope-valued inputs with chain-wide unscoped semantics in bounded mechanic scenarios.
|
|
29
|
+
- Evaluate `has_variable` against declared scenario state so guarded defaults and subsequent temporary-variable arithmetic remain deterministic.
|
|
30
|
+
- Follow a live replacement worker through its terminal publication instead of returning a nonterminal job record during concurrent checkpoint recovery.
|
|
31
|
+
|
|
5
32
|
## 3.4.0 - 2026-09-20
|
|
6
33
|
|
|
7
34
|
- Add branch-local focus cleanup with pinned anchors and symmetry groups, connector measurements, source-linked crops, and scenario-backed structural route diagnostics.
|
package/README.md
CHANGED
|
@@ -11,34 +11,35 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
11
11
|
- Cross-system impact and decisions: trace definitions and consumers across source systems, evaluate declared actor and target scenarios, inspect costs and mission paths, and compare in-memory proposals.
|
|
12
12
|
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
13
13
|
- AI and MTTH: evaluate weighted choices and timing across explicit scenarios, bind special scope chains, enumerate dynamic target pools, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
|
|
14
|
+
- Local references: search and read cited offline wiki pages, installed game documentation, optional script documentation, and exact source symbols without loading whole manuals into the conversation.
|
|
14
15
|
|
|
15
16
|
## Real results
|
|
16
17
|
|
|
17
|
-
The
|
|
18
|
+
The examples use real installed vanilla and Chaos Redux source, supplied game captures, and explicit reproducible display scenarios. They demonstrate source-backed review, not a claim of 99.9% whole-image accuracy. Images, source revisions, hashes, and known differences are in [Visual comparisons](docs/visual-comparisons.md) and [Interface examples](docs/gui-comparisons.md).
|
|
18
19
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|  |  |
|
|
22
23
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|  |  |
|
|
26
27
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
| Trigger Events settings | Advanced settings |
|
|
30
|
-
| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
31
|
-
|  |  |
|
|
28
|
+
The interface gallery also includes Chaos Redux Settings, Event Log, and Chaos Meter, plus populated vanilla decision and occupation templates. Their manifests separate source-defined layout from declared native runtime fields.
|
|
32
29
|
|
|
33
30
|
The other workbenches return complete source-linked layouts and catalogs rather than prose-only summaries.
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
|
32
|
+
For the supplied focus and technology game captures beside current-source MCP renders, see [Visual examples and comparison limits](https://github.com/klimPaskov/hoi4-agent-tools/blob/main/docs/visual-comparisons.md). These comparisons do not establish 99.9% whole-image accuracy.
|
|
33
|
+
|
|
34
|
+
| Fury focus tree from Chaos Redux | Infantry technology folder from Chaos Redux and vanilla |
|
|
35
|
+
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
36
|
+
|  |  |
|
|
37
|
+
|
|
38
|
+
| Map | Event options |
|
|
39
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
40
|
+
|  |  |
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
41
|
-
|  |  |  |
|
|
42
|
+
These two views retain native output resolution. Their [source manifest](docs/images/comparisons/source/manifest.json) records the map revision and the exact Fury event used for the option view. The event renderer's omitted-node count includes the rest of the workspace graph outside that selection.
|
|
42
43
|
|
|
43
44
|
## Use from a coding agent
|
|
44
45
|
|
|
@@ -51,6 +52,8 @@ npm install --global hoi4-agent-tools@latest
|
|
|
51
52
|
```toml
|
|
52
53
|
[mcp_servers.hoi4_agent_tools]
|
|
53
54
|
command = "hoi4-agent-tools.cmd"
|
|
55
|
+
startup_timeout_sec = 120
|
|
56
|
+
tool_timeout_sec = 600
|
|
54
57
|
```
|
|
55
58
|
|
|
56
59
|
On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repositories can include these steps in their agent template so the coding agent installs and registers the server itself; manual installation is not required in that workflow. Once connected, the server follows the active mod supplied by the client and the agent can call its HOI4 tools directly.
|
|
@@ -69,6 +72,10 @@ On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repo
|
|
|
69
72
|
| `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
|
|
70
73
|
| `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
|
|
71
74
|
| `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
|
|
75
|
+
| `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
|
|
76
|
+
| `hoi4.reference_search` | Search bounded local documentation sections. |
|
|
77
|
+
| `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
|
|
78
|
+
| `hoi4.source_lookup` | Find exact definitions, overrides, usages, and narrow source blocks. |
|
|
72
79
|
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
73
80
|
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
74
81
|
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
@@ -114,6 +121,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
114
121
|
- Mechanics and packages: "Test this transfer's conservation and payment," or "Check every declared package link and resume its named scenario suite." See [Mechanic tests and scenario suites](docs/mechanics.md).
|
|
115
122
|
- Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
|
|
116
123
|
- AI and MTTH: "Compare these focus weights across peace, defensive-war, and low-stability scenarios," or "Show when this MTTH event becomes likely and which unknown inputs control the result." See [AI and MTTH analysis](docs/probability.md).
|
|
124
|
+
- References: "Find the installed effect signature and an exact vanilla example," or "Show the pertinent wiki and installed documentation for this GUI task." See [Local references](docs/reference.md).
|
|
117
125
|
|
|
118
126
|
## HTTP
|
|
119
127
|
|
|
@@ -176,8 +176,17 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
176
176
|
const malformed = malformedBlock(assignment.value, candidate);
|
|
177
177
|
if (malformed !== undefined)
|
|
178
178
|
return malformed;
|
|
179
|
-
if (key === 'AND'
|
|
180
|
-
|
|
179
|
+
if (key === 'AND' ||
|
|
180
|
+
key === 'hidden_trigger' ||
|
|
181
|
+
key === 'custom_trigger_tooltip' ||
|
|
182
|
+
key === 'custom_override_tooltip')
|
|
183
|
+
return evaluateTriggerBlock(key === 'custom_trigger_tooltip' || key === 'custom_override_tooltip'
|
|
184
|
+
? {
|
|
185
|
+
...assignment.value,
|
|
186
|
+
entries: assignment.value.entries.filter((entry) => entry.type !== 'assignment' ||
|
|
187
|
+
!['tooltip', 'not_tooltip'].includes(entry.key.value)),
|
|
188
|
+
}
|
|
189
|
+
: assignment.value, scenario, candidate, definitions, helperStack, scopeContext);
|
|
181
190
|
if (key === 'OR') {
|
|
182
191
|
const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack, scopeContext));
|
|
183
192
|
return {
|
|
@@ -278,6 +287,13 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
278
287
|
unresolved: [],
|
|
279
288
|
};
|
|
280
289
|
}
|
|
290
|
+
if (key === 'has_variable') {
|
|
291
|
+
const state = scopeContext.expression === 'ROOT' ? scenario.state : scopeContext.binding.state;
|
|
292
|
+
return {
|
|
293
|
+
state: Object.hasOwn(state, right) || Object.hasOwn(state, `variable.${right}`) ? 'true' : 'false',
|
|
294
|
+
unresolved: [],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
281
297
|
if (key === 'tag' || key === 'original_tag') {
|
|
282
298
|
const actor = scopeContext.expression === 'ROOT'
|
|
283
299
|
? scenario.actor
|
|
@@ -311,6 +327,36 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
311
327
|
unresolved: [],
|
|
312
328
|
};
|
|
313
329
|
}
|
|
330
|
+
if ((key === 'controls_state' || key === 'is_controlled_by') && scenario.controls !== undefined) {
|
|
331
|
+
const targetScope = resolveScopeContext(scenario, right, scopeContext);
|
|
332
|
+
const stateId = key === 'controls_state'
|
|
333
|
+
? /^[1-9][0-9]*$/u.test(right)
|
|
334
|
+
? right
|
|
335
|
+
: targetScope?.binding.id
|
|
336
|
+
: scopeContext.binding.id;
|
|
337
|
+
const country = key === 'controls_state'
|
|
338
|
+
? scopeContext.expression === 'ROOT'
|
|
339
|
+
? scenario.actor
|
|
340
|
+
: scopeIdentity(scopeContext)
|
|
341
|
+
: targetScope === undefined
|
|
342
|
+
? /^[A-Z0-9]{3}$/u.test(right)
|
|
343
|
+
? right
|
|
344
|
+
: undefined
|
|
345
|
+
: targetScope.expression === 'ROOT' && scenario.actor === undefined
|
|
346
|
+
? undefined
|
|
347
|
+
: scopeIdentity(targetScope);
|
|
348
|
+
if (stateId === undefined || !/^[1-9][0-9]*$/u.test(stateId) || country === undefined)
|
|
349
|
+
return unresolved(assignment, candidate, `Scenario does not bind the state and country for ${key}`, 'controls');
|
|
350
|
+
const controller = scenario.controls[stateId];
|
|
351
|
+
if (controller === undefined)
|
|
352
|
+
return unresolved(assignment, candidate, `Scenario does not declare the controller of state ${stateId}`, `controls.${stateId}`);
|
|
353
|
+
if (!['=', '!='].includes(assignment.operator.text))
|
|
354
|
+
return unresolved(assignment, candidate, `Unsupported controller comparison ${assignment.operator.text}`);
|
|
355
|
+
return {
|
|
356
|
+
state: compare(controller, country, assignment.operator.text) ? 'true' : 'false',
|
|
357
|
+
unresolved: [],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
314
360
|
const helper = definitions.scriptedTriggers.get(key);
|
|
315
361
|
if (helper !== undefined && bool(right) !== undefined) {
|
|
316
362
|
if (helperStack.includes(key))
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export const ConditionControlMapSchema = z
|
|
3
|
+
.record(z.string().regex(/^[1-9][0-9]{0,8}$/u), z.string().regex(/^[A-Z0-9]{3}$/u))
|
|
4
|
+
.refine((value) => Object.keys(value).length <= 2_048, 'Too many declared state controllers');
|
|
5
|
+
export const GuiScenarioDateSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.max(16)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
const match = /^(\d{1,4})[.-](\d{1,2})[.-](\d{1,2})$/u.exec(value);
|
|
10
|
+
if (match === null)
|
|
11
|
+
return false;
|
|
12
|
+
const year = Number(match[1]);
|
|
13
|
+
const month = Number(match[2]);
|
|
14
|
+
const day = Number(match[3]);
|
|
15
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
16
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
17
|
+
return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];
|
|
18
|
+
}, 'Use a valid YYYY.M.D or YYYY-MM-DD date')
|
|
19
|
+
.transform((value) => value.split(/[.-]/u).map(Number).join('.'));
|
|
2
20
|
export const ScenarioScalarSchema = z.union([
|
|
3
21
|
z.string().max(16_384),
|
|
4
22
|
z.number(),
|
|
@@ -96,6 +96,16 @@ export const workspaceRegistrationSchema = z
|
|
|
96
96
|
artifactRoot: z.string().min(1).optional(),
|
|
97
97
|
cacheRoot: z.string().min(1).optional(),
|
|
98
98
|
fixtureRoot: z.string().min(1).optional(),
|
|
99
|
+
wikiRoot: z
|
|
100
|
+
.string()
|
|
101
|
+
.min(1)
|
|
102
|
+
.refine((value) => path.isAbsolute(value), 'Wiki root must be absolute')
|
|
103
|
+
.optional(),
|
|
104
|
+
scriptDocsRoot: z
|
|
105
|
+
.string()
|
|
106
|
+
.min(1)
|
|
107
|
+
.refine((value) => path.isAbsolute(value), 'Script documentation root must be absolute')
|
|
108
|
+
.optional(),
|
|
99
109
|
})
|
|
100
110
|
.strict()
|
|
101
111
|
.superRefine((value, context) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compareCodeUnits } from './canonical.js';
|
|
2
|
+
import { ServiceError } from './result.js';
|
|
2
3
|
function under(roots, glob) {
|
|
3
4
|
return roots.map((root) => `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/${glob}`);
|
|
4
5
|
}
|
|
@@ -39,7 +40,14 @@ export function focusDomainScanPatterns(workspace) {
|
|
|
39
40
|
/** Weighted source surfaces and shared definitions used by probability analysis. */
|
|
40
41
|
export function probabilityDomainScanPatterns(workspace, sourcePaths = []) {
|
|
41
42
|
const roots = workspace.registration.roots;
|
|
42
|
-
const normalizedSourcePaths = sourcePaths.map((sourcePath) =>
|
|
43
|
+
const normalizedSourcePaths = sourcePaths.map((sourcePath) => {
|
|
44
|
+
const normalized = sourcePath.replaceAll('\\', '/').replace(/^\.\//u, '');
|
|
45
|
+
if (normalized.startsWith('/') ||
|
|
46
|
+
normalized.split('/').includes('..') ||
|
|
47
|
+
/[:*?[\]{}!\0]/u.test(normalized))
|
|
48
|
+
throw new ServiceError('PROBABILITY_SOURCE_PATH_INVALID', 'Probability source paths must be exact workspace-relative files');
|
|
49
|
+
return normalized;
|
|
50
|
+
});
|
|
43
51
|
if (normalizedSourcePaths.length > 0)
|
|
44
52
|
return uniquePatterns([...normalizedSourcePaths, ...probabilitySharedDefinitionPatterns()]);
|
|
45
53
|
return uniquePatterns([
|
|
@@ -56,12 +56,18 @@ function stateKeyTarget(assignment, kind) {
|
|
|
56
56
|
function normalized(file) {
|
|
57
57
|
return file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
58
58
|
}
|
|
59
|
-
function ownerFor(snapshot, file, assignment, ancestors) {
|
|
59
|
+
function ownerFor(snapshot, file, assignment, ancestors, includeOnActions) {
|
|
60
60
|
if (assignment.value.type !== 'block')
|
|
61
61
|
return undefined;
|
|
62
62
|
const key = assignment.key.value;
|
|
63
63
|
const sourcePath = normalized(file);
|
|
64
64
|
let owner;
|
|
65
|
+
if (includeOnActions &&
|
|
66
|
+
sourcePath.startsWith('common/on_actions/') &&
|
|
67
|
+
ancestors.length === 1 &&
|
|
68
|
+
ancestors[0] === 'on_actions') {
|
|
69
|
+
return { kind: 'on_action', id: key };
|
|
70
|
+
}
|
|
65
71
|
if (sourcePath.startsWith('events/') && ancestors.length === 0 && eventKeys.has(key)) {
|
|
66
72
|
const id = firstScalar(assignment.value, 'id')?.value;
|
|
67
73
|
if (id !== undefined)
|
|
@@ -114,7 +120,7 @@ function staticTarget(value) {
|
|
|
114
120
|
!/^(?:ROOT|FROM|PREV|THIS)$/iu.test(value));
|
|
115
121
|
}
|
|
116
122
|
/** Extracts typed script edges while preserving unknown dynamic references. */
|
|
117
|
-
export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000) {
|
|
123
|
+
export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000, options = {}) {
|
|
118
124
|
if (!Number.isSafeInteger(maxReferences) || maxReferences < 1 || maxReferences > 1_000_000)
|
|
119
125
|
throw new RangeError('Semantic reference limit is outside the supported range');
|
|
120
126
|
const references = [];
|
|
@@ -148,7 +154,7 @@ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000)
|
|
|
148
154
|
};
|
|
149
155
|
const walk = (file, document, block, ancestors, owner) => {
|
|
150
156
|
for (const assignment of assignments(block)) {
|
|
151
|
-
const identified = ownerFor(snapshot, file, assignment, ancestors);
|
|
157
|
+
const identified = ownerFor(snapshot, file, assignment, ancestors, options.includeOnActions === true);
|
|
152
158
|
const current = identified ?? owner;
|
|
153
159
|
if (identified?.kind === 'decision' && ancestors.length === 1)
|
|
154
160
|
emit(file, document, assignment, identified, 'decision_category', ancestors[0], 'decision_category_membership');
|
|
@@ -237,7 +243,8 @@ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000)
|
|
|
237
243
|
path.startsWith('common/technologies/') ||
|
|
238
244
|
path.startsWith('common/scripted_effects/') ||
|
|
239
245
|
path.startsWith('common/scripted_triggers/') ||
|
|
240
|
-
path.startsWith('common/scripted_guis/')
|
|
246
|
+
path.startsWith('common/scripted_guis/') ||
|
|
247
|
+
(options.includeOnActions === true && path.startsWith('common/on_actions/'))))
|
|
241
248
|
continue;
|
|
242
249
|
if (snapshot.index.isSourceSkipped(file.displayPath)) {
|
|
243
250
|
complete = false;
|
|
@@ -1369,6 +1369,7 @@ export class SymbolIndex {
|
|
|
1369
1369
|
this.#active.set(key, active);
|
|
1370
1370
|
const additiveCategory = active.kind === 'decision_category' ||
|
|
1371
1371
|
active.kind === 'technology_category' ||
|
|
1372
|
+
active.kind === 'gui_element' ||
|
|
1372
1373
|
active.kind === 'texture';
|
|
1373
1374
|
for (const candidate of group) {
|
|
1374
1375
|
candidate.overridden = additiveCategory ? candidate.sourceShadowed : candidate !== active;
|
|
@@ -73,7 +73,12 @@ export class JobWorkerHost {
|
|
|
73
73
|
for (;;) {
|
|
74
74
|
try {
|
|
75
75
|
await this.scheduler.run(this.owner, 1024, signal, () => this.capacity.run(signal, (lease) => this.launch(workspaceId, id, lease, principal)));
|
|
76
|
-
|
|
76
|
+
const current = await this.jobs.get(workspaceId, id, principal);
|
|
77
|
+
if (['completed', 'cancelled', 'failed'].includes(current.status))
|
|
78
|
+
return current;
|
|
79
|
+
if (current.owner !== undefined && jobOwnerLiveness(current.owner) === 'alive')
|
|
80
|
+
return await this.followOwner(workspaceId, id, principal);
|
|
81
|
+
throw new ServiceError('JOB_WORKER_EXIT', 'The worker stopped without publishing a terminal job outcome');
|
|
77
82
|
}
|
|
78
83
|
catch (error) {
|
|
79
84
|
const current = await this.jobs.get(workspaceId, id, principal);
|
|
@@ -108,6 +113,19 @@ export class JobWorkerHost {
|
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
}
|
|
116
|
+
async followOwner(workspaceId, id, principal) {
|
|
117
|
+
for (;;) {
|
|
118
|
+
const record = await this.jobs.get(workspaceId, id, principal);
|
|
119
|
+
if (['completed', 'failed', 'cancelled'].includes(record.status))
|
|
120
|
+
return record;
|
|
121
|
+
const liveness = record.owner === undefined ? 'dead' : jobOwnerLiveness(record.owner);
|
|
122
|
+
if (liveness !== 'alive')
|
|
123
|
+
throw new ServiceError(liveness === 'unknown' ? 'JOB_OWNER_UNRESOLVED' : 'JOB_WORKER_EXIT', liveness === 'unknown'
|
|
124
|
+
? 'The replacement execution owner cannot be verified on this host'
|
|
125
|
+
: 'The replacement worker stopped without publishing a terminal job outcome');
|
|
126
|
+
await delay(100);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
111
129
|
async launch(workspaceId, id, lease, principal) {
|
|
112
130
|
// Resolve the same configured workspace and grants again immediately before dispatch.
|
|
113
131
|
await this.jobs.get(workspaceId, id, principal);
|
|
@@ -114,6 +114,9 @@ function boundedResult(result, actualBytes) {
|
|
|
114
114
|
: 'Tool output exceeded the wire budget; nonessential inline details were omitted',
|
|
115
115
|
details: { actualBytes, maxBytes: MAX_TOOL_RESULT_BYTES },
|
|
116
116
|
};
|
|
117
|
+
const compactData = result.data !== null && typeof result.data === 'object' && !Array.isArray(result.data)
|
|
118
|
+
? result.data
|
|
119
|
+
: {};
|
|
117
120
|
return {
|
|
118
121
|
status: result.status,
|
|
119
122
|
code: result.code.slice(0, 256),
|
|
@@ -141,7 +144,7 @@ function boundedResult(result, actualBytes) {
|
|
|
141
144
|
},
|
|
142
145
|
},
|
|
143
146
|
],
|
|
144
|
-
data: {},
|
|
147
|
+
data: jsonBytesWithin(compactData, 2_048) ? compactData : {},
|
|
145
148
|
};
|
|
146
149
|
}
|
|
147
150
|
function jsonStringBytes(value) {
|
|
@@ -180,7 +180,7 @@ export class WorkspaceScanner {
|
|
|
180
180
|
return result;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
async function readBoundedFile(handle, remainingBytes, signal) {
|
|
183
|
+
export async function readBoundedFile(handle, remainingBytes, signal) {
|
|
184
184
|
const chunks = [];
|
|
185
185
|
let total = 0;
|
|
186
186
|
for (;;) {
|
|
@@ -34,6 +34,9 @@ export function scenarioFromGui(base, values = base.values) {
|
|
|
34
34
|
for (const [key, value] of Object.entries(base.flags))
|
|
35
35
|
state[`flag.${key}`] = value;
|
|
36
36
|
const scopes = structuredClone(base.scopes ?? {});
|
|
37
|
+
for (const stateId of Object.keys(base.controls ?? {})) {
|
|
38
|
+
scopes[stateId] ??= { id: stateId, type: 'state', state: {} };
|
|
39
|
+
}
|
|
37
40
|
for (const [key, value] of Object.entries(state)) {
|
|
38
41
|
const match = /^(ROOT|THIS|FROM(?:\.FROM)*|PREV)\.(.+)$/u.exec(key);
|
|
39
42
|
if (match === null)
|
|
@@ -49,6 +52,8 @@ export function scenarioFromGui(base, values = base.values) {
|
|
|
49
52
|
return {
|
|
50
53
|
schemaVersion: '1.0',
|
|
51
54
|
id: base.id,
|
|
55
|
+
...(base.date === undefined ? {} : { date: base.date }),
|
|
56
|
+
...(base.controls === undefined ? {} : { controls: { ...base.controls } }),
|
|
52
57
|
state,
|
|
53
58
|
scopes,
|
|
54
59
|
closedFlags: false,
|
|
@@ -62,6 +67,7 @@ export function scenarioFromProbability(base) {
|
|
|
62
67
|
id: base.id,
|
|
63
68
|
...(base.actor === undefined ? {} : { actor: base.actor }),
|
|
64
69
|
...(base.date === undefined ? {} : { date: base.date }),
|
|
70
|
+
...(base.controls === undefined ? {} : { controls: { ...base.controls } }),
|
|
65
71
|
state: structuredClone(base.state),
|
|
66
72
|
...(base.flags === undefined ? {} : { flags: [...base.flags] }),
|
|
67
73
|
...(base.eventTargets === undefined ? {} : { eventTargets: { ...base.eventTargets } }),
|
|
@@ -42,6 +42,9 @@ export function resolveScopeContext(scenario, expression, current) {
|
|
|
42
42
|
const direct = explicitScope(scenario, expression, current);
|
|
43
43
|
if (direct !== undefined)
|
|
44
44
|
return direct;
|
|
45
|
+
if (/^[1-9][0-9]*$/u.test(expression) && Object.hasOwn(scenario.controls ?? {}, expression)) {
|
|
46
|
+
return { expression, binding: { id: expression, type: 'state', state: {} }, parent: current };
|
|
47
|
+
}
|
|
45
48
|
const targetMatch = /^(?:event_target|scope):(.+)$/iu.exec(expression);
|
|
46
49
|
const targetId = targetMatch === null
|
|
47
50
|
? undefined
|
|
@@ -488,6 +488,10 @@ export class WorkspaceResolver {
|
|
|
488
488
|
const fixtureRoot = registration.fixtureRoot === undefined
|
|
489
489
|
? undefined
|
|
490
490
|
: await canonicalPath(registration.fixtureRoot);
|
|
491
|
+
const wikiRoot = registration.wikiRoot === undefined ? undefined : await canonicalPath(registration.wikiRoot);
|
|
492
|
+
const scriptDocsRoot = registration.scriptDocsRoot === undefined
|
|
493
|
+
? undefined
|
|
494
|
+
: await canonicalPath(registration.scriptDocsRoot);
|
|
491
495
|
const generatedAllowed = (candidate, generatedKind) => (registration.kind === 'mod' &&
|
|
492
496
|
isWithin(path.join(modRoot, '.hoi4-agent', generatedKind), candidate)) ||
|
|
493
497
|
this.#storageRoots.some((root) => isWithin(root, candidate));
|
|
@@ -524,7 +528,13 @@ export class WorkspaceResolver {
|
|
|
524
528
|
}
|
|
525
529
|
}
|
|
526
530
|
if (this.#serverStateRoot !== undefined &&
|
|
527
|
-
[
|
|
531
|
+
[
|
|
532
|
+
modRoot,
|
|
533
|
+
...sourceRoots,
|
|
534
|
+
artifactRoot,
|
|
535
|
+
cacheRoot,
|
|
536
|
+
...[wikiRoot, scriptDocsRoot].filter((root) => root !== undefined),
|
|
537
|
+
].some((root) => pathsOverlap(this.#serverStateRoot, root))) {
|
|
528
538
|
throw new ServiceError('SERVER_STATE_ROOT_OVERLAP', 'Server state root must not overlap a workspace source or generated-storage root', { workspaceId: registration.id });
|
|
529
539
|
}
|
|
530
540
|
await mkdir(artifactRoot, { recursive: true });
|
|
@@ -595,6 +605,8 @@ export class WorkspaceResolver {
|
|
|
595
605
|
artifactRoot,
|
|
596
606
|
cacheRoot,
|
|
597
607
|
...(fixtureRoot === undefined ? {} : { fixtureRoot }),
|
|
608
|
+
...(wikiRoot === undefined ? {} : { wikiRoot }),
|
|
609
|
+
...(scriptDocsRoot === undefined ? {} : { scriptDocsRoot }),
|
|
598
610
|
writeEnabled: registration.kind === 'mod',
|
|
599
611
|
};
|
|
600
612
|
const workspaceIdentity = resolvedWorkspaceIdentity(workspaceBase);
|
|
@@ -848,7 +848,14 @@ function ensureReferencedNodes(graph) {
|
|
|
848
848
|
id: edge.to,
|
|
849
849
|
kind: 'unresolved',
|
|
850
850
|
label: `Missing ${isEvent ? 'event' : 'helper'}: ${expression}`,
|
|
851
|
-
...(isEvent
|
|
851
|
+
...(isEvent
|
|
852
|
+
? {
|
|
853
|
+
eventId: expression,
|
|
854
|
+
namespace: expression.includes('.')
|
|
855
|
+
? expression.slice(0, expression.lastIndexOf('.'))
|
|
856
|
+
: expression,
|
|
857
|
+
}
|
|
858
|
+
: {}),
|
|
852
859
|
sourcePath: edge.location.path,
|
|
853
860
|
location: edge.location,
|
|
854
861
|
metadata: { expression, unresolvedKind },
|
|
@@ -37,21 +37,27 @@ export function eventGraphCounts(graph, artifacts) {
|
|
|
37
37
|
}
|
|
38
38
|
export function eventGraphValidation(graph) {
|
|
39
39
|
const counts = eventGraphCounts(graph, 0);
|
|
40
|
+
const analysisBoundaryPassed = graph.complete || (graph.analysisMode === 'focused' && graph.skippedSourceCount === 0);
|
|
41
|
+
const passed = analysisBoundaryPassed && counts.blockingDiagnostics === 0;
|
|
40
42
|
return {
|
|
41
|
-
passed
|
|
43
|
+
passed,
|
|
42
44
|
checks: [
|
|
43
45
|
{
|
|
44
46
|
id: 'event-analysis',
|
|
45
|
-
passed
|
|
46
|
-
message:
|
|
47
|
-
?
|
|
47
|
+
passed,
|
|
48
|
+
message: counts.blockingDiagnostics > 0
|
|
49
|
+
? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
|
|
48
50
|
: graph.complete
|
|
49
|
-
?
|
|
50
|
-
: graph.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
? 'The event-chain analysis is complete and has no blocking diagnostics'
|
|
52
|
+
: graph.analysisMode === 'focused' && graph.skippedSourceCount === 0
|
|
53
|
+
? 'Whole-workspace helper projections and lifecycle passes were deferred; the requested event analysis retains direct source evidence'
|
|
54
|
+
: graph.analysisMode === 'focused'
|
|
55
|
+
? `${graph.skippedSourceCount} event-analysis source(s) were skipped; linked evidence records the partial boundary`
|
|
56
|
+
: graph.issues.some(({ code }) => code === 'EVENT_HELPER_DEPTH_LIMIT' ||
|
|
57
|
+
code === 'EVENT_HELPER_PROJECTION_LIMIT' ||
|
|
58
|
+
code === 'EVENT_HELPER_STATE_PROJECTION_LIMIT')
|
|
59
|
+
? 'Helper expansion reached a depth or materialization boundary; helper_expansion mode provides bounded source-linked continuation'
|
|
60
|
+
: `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
|
|
55
61
|
},
|
|
56
62
|
],
|
|
57
63
|
};
|
|
@@ -984,6 +984,7 @@ export class EventChainViewer {
|
|
|
984
984
|
const renderOptions = {
|
|
985
985
|
view: input.view,
|
|
986
986
|
budget,
|
|
987
|
+
compactLayout: input.compactLayout ?? input.selector !== undefined,
|
|
987
988
|
...(input.selector === undefined ? {} : { selector: input.selector }),
|
|
988
989
|
...(input.direction === undefined ? {} : { direction: input.direction }),
|
|
989
990
|
...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
|
|
@@ -450,7 +450,7 @@ class FragmentAnalyzer {
|
|
|
450
450
|
kind: 'event',
|
|
451
451
|
label: id,
|
|
452
452
|
eventId: id,
|
|
453
|
-
namespace: id.includes('.') ? id.slice(0, id.
|
|
453
|
+
namespace: id.includes('.') ? id.slice(0, id.lastIndexOf('.')) : id,
|
|
454
454
|
sourcePath: this.file.displayPath,
|
|
455
455
|
location: this.location(assignment, id),
|
|
456
456
|
metadata: {
|
|
@@ -204,7 +204,12 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
204
204
|
diagnosticMap.set(focusId, group);
|
|
205
205
|
}
|
|
206
206
|
const prerequisiteEdges = plan.focuses
|
|
207
|
-
.flatMap((focus) => focus.prerequisites.groups.flatMap((group, groupIndex) => group.focusIds.map((parentId) => ({
|
|
207
|
+
.flatMap((focus) => focus.prerequisites.groups.flatMap((group, groupIndex) => group.focusIds.map((parentId) => ({
|
|
208
|
+
parentId,
|
|
209
|
+
childId: focus.id,
|
|
210
|
+
groupIndex,
|
|
211
|
+
alternative: group.focusIds.length > 1,
|
|
212
|
+
}))))
|
|
208
213
|
.filter(({ parentId, childId }) => pixel.has(parentId) && pixel.has(childId))
|
|
209
214
|
.sort((left, right) => compareCodeUnits(left.parentId, right.parentId) ||
|
|
210
215
|
compareCodeUnits(left.childId, right.childId) ||
|
|
@@ -239,7 +244,10 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
239
244
|
originX: padding - minimumX * horizontal,
|
|
240
245
|
originY: padding - minimumY * vertical,
|
|
241
246
|
});
|
|
242
|
-
|
|
247
|
+
const stroke = edge.alternative
|
|
248
|
+
? 'stroke-width="2.5" stroke-dasharray="2 4" stroke-linecap="round"'
|
|
249
|
+
: 'stroke-width="2"';
|
|
250
|
+
parts.push(`<path d="${focusConnectorSvgPath(curve)}" fill="none" stroke="#8da2bc" ${stroke} marker-end="url(#arrow)" data-parent="${escapeXml(edge.parentId)}" data-child="${escapeXml(edge.childId)}" data-prerequisite-group="${edge.groupIndex}" data-prerequisite-kind="${edge.alternative ? 'or' : 'required'}"/>`);
|
|
243
251
|
}
|
|
244
252
|
parts.push('</g>', '<g id="mutual-exclusions">');
|
|
245
253
|
for (const [leftId, rightId] of exclusionEdges) {
|