hoi4-agent-tools 2.2.0 → 2.3.3
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 +28 -0
- package/README.md +30 -21
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +5 -0
- package/dist/hoi4_agent_tools/core/index.js +57 -1
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +178 -1
- package/dist/hoi4_agent_tools/event/graph.js +24 -9
- package/dist/hoi4_agent_tools/event/limits.js +5 -2
- package/dist/hoi4_agent_tools/event/service.js +182 -45
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +13 -63
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/runtime.js +109 -7
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/technology/graph.js +30 -3
- package/dist/hoi4_agent_tools/technology/queries.js +70 -3
- package/dist/hoi4_agent_tools/technology/service.js +38 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +2 -0
- package/docs/events.md +2 -2
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +2 -0
- package/docs/technology.md +4 -2
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +19 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.3 - 2026-07-26
|
|
4
|
+
|
|
5
|
+
- Regenerated the npm lockfile with the pinned release npm so clean installs include the patched optional native runtime packages.
|
|
6
|
+
|
|
7
|
+
## 2.3.2 - 2026-07-26
|
|
8
|
+
|
|
9
|
+
- Pinned patched transitive `brace-expansion` and `postcss` releases so the CI and release audit remain clean without changing the MCP Inspector surface.
|
|
10
|
+
|
|
11
|
+
## 2.3.1 - 2026-07-26
|
|
12
|
+
|
|
13
|
+
- Fixed large vanilla-plus-mod technology scans that could consume multiple gigabytes and leave the MCP transport open until a client timeout closed it.
|
|
14
|
+
- Added automatic focused technology analysis for game-backed and large workspaces, retaining direct source evidence and helper-call indexing while deferring workspace-wide scripted-effect projections.
|
|
15
|
+
|
|
16
|
+
## 2.3.0 - 2026-07-22
|
|
17
|
+
|
|
18
|
+
- Added a read-only AI and MTTH Scenario Analyzer with inspect, evaluate, sweep, simulate, sequence, compare, and render tools.
|
|
19
|
+
- Added versioned adapters for event MTTH and options, decisions, missions, focuses, technologies, doctrines, direct random chance, `random_list`, supported AI strategy factors, and declared custom weighted pools without treating unlike HOI4 systems as one probability formula.
|
|
20
|
+
- Added exact and bounded scenario evaluation, AST-path provenance, nested-random path probabilities, ranges, alternatives, distributions, numeric correlations, configurable diagnostics, named acceptance bands, and explicit external-factor support.
|
|
21
|
+
- Added threshold-aware sweeps with trigger-adjacent breakpoints, local elasticities, pairwise interactions, rank reversals, cliffs, and missed target bands.
|
|
22
|
+
- Added constant-memory Latin hypercube and seeded pseudo-random simulation, global input importance, Wilson intervals, and discrete daily-hazard MTTH samples with bounded quantile retention and uncertainty evidence.
|
|
23
|
+
- Added declared-manifest sequence analysis for recovery, caps, cooldowns, removal, resets, timer changes, terminal states, and per-category outcomes without executing effects or inferring campaign state.
|
|
24
|
+
- Added scenario-hash-bound deterministic ranking, matrix, waterfall, timing-survival, sensitivity, threshold, sequence, comparison, and unresolved renders with scenario, candidate, and metric filters.
|
|
25
|
+
- Added fail-closed installed-game build verification for the versioned probability adapters.
|
|
26
|
+
- Added generated public schemas, callable examples, source evidence, deterministic artifacts, and a project-owned fixture containing more than 150 weighted blocks, 250 scenarios, exact expectations, unresolved cases, and a stateful pool.
|
|
27
|
+
- Verified analyzer discovery and workflows over stdio and authenticated Streamable HTTP, package installation, resource retrieval, cancellation, stale-result handling, workspace isolation, large candidate pools, and the official MCP Inspector.
|
|
28
|
+
- Expanded event-graph capacity for installed-game and large-mod analysis beyond 100,000 nodes while retaining bounded artifact validation.
|
|
29
|
+
- Reconfirmed MCP protocol `2025-11-25`, TypeScript SDK 1.29.0, Inspector 1.0.0, Registry schema `2025-12-11`, and Registry publisher 1.8.0 for this release.
|
|
30
|
+
|
|
3
31
|
## 2.2.0 - 2026-07-22
|
|
4
32
|
|
|
5
33
|
- Added a read-only Technology Tree Viewer for classic technologies, legacy and current doctrines, source folder layouts, prerequisites, exclusive choices, categories, tags, unlocks, bonuses, grants, metadata, localisation, icons, and cross-file references.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HOI4 Agent Tools
|
|
2
2
|
|
|
3
|
-
HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron IV event chains
|
|
3
|
+
HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron IV event chains, technology systems, AI weights, and MTTH timing and to inspect, create, or clean up focus trees, scripted GUIs, and maps. Start it in a mod folder and it works there immediately, with offline renders for review.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
9
9
|
- Maps: inspect provinces, states, regions, adjacency, supply, and railways; render layers; create and repair exact map data.
|
|
10
10
|
- Event chains: scan definitions and call sites, trace routes and state flow, lint references, render graphs, and compare revisions without editing event source.
|
|
11
11
|
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
12
|
+
- AI and MTTH: evaluate weighted choices and timing across explicit scenarios, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
|
|
12
13
|
|
|
13
14
|
## Setup
|
|
14
15
|
|
|
@@ -30,32 +31,39 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
|
|
|
30
31
|
|
|
31
32
|
## Tools
|
|
32
33
|
|
|
33
|
-
| Tool
|
|
34
|
-
|
|
|
35
|
-
| `hoi4.focus_inspect`
|
|
36
|
-
| `hoi4.focus_render`
|
|
37
|
-
| `hoi4.focus_raster`
|
|
38
|
-
| `hoi4.focus_rewrite`
|
|
39
|
-
| `hoi4.gui_inspect`
|
|
40
|
-
| `hoi4.gui_render`
|
|
41
|
-
| `hoi4.gui_rewrite`
|
|
42
|
-
| `hoi4.map_inspect`
|
|
43
|
-
| `hoi4.map_render`
|
|
44
|
-
| `hoi4.map_rewrite`
|
|
45
|
-
| `hoi4.event_inspect`
|
|
46
|
-
| `hoi4.event_render`
|
|
47
|
-
| `hoi4.event_compare`
|
|
48
|
-
| `hoi4.tech_inspect`
|
|
49
|
-
| `hoi4.tech_render`
|
|
50
|
-
| `hoi4.tech_compare`
|
|
34
|
+
| Tool | Purpose |
|
|
35
|
+
| --------------------------- | --------------------------------------------------------------------------------------- |
|
|
36
|
+
| `hoi4.focus_inspect` | Read focus trees and report structural or reference problems. |
|
|
37
|
+
| `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
|
|
38
|
+
| `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
|
|
39
|
+
| `hoi4.focus_rewrite` | Create or update a focus tree. |
|
|
40
|
+
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
|
|
41
|
+
| `hoi4.gui_render` | Render GUI states and resolutions offline. |
|
|
42
|
+
| `hoi4.gui_rewrite` | Create or update a GUI source package. |
|
|
43
|
+
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
|
|
44
|
+
| `hoi4.map_render` | Render map layers and overlays. |
|
|
45
|
+
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes. |
|
|
46
|
+
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
47
|
+
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
48
|
+
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
49
|
+
| `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
|
|
50
|
+
| `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
|
|
51
|
+
| `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
|
|
52
|
+
| `hoi4.probability_inspect` | Locate weighted logic, candidates, required inputs, and supported analysis. |
|
|
53
|
+
| `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
|
|
54
|
+
| `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
|
|
55
|
+
| `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
|
|
56
|
+
| `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
|
|
57
|
+
| `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
|
|
58
|
+
| `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
|
|
51
59
|
|
|
52
60
|
Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
|
|
53
61
|
|
|
54
62
|
## Coexistence with agent workflows
|
|
55
63
|
|
|
56
|
-
HOI4 Agent Tools provides HOI4 domain operations
|
|
64
|
+
HOI4 Agent Tools provides HOI4 domain operations without replacing repository instructions such as `AGENTS.md`, skills, plans, or subagents. Its optional weighted-logic prompt helps an agent scope one probability analysis and then returns control to the normal workflow.
|
|
57
65
|
|
|
58
|
-
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. Event and
|
|
66
|
+
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. Event, technology, and probability tools analyze source without editing it; only `hoi4.*_rewrite` calls edit mod source.
|
|
59
67
|
|
|
60
68
|
## Create or clean content
|
|
61
69
|
|
|
@@ -66,6 +74,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
66
74
|
- Maps: "Create a state from these exact provinces," or "Inspect this state and split these provinces while keeping supply and railway references valid." See [Maps](docs/map.md).
|
|
67
75
|
- Event chains: "Trace every route from this event and explain where its flags and variables change," or "Compare the workspace event graph with its previous revision and render the affected routes." See [Event chains](docs/events.md).
|
|
68
76
|
- 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).
|
|
77
|
+
- 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).
|
|
69
78
|
|
|
70
79
|
## HTTP
|
|
71
80
|
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { compareCodeUnits } from './canonical.js';
|
|
2
|
+
import { assignments, parseClausewitz, } from './source/index.js';
|
|
3
|
+
const definitionCache = new WeakMap();
|
|
4
|
+
function activeTextFiles(snapshot, prefix) {
|
|
5
|
+
return snapshot.files
|
|
6
|
+
.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
|
|
7
|
+
relativePath.replaceAll('\\', '/').toLowerCase().startsWith(prefix) &&
|
|
8
|
+
relativePath.toLowerCase().endsWith('.txt'))
|
|
9
|
+
.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
10
|
+
}
|
|
11
|
+
function scalarAssignments(block) {
|
|
12
|
+
return assignments(block).filter(({ value }) => value.type === 'scalar');
|
|
13
|
+
}
|
|
14
|
+
function flattenConstants(block, prefix, output, file, document) {
|
|
15
|
+
for (const assignment of assignments(block)) {
|
|
16
|
+
if (assignment.key.value === 'schema')
|
|
17
|
+
continue;
|
|
18
|
+
const id = prefix === '' ? assignment.key.value : `${prefix}.${assignment.key.value}`;
|
|
19
|
+
if (assignment.value.type === 'scalar') {
|
|
20
|
+
output.set(id, { id, value: assignment.value.value, node: assignment, file, document });
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
flattenConstants(assignment.value, id, output, file, document);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Shared, source-linked definitions used by analysis domains. */
|
|
28
|
+
export class ClausewitzEvaluationDefinitions {
|
|
29
|
+
scriptConstants = new Map();
|
|
30
|
+
mtthVariables = new Map();
|
|
31
|
+
scriptedTriggers = new Map();
|
|
32
|
+
localConstants = new Map();
|
|
33
|
+
static build(snapshot) {
|
|
34
|
+
const cached = definitionCache.get(snapshot);
|
|
35
|
+
if (cached !== undefined)
|
|
36
|
+
return cached;
|
|
37
|
+
const result = new ClausewitzEvaluationDefinitions();
|
|
38
|
+
for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined && relativePath.toLowerCase().endsWith('.txt'))) {
|
|
39
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
40
|
+
const locals = new Map();
|
|
41
|
+
for (const assignment of scalarAssignments(document.root)) {
|
|
42
|
+
if (!assignment.key.value.startsWith('@') || assignment.value.type !== 'scalar')
|
|
43
|
+
continue;
|
|
44
|
+
locals.set(assignment.key.value, {
|
|
45
|
+
id: assignment.key.value,
|
|
46
|
+
value: assignment.value.value,
|
|
47
|
+
node: assignment,
|
|
48
|
+
file,
|
|
49
|
+
document,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (locals.size > 0)
|
|
53
|
+
result.localConstants.set(file.displayPath, locals);
|
|
54
|
+
}
|
|
55
|
+
for (const file of activeTextFiles(snapshot, 'common/script_constants/')) {
|
|
56
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
57
|
+
flattenConstants(document.root, '', result.scriptConstants, file, document);
|
|
58
|
+
}
|
|
59
|
+
for (const file of activeTextFiles(snapshot, 'common/mtth/')) {
|
|
60
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
61
|
+
for (const assignment of assignments(document.root)) {
|
|
62
|
+
if (assignment.value.type !== 'block' || assignment.key.value.startsWith('@'))
|
|
63
|
+
continue;
|
|
64
|
+
result.mtthVariables.set(assignment.key.value, {
|
|
65
|
+
id: assignment.key.value,
|
|
66
|
+
value: assignment.value,
|
|
67
|
+
node: assignment,
|
|
68
|
+
file,
|
|
69
|
+
document,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const file of activeTextFiles(snapshot, 'common/scripted_triggers/')) {
|
|
74
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
75
|
+
for (const assignment of assignments(document.root)) {
|
|
76
|
+
if (assignment.value.type !== 'block')
|
|
77
|
+
continue;
|
|
78
|
+
result.scriptedTriggers.set(assignment.key.value, {
|
|
79
|
+
id: assignment.key.value,
|
|
80
|
+
value: assignment.value,
|
|
81
|
+
node: assignment,
|
|
82
|
+
file,
|
|
83
|
+
document,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
definitionCache.set(snapshot, result);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -33,6 +33,11 @@ function defaultPatterns(workspace) {
|
|
|
33
33
|
'common/characters/**/*.txt',
|
|
34
34
|
'common/scripted_effects/**/*.txt',
|
|
35
35
|
'common/scripted_triggers/**/*.txt',
|
|
36
|
+
'common/script_constants/**/*.txt',
|
|
37
|
+
'common/mtth/**/*.txt',
|
|
38
|
+
'common/ai_strategy/**/*.txt',
|
|
39
|
+
'common/ai_strategy_plans/**/*.txt',
|
|
40
|
+
'common/ai_focuses/**/*.txt',
|
|
36
41
|
'common/on_actions/**/*.txt',
|
|
37
42
|
'common/operations/**/*.txt',
|
|
38
43
|
'common/raids/**/*.txt',
|
|
@@ -109,6 +109,14 @@ function possibleSymbolKinds(file, definitionFiles) {
|
|
|
109
109
|
kinds.add('scripted_effect');
|
|
110
110
|
if (sourcePath.startsWith('common/scripted_triggers/'))
|
|
111
111
|
kinds.add('scripted_trigger');
|
|
112
|
+
if (sourcePath.startsWith('common/script_constants/'))
|
|
113
|
+
kinds.add('script_constant');
|
|
114
|
+
if (sourcePath.startsWith('common/mtth/'))
|
|
115
|
+
kinds.add('mtth_variable');
|
|
116
|
+
if (sourcePath.startsWith('common/ai_strategy/'))
|
|
117
|
+
kinds.add('ai_strategy');
|
|
118
|
+
if (sourcePath.startsWith('common/ai_strategy_plans/'))
|
|
119
|
+
kinds.add('ai_strategy_plan');
|
|
112
120
|
if (sourcePath.startsWith('common/decisions/')) {
|
|
113
121
|
kinds.add('decision');
|
|
114
122
|
kinds.add('decision_category');
|
|
@@ -467,8 +475,23 @@ export class SymbolIndex {
|
|
|
467
475
|
const sourcePath = file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
468
476
|
for (const assignment of assignments(block)) {
|
|
469
477
|
const key = assignment.key.value;
|
|
470
|
-
if (assignment.value.type !== 'block')
|
|
478
|
+
if (assignment.value.type !== 'block') {
|
|
479
|
+
if (sourcePath.startsWith('common/script_constants/') &&
|
|
480
|
+
key !== 'schema' &&
|
|
481
|
+
!ancestors.includes('schema')) {
|
|
482
|
+
const id = [...ancestors, key].join('.');
|
|
483
|
+
this.addSymbol({
|
|
484
|
+
kind: 'script_constant',
|
|
485
|
+
id,
|
|
486
|
+
path: file.displayPath,
|
|
487
|
+
rootKind: file.rootKind,
|
|
488
|
+
loadOrder: file.loadOrder,
|
|
489
|
+
location: nodeLocation(document, assignment, id),
|
|
490
|
+
metadata: { value: assignment.value.value },
|
|
491
|
+
});
|
|
492
|
+
}
|
|
471
493
|
continue;
|
|
494
|
+
}
|
|
472
495
|
const child = assignment.value;
|
|
473
496
|
const id = firstScalar(child, 'id')?.value;
|
|
474
497
|
if (key === 'focus_tree' && id !== undefined) {
|
|
@@ -873,6 +896,39 @@ export class SymbolIndex {
|
|
|
873
896
|
metadata: {},
|
|
874
897
|
});
|
|
875
898
|
}
|
|
899
|
+
else if (sourcePath.startsWith('common/mtth/') && ancestors.length === 0) {
|
|
900
|
+
this.addSymbol({
|
|
901
|
+
kind: 'mtth_variable',
|
|
902
|
+
id: key,
|
|
903
|
+
path: file.displayPath,
|
|
904
|
+
rootKind: file.rootKind,
|
|
905
|
+
loadOrder: file.loadOrder,
|
|
906
|
+
location: nodeLocation(document, assignment, key),
|
|
907
|
+
metadata: {},
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
else if (sourcePath.startsWith('common/ai_strategy/') && ancestors.length === 0) {
|
|
911
|
+
this.addSymbol({
|
|
912
|
+
kind: 'ai_strategy',
|
|
913
|
+
id: key,
|
|
914
|
+
path: file.displayPath,
|
|
915
|
+
rootKind: file.rootKind,
|
|
916
|
+
loadOrder: file.loadOrder,
|
|
917
|
+
location: nodeLocation(document, assignment, key),
|
|
918
|
+
metadata: {},
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
else if (sourcePath.startsWith('common/ai_strategy_plans/') && ancestors.length === 0) {
|
|
922
|
+
this.addSymbol({
|
|
923
|
+
kind: 'ai_strategy_plan',
|
|
924
|
+
id: key,
|
|
925
|
+
path: file.displayPath,
|
|
926
|
+
rootKind: file.rootKind,
|
|
927
|
+
loadOrder: file.loadOrder,
|
|
928
|
+
location: nodeLocation(document, assignment, key),
|
|
929
|
+
metadata: {},
|
|
930
|
+
});
|
|
931
|
+
}
|
|
876
932
|
else if (sourcePath.startsWith('common/decisions/') && ancestors.length === 0) {
|
|
877
933
|
this.addSymbol({
|
|
878
934
|
kind: 'decision_category',
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const cachedPaths = new WeakMap();
|
|
2
|
+
function indexBlock(block, path, output) {
|
|
3
|
+
output.set(block, path);
|
|
4
|
+
const occurrences = new Map();
|
|
5
|
+
for (const [index, entry] of block.entries.entries()) {
|
|
6
|
+
if (entry.type === 'assignment') {
|
|
7
|
+
const key = entry.key.value;
|
|
8
|
+
const occurrence = occurrences.get(key) ?? 0;
|
|
9
|
+
occurrences.set(key, occurrence + 1);
|
|
10
|
+
const entryPath = [...path, `${key}[${occurrence}]`];
|
|
11
|
+
output.set(entry, entryPath);
|
|
12
|
+
output.set(entry.key, [...entryPath, '$key']);
|
|
13
|
+
output.set(entry.value, entryPath);
|
|
14
|
+
if (entry.value.type === 'block')
|
|
15
|
+
indexBlock(entry.value, entryPath, output);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const entryPath = [...path, `#${entry.type}[${index}]`];
|
|
19
|
+
output.set(entry, entryPath);
|
|
20
|
+
if (entry.type === 'block')
|
|
21
|
+
indexBlock(entry, entryPath, output);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function pathsFor(document) {
|
|
25
|
+
const existing = cachedPaths.get(document);
|
|
26
|
+
if (existing !== undefined)
|
|
27
|
+
return existing;
|
|
28
|
+
const indexed = new WeakMap();
|
|
29
|
+
indexBlock(document.root, [], indexed);
|
|
30
|
+
cachedPaths.set(document, indexed);
|
|
31
|
+
return indexed;
|
|
32
|
+
}
|
|
33
|
+
/** Stable key-and-occurrence path for a node inside one parsed Clausewitz document. */
|
|
34
|
+
export function astPathFor(document, node) {
|
|
35
|
+
const path = pathsFor(document).get(node);
|
|
36
|
+
return path === undefined ? undefined : [...path];
|
|
37
|
+
}
|
|
38
|
+
/** Test and diagnostic helper for callers that already hold a generic source entry. */
|
|
39
|
+
export function sourceEntryAstPath(document, entry) {
|
|
40
|
+
return astPathFor(document, entry);
|
|
41
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compareCodeUnits } from '../core/canonical.js';
|
|
1
|
+
import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
2
2
|
import { EventAnalysisBudget } from './limits.js';
|
|
3
3
|
import { iterativeStronglyConnectedComponents } from './scc.js';
|
|
4
4
|
function sortedUnique(values) {
|
|
@@ -41,6 +41,178 @@ function edgeMaps(edges) {
|
|
|
41
41
|
}
|
|
42
42
|
return { outgoing, incoming };
|
|
43
43
|
}
|
|
44
|
+
function helperName(node) {
|
|
45
|
+
const name = node.metadata.name;
|
|
46
|
+
if (typeof name === 'string')
|
|
47
|
+
return name;
|
|
48
|
+
return node.label || node.id.replace(/^helper:/u, '');
|
|
49
|
+
}
|
|
50
|
+
function collapsedHelperEdge(path, helperStack, from, to) {
|
|
51
|
+
const first = path[0];
|
|
52
|
+
const last = path.at(-1);
|
|
53
|
+
if (helperStack.length === 0)
|
|
54
|
+
return first;
|
|
55
|
+
return {
|
|
56
|
+
id: `edge:lazy-helper-projection:${hashCanonical({
|
|
57
|
+
from,
|
|
58
|
+
to,
|
|
59
|
+
edges: path.map(({ id }) => id),
|
|
60
|
+
}).slice(0, 32)}`,
|
|
61
|
+
from,
|
|
62
|
+
to,
|
|
63
|
+
reason: 'scripted_effect_expansion',
|
|
64
|
+
conditions: path.flatMap(({ conditions }) => conditions),
|
|
65
|
+
helperStack: [...helperStack],
|
|
66
|
+
location: first.location,
|
|
67
|
+
provenance: path.flatMap(({ provenance }) => provenance),
|
|
68
|
+
confidence: last.confidence,
|
|
69
|
+
derived: true,
|
|
70
|
+
...(last.timing === undefined ? {} : { timing: last.timing }),
|
|
71
|
+
...(last.weight === undefined ? {} : { weight: last.weight }),
|
|
72
|
+
...(last.scope === undefined ? {} : { scope: last.scope }),
|
|
73
|
+
metadata: { structuralEdgeIds: path.map(({ id }) => id) },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build only the helper projections reached by one bounded trace. Full-workspace
|
|
78
|
+
* helper materialisation is useful for broad reports, but it is unnecessarily
|
|
79
|
+
* expensive when an agent asks for one route in a large installed game.
|
|
80
|
+
*/
|
|
81
|
+
function lazyCollapsedEdges(graph, nodeId, direction, maps, maximumPaths, signal) {
|
|
82
|
+
const nodes = nodeMap(graph);
|
|
83
|
+
const retained = new Map();
|
|
84
|
+
let pathVisits = 0;
|
|
85
|
+
const add = (edge) => {
|
|
86
|
+
retained.set(edge.id, edge);
|
|
87
|
+
};
|
|
88
|
+
const downstream = (edge, path, stack, visited) => {
|
|
89
|
+
signal?.throwIfAborted();
|
|
90
|
+
if (++pathVisits > maximumPaths)
|
|
91
|
+
return;
|
|
92
|
+
if (path.length > 64)
|
|
93
|
+
return;
|
|
94
|
+
const target = nodes.get(edge.to);
|
|
95
|
+
if (target?.kind !== 'helper') {
|
|
96
|
+
add(collapsedHelperEdge(path, stack, nodeId, edge.to));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (visited.has(target.id))
|
|
100
|
+
return;
|
|
101
|
+
for (const next of maps.outgoing.get(target.id) ?? []) {
|
|
102
|
+
downstream(next, [...path, next], [...stack, helperName(target)], new Set([...visited, target.id]));
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const upstream = (edge, path, stack, visited) => {
|
|
106
|
+
signal?.throwIfAborted();
|
|
107
|
+
if (++pathVisits > maximumPaths)
|
|
108
|
+
return;
|
|
109
|
+
if (path.length > 64)
|
|
110
|
+
return;
|
|
111
|
+
const source = nodes.get(edge.from);
|
|
112
|
+
if (source?.kind !== 'helper') {
|
|
113
|
+
const forwardPath = [...path].reverse();
|
|
114
|
+
add(collapsedHelperEdge(forwardPath, [...stack].reverse(), edge.from, nodeId));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (visited.has(source.id))
|
|
118
|
+
return;
|
|
119
|
+
for (const previous of maps.incoming.get(source.id) ?? []) {
|
|
120
|
+
upstream(previous, [...path, previous], [...stack, helperName(source)], new Set([...visited, source.id]));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (direction !== 'upstream') {
|
|
124
|
+
for (const edge of maps.outgoing.get(nodeId) ?? [])
|
|
125
|
+
downstream(edge, [edge], [], new Set());
|
|
126
|
+
}
|
|
127
|
+
if (direction !== 'downstream') {
|
|
128
|
+
for (const edge of maps.incoming.get(nodeId) ?? [])
|
|
129
|
+
upstream(edge, [edge], [], new Set());
|
|
130
|
+
}
|
|
131
|
+
return [...retained.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
132
|
+
}
|
|
133
|
+
function traceCollapsedHelpers(graph, starts, boundary, signal) {
|
|
134
|
+
const byId = nodeMap(graph);
|
|
135
|
+
const maps = edgeMaps(graph.edges.filter(({ derived }) => !derived));
|
|
136
|
+
const edgeCache = new Map();
|
|
137
|
+
const edgesFor = (nodeId) => {
|
|
138
|
+
const key = `${nodeId}\0${boundary.direction}`;
|
|
139
|
+
const cached = edgeCache.get(key);
|
|
140
|
+
if (cached !== undefined)
|
|
141
|
+
return cached;
|
|
142
|
+
const edges = lazyCollapsedEdges(graph, nodeId, boundary.direction, maps, Math.max(256, boundary.maxEdges * 2), signal);
|
|
143
|
+
edgeCache.set(key, edges);
|
|
144
|
+
return edges;
|
|
145
|
+
};
|
|
146
|
+
const depthByNode = new Map(starts.map((id) => [id, 0]));
|
|
147
|
+
const queue = [...starts];
|
|
148
|
+
const retainedEdges = new Map();
|
|
149
|
+
const boundaryNodes = new Set();
|
|
150
|
+
let truncated = false;
|
|
151
|
+
let cursor = 0;
|
|
152
|
+
while (cursor < queue.length) {
|
|
153
|
+
if ((cursor & 255) === 0)
|
|
154
|
+
signal?.throwIfAborted();
|
|
155
|
+
const current = queue[cursor++];
|
|
156
|
+
const depth = depthByNode.get(current) ?? 0;
|
|
157
|
+
const nextEdges = edgesFor(current);
|
|
158
|
+
if (depth >= boundary.maxDepth) {
|
|
159
|
+
if (nextEdges.length > 0) {
|
|
160
|
+
boundaryNodes.add(current);
|
|
161
|
+
truncated = true;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
for (const edge of nextEdges) {
|
|
166
|
+
if (retainedEdges.size >= boundary.maxEdges) {
|
|
167
|
+
boundaryNodes.add(current);
|
|
168
|
+
truncated = true;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
const next = boundary.direction === 'upstream'
|
|
172
|
+
? edge.from
|
|
173
|
+
: boundary.direction === 'downstream'
|
|
174
|
+
? edge.to
|
|
175
|
+
: edge.from === current
|
|
176
|
+
? edge.to
|
|
177
|
+
: edge.from;
|
|
178
|
+
const known = depthByNode.has(next);
|
|
179
|
+
if (!known && depthByNode.size >= boundary.maxNodes) {
|
|
180
|
+
boundaryNodes.add(current);
|
|
181
|
+
truncated = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
retainedEdges.set(edge.id, edge);
|
|
185
|
+
if (!known) {
|
|
186
|
+
depthByNode.set(next, depth + 1);
|
|
187
|
+
queue.push(next);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const retainedNodes = [...depthByNode.keys()]
|
|
192
|
+
.flatMap((id) => {
|
|
193
|
+
const node = byId.get(id);
|
|
194
|
+
return node === undefined ? [] : [node];
|
|
195
|
+
})
|
|
196
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
197
|
+
const layers = [...new Set(depthByNode.values())]
|
|
198
|
+
.sort((left, right) => left - right)
|
|
199
|
+
.map((depth) => ({
|
|
200
|
+
depth,
|
|
201
|
+
nodeIds: [...depthByNode.entries()]
|
|
202
|
+
.filter(([, value]) => value === depth)
|
|
203
|
+
.map(([id]) => id)
|
|
204
|
+
.sort(compareCodeUnits),
|
|
205
|
+
}));
|
|
206
|
+
return {
|
|
207
|
+
startNodeIds: [...starts],
|
|
208
|
+
nodes: retainedNodes,
|
|
209
|
+
edges: [...retainedEdges.values()].sort((left, right) => compareCodeUnits(left.id, right.id)),
|
|
210
|
+
layers,
|
|
211
|
+
boundary,
|
|
212
|
+
boundaryNodeIds: [...boundaryNodes].sort(compareCodeUnits),
|
|
213
|
+
truncated,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
44
216
|
function edgesForDirection(nodeId, direction, maps) {
|
|
45
217
|
const results = [];
|
|
46
218
|
if (direction !== 'upstream') {
|
|
@@ -58,6 +230,11 @@ export function traceEventGraph(graph, startNodeIds, boundary, signal) {
|
|
|
58
230
|
const byId = nodeMap(graph);
|
|
59
231
|
const resolvedStarts = sortedUnique(startNodeIds).filter((id) => byId.has(id));
|
|
60
232
|
const starts = resolvedStarts.slice(0, Math.max(0, boundary.maxNodes));
|
|
233
|
+
if (!boundary.expandHelpers &&
|
|
234
|
+
!graph.edges.some(({ derived }) => derived) &&
|
|
235
|
+
starts.every((id) => byId.get(id)?.kind !== 'helper')) {
|
|
236
|
+
return traceCollapsedHelpers(graph, starts, boundary, signal);
|
|
237
|
+
}
|
|
61
238
|
const edges = eventFlowEdges(graph, boundary.expandHelpers);
|
|
62
239
|
const maps = edgeMaps(edges);
|
|
63
240
|
const depthByNode = new Map(starts.map((id) => [id, 0]));
|
|
@@ -29,6 +29,14 @@ function activeFiles(snapshot) {
|
|
|
29
29
|
!/(?:^|\/)(?:supply_nodes|railways|buildings|unitstacks|weatherpositions)\.txt$/iu.test(relativePath.replaceAll('\\', '/')))
|
|
30
30
|
.sort((left, right) => compareCodeUnits(left.displayPath, right.displayPath));
|
|
31
31
|
}
|
|
32
|
+
const EVENT_SETUP_CALL_PATTERN = /\b(?:country_event|news_event|state_event|unit_leader_event|operative_leader_event|fire_event|event)\s*=/iu;
|
|
33
|
+
function shouldAnalyzeEventFile(file) {
|
|
34
|
+
const sourcePath = file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
35
|
+
if (!sourcePath.startsWith('history/countries/') && !sourcePath.startsWith('history/states/')) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return EVENT_SETUP_CALL_PATTERN.test(file.bytes.toString('utf8'));
|
|
39
|
+
}
|
|
32
40
|
function catalogFor(snapshot) {
|
|
33
41
|
const unshadowedEventSymbols = snapshot.index
|
|
34
42
|
.findAll('event')
|
|
@@ -1444,6 +1452,8 @@ function analyzeFragments(snapshot, files, catalog, options, budget) {
|
|
|
1444
1452
|
const fragments = [];
|
|
1445
1453
|
for (const file of files) {
|
|
1446
1454
|
budget.spend('source_fragment');
|
|
1455
|
+
if (!shouldAnalyzeEventFile(file))
|
|
1456
|
+
continue;
|
|
1447
1457
|
if (snapshot.index.isSourceSkipped(file.displayPath))
|
|
1448
1458
|
continue;
|
|
1449
1459
|
const cacheKey = eventSemanticFragmentCacheKey(file, catalog.fingerprint);
|
|
@@ -1560,14 +1570,17 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1560
1570
|
graph.edges = stableUnique(graph.edges);
|
|
1561
1571
|
graph.stateAccesses = stableUnique(graph.stateAccesses);
|
|
1562
1572
|
graph.unresolved = stableUnique(graph.unresolved);
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1573
|
+
const focused = options.analysisMode === 'focused';
|
|
1574
|
+
const stateLinks = focused ? [] : stateLinksFor(graph, budget);
|
|
1575
|
+
if (!focused) {
|
|
1576
|
+
duplicateAndReferenceIssues(graph);
|
|
1577
|
+
unresolvedIssues(graph);
|
|
1578
|
+
structuralCallIssues(graph, budget);
|
|
1579
|
+
localisationIssues(graph);
|
|
1580
|
+
stateLifecycleIssues(graph, stateLinks, budget);
|
|
1581
|
+
unreachableIssues(graph, budget);
|
|
1582
|
+
normalizeIssueSubjects(graph);
|
|
1583
|
+
}
|
|
1571
1584
|
graph.issues = stableIssues(graph.issues);
|
|
1572
1585
|
assertGraphLimit(graph.nodes.length, EVENT_GRAPH_MAX_NODES, 'EVENT_NODE_LIMIT', 'node count');
|
|
1573
1586
|
assertGraphLimit(graph.edges.length, EVENT_GRAPH_MAX_EDGES, 'EVENT_EDGE_LIMIT', 'edge count');
|
|
@@ -1577,7 +1590,8 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1577
1590
|
assertGraphLimit(graph.unresolved.length, EVENT_GRAPH_MAX_UNRESOLVED, 'EVENT_UNRESOLVED_LIMIT', 'unresolved-analysis count');
|
|
1578
1591
|
budget.check();
|
|
1579
1592
|
const diagnostics = sortDiagnostics([...snapshot.diagnostics, ...partial.diagnostics]);
|
|
1580
|
-
const complete =
|
|
1593
|
+
const complete = !focused &&
|
|
1594
|
+
snapshot.complete &&
|
|
1581
1595
|
graph.unresolved.length === 0 &&
|
|
1582
1596
|
!graph.issues.some(({ classification, severity }) => classification === 'unresolved_analysis' || severity === 'blocker');
|
|
1583
1597
|
const nodes = sortNodes(graph.nodes);
|
|
@@ -1588,6 +1602,7 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1588
1602
|
return {
|
|
1589
1603
|
schemaVersion: EVENT_GRAPH_SCHEMA_VERSION,
|
|
1590
1604
|
parserVersion: EVENT_GRAPH_PARSER_VERSION,
|
|
1605
|
+
analysisMode: options.analysisMode ?? 'full',
|
|
1591
1606
|
workspaceId: snapshot.workspaceId,
|
|
1592
1607
|
workspaceIdentity: options.workspaceIdentity ?? hashCanonical({ workspaceId: snapshot.workspaceId }),
|
|
1593
1608
|
revision: snapshot.revision,
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { ServiceError } from '../core/result.js';
|
|
2
|
-
export const EVENT_GRAPH_MAX_NODES =
|
|
2
|
+
export const EVENT_GRAPH_MAX_NODES = 250_000;
|
|
3
3
|
export const EVENT_GRAPH_MAX_EDGES = 500_000;
|
|
4
4
|
export const EVENT_GRAPH_MAX_STATE_ACCESSES = 250_000;
|
|
5
5
|
export const EVENT_GRAPH_MAX_STATE_LINKS = 250_000;
|
|
6
6
|
export const EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES = 500_000;
|
|
7
|
-
|
|
7
|
+
// Installed-game scans can legitimately produce more than twenty thousand source-linked
|
|
8
|
+
// findings before an agent narrows the query. Keep the graph bounded, but do not reject a
|
|
9
|
+
// complete large workspace merely because its diagnostics are numerous.
|
|
10
|
+
export const EVENT_GRAPH_MAX_ISSUES = 100_000;
|
|
8
11
|
export const EVENT_GRAPH_MAX_UNRESOLVED = 50_000;
|
|
9
12
|
export const EVENT_GRAPH_MAX_HELPER_DEPTH = 64;
|
|
10
13
|
export const EVENT_GRAPH_MAX_HELPER_PROJECTIONS = 200_000;
|