hoi4-agent-tools 2.3.13 → 2.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 +16 -0
- package/README.md +28 -28
- package/dist/hoi4_agent_tools/gui/layout.js +12 -6
- package/dist/hoi4_agent_tools/gui/scenario.js +10 -0
- package/dist/hoi4_agent_tools/gui/studio.js +113 -8
- package/dist/hoi4_agent_tools/gui/validators.js +198 -11
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +8 -1
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +13 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +52 -6
- package/dist/hoi4_agent_tools/probability/service.js +81 -7
- package/dist/hoi4_agent_tools/probability/source-analysis.js +83 -32
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +25 -5
- package/docs/probability.md +3 -1
- package/docs/setup.md +1 -1
- package/package.json +13 -12
- package/schemas/configuration.schema.json +1 -1
- 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 +57 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- 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
|
@@ -49,6 +49,7 @@ const guiRenderOutputSchema = strictOperationResultSchema(z
|
|
|
49
49
|
.strict())
|
|
50
50
|
.max(64),
|
|
51
51
|
stateCount: nonNegativeIntegerSchema,
|
|
52
|
+
scenarioCount: nonNegativeIntegerSchema,
|
|
52
53
|
resolutionCount: nonNegativeIntegerSchema,
|
|
53
54
|
comparison: z
|
|
54
55
|
.object({ changedPixels: nonNegativeIntegerSchema, changedRatio: z.number().min(0).max(1) })
|
|
@@ -281,6 +282,9 @@ export function registerGuiTools(server, engine, context) {
|
|
|
281
282
|
...(uiScale === undefined ? {} : { uiScale }),
|
|
282
283
|
})),
|
|
283
284
|
}),
|
|
285
|
+
...(input.relatedScenarios === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: { relatedScenarios: input.relatedScenarios }),
|
|
284
288
|
...(input.comparisonScenario === undefined
|
|
285
289
|
? {}
|
|
286
290
|
: { comparisonScenario: input.comparisonScenario }),
|
|
@@ -300,6 +304,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
300
304
|
height,
|
|
301
305
|
})),
|
|
302
306
|
stateCount: rendered.stateScenes.length,
|
|
307
|
+
scenarioCount: rendered.scenarioScenes.length,
|
|
303
308
|
resolutionCount: rendered.resolutionScenes.length,
|
|
304
309
|
comparison: {
|
|
305
310
|
changedPixels: rendered.comparison.changedPixels,
|
|
@@ -327,10 +332,11 @@ export function registerGuiTools(server, engine, context) {
|
|
|
327
332
|
.extend({
|
|
328
333
|
states: z.array(previewStateSchema).max(14).optional(),
|
|
329
334
|
resolutions: z.array(resolutionSchema).min(1).max(16).optional(),
|
|
335
|
+
relatedScenarios: z.array(compactGuiScenarioSchema).max(32).optional(),
|
|
330
336
|
comparisonScenario: compactGuiScenarioSchema.optional(),
|
|
331
337
|
})
|
|
332
338
|
.strict()
|
|
333
|
-
.superRefine(({ scenario, states, resolutions }, context) => {
|
|
339
|
+
.superRefine(({ scenario, states, resolutions, relatedScenarios }, context) => {
|
|
334
340
|
const budget = new RenderBudget();
|
|
335
341
|
try {
|
|
336
342
|
const stateCount = states?.length ?? 14;
|
|
@@ -345,6 +351,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
345
351
|
]) {
|
|
346
352
|
budget.reserve(resolution.width, resolution.height, 'GUI resolution variant');
|
|
347
353
|
}
|
|
354
|
+
const scenarioCount = 1 + (relatedScenarios?.length ?? 0);
|
|
348
355
|
const stateColumns = Math.min(3, Math.max(1, stateCount));
|
|
349
356
|
const stateRows = Math.max(1, Math.ceil(stateCount / stateColumns));
|
|
350
357
|
budget.reserve(stateColumns * 420, 46 + stateRows * 280, 'GUI state gallery');
|
|
@@ -352,6 +359,10 @@ export function registerGuiTools(server, engine, context) {
|
|
|
352
359
|
const resolutionColumns = Math.min(3, Math.max(1, resolutionCount));
|
|
353
360
|
const resolutionRows = Math.max(1, Math.ceil(resolutionCount / resolutionColumns));
|
|
354
361
|
budget.reserve(resolutionColumns * 420, 46 + resolutionRows * 280, 'GUI resolution gallery');
|
|
362
|
+
const scenarioBudget = new RenderBudget();
|
|
363
|
+
const scenarioColumns = Math.min(3, Math.max(1, scenarioCount));
|
|
364
|
+
const scenarioRows = Math.max(1, Math.ceil(scenarioCount / scenarioColumns));
|
|
365
|
+
scenarioBudget.reserve(scenarioColumns * 420, 46 + scenarioRows * 280, 'GUI scripted scenario gallery');
|
|
355
366
|
}
|
|
356
367
|
catch (error) {
|
|
357
368
|
if (error instanceof ServiceError) {
|
|
@@ -363,7 +374,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
363
374
|
});
|
|
364
375
|
server.registerTool('hoi4.gui_render', {
|
|
365
376
|
title: 'Render scripted GUI artifacts',
|
|
366
|
-
description: 'Render deterministic
|
|
377
|
+
description: 'Render deterministic GUI review artifacts, named value-driven scenario variants, generic states, resolutions, click regions, hierarchy, comparisons, and layout diagnostics.',
|
|
367
378
|
inputSchema: renderInput,
|
|
368
379
|
outputSchema: guiRenderOutputSchema,
|
|
369
380
|
annotations: artifactProducing,
|
|
@@ -39,10 +39,10 @@ const inspectInput = z
|
|
|
39
39
|
})
|
|
40
40
|
.strict()
|
|
41
41
|
.superRefine((value, context) => {
|
|
42
|
-
if (
|
|
42
|
+
if (value.adapter !== undefined && value.source === undefined)
|
|
43
43
|
context.addIssue({
|
|
44
44
|
code: 'custom',
|
|
45
|
-
message: '
|
|
45
|
+
message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
const evaluateInput = z.object(commonShape).strict();
|
|
@@ -135,9 +135,32 @@ const inspectDataSchema = z
|
|
|
135
135
|
.object({
|
|
136
136
|
adapters: nonNegativeIntegerSchema,
|
|
137
137
|
adapterId: z.string().max(256).optional(),
|
|
138
|
+
requestedAdapter: z.string().max(256).optional(),
|
|
139
|
+
suggestedAdapter: z.string().max(256).optional(),
|
|
140
|
+
discoveryReason: z
|
|
141
|
+
.enum([
|
|
142
|
+
'source_inventory',
|
|
143
|
+
'requested_adapter_empty',
|
|
144
|
+
'identifier_not_found',
|
|
145
|
+
'candidate_pool_not_found',
|
|
146
|
+
'no_weighted_surfaces',
|
|
147
|
+
])
|
|
148
|
+
.optional(),
|
|
138
149
|
sourceRevision: sha256Schema.optional(),
|
|
139
150
|
sourceHash: sha256Schema.optional(),
|
|
140
151
|
candidates: nonNegativeIntegerSchema,
|
|
152
|
+
availableCandidates: nonNegativeIntegerSchema,
|
|
153
|
+
availableAdapters: z
|
|
154
|
+
.array(z
|
|
155
|
+
.object({
|
|
156
|
+
adapterId: z.string().max(256),
|
|
157
|
+
candidates: nonNegativeIntegerSchema,
|
|
158
|
+
identifierMatches: nonNegativeIntegerSchema,
|
|
159
|
+
candidatePoolMatches: nonNegativeIntegerSchema,
|
|
160
|
+
})
|
|
161
|
+
.strict())
|
|
162
|
+
.max(10),
|
|
163
|
+
candidateExamples: z.array(z.string().max(512)).max(10),
|
|
141
164
|
poolComplete: z.boolean().optional(),
|
|
142
165
|
requiredInputs: nonNegativeIntegerSchema,
|
|
143
166
|
unresolved: nonNegativeIntegerSchema,
|
|
@@ -211,7 +234,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
211
234
|
const analyzer = new ProbabilityAnalyzer(engine);
|
|
212
235
|
server.registerTool('hoi4.probability_inspect', {
|
|
213
236
|
title: 'Inspect AI and MTTH weighted logic',
|
|
214
|
-
description: 'List versioned adapters or discover weighted
|
|
237
|
+
description: 'List versioned adapters, or provide a source alone to discover compatible weighted adapters, candidate pools, capabilities, provenance, and unsupported constructs before narrowing the request.',
|
|
215
238
|
inputSchema: inspectInput,
|
|
216
239
|
outputSchema: inspectOutput,
|
|
217
240
|
annotations: readOnly,
|
|
@@ -231,14 +254,37 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
231
254
|
sourceHash: inspected.surface.sourceHash,
|
|
232
255
|
poolComplete: inspected.surface.poolComplete,
|
|
233
256
|
}),
|
|
257
|
+
...(inspected.discovery === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: {
|
|
260
|
+
...(inspected.discovery.requestedAdapter === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { requestedAdapter: inspected.discovery.requestedAdapter }),
|
|
263
|
+
...(inspected.discovery.suggestedAdapter === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { suggestedAdapter: inspected.discovery.suggestedAdapter }),
|
|
266
|
+
discoveryReason: inspected.discovery.reason,
|
|
267
|
+
sourceRevision: inspected.discovery.sourceRevision,
|
|
268
|
+
sourceHash: inspected.discovery.sourceHash,
|
|
269
|
+
}),
|
|
234
270
|
candidates: inspected.surface?.candidateCount ?? 0,
|
|
271
|
+
availableCandidates: inspected.discovery?.availableAdapters.reduce((sum, { candidateCount }) => sum + candidateCount, 0) ?? 0,
|
|
272
|
+
availableAdapters: inspected.discovery?.availableAdapters.map((available) => ({
|
|
273
|
+
adapterId: available.adapterId,
|
|
274
|
+
candidates: available.candidateCount,
|
|
275
|
+
identifierMatches: available.identifierMatchCount,
|
|
276
|
+
candidatePoolMatches: available.candidatePoolMatchCount,
|
|
277
|
+
})) ?? [],
|
|
278
|
+
candidateExamples: inspected.discovery?.exampleCandidateIds ?? [],
|
|
235
279
|
requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
|
|
236
280
|
unresolved: inspected.surface?.unsupported.length ?? 0,
|
|
237
281
|
});
|
|
238
282
|
result.code =
|
|
239
|
-
inspected.surface
|
|
240
|
-
? '
|
|
241
|
-
:
|
|
283
|
+
inspected.surface !== undefined
|
|
284
|
+
? 'PROBABILITY_SOURCE_INSPECTED'
|
|
285
|
+
: inspected.discovery !== undefined
|
|
286
|
+
? 'PROBABILITY_SOURCE_DISCOVERED'
|
|
287
|
+
: 'PROBABILITY_ADAPTERS_LISTED';
|
|
242
288
|
result.artifacts = inspected.artifacts;
|
|
243
289
|
setInlineFilesScanned(result, inspected.filesScanned);
|
|
244
290
|
await progress.report(2, 2, 'Weighted source inspection complete');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { canonicalJson, hashCanonical } from '../core/canonical.js';
|
|
3
|
+
import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
4
4
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
5
5
|
import { probabilityDomainScanPatterns } from '../core/domain-scan-patterns.js';
|
|
6
6
|
import { publicArtifactLink } from '../core/artifacts.js';
|
|
@@ -13,7 +13,7 @@ import { VERIFIED_GAME_CHECKSUM, VERIFIED_GAME_RAW_VERSION, VERIFIED_GAME_VERSIO
|
|
|
13
13
|
import { renderProbabilityResult } from './render.js';
|
|
14
14
|
import { analyzeSequence } from './sequence.js';
|
|
15
15
|
import { simulateSurface } from './simulation.js';
|
|
16
|
-
import { discoverWeightedSurface } from './source-analysis.js';
|
|
16
|
+
import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
|
|
17
17
|
import { sweepSurface } from './sweep.js';
|
|
18
18
|
const states = new WeakMap();
|
|
19
19
|
function analyzerState(engine) {
|
|
@@ -775,18 +775,92 @@ export class ProbabilityAnalyzer {
|
|
|
775
775
|
return complete;
|
|
776
776
|
}
|
|
777
777
|
async inspect(context, adapter, source, candidatePool = []) {
|
|
778
|
-
if (
|
|
778
|
+
if (source === undefined)
|
|
779
779
|
return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
|
|
780
|
-
const gameVersionVerification = await this.verifyGameVersion(context, adapter);
|
|
781
780
|
const snapshot = await this.scan(context, [source.path]);
|
|
782
|
-
const
|
|
781
|
+
const storeDiscovery = async (inventory, reason) => {
|
|
782
|
+
const availableAdapters = inventory.adapters.filter(({ candidateCount }) => candidateCount > 0);
|
|
783
|
+
const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) => source.identifier !== undefined && identifierMatchCount > 0);
|
|
784
|
+
const matchingPool = availableAdapters.filter(({ candidatePoolMatchCount }) => candidatePool.length > 0 && candidatePoolMatchCount > 0);
|
|
785
|
+
const requested = availableAdapters.find(({ adapterId }) => adapterId === adapter);
|
|
786
|
+
const ranked = [
|
|
787
|
+
...(matchingIdentifier.length > 0
|
|
788
|
+
? matchingIdentifier
|
|
789
|
+
: matchingPool.length > 0
|
|
790
|
+
? matchingPool
|
|
791
|
+
: requested === undefined
|
|
792
|
+
? availableAdapters
|
|
793
|
+
: [requested]),
|
|
794
|
+
].sort((left, right) => right.identifierMatchCount - left.identifierMatchCount ||
|
|
795
|
+
right.candidatePoolMatchCount - left.candidatePoolMatchCount ||
|
|
796
|
+
right.candidateCount - left.candidateCount ||
|
|
797
|
+
compareCodeUnits(left.adapterId, right.adapterId));
|
|
798
|
+
const suggested = ranked[0];
|
|
799
|
+
const discovery = {
|
|
800
|
+
...(adapter === undefined ? {} : { requestedAdapter: adapter }),
|
|
801
|
+
reason,
|
|
802
|
+
sourceRevision: inventory.sourceRevision,
|
|
803
|
+
sourceHash: inventory.sourceHash,
|
|
804
|
+
availableAdapters,
|
|
805
|
+
...(suggested === undefined ? {} : { suggestedAdapter: suggested.adapterId }),
|
|
806
|
+
exampleCandidateIds: suggested?.exampleCandidateIds ?? [],
|
|
807
|
+
};
|
|
808
|
+
const report = {
|
|
809
|
+
schemaVersion: 'probability-inspection.v2',
|
|
810
|
+
workspaceId: context.workspaceId,
|
|
811
|
+
discovery,
|
|
812
|
+
};
|
|
813
|
+
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
814
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${inventory.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
815
|
+
kind: 'probability-inspection',
|
|
816
|
+
toolVersion: PACKAGE_VERSION,
|
|
817
|
+
schemaVersion: 'probability-inspection.v2',
|
|
818
|
+
sourceHashes: { aggregate: inventory.sourceHash },
|
|
819
|
+
}, 'Weighted-source adapter inventory and candidate examples', context.signal);
|
|
820
|
+
return {
|
|
821
|
+
adapters: probabilityAdapters(),
|
|
822
|
+
discovery,
|
|
823
|
+
artifacts: [publicArtifactLink(artifact)],
|
|
824
|
+
filesScanned: inventory.filesScanned,
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
if (adapter === undefined) {
|
|
828
|
+
const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
|
|
829
|
+
return storeDiscovery(inventory, inventory.adapters.some(({ candidateCount }) => candidateCount > 0)
|
|
830
|
+
? 'source_inventory'
|
|
831
|
+
: 'no_weighted_surfaces');
|
|
832
|
+
}
|
|
833
|
+
const gameVersionVerification = await this.verifyGameVersion(context, adapter);
|
|
834
|
+
let surface;
|
|
835
|
+
try {
|
|
836
|
+
surface = discoverWeightedSurface(snapshot, adapter, source, candidatePool);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
if (!(error instanceof ServiceError) ||
|
|
840
|
+
(error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
|
|
841
|
+
error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
|
|
842
|
+
throw error;
|
|
843
|
+
const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
|
|
844
|
+
const requested = inventory.adapters.find(({ adapterId }) => adapterId === adapter);
|
|
845
|
+
const reason = error.code === 'PROBABILITY_IDENTIFIER_NOT_FOUND'
|
|
846
|
+
? 'identifier_not_found'
|
|
847
|
+
: requested !== undefined &&
|
|
848
|
+
requested.candidateCount > 0 &&
|
|
849
|
+
candidatePool.length > 0 &&
|
|
850
|
+
requested.candidatePoolMatchCount === 0
|
|
851
|
+
? 'candidate_pool_not_found'
|
|
852
|
+
: inventory.adapters.every(({ candidateCount }) => candidateCount === 0)
|
|
853
|
+
? 'no_weighted_surfaces'
|
|
854
|
+
: 'requested_adapter_empty';
|
|
855
|
+
return storeDiscovery(inventory, reason);
|
|
856
|
+
}
|
|
783
857
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
784
858
|
const inspectedCandidates = evaluateExactCandidates(surface, { id: 'inspection', state: {} }, definitions);
|
|
785
859
|
const requiredInputs = [
|
|
786
860
|
...new Set(inspectedCandidates.flatMap(({ unresolved }) => unresolved.flatMap(({ path }) => (path === undefined ? [] : [path])))),
|
|
787
861
|
].sort();
|
|
788
862
|
const report = {
|
|
789
|
-
schemaVersion: 'probability-inspection.
|
|
863
|
+
schemaVersion: 'probability-inspection.v2',
|
|
790
864
|
workspaceId: context.workspaceId,
|
|
791
865
|
surface: {
|
|
792
866
|
id: surface.id,
|
|
@@ -826,7 +900,7 @@ export class ProbabilityAnalyzer {
|
|
|
826
900
|
const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${surface.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
827
901
|
kind: 'probability-inspection',
|
|
828
902
|
toolVersion: PACKAGE_VERSION,
|
|
829
|
-
schemaVersion: 'probability-inspection.
|
|
903
|
+
schemaVersion: 'probability-inspection.v2',
|
|
830
904
|
sourceHashes: { aggregate: surface.sourceHash },
|
|
831
905
|
}, 'Discovered weighted surfaces and adapter capabilities', context.signal);
|
|
832
906
|
return {
|
|
@@ -2,6 +2,18 @@ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
|
2
2
|
import { ServiceError } from '../core/result.js';
|
|
3
3
|
import { assignments, astPathFor, childBlocks, firstScalar, nodeLocation, parseClausewitz, } from '../core/source/index.js';
|
|
4
4
|
import { probabilityAdapter } from './adapters.js';
|
|
5
|
+
const sourceBackedAdapterIds = [
|
|
6
|
+
'event_mean_time_to_happen',
|
|
7
|
+
'event_option_ai_chance',
|
|
8
|
+
'decision_ai_will_do',
|
|
9
|
+
'mission_ai_will_do',
|
|
10
|
+
'national_focus_ai_will_do',
|
|
11
|
+
'technology_ai_will_do',
|
|
12
|
+
'doctrine_ai_will_do',
|
|
13
|
+
'direct_random',
|
|
14
|
+
'random_list',
|
|
15
|
+
'ai_strategy_factor',
|
|
16
|
+
];
|
|
5
17
|
const eventKeys = new Set([
|
|
6
18
|
'country_event',
|
|
7
19
|
'news_event',
|
|
@@ -116,6 +128,9 @@ function focusCandidates(context) {
|
|
|
116
128
|
return output;
|
|
117
129
|
}
|
|
118
130
|
function decisionCandidates(context, adapterId) {
|
|
131
|
+
const sourcePath = normalized(context.file.relativePath);
|
|
132
|
+
if (context.file.absolutePath !== '<inline>' && !sourcePath.startsWith('common/decisions/'))
|
|
133
|
+
return [];
|
|
119
134
|
const output = [];
|
|
120
135
|
const ignored = new Set([
|
|
121
136
|
'allowed',
|
|
@@ -131,6 +146,10 @@ function decisionCandidates(context, adapterId) {
|
|
|
131
146
|
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
132
147
|
if (assignment.value.type !== 'block' ||
|
|
133
148
|
ancestors.length !== 1 ||
|
|
149
|
+
ancestors[0].key === 'focus_tree' ||
|
|
150
|
+
ancestors[0].key === 'continuous_focus_palette' ||
|
|
151
|
+
ancestors[0].key === 'technologies' ||
|
|
152
|
+
eventKeys.has(ancestors[0].key) ||
|
|
134
153
|
ignored.has(assignment.key.value))
|
|
135
154
|
continue;
|
|
136
155
|
const mission = firstScalar(assignment.value, 'is_mission')?.value === 'yes' ||
|
|
@@ -353,42 +372,74 @@ function samePool(candidate, selected) {
|
|
|
353
372
|
return candidate.metadata.randomListId === selected.metadata.randomListId;
|
|
354
373
|
return candidate.id === selected.id;
|
|
355
374
|
}
|
|
375
|
+
function candidatesAtSourceLine(candidates, adapterId, line) {
|
|
376
|
+
if (line === undefined)
|
|
377
|
+
return candidates;
|
|
378
|
+
let selected = candidates.filter(({ metadata, provenance: [first] }) => {
|
|
379
|
+
const location = first?.location;
|
|
380
|
+
const poolStartLine = metadata.poolStartLine;
|
|
381
|
+
const poolEndLine = metadata.poolEndLine;
|
|
382
|
+
return ((location !== undefined && line >= location.start.line && line <= location.end.line) ||
|
|
383
|
+
(typeof poolStartLine === 'number' &&
|
|
384
|
+
typeof poolEndLine === 'number' &&
|
|
385
|
+
line >= poolStartLine &&
|
|
386
|
+
line <= poolEndLine));
|
|
387
|
+
});
|
|
388
|
+
if (adapterId !== 'event_option_ai_chance' && adapterId !== 'random_list')
|
|
389
|
+
return selected;
|
|
390
|
+
const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
|
|
391
|
+
const pools = new Map();
|
|
392
|
+
for (const candidate of selected) {
|
|
393
|
+
const start = candidate.metadata.poolStartLine;
|
|
394
|
+
const end = candidate.metadata.poolEndLine;
|
|
395
|
+
const span = typeof start === 'number' && typeof end === 'number' ? end - start : Number.MAX_SAFE_INTEGER;
|
|
396
|
+
const key = candidate.metadata[poolProperty];
|
|
397
|
+
const existing = pools.get(key);
|
|
398
|
+
if (existing === undefined || span < existing.span)
|
|
399
|
+
pools.set(key, { span });
|
|
400
|
+
}
|
|
401
|
+
const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
|
|
402
|
+
if (selectedPool !== undefined)
|
|
403
|
+
selected = selected.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
|
|
404
|
+
return selected;
|
|
405
|
+
}
|
|
406
|
+
export function inventoryWeightedSurfaces(snapshot, source, candidatePool = [], adapterIds = sourceBackedAdapterIds) {
|
|
407
|
+
const contexts = sourceContexts(snapshot, source);
|
|
408
|
+
const wanted = new Set(candidatePool);
|
|
409
|
+
const adapters = adapterIds
|
|
410
|
+
.filter((adapterId) => adapterId !== 'custom_weighted_pool')
|
|
411
|
+
.map((adapterId) => {
|
|
412
|
+
const candidates = candidatesAtSourceLine(contexts.flatMap((context) => candidatesFor(context, adapterId)), adapterId, source.line).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
413
|
+
return {
|
|
414
|
+
adapterId,
|
|
415
|
+
candidateCount: candidates.length,
|
|
416
|
+
identifierMatchCount: source.identifier === undefined
|
|
417
|
+
? candidates.length
|
|
418
|
+
: candidates.filter((candidate) => identifierMatches(candidate, source.identifier))
|
|
419
|
+
.length,
|
|
420
|
+
candidatePoolMatchCount: candidatePool.length === 0
|
|
421
|
+
? candidates.length
|
|
422
|
+
: candidates.filter(({ id }) => wanted.has(id)).length,
|
|
423
|
+
exampleCandidateIds: [...new Set(candidates.map(({ id }) => id))].slice(0, 10),
|
|
424
|
+
sourcePaths: [
|
|
425
|
+
...new Set(candidates.flatMap(({ provenance }) => provenance.map(({ path }) => path))),
|
|
426
|
+
].sort(compareCodeUnits),
|
|
427
|
+
};
|
|
428
|
+
})
|
|
429
|
+
.sort((left, right) => compareCodeUnits(left.adapterId, right.adapterId));
|
|
430
|
+
const hashes = Object.fromEntries(contexts.map(({ file }) => [file.displayPath, file.sha256]));
|
|
431
|
+
return {
|
|
432
|
+
sourceRevision: snapshot.revision,
|
|
433
|
+
sourceHash: hashCanonical(hashes),
|
|
434
|
+
filesScanned: contexts.map(({ file }) => file.displayPath),
|
|
435
|
+
adapters,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
356
438
|
export function discoverWeightedSurface(snapshot, adapterId, source, candidatePool = []) {
|
|
357
439
|
const contexts = sourceContexts(snapshot, source);
|
|
358
440
|
let candidates = contexts.flatMap((context) => candidatesFor(context, adapterId));
|
|
359
441
|
candidates.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
360
|
-
|
|
361
|
-
candidates = candidates.filter(({ metadata, provenance: [first] }) => {
|
|
362
|
-
const location = first?.location;
|
|
363
|
-
const poolStartLine = metadata.poolStartLine;
|
|
364
|
-
const poolEndLine = metadata.poolEndLine;
|
|
365
|
-
return ((location !== undefined &&
|
|
366
|
-
source.line >= location.start.line &&
|
|
367
|
-
source.line <= location.end.line) ||
|
|
368
|
-
(typeof poolStartLine === 'number' &&
|
|
369
|
-
typeof poolEndLine === 'number' &&
|
|
370
|
-
source.line >= poolStartLine &&
|
|
371
|
-
source.line <= poolEndLine));
|
|
372
|
-
});
|
|
373
|
-
if (adapterId === 'event_option_ai_chance' || adapterId === 'random_list') {
|
|
374
|
-
const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
|
|
375
|
-
const pools = new Map();
|
|
376
|
-
for (const candidate of candidates) {
|
|
377
|
-
const start = candidate.metadata.poolStartLine;
|
|
378
|
-
const end = candidate.metadata.poolEndLine;
|
|
379
|
-
const span = typeof start === 'number' && typeof end === 'number'
|
|
380
|
-
? end - start
|
|
381
|
-
: Number.MAX_SAFE_INTEGER;
|
|
382
|
-
const key = candidate.metadata[poolProperty];
|
|
383
|
-
const existing = pools.get(key);
|
|
384
|
-
if (existing === undefined || span < existing.span)
|
|
385
|
-
pools.set(key, { span });
|
|
386
|
-
}
|
|
387
|
-
const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
|
|
388
|
-
if (selectedPool !== undefined)
|
|
389
|
-
candidates = candidates.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
442
|
+
candidates = candidatesAtSourceLine(candidates, adapterId, source.line);
|
|
392
443
|
const localCategoricalPool = adapterId === 'event_option_ai_chance' || adapterId === 'random_list';
|
|
393
444
|
let completeLocalPoolIds;
|
|
394
445
|
if (source.identifier !== undefined) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export const PACKAGE_NAME = 'hoi4-agent-tools';
|
|
3
|
-
export const PACKAGE_VERSION = '2.
|
|
3
|
+
export const PACKAGE_VERSION = '2.5.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/focus.md
CHANGED
|
@@ -44,7 +44,7 @@ If malformed or unsupported script makes a requested change ambiguous, the rewri
|
|
|
44
44
|
|
|
45
45
|
## What inspection checks
|
|
46
46
|
|
|
47
|
-
Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. Renders are offline review artifacts, not game screenshots.
|
|
47
|
+
Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. National-tree inspection also returns the exact `continuous_focus_position` coordinates and linked palette IDs in its compact result, so an agent can identify and move the continuous-focus area without opening the full artifact. Renders are offline review artifacts, not game screenshots.
|
|
48
48
|
|
|
49
49
|
`hoi4.focus_render` is the normal structural view and writes complete HTML, SVG, JSON, and source-map artifacts without decoding every icon or creating a PNG. `hoi4.focus_raster` adds decoded icons and the high-fidelity PNG. National trees with 200 or more focuses raster at half scale by default; pass `reviewScale` from `0.25` through `1` when a different PNG size is needed.
|
|
50
50
|
|
package/docs/gui.md
CHANGED
|
@@ -24,11 +24,31 @@ Rewrite once, then inspect and render the result.
|
|
|
24
24
|
|
|
25
25
|
1. Inspect the root window and its linked GUI, GFX, scripted-GUI, and localisation source.
|
|
26
26
|
2. Render the normal state and the states relevant to the task, such as hover, selected, disabled, warning, empty list, full list, minimum value, maximum value, or long text.
|
|
27
|
-
3.
|
|
28
|
-
4.
|
|
29
|
-
5.
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
3. Add `relatedScenarios` for every meaningful value-driven version of the window, such as open and closed panels, empty and populated lists, unlocked and locked actions, low and high values, and alternate scripted-GUI property results.
|
|
28
|
+
4. Check common resolutions and UI scales when positioning or clipping can change.
|
|
29
|
+
5. Fix the existing mod-owned file with targeted patches, a complete supported source replacement, or a structured helper replacement.
|
|
30
|
+
6. Rewrite once, then inspect and render the result.
|
|
31
|
+
|
|
32
|
+
Inspection checks missing assets and localisation, invalid sizes, overlap, clipping, overflow, button-label centering, content crossing a background boundary, conflicting click regions, invisible blockers, broken parents or contexts, list-row cuts, state conflicts, trigger/effect gaps, and resolution drift. `hoi4.gui_render` returns a scripted-scenario gallery and JSON delta report alongside the generic state and resolution galleries.
|
|
33
|
+
|
|
34
|
+
Preview scenarios can include exact layout expectations:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"id": "populated-unlocked",
|
|
39
|
+
"flags": { "panel_open": true },
|
|
40
|
+
"lists": { "target_list": [{ "id": 1, "label": "First target" }] },
|
|
41
|
+
"visibility": { "target_list": true, "confirm_button": true },
|
|
42
|
+
"expectations": {
|
|
43
|
+
"visible": ["target_list", "confirm_button"],
|
|
44
|
+
"hidden": ["empty_message"],
|
|
45
|
+
"containedBy": { "confirm_button": "main_panel" },
|
|
46
|
+
"centeredOn": { "confirm_label": "confirm_button" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Selectors accept an element name, instance ID, or source ID. Visibility expectations diagnose elements that are missing or in the wrong scripted version. `containedBy` diagnoses text or controls that leave their intended background or panel. `centeredOn` uses rendered glyph bounds, so it catches labels that occupy the right text box but are visibly off-center on the button.
|
|
32
52
|
|
|
33
53
|
Broad inspection indexes localisation actually referenced by GUI source and returns a connected workspace projection when the complete source graph is very large. The resource records full and returned node and edge counts, so an agent can identify the relevant window without loading unrelated vanilla UI into its prompt. Rendering remains targeted to the selected root, state, and resolution.
|
|
34
54
|
|
package/docs/probability.md
CHANGED
|
@@ -6,7 +6,7 @@ Use them for event timing and option weights, decision and mission scores, focus
|
|
|
6
6
|
|
|
7
7
|
| Tool | Use |
|
|
8
8
|
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
-
| `hoi4.probability_inspect` | Discover weighted blocks, adapters, candidates, provenance, capabilities, and unsupported constructs.
|
|
9
|
+
| `hoi4.probability_inspect` | Discover weighted blocks, compatible adapters, candidates, provenance, capabilities, and unsupported constructs. |
|
|
10
10
|
| `hoi4.probability_evaluate` | Evaluate eligibility, modifier traces, raw values, proven probabilities, and MTTH horizon chances across scenarios. |
|
|
11
11
|
| `hoi4.probability_sweep` | Sweep declared ranges and locate sensitivity changes, breakpoints, and rank reversals. |
|
|
12
12
|
| `hoi4.probability_simulate` | Sample declared distributions with a deterministic seed and confidence intervals. |
|
|
@@ -16,6 +16,8 @@ Use them for event timing and option weights, decision and mission scores, focus
|
|
|
16
16
|
|
|
17
17
|
All seven tools are read-only. Proposed source is parsed in memory and never written. When an installed game root is configured, evaluation fails closed unless `launcher-settings.json` identifies the supported HOI4 build and checksum. Results otherwise state that they target the adapter version without claiming local-game verification.
|
|
18
18
|
|
|
19
|
+
Pass a source without an adapter when the surface type is not yet known. Inspection returns every compatible adapter, candidate counts, and example candidate IDs. If a requested adapter, identifier, or candidate pool does not match the source, inspection returns the same discovery result with an explanation and suggested adapter instead of `PROBABILITY_SURFACE_EMPTY`; evaluation, sweeps, and simulation remain strict and still reject an empty requested surface.
|
|
20
|
+
|
|
19
21
|
## Scenarios
|
|
20
22
|
|
|
21
23
|
A scenario contains only state the caller is willing to declare. Missing values stay unresolved. Alternatives and ranges produce exact branches or bounds; probability distributions require `hoi4.probability_simulate`.
|
package/docs/setup.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoi4-agent-tools",
|
|
3
3
|
"mcpName": "io.github.klimPaskov/hoi4-agent-tools",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.5.0",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"hoi4-map"
|
|
33
33
|
],
|
|
34
34
|
"engines": {
|
|
35
|
-
"node": "^22.
|
|
35
|
+
"node": "^22.19.0 || ^24.0.0"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist/",
|
|
@@ -95,19 +95,19 @@
|
|
|
95
95
|
"prepack": "npm run build"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@fontsource-variable/roboto": "5.
|
|
99
|
-
"@modelcontextprotocol/sdk": "1.
|
|
98
|
+
"@fontsource-variable/roboto": "5.3.0",
|
|
99
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
100
100
|
"express": "5.2.1",
|
|
101
101
|
"fast-glob": "3.3.3",
|
|
102
102
|
"fontkit": "2.0.4",
|
|
103
103
|
"iconv-lite": "0.7.3",
|
|
104
|
-
"jose": "6.2.
|
|
104
|
+
"jose": "6.2.8",
|
|
105
105
|
"sharp": "0.35.3",
|
|
106
106
|
"zod": "4.4.3"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@eslint/js": "10.0.1",
|
|
110
|
-
"@modelcontextprotocol/inspector": "1.0
|
|
110
|
+
"@modelcontextprotocol/inspector": "2.1.0",
|
|
111
111
|
"@types/express": "5.0.6",
|
|
112
112
|
"@types/fontkit": "2.0.9",
|
|
113
113
|
"@types/node": "22.20.1",
|
|
@@ -115,22 +115,23 @@
|
|
|
115
115
|
"@vitest/coverage-v8": "4.1.10",
|
|
116
116
|
"ajv": "8.20.0",
|
|
117
117
|
"ajv-formats": "3.0.1",
|
|
118
|
-
"eslint": "10.
|
|
118
|
+
"eslint": "10.8.1",
|
|
119
119
|
"fast-check": "4.9.0",
|
|
120
120
|
"pixelmatch": "7.2.0",
|
|
121
121
|
"prettier": "3.9.6",
|
|
122
|
-
"tsx": "4.23.
|
|
122
|
+
"tsx": "4.23.11",
|
|
123
123
|
"typescript": "6.0.3",
|
|
124
|
-
"typescript-eslint": "8.
|
|
124
|
+
"typescript-eslint": "8.66.0",
|
|
125
125
|
"vitest": "4.1.10"
|
|
126
126
|
},
|
|
127
127
|
"overrides": {
|
|
128
|
-
"@hono/node-server": "2.0
|
|
128
|
+
"@hono/node-server": "2.1.0",
|
|
129
129
|
"brace-expansion": "5.0.9",
|
|
130
130
|
"fast-uri": "3.1.5",
|
|
131
|
-
"hono": "4.13.
|
|
131
|
+
"hono": "4.13.1",
|
|
132
|
+
"nanoid": "3.3.18",
|
|
132
133
|
"ip-address": "10.4.0",
|
|
133
|
-
"postcss": "8.5.
|
|
134
|
+
"postcss": "8.5.26",
|
|
134
135
|
"shell-quote": "1.10.0"
|
|
135
136
|
},
|
|
136
137
|
"publishConfig": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/configuration.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/continuous-focus-palette.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/custom-weighted-pool.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/event-feature-manifest.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/focus-plan.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.0/schemas/focus-planning-sidecar.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|