hoi4-agent-tools 2.8.11 → 3.0.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 +10 -0
- package/README.md +35 -10
- package/dist/bin/stdio.js +0 -4
- package/dist/hoi4_agent_tools/core/configuration.js +16 -3
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +10 -13
- package/dist/hoi4_agent_tools/core/engine.js +3 -0
- package/dist/hoi4_agent_tools/core/scanner.js +63 -7
- package/dist/hoi4_agent_tools/event/service.js +30 -23
- package/dist/hoi4_agent_tools/gui/assets.js +218 -0
- package/dist/hoi4_agent_tools/gui/layout.js +70 -9
- package/dist/hoi4_agent_tools/gui/renderer.js +80 -11
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +36 -0
- package/dist/hoi4_agent_tools/gui/studio.js +90 -25
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +1 -1
- package/dist/hoi4_agent_tools/probability/service.js +33 -8
- package/dist/hoi4_agent_tools/schemas/probability.js +2 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/development.md +2 -0
- package/docs/gui.md +14 -3
- 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/package.json +3 -1
- package/schemas/configuration.schema.json +3 -3
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- 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 +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +58 -39
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +2 -2
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +0 -483
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes.
|
|
6
6
|
|
|
7
|
+
## 3.0.0 - 2026-08-31
|
|
8
|
+
|
|
9
|
+
- Promote the complete focus, GUI, map, event, technology, and probability MCP surface to the stable 3.0 release line after full synthetic, installed-game, external-workspace, transport, package, Inspector, and Registry qualification.
|
|
10
|
+
- Render installed HOI4 bitmap fonts and language overrides from their native atlases with deterministic metrics, borders, crisp scaling, and localisation colours; keep the exhaustive installed-font audit as a release regression gate.
|
|
11
|
+
- Resolve scenario and dynamic-list country scopes into real ideology-aware flags using each masked-shield definition's own asset dimensions, and render inline HOI4 text icons that appear only after dynamic text substitution.
|
|
12
|
+
- Keep large legitimate event and technology workspaces complete and support scenario-resolved dynamic weighted pools without automatic partial-analysis fallbacks.
|
|
13
|
+
- Keep explicit-file probability inspection narrow, bound retained source and immutable-game scan caches, and avoid full-workspace rescans when rendering inline or declared custom-pool analyses.
|
|
14
|
+
- Build event graphs from one merged source index, retain raw comparison sources instead of parsed scan snapshots, and route selector-scoped inspection and rendering through the focused event graph.
|
|
15
|
+
- Keep the public repository focused on the supported release by using one maintained branch and manually reviewed dependency updates.
|
|
16
|
+
|
|
7
17
|
## 2.8.11 - 2026-08-30
|
|
8
18
|
|
|
9
19
|
- Render vanilla HOI4 bitmap fonts with their native atlas masks, legacy page fallbacks, inverted-mask handling, declared glyph channels, crisp scaled sampling, borders, and localisation colours; qualify every shipped descriptor and base font family.
|
package/README.md
CHANGED
|
@@ -1,17 +1,45 @@
|
|
|
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, technology systems, AI weights, and MTTH timing and to inspect, create, or clean up focus trees, scripted GUIs, and maps.
|
|
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. It produces source-linked structural and visual evidence that agents can use inside a larger modding workflow.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
- Focus trees: inspect structure and references, render layouts, create trees, and reorganize existing branches.
|
|
8
|
-
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render value-driven variants, states, resolutions, hierarchy, and click regions; diagnose alignment, visibility, clipping, panel containment, and button-label centering; create or repair interface source.
|
|
8
|
+
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render value-driven variants, dynamic country flags, inline text icons, states, resolutions, hierarchy, and click regions; diagnose alignment, visibility, clipping, panel containment, and button-label centering; create or repair interface source.
|
|
9
9
|
- Maps: navigate the complete rendered map by ID or localised name; inspect provinces, states, regions, adjacency, supply, railways, and positions; create states and provinces; change IDs; and repair connected 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
12
|
- 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.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Real results
|
|
15
|
+
|
|
16
|
+
The GUI renderer resolves the mod and installed-game sources, fonts, localisation colours, sprites, scripted visibility, dynamic values, lists, controls, and panel composition. These pairs show the in-game surface beside the MCP result.
|
|
17
|
+
|
|
18
|
+
| Communist Insurgency in game | Communist Insurgency from MCP |
|
|
19
|
+
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
20
|
+
|  |  |
|
|
21
|
+
|
|
22
|
+
| Chaos Meter in game | Chaos Meter from MCP |
|
|
23
|
+
| ----------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
24
|
+
|  |  |
|
|
25
|
+
|
|
26
|
+
The same scenario-aware renderer can select mutually exclusive settings panes and populate plausible runtime values.
|
|
27
|
+
|
|
28
|
+
| Trigger Events settings | Advanced settings |
|
|
29
|
+
| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
30
|
+
|  |  |
|
|
31
|
+
|
|
32
|
+
The other workbenches return complete source-linked layouts and catalogs rather than prose-only summaries.
|
|
33
|
+
|
|
34
|
+
| Focus tree | Technology tree |
|
|
35
|
+
| ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
|
|
36
|
+
|  |  |
|
|
37
|
+
|
|
38
|
+
| Map | Event chain | Probability ranking |
|
|
39
|
+
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
40
|
+
|  |  |  |
|
|
41
|
+
|
|
42
|
+
## Use from a coding agent
|
|
15
43
|
|
|
16
44
|
Requires Node.js 22.19 or later in the Node 22 line, or Node.js 24.
|
|
17
45
|
|
|
@@ -19,15 +47,12 @@ Requires Node.js 22.19 or later in the Node 22 line, or Node.js 24.
|
|
|
19
47
|
npm install --global hoi4-agent-tools@latest
|
|
20
48
|
```
|
|
21
49
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
hoi4-agent-tools-setup --print-client-config
|
|
50
|
+
```toml
|
|
51
|
+
[mcp_servers.hoi4_agent_tools]
|
|
52
|
+
command = "hoi4-agent-tools.cmd"
|
|
28
53
|
```
|
|
29
54
|
|
|
30
|
-
|
|
55
|
+
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.
|
|
31
56
|
|
|
32
57
|
## Tools
|
|
33
58
|
|
package/dist/bin/stdio.js
CHANGED
|
@@ -8,10 +8,6 @@ async function main() {
|
|
|
8
8
|
const engine = await createEngine();
|
|
9
9
|
const context = {};
|
|
10
10
|
const server = createMcpServer(engine, context);
|
|
11
|
-
if (process.env.HOI4_AGENT_TOOLS_CHAOSX === '1') {
|
|
12
|
-
const { registerChaosxTools } = await import('../hoi4_agent_tools/mcp/tools/chaosx.js');
|
|
13
|
-
registerChaosxTools(server, engine, context);
|
|
14
|
-
}
|
|
15
11
|
server.server.onerror = (error) => {
|
|
16
12
|
const fatalInputError = error instanceof StdioFrameLimitError || error instanceof StdioInvalidUtf8Error;
|
|
17
13
|
const rejectedMessage = error instanceof StdioInvalidMessageError;
|
|
@@ -10,6 +10,9 @@ export const HTTP_DEFAULT_SESSION_EVENT_BYTES = 2_097_152;
|
|
|
10
10
|
export const WORKSPACE_MAX_REGISTRATIONS = 1_000;
|
|
11
11
|
const WORKSPACE_MAX_SOURCE_ROOTS = 16;
|
|
12
12
|
const WORKSPACE_MAX_PATHS = 1_000;
|
|
13
|
+
export const DEFAULT_SCAN_MAX_FILES = 100_000;
|
|
14
|
+
export const DEFAULT_SCAN_MAX_BYTES = 268_435_456;
|
|
15
|
+
export const DEFAULT_SCAN_MAX_FILE_BYTES = 67_108_864;
|
|
13
16
|
const allowedOriginSchema = z.url().refine((value) => {
|
|
14
17
|
const parsed = new URL(value);
|
|
15
18
|
return ((parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
|
|
@@ -128,9 +131,19 @@ export const serverConfigurationSchema = z
|
|
|
128
131
|
message: 'Server state root must be an absolute private path',
|
|
129
132
|
})
|
|
130
133
|
.optional(),
|
|
131
|
-
scanMaxFiles: z.number().int().min(1).max(1_000_000).default(
|
|
132
|
-
scanMaxBytes: z
|
|
133
|
-
|
|
134
|
+
scanMaxFiles: z.number().int().min(1).max(1_000_000).default(DEFAULT_SCAN_MAX_FILES),
|
|
135
|
+
scanMaxBytes: z
|
|
136
|
+
.number()
|
|
137
|
+
.int()
|
|
138
|
+
.min(1_048_576)
|
|
139
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
140
|
+
.default(DEFAULT_SCAN_MAX_BYTES),
|
|
141
|
+
scanMaxFileBytes: z
|
|
142
|
+
.number()
|
|
143
|
+
.int()
|
|
144
|
+
.min(65_536)
|
|
145
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
146
|
+
.default(DEFAULT_SCAN_MAX_FILE_BYTES),
|
|
134
147
|
artifactMaxBytes: z
|
|
135
148
|
.number()
|
|
136
149
|
.int()
|
|
@@ -8,6 +8,14 @@ function uniquePatterns(patterns) {
|
|
|
8
8
|
function englishLocalisationPatterns(roots) {
|
|
9
9
|
return [...under(roots, 'english/**/*.{yml,yaml}'), ...under(roots, '*.{yml,yaml}')];
|
|
10
10
|
}
|
|
11
|
+
export function probabilitySharedDefinitionPatterns() {
|
|
12
|
+
return uniquePatterns([
|
|
13
|
+
'common/scripted_effects/**/*.txt',
|
|
14
|
+
'common/scripted_triggers/**/*.txt',
|
|
15
|
+
'common/script_constants/**/*.txt',
|
|
16
|
+
'common/mtth/**/*.txt',
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
11
19
|
/** Sources required to import, link, lint, render, and rewrite focus content. */
|
|
12
20
|
export function focusDomainScanPatterns(workspace) {
|
|
13
21
|
const roots = workspace.registration.roots;
|
|
@@ -32,8 +40,9 @@ export function focusDomainScanPatterns(workspace) {
|
|
|
32
40
|
export function probabilityDomainScanPatterns(workspace, sourcePaths = []) {
|
|
33
41
|
const roots = workspace.registration.roots;
|
|
34
42
|
const normalizedSourcePaths = sourcePaths.map((sourcePath) => sourcePath.replace(/^.*?:/u, '').replaceAll('\\', '/').replace(/^\.\//u, ''));
|
|
43
|
+
if (normalizedSourcePaths.length > 0)
|
|
44
|
+
return uniquePatterns([...normalizedSourcePaths, ...probabilitySharedDefinitionPatterns()]);
|
|
35
45
|
return uniquePatterns([
|
|
36
|
-
...normalizedSourcePaths,
|
|
37
46
|
...under(roots.focus, '**/*.txt'),
|
|
38
47
|
'common/decisions/**/*.txt',
|
|
39
48
|
'common/technologies/**/*.txt',
|
|
@@ -49,18 +58,6 @@ export function probabilityDomainScanPatterns(workspace, sourcePaths = []) {
|
|
|
49
58
|
'events/**/*.txt',
|
|
50
59
|
]);
|
|
51
60
|
}
|
|
52
|
-
/** Text sources needed to discover country flags and leader portraits. */
|
|
53
|
-
export function countryAssetDiscoveryPatterns(workspace) {
|
|
54
|
-
const roots = workspace.registration.roots;
|
|
55
|
-
return uniquePatterns([
|
|
56
|
-
'history/countries/**/*.txt',
|
|
57
|
-
'common/characters/**/*.txt',
|
|
58
|
-
'common/scripted_effects/**/*.txt',
|
|
59
|
-
'events/**/*.txt',
|
|
60
|
-
...under(roots.interface, '**/*.gfx'),
|
|
61
|
-
...under(roots.gfx, '**/*.gfx'),
|
|
62
|
-
]);
|
|
63
|
-
}
|
|
64
61
|
export function exactChangedFilePatterns(relativePaths) {
|
|
65
62
|
return uniquePatterns(relativePaths.map((relativePath) => relativePath.replaceAll('\\', '/').replace(/^\.\//u, '')));
|
|
66
63
|
}
|
|
@@ -139,6 +139,9 @@ export class CoreEngine {
|
|
|
139
139
|
})}`;
|
|
140
140
|
let flight = this.#scanFlights.get(requestKey);
|
|
141
141
|
if (flight === undefined) {
|
|
142
|
+
for (const key of this.#scanCache.keys())
|
|
143
|
+
if (key.startsWith(`${workspaceId}:`) && !key.startsWith(`${requestKey}:`))
|
|
144
|
+
this.#scanCache.delete(key);
|
|
142
145
|
const controller = new AbortController();
|
|
143
146
|
const promise = (async () => {
|
|
144
147
|
return this.withScanAdmission(controller.signal, async () => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { open } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fg from 'fast-glob';
|
|
4
|
-
import { compareCodeUnits, sha256Bytes } from './canonical.js';
|
|
4
|
+
import { compareCodeUnits, hashCanonical, sha256Bytes } from './canonical.js';
|
|
5
|
+
import { DEFAULT_SCAN_MAX_BYTES, DEFAULT_SCAN_MAX_FILE_BYTES, DEFAULT_SCAN_MAX_FILES, } from './configuration.js';
|
|
5
6
|
import { ServiceError } from './result.js';
|
|
6
7
|
function normalizeRelative(value) {
|
|
7
8
|
return value.replaceAll('\\', '/').replace(/^\.\//u, '');
|
|
@@ -26,13 +27,14 @@ export class WorkspaceScanner {
|
|
|
26
27
|
serverMaxBytes;
|
|
27
28
|
serverMaxFileBytes;
|
|
28
29
|
#sourceCache = new Map();
|
|
30
|
+
#gameScanCache = new Map();
|
|
29
31
|
#sourceCacheBytes = 0;
|
|
30
32
|
#sourceCacheMaxBytes;
|
|
31
|
-
constructor(serverMaxFiles =
|
|
33
|
+
constructor(serverMaxFiles = DEFAULT_SCAN_MAX_FILES, serverMaxBytes = DEFAULT_SCAN_MAX_BYTES, serverMaxFileBytes = DEFAULT_SCAN_MAX_FILE_BYTES) {
|
|
32
34
|
this.serverMaxFiles = serverMaxFiles;
|
|
33
35
|
this.serverMaxBytes = serverMaxBytes;
|
|
34
36
|
this.serverMaxFileBytes = serverMaxFileBytes;
|
|
35
|
-
this.#sourceCacheMaxBytes = Math.max(serverMaxFileBytes, Math.min(
|
|
37
|
+
this.#sourceCacheMaxBytes = Math.max(serverMaxFileBytes, Math.min(134_217_728, serverMaxBytes));
|
|
36
38
|
}
|
|
37
39
|
cacheKey(absolutePath) {
|
|
38
40
|
const resolved = path.resolve(absolutePath);
|
|
@@ -53,6 +55,24 @@ export class WorkspaceScanner {
|
|
|
53
55
|
this.#sourceCacheBytes -= oldest[1].bytes.length;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
58
|
+
gameScanKey(workspace, root, options) {
|
|
59
|
+
return hashCanonical({
|
|
60
|
+
workspaceId: workspace.id,
|
|
61
|
+
root: root.path,
|
|
62
|
+
patterns: [...options.patterns].sort(compareCodeUnits),
|
|
63
|
+
ignore: [...(options.ignore ?? ['**/.hoi4-agent/**'])].sort(compareCodeUnits),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
retainGameScan(key, files) {
|
|
67
|
+
this.#gameScanCache.delete(key);
|
|
68
|
+
this.#gameScanCache.set(key, files.map((file) => ({ ...file })));
|
|
69
|
+
while (this.#gameScanCache.size > 8) {
|
|
70
|
+
const oldest = this.#gameScanCache.keys().next().value;
|
|
71
|
+
if (oldest === undefined)
|
|
72
|
+
break;
|
|
73
|
+
this.#gameScanCache.delete(oldest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
56
76
|
async scan(workspace, options) {
|
|
57
77
|
options.signal?.throwIfAborted();
|
|
58
78
|
const maxFiles = options.maxFiles ?? this.serverMaxFiles;
|
|
@@ -77,6 +97,29 @@ export class WorkspaceScanner {
|
|
|
77
97
|
let enumeratedFiles = 0;
|
|
78
98
|
for (const root of roots) {
|
|
79
99
|
options.signal?.throwIfAborted();
|
|
100
|
+
const gameScanKey = root.kind === 'game' ? this.gameScanKey(workspace, root, options) : undefined;
|
|
101
|
+
const cachedGameFiles = gameScanKey === undefined ? undefined : this.#gameScanCache.get(gameScanKey);
|
|
102
|
+
if (cachedGameFiles !== undefined && gameScanKey !== undefined) {
|
|
103
|
+
this.#gameScanCache.delete(gameScanKey);
|
|
104
|
+
this.#gameScanCache.set(gameScanKey, cachedGameFiles);
|
|
105
|
+
enumeratedFiles += cachedGameFiles.length;
|
|
106
|
+
if (enumeratedFiles > maxFiles)
|
|
107
|
+
throw new ServiceError('SCAN_FILE_LIMIT', 'Scan exceeds the configured file limit', {
|
|
108
|
+
files: enumeratedFiles,
|
|
109
|
+
limit: maxFiles,
|
|
110
|
+
});
|
|
111
|
+
for (const file of cachedGameFiles) {
|
|
112
|
+
totalBytes += file.size;
|
|
113
|
+
if (totalBytes > maxBytes)
|
|
114
|
+
throw new ServiceError('SCAN_BYTE_LIMIT', 'Scan exceeds the configured byte limit', {
|
|
115
|
+
bytes: totalBytes,
|
|
116
|
+
limit: maxBytes,
|
|
117
|
+
});
|
|
118
|
+
result.push({ ...file });
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const rootFiles = [];
|
|
80
123
|
const matches = fg.stream(options.patterns, {
|
|
81
124
|
cwd: root.path,
|
|
82
125
|
onlyFiles: true,
|
|
@@ -89,7 +132,10 @@ export class WorkspaceScanner {
|
|
|
89
132
|
options.signal?.throwIfAborted();
|
|
90
133
|
enumeratedFiles += 1;
|
|
91
134
|
if (enumeratedFiles > maxFiles) {
|
|
92
|
-
throw new ServiceError('SCAN_FILE_LIMIT', 'Scan exceeds the configured file limit'
|
|
135
|
+
throw new ServiceError('SCAN_FILE_LIMIT', 'Scan exceeds the configured file limit', {
|
|
136
|
+
files: enumeratedFiles,
|
|
137
|
+
limit: maxFiles,
|
|
138
|
+
});
|
|
93
139
|
}
|
|
94
140
|
const relativePath = normalizeRelative(String(match));
|
|
95
141
|
if (hiddenByReplacePath(workspace, root, relativePath))
|
|
@@ -103,7 +149,13 @@ export class WorkspaceScanner {
|
|
|
103
149
|
continue;
|
|
104
150
|
const remaining = maxBytes - totalBytes;
|
|
105
151
|
if (metadata.size > remaining || metadata.size > this.serverMaxFileBytes) {
|
|
106
|
-
throw new ServiceError('SCAN_BYTE_LIMIT', 'Scan exceeds the configured byte limit'
|
|
152
|
+
throw new ServiceError('SCAN_BYTE_LIMIT', 'Scan exceeds the configured byte limit', {
|
|
153
|
+
file: relativePath,
|
|
154
|
+
fileBytes: metadata.size,
|
|
155
|
+
bytes: totalBytes,
|
|
156
|
+
limit: maxBytes,
|
|
157
|
+
perFileLimit: this.serverMaxFileBytes,
|
|
158
|
+
});
|
|
107
159
|
}
|
|
108
160
|
const cached = this.#sourceCache.get(cacheKey);
|
|
109
161
|
const retained = cached?.size === metadata.size &&
|
|
@@ -124,7 +176,7 @@ export class WorkspaceScanner {
|
|
|
124
176
|
bytes,
|
|
125
177
|
});
|
|
126
178
|
totalBytes += bytes.length;
|
|
127
|
-
|
|
179
|
+
const scanned = {
|
|
128
180
|
absolutePath,
|
|
129
181
|
displayPath: `${rootLabel(root)}:${relativePath}`,
|
|
130
182
|
relativePath,
|
|
@@ -134,12 +186,16 @@ export class WorkspaceScanner {
|
|
|
134
186
|
modifiedMs: metadata.mtimeMs,
|
|
135
187
|
sha256,
|
|
136
188
|
bytes,
|
|
137
|
-
}
|
|
189
|
+
};
|
|
190
|
+
result.push(scanned);
|
|
191
|
+
rootFiles.push(scanned);
|
|
138
192
|
}
|
|
139
193
|
finally {
|
|
140
194
|
await handle.close();
|
|
141
195
|
}
|
|
142
196
|
}
|
|
197
|
+
if (gameScanKey !== undefined)
|
|
198
|
+
this.retainGameScan(gameScanKey, rootFiles);
|
|
143
199
|
}
|
|
144
200
|
result.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.relativePath, right.relativePath));
|
|
145
201
|
const groups = new Map();
|
|
@@ -175,19 +175,21 @@ function eventScanPatterns(workspace, focused = false) {
|
|
|
175
175
|
...under(roots.localisation, '*.{yml,yaml}'),
|
|
176
176
|
].sort(compareCodeUnits);
|
|
177
177
|
}
|
|
178
|
-
async function scanEventSources(engine, workspaceId, workspace, focused = false,
|
|
178
|
+
async function scanEventSources(engine, workspaceId, workspace, focused = false, signal) {
|
|
179
179
|
const patterns = eventScanPatterns(workspace, focused);
|
|
180
180
|
const gamePatterns = eventScanPatterns(workspace, focused);
|
|
181
181
|
const sourceKinds = ['mod', 'dependency', 'fixture'];
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
182
|
+
const files = await engine.scanner.scan(workspace, {
|
|
183
|
+
patterns,
|
|
184
|
+
rootKinds: sourceKinds,
|
|
185
|
+
...(signal === undefined ? {} : { signal }),
|
|
186
|
+
});
|
|
187
|
+
if (workspace.roots.some(({ kind }) => kind === 'game'))
|
|
188
|
+
files.push(...(await engine.scanner.scan(workspace, {
|
|
189
|
+
patterns: gamePatterns,
|
|
190
|
+
rootKinds: ['game'],
|
|
191
|
+
...(signal === undefined ? {} : { signal }),
|
|
192
|
+
})));
|
|
191
193
|
recomputeShadowing(files);
|
|
192
194
|
files.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
193
195
|
const index = engine.indexFiles(files);
|
|
@@ -196,10 +198,10 @@ async function scanEventSources(engine, workspaceId, workspace, focused = false,
|
|
|
196
198
|
revision: snapshotRevision(files),
|
|
197
199
|
files,
|
|
198
200
|
index,
|
|
199
|
-
complete:
|
|
201
|
+
complete: index.complete,
|
|
200
202
|
skippedSourceCount: index.skippedSourceCount,
|
|
201
203
|
skippedSources: index.skippedSources,
|
|
202
|
-
diagnostics:
|
|
204
|
+
diagnostics: index.diagnostics,
|
|
203
205
|
};
|
|
204
206
|
}
|
|
205
207
|
function rememberGraph(state, workspaceId, graph) {
|
|
@@ -707,18 +709,17 @@ export class EventChainViewer {
|
|
|
707
709
|
if (options.refresh !== true && cached?.generation === generation) {
|
|
708
710
|
return cached.graph;
|
|
709
711
|
}
|
|
710
|
-
|
|
711
|
-
this.#state.current.get(eventGraphCacheKey(workspaceId,
|
|
712
|
-
|
|
713
|
-
|
|
712
|
+
if (options.refresh !== true && requestedAnalysisMode === 'focused') {
|
|
713
|
+
const fullSibling = this.#state.current.get(eventGraphCacheKey(workspaceId, cacheProjectHelpers, 'full')) ??
|
|
714
|
+
this.#state.current.get(eventGraphCacheKey(workspaceId, !cacheProjectHelpers, 'full'));
|
|
715
|
+
if (fullSibling?.generation === generation)
|
|
716
|
+
return fullSibling.graph;
|
|
717
|
+
}
|
|
714
718
|
const scanFocused = requestedAnalysisMode === 'focused';
|
|
715
|
-
const snapshot =
|
|
716
|
-
? sibling.snapshot
|
|
717
|
-
: await scanEventSources(this.engine, workspaceId, workspace, scanFocused, options.principal, options.signal);
|
|
719
|
+
const snapshot = await scanEventSources(this.engine, workspaceId, workspace, scanFocused, options.signal);
|
|
718
720
|
options.signal?.throwIfAborted();
|
|
719
|
-
if (cached?.
|
|
721
|
+
if (cached?.graph.revision === snapshot.revision && cached.generation === generation)
|
|
720
722
|
return cached.graph;
|
|
721
|
-
}
|
|
722
723
|
const buildAnalysisMode = requestedAnalysisMode;
|
|
723
724
|
const projectHelpers = buildAnalysisMode === 'focused' ? false : requestedProjectHelpers;
|
|
724
725
|
const graph = buildEventGraph(snapshot, {
|
|
@@ -728,13 +729,17 @@ export class EventChainViewer {
|
|
|
728
729
|
projectHelpers,
|
|
729
730
|
analysisMode: buildAnalysisMode,
|
|
730
731
|
});
|
|
731
|
-
this.#state.current.set(cacheKey, { generation, snapshot, graph });
|
|
732
|
+
this.#state.current.set(cacheKey, { generation, files: snapshot.files, graph });
|
|
732
733
|
if (buildAnalysisMode === 'full')
|
|
733
734
|
rememberGraph(this.#state, workspaceId, graph);
|
|
734
735
|
return graph;
|
|
735
736
|
}
|
|
736
737
|
async inspect(input) {
|
|
737
738
|
input.signal?.throwIfAborted();
|
|
739
|
+
const focused = input.mode === 'trace' ||
|
|
740
|
+
input.mode === 'explain_path' ||
|
|
741
|
+
(input.selector !== undefined &&
|
|
742
|
+
(input.mode === 'scan' || input.mode === 'roots' || input.mode === 'lint'));
|
|
738
743
|
const scannedGraph = await this.scan(input.workspaceId, {
|
|
739
744
|
refresh: input.refresh ?? input.mode === 'scan',
|
|
740
745
|
// Focused routes traverse helper nodes directly and collapse only the
|
|
@@ -743,6 +748,7 @@ export class EventChainViewer {
|
|
|
743
748
|
input.mode !== 'roots' &&
|
|
744
749
|
input.mode !== 'trace' &&
|
|
745
750
|
input.mode !== 'explain_path',
|
|
751
|
+
analysisMode: focused ? 'focused' : 'full',
|
|
746
752
|
...(input.principal === undefined ? {} : { principal: input.principal }),
|
|
747
753
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
748
754
|
});
|
|
@@ -923,6 +929,7 @@ export class EventChainViewer {
|
|
|
923
929
|
const graph = await this.scan(input.workspaceId, {
|
|
924
930
|
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
925
931
|
projectHelpers: true,
|
|
932
|
+
analysisMode: input.selector === undefined ? 'full' : 'focused',
|
|
926
933
|
...(input.principal === undefined ? {} : { principal: input.principal }),
|
|
927
934
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
928
935
|
});
|
|
@@ -1135,7 +1142,7 @@ export class EventChainViewer {
|
|
|
1135
1142
|
if (new Set(paths.map((value) => value.toLowerCase())).size !== paths.length) {
|
|
1136
1143
|
throw new ServiceError('EVENT_PROPOSED_PATH_DUPLICATE', 'Proposed source paths must be unique');
|
|
1137
1144
|
}
|
|
1138
|
-
const files = cached.
|
|
1145
|
+
const files = cached.files.map((file) => ({
|
|
1139
1146
|
...file,
|
|
1140
1147
|
bytes: Buffer.from(file.bytes),
|
|
1141
1148
|
}));
|