hoi4-agent-tools 2.8.10 → 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 +17 -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/core/source/limits.js +6 -7
- package/dist/hoi4_agent_tools/event/graph.js +3 -4
- package/dist/hoi4_agent_tools/event/limits.js +4 -4
- package/dist/hoi4_agent_tools/event/service.js +34 -39
- package/dist/hoi4_agent_tools/gui/assets.js +280 -12
- package/dist/hoi4_agent_tools/gui/layout.js +70 -9
- package/dist/hoi4_agent_tools/gui/renderer.js +81 -12
- 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/mcp/tools/probability.js +72 -14
- package/dist/hoi4_agent_tools/probability/sequence.js +43 -18
- package/dist/hoi4_agent_tools/probability/service.js +187 -24
- package/dist/hoi4_agent_tools/schemas/probability.js +2 -0
- package/dist/hoi4_agent_tools/technology/service.js +41 -8
- 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
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { sha256Bytes } from '../core/canonical.js';
|
|
2
2
|
import { parsePreviewScenario } from './scenario.js';
|
|
3
3
|
const countryNames = ['Germany', 'France', 'Poland', 'Italy', 'Brazil', 'India', 'Canada'];
|
|
4
|
+
const countryTags = ['GER', 'FRA', 'POL', 'ITA', 'BRA', 'RAJ', 'CAN'];
|
|
5
|
+
const countryIdeologies = [
|
|
6
|
+
'fascism',
|
|
7
|
+
'democratic',
|
|
8
|
+
'democratic',
|
|
9
|
+
'fascism',
|
|
10
|
+
'democratic',
|
|
11
|
+
'neutrality',
|
|
12
|
+
'democratic',
|
|
13
|
+
];
|
|
4
14
|
const countryAdjectives = [
|
|
5
15
|
'German',
|
|
6
16
|
'French',
|
|
@@ -447,6 +457,8 @@ function generatedRows(count, random, options) {
|
|
|
447
457
|
return Array.from({ length: count }, (_unused, index) => {
|
|
448
458
|
const countryIndex = (countryOffset + index) % countryNames.length;
|
|
449
459
|
const name = countryNames[countryIndex];
|
|
460
|
+
const tag = countryTags[countryIndex];
|
|
461
|
+
const ideology = countryIdeologies[countryIndex];
|
|
450
462
|
const value = generatedNumber(options, random);
|
|
451
463
|
return {
|
|
452
464
|
index: index + 1,
|
|
@@ -455,6 +467,12 @@ function generatedRows(count, random, options) {
|
|
|
455
467
|
name,
|
|
456
468
|
GetName: name,
|
|
457
469
|
GetAdjective: countryAdjectives[countryIndex],
|
|
470
|
+
GetTag: tag,
|
|
471
|
+
tag,
|
|
472
|
+
countryTag: tag,
|
|
473
|
+
ideology,
|
|
474
|
+
countryIdeology: ideology,
|
|
475
|
+
countryScope: true,
|
|
458
476
|
value,
|
|
459
477
|
amount: value,
|
|
460
478
|
percentage: Math.max(0, Math.min(100, value)),
|
|
@@ -532,6 +550,8 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
|
|
|
532
550
|
const visibility = generatedVisibility(graph, scripted, options, random, base.visibility);
|
|
533
551
|
const elementStates = { ...base.elementStates };
|
|
534
552
|
const scopedCountries = new Map();
|
|
553
|
+
const generatedCountryIndex = random.integer(0, countryNames.length - 1);
|
|
554
|
+
const generatedCountryTag = countryTags[generatedCountryIndex];
|
|
535
555
|
for (const key of tokens.numeric)
|
|
536
556
|
setIfMissing(values, key, generatedNumberForKey(key, options, random, numericConstraints.ranges.get(key)));
|
|
537
557
|
for (const [elementName, { minimum, maximum }] of progressRanges) {
|
|
@@ -580,6 +600,10 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
|
|
|
580
600
|
for (const [key, template] of scriptedTextTemplates)
|
|
581
601
|
values[key] = materializeDynamicText(template, values);
|
|
582
602
|
for (const key of imageTokens) {
|
|
603
|
+
if (/(?:^|\.)GetFlag$/u.test(key)) {
|
|
604
|
+
setIfMissing(values, key, generatedCountryTag);
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
583
607
|
const sprite = scriptedLocalisationValue(graph, key, base.language, random, 'image', values);
|
|
584
608
|
if (sprite !== undefined)
|
|
585
609
|
setIfMissing(values, key, sprite);
|
|
@@ -603,6 +627,18 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
|
|
|
603
627
|
id: `${base.id}-${options.idPrefix}-${scenarioIndex + 1}`,
|
|
604
628
|
description: `Generated source-aware scenario ${scenarioIndex + 1} (seed ${sourceSeed})`,
|
|
605
629
|
values,
|
|
630
|
+
country: [...imageTokens].some((key) => /(?:^|\.)GetFlag$/u.test(key))
|
|
631
|
+
? {
|
|
632
|
+
GetName: countryNames[generatedCountryIndex],
|
|
633
|
+
GetAdjective: countryAdjectives[generatedCountryIndex],
|
|
634
|
+
GetTag: generatedCountryTag,
|
|
635
|
+
tag: generatedCountryTag,
|
|
636
|
+
countryTag: generatedCountryTag,
|
|
637
|
+
ideology: countryIdeologies[generatedCountryIndex],
|
|
638
|
+
countryIdeology: countryIdeologies[generatedCountryIndex],
|
|
639
|
+
...base.country,
|
|
640
|
+
}
|
|
641
|
+
: base.country,
|
|
606
642
|
lists,
|
|
607
643
|
visibility,
|
|
608
644
|
elementStates,
|
|
@@ -241,7 +241,7 @@ function basenameFallbackPatterns(exactPaths, exactFiles) {
|
|
|
241
241
|
.map(({ relativePath }) => relativePath.replaceAll('\\', '/').toLowerCase()));
|
|
242
242
|
return [
|
|
243
243
|
...new Set(exactPaths
|
|
244
|
-
.filter((candidate) => !resolved.has(candidate.toLowerCase()))
|
|
244
|
+
.filter((candidate) => !/[*?[\]{}!]/u.test(candidate) && !resolved.has(candidate.toLowerCase()))
|
|
245
245
|
.map((candidate) => `**/${path.posix.basename(candidate)}`)),
|
|
246
246
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
247
247
|
}
|
|
@@ -341,6 +341,22 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
341
341
|
if (textValues.has(localisation.key))
|
|
342
342
|
textValues.add(localisation.value);
|
|
343
343
|
}
|
|
344
|
+
const dynamicTextNames = new Set();
|
|
345
|
+
for (const value of textValues) {
|
|
346
|
+
for (const match of value.matchAll(/\[(?:\?[^\]]*\.)?([A-Za-z_][A-Za-z0-9_.:-]*)[^\]]*\]/gu)) {
|
|
347
|
+
const token = match[1];
|
|
348
|
+
if (token === undefined)
|
|
349
|
+
continue;
|
|
350
|
+
dynamicTextNames.add(token);
|
|
351
|
+
dynamicTextNames.add(token.slice(token.lastIndexOf('.') + 1));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const dynamicTextLocalisationKeys = new Set(graph.scriptedLocalisation
|
|
355
|
+
.filter(({ name }) => dynamicTextNames.has(name))
|
|
356
|
+
.flatMap(({ localisationKeys }) => localisationKeys));
|
|
357
|
+
for (const localisation of graph.localisation)
|
|
358
|
+
if (dynamicTextLocalisationKeys.has(localisation.key))
|
|
359
|
+
textValues.add(localisation.value);
|
|
344
360
|
for (const value of textValues) {
|
|
345
361
|
for (const token of localisationIconTokens(value)) {
|
|
346
362
|
for (const candidate of localisationIconSpriteCandidates(token))
|
|
@@ -372,6 +388,12 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
372
388
|
const match = /^\[([^\]]+)\]$/u.exec(image);
|
|
373
389
|
return match?.[1] === undefined ? [] : [match[1]];
|
|
374
390
|
}));
|
|
391
|
+
const usesCountryFlags = graph.scriptedGuis
|
|
392
|
+
.filter(({ name }) => relatedScriptedGuiNames.has(name))
|
|
393
|
+
.flatMap(({ propertyDefinitions }) => propertyDefinitions)
|
|
394
|
+
.some(({ attributes }) => Object.entries(attributes).some(([key, value]) => key.toLocaleLowerCase('en-US') === 'image' &&
|
|
395
|
+
typeof value === 'string' &&
|
|
396
|
+
/(?:^|\.)GetFlag\]$/u.test(value)));
|
|
375
397
|
for (const definition of graph.scriptedLocalisation) {
|
|
376
398
|
if (!dynamicImageTokens.has(definition.name))
|
|
377
399
|
continue;
|
|
@@ -417,17 +439,41 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
417
439
|
...manifest.sourceFrames.map(({ path: framePath }) => framePath),
|
|
418
440
|
]),
|
|
419
441
|
];
|
|
420
|
-
return [...new Set(references.flatMap(referenceVariants))].sort((left, right) => compareCodeUnits(left, right));
|
|
421
|
-
}
|
|
422
|
-
function scenarioSpriteNames(scenarios) {
|
|
423
442
|
return [
|
|
424
|
-
...new Set(
|
|
425
|
-
.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
443
|
+
...new Set([
|
|
444
|
+
...references.flatMap(referenceVariants),
|
|
445
|
+
...(usesCountryFlags
|
|
446
|
+
? [
|
|
447
|
+
'gfx/flags/*.{bmp,dds,png,tga}',
|
|
448
|
+
'gfx/flags/medium/*.{bmp,dds,png,tga}',
|
|
449
|
+
'gfx/flags/small/*.{bmp,dds,png,tga}',
|
|
450
|
+
]
|
|
451
|
+
: []),
|
|
452
|
+
]),
|
|
429
453
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
430
454
|
}
|
|
455
|
+
function scenarioSpriteNames(scenarios, additionalText = []) {
|
|
456
|
+
const sprites = new Set();
|
|
457
|
+
const pending = [...scenarios, ...additionalText];
|
|
458
|
+
while (pending.length > 0) {
|
|
459
|
+
const value = pending.pop();
|
|
460
|
+
if (typeof value === 'string') {
|
|
461
|
+
if (/^GFX_[A-Za-z0-9_.:+-]+$/u.test(value))
|
|
462
|
+
sprites.add(value);
|
|
463
|
+
for (const token of localisationIconTokens(value))
|
|
464
|
+
for (const candidate of localisationIconSpriteCandidates(token))
|
|
465
|
+
sprites.add(candidate);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (value === null || typeof value !== 'object')
|
|
469
|
+
continue;
|
|
470
|
+
if (Array.isArray(value))
|
|
471
|
+
pending.push(...value);
|
|
472
|
+
else
|
|
473
|
+
pending.push(...Object.values(value));
|
|
474
|
+
}
|
|
475
|
+
return [...sprites].sort((left, right) => compareCodeUnits(left, right));
|
|
476
|
+
}
|
|
431
477
|
function bmFontPagePatterns(files) {
|
|
432
478
|
const references = files.flatMap((file) => {
|
|
433
479
|
if (!file.relativePath.toLowerCase().endsWith('.fnt') || file.shadowedBy !== undefined)
|
|
@@ -570,6 +616,14 @@ function fullImage(render) {
|
|
|
570
616
|
throw new Error('GUI render did not produce an image.');
|
|
571
617
|
return image;
|
|
572
618
|
}
|
|
619
|
+
function visualSceneKey(scene) {
|
|
620
|
+
const { id: _id, description: _description, ...scenario } = scene.scenario;
|
|
621
|
+
return hashCanonical({
|
|
622
|
+
windowName: scene.windowName,
|
|
623
|
+
sourceRevision: scene.sourceRevision,
|
|
624
|
+
scenario,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
573
627
|
export function guiArtifactProvenance(graph, kind, primaryScene, scenarioScenes = [primaryScene], metadata = {}) {
|
|
574
628
|
const { scenario } = primaryScene;
|
|
575
629
|
const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
|
|
@@ -657,7 +711,7 @@ export class ScriptedGuiStudio {
|
|
|
657
711
|
graph: buildGuiSourceGraph(retainedFiles, this.engine.indexFiles(retainedFiles)),
|
|
658
712
|
};
|
|
659
713
|
this.#graphCache.set(key, scanned);
|
|
660
|
-
while (this.#graphCache.size >
|
|
714
|
+
while (this.#graphCache.size > 1) {
|
|
661
715
|
const oldest = this.#graphCache.keys().next().value;
|
|
662
716
|
if (oldest === undefined)
|
|
663
717
|
break;
|
|
@@ -665,6 +719,10 @@ export class ScriptedGuiStudio {
|
|
|
665
719
|
}
|
|
666
720
|
return scanned;
|
|
667
721
|
}
|
|
722
|
+
/** Release parsed GUI graphs after a bounded batch job has finished using the studio. */
|
|
723
|
+
clearCaches() {
|
|
724
|
+
this.#graphCache.clear();
|
|
725
|
+
}
|
|
668
726
|
async scan(workspaceId, principal, signal, languages = ['l_english']) {
|
|
669
727
|
signal?.throwIfAborted();
|
|
670
728
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
@@ -746,10 +804,10 @@ export class ScriptedGuiStudio {
|
|
|
746
804
|
input.signal?.throwIfAborted();
|
|
747
805
|
const placeholderScenario = parsePreviewScenario(input.scenario);
|
|
748
806
|
const explicitRelatedScenarios = (input.relatedScenarios ?? []).map(parsePreviewScenario);
|
|
749
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios]));
|
|
750
807
|
const generatedOptions = input.generatedScenarios === undefined
|
|
751
808
|
? undefined
|
|
752
809
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
810
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples));
|
|
753
811
|
const generatedScenarios = generatedOptions === undefined
|
|
754
812
|
? []
|
|
755
813
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
|
|
@@ -817,6 +875,9 @@ export class ScriptedGuiStudio {
|
|
|
817
875
|
const requestedBaselineScenario = input.comparisonScenario === undefined
|
|
818
876
|
? undefined
|
|
819
877
|
: parsePreviewScenario(input.comparisonScenario);
|
|
878
|
+
const generatedOptions = input.generatedScenarios === undefined
|
|
879
|
+
? undefined
|
|
880
|
+
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
820
881
|
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [
|
|
821
882
|
placeholderScenario.language,
|
|
822
883
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario.language]),
|
|
@@ -825,10 +886,7 @@ export class ScriptedGuiStudio {
|
|
|
825
886
|
placeholderScenario,
|
|
826
887
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
827
888
|
...explicitRelatedScenarios,
|
|
828
|
-
]));
|
|
829
|
-
const generatedOptions = input.generatedScenarios === undefined
|
|
830
|
-
? undefined
|
|
831
|
-
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
889
|
+
], generatedOptions?.textSamples));
|
|
832
890
|
const generatedScenarios = generatedOptions === undefined
|
|
833
891
|
? []
|
|
834
892
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
|
|
@@ -846,6 +904,18 @@ export class ScriptedGuiStudio {
|
|
|
846
904
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, scenario.language);
|
|
847
905
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
848
906
|
const render = await renderGuiScene(scene, undefined, input.signal, budget);
|
|
907
|
+
const fullRenderCache = new Map([
|
|
908
|
+
[visualSceneKey(scene), fullImage(render)],
|
|
909
|
+
]);
|
|
910
|
+
const renderFull = async (target) => {
|
|
911
|
+
const key = visualSceneKey(target);
|
|
912
|
+
const cached = fullRenderCache.get(key);
|
|
913
|
+
if (cached !== undefined)
|
|
914
|
+
return cached;
|
|
915
|
+
const image = fullImage(await renderGuiScene(target, ['full'], input.signal, budget));
|
|
916
|
+
fullRenderCache.set(key, image);
|
|
917
|
+
return image;
|
|
918
|
+
};
|
|
849
919
|
const scenarioScenes = [scene];
|
|
850
920
|
const scenarioItems = [];
|
|
851
921
|
for (const variantScenario of [scenario, ...relatedScenarios]) {
|
|
@@ -855,10 +925,7 @@ export class ScriptedGuiStudio {
|
|
|
855
925
|
: await buildGuiScene(scanned.graph, scanned.files, input.windowName, variantScenario, catalog);
|
|
856
926
|
if (variantScene !== scene)
|
|
857
927
|
scenarioScenes.push(variantScene);
|
|
858
|
-
const
|
|
859
|
-
? render
|
|
860
|
-
: await renderGuiScene(variantScene, ['full'], input.signal, new RenderBudget());
|
|
861
|
-
const image = fullImage(variantRender);
|
|
928
|
+
const image = variantScene === scene ? fullImage(render) : await renderFull(variantScene);
|
|
862
929
|
scenarioItems.push({
|
|
863
930
|
label: variantScenario.description ?? variantScenario.id,
|
|
864
931
|
png: image.png,
|
|
@@ -873,8 +940,7 @@ export class ScriptedGuiStudio {
|
|
|
873
940
|
const stateScenario = scenarioWith(scenario, { id: `${scenario.id}-${state}`, state });
|
|
874
941
|
const stateScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, stateScenario, catalog);
|
|
875
942
|
stateScenes.push(stateScene);
|
|
876
|
-
const
|
|
877
|
-
const image = fullImage(stateRender);
|
|
943
|
+
const image = await renderFull(stateScene);
|
|
878
944
|
stateItems.push({ label: state, png: image.png, width: image.width, height: image.height });
|
|
879
945
|
}
|
|
880
946
|
const resolutionScenes = [];
|
|
@@ -895,8 +961,7 @@ export class ScriptedGuiStudio {
|
|
|
895
961
|
});
|
|
896
962
|
const resolutionScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, resolutionScenario, catalog);
|
|
897
963
|
resolutionScenes.push(resolutionScene);
|
|
898
|
-
const
|
|
899
|
-
const image = fullImage(resolutionRender);
|
|
964
|
+
const image = await renderFull(resolutionScene);
|
|
900
965
|
resolutionItems.push({
|
|
901
966
|
label: `${resolution.width}×${resolution.height} · UI ${uiScale}`,
|
|
902
967
|
png: image.png,
|
|
@@ -905,8 +970,8 @@ export class ScriptedGuiStudio {
|
|
|
905
970
|
});
|
|
906
971
|
}
|
|
907
972
|
const baselineScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, baselineScenario, catalog);
|
|
908
|
-
const
|
|
909
|
-
const comparison = await compareGuiImages(
|
|
973
|
+
const baselineImage = await renderFull(baselineScene);
|
|
974
|
+
const comparison = await compareGuiImages(baselineImage.png, fullImage(render).png, budget, input.signal);
|
|
910
975
|
const renderedSourceIds = [
|
|
911
976
|
...scenarioScenes,
|
|
912
977
|
...stateScenes,
|
|
@@ -391,7 +391,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
391
391
|
});
|
|
392
392
|
server.registerTool('hoi4.gui_render', {
|
|
393
393
|
title: 'Render scripted GUI artifacts',
|
|
394
|
-
description: 'Render GUI states, resolutions, generated or explicit scenarios, hierarchy, comparisons, and diagnostics.',
|
|
394
|
+
description: 'Render GUI states, resolutions, generated or explicit scenarios, dynamic flags and text icons, hierarchy, comparisons, and diagnostics.',
|
|
395
395
|
inputSchema: renderInput,
|
|
396
396
|
outputSchema: guiRenderOutputSchema,
|
|
397
397
|
annotations: artifactProducing,
|
|
@@ -19,7 +19,6 @@ const outputs = z.array(probabilityOutputSchema).max(10).optional();
|
|
|
19
19
|
const commonShape = {
|
|
20
20
|
workspaceId: workspaceIdSchema,
|
|
21
21
|
adapter: probabilityAdapterIdSchema,
|
|
22
|
-
source: nestedSource,
|
|
23
22
|
scenarioSet: nestedScenarios,
|
|
24
23
|
candidatePool,
|
|
25
24
|
horizonDays: z.number().positive().max(1_000_000).optional(),
|
|
@@ -34,21 +33,55 @@ const inspectInput = z
|
|
|
34
33
|
workspaceId: workspaceIdSchema,
|
|
35
34
|
adapter: probabilityAdapterIdSchema.optional(),
|
|
36
35
|
source: nestedSource.optional(),
|
|
36
|
+
customPoolManifest: nestedManifest.optional(),
|
|
37
37
|
candidatePool,
|
|
38
38
|
refresh: z.boolean().optional(),
|
|
39
39
|
})
|
|
40
40
|
.strict()
|
|
41
41
|
.superRefine((value, context) => {
|
|
42
|
-
if (value.
|
|
42
|
+
if (value.source !== undefined && value.customPoolManifest !== undefined)
|
|
43
|
+
context.addIssue({
|
|
44
|
+
code: 'custom',
|
|
45
|
+
message: 'Provide either source or customPoolManifest',
|
|
46
|
+
});
|
|
47
|
+
if (value.adapter !== undefined &&
|
|
48
|
+
value.source === undefined &&
|
|
49
|
+
value.customPoolManifest === undefined)
|
|
43
50
|
context.addIssue({
|
|
44
51
|
code: 'custom',
|
|
45
52
|
message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
|
|
46
53
|
});
|
|
54
|
+
if (value.customPoolManifest !== undefined && value.adapter !== 'custom_weighted_pool')
|
|
55
|
+
context.addIssue({
|
|
56
|
+
code: 'custom',
|
|
57
|
+
path: ['adapter'],
|
|
58
|
+
message: 'customPoolManifest requires adapter custom_weighted_pool',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
const evaluateInput = z
|
|
62
|
+
.object({
|
|
63
|
+
...commonShape,
|
|
64
|
+
source: nestedSource.optional(),
|
|
65
|
+
customPoolManifest: nestedManifest.optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict()
|
|
68
|
+
.superRefine((value, context) => {
|
|
69
|
+
if ((value.source === undefined) === (value.customPoolManifest === undefined))
|
|
70
|
+
context.addIssue({
|
|
71
|
+
code: 'custom',
|
|
72
|
+
message: 'Provide exactly one source or customPoolManifest',
|
|
73
|
+
});
|
|
74
|
+
if (value.customPoolManifest !== undefined && value.adapter !== 'custom_weighted_pool')
|
|
75
|
+
context.addIssue({
|
|
76
|
+
code: 'custom',
|
|
77
|
+
path: ['adapter'],
|
|
78
|
+
message: 'customPoolManifest requires adapter custom_weighted_pool',
|
|
79
|
+
});
|
|
47
80
|
});
|
|
48
|
-
const evaluateInput = z.object(commonShape).strict();
|
|
49
81
|
const sweepInput = z
|
|
50
82
|
.object({
|
|
51
83
|
...commonShape,
|
|
84
|
+
source: nestedSource,
|
|
52
85
|
sweep: z
|
|
53
86
|
.object({
|
|
54
87
|
paths: z.array(z.string().min(1).max(1024)).min(1).max(32),
|
|
@@ -62,6 +95,7 @@ const sweepInput = z
|
|
|
62
95
|
const simulateInput = z
|
|
63
96
|
.object({
|
|
64
97
|
...commonShape,
|
|
98
|
+
source: nestedSource,
|
|
65
99
|
samples: z.number().int().min(100).max(10_000_000).default(100_000),
|
|
66
100
|
seed: z.number().int().min(-2_147_483_648).max(2_147_483_647).default(1),
|
|
67
101
|
confidenceLevel: z.number().min(0.5).max(0.9999).default(0.95),
|
|
@@ -87,8 +121,10 @@ const compareInput = z
|
|
|
87
121
|
.object({
|
|
88
122
|
workspaceId: workspaceIdSchema,
|
|
89
123
|
adapter: probabilityAdapterIdSchema,
|
|
90
|
-
before: nestedSource,
|
|
91
|
-
after: nestedSource,
|
|
124
|
+
before: nestedSource.optional(),
|
|
125
|
+
after: nestedSource.optional(),
|
|
126
|
+
beforeManifest: nestedManifest.optional(),
|
|
127
|
+
afterManifest: nestedManifest.optional(),
|
|
92
128
|
scenarioSet: nestedScenarios,
|
|
93
129
|
candidatePool,
|
|
94
130
|
horizonDays: z.number().positive().max(1_000_000).optional(),
|
|
@@ -97,7 +133,29 @@ const compareInput = z
|
|
|
97
133
|
outputs,
|
|
98
134
|
refresh: z.boolean().optional(),
|
|
99
135
|
})
|
|
100
|
-
.strict()
|
|
136
|
+
.strict()
|
|
137
|
+
.superRefine((value, context) => {
|
|
138
|
+
const sourceMode = value.before !== undefined || value.after !== undefined;
|
|
139
|
+
const manifestMode = value.beforeManifest !== undefined || value.afterManifest !== undefined;
|
|
140
|
+
if (sourceMode === manifestMode)
|
|
141
|
+
context.addIssue({
|
|
142
|
+
code: 'custom',
|
|
143
|
+
message: 'Compare either before/after sources or beforeManifest/afterManifest',
|
|
144
|
+
});
|
|
145
|
+
if (sourceMode && (value.before === undefined || value.after === undefined))
|
|
146
|
+
context.addIssue({ code: 'custom', message: 'Source comparison requires before and after' });
|
|
147
|
+
if (manifestMode && (value.beforeManifest === undefined || value.afterManifest === undefined))
|
|
148
|
+
context.addIssue({
|
|
149
|
+
code: 'custom',
|
|
150
|
+
message: 'Custom-pool comparison requires beforeManifest and afterManifest',
|
|
151
|
+
});
|
|
152
|
+
if (manifestMode && value.adapter !== 'custom_weighted_pool')
|
|
153
|
+
context.addIssue({
|
|
154
|
+
code: 'custom',
|
|
155
|
+
path: ['adapter'],
|
|
156
|
+
message: 'Manifest comparison requires adapter custom_weighted_pool',
|
|
157
|
+
});
|
|
158
|
+
});
|
|
101
159
|
const renderInput = z
|
|
102
160
|
.object({
|
|
103
161
|
workspaceId: workspaceIdSchema,
|
|
@@ -243,7 +301,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
243
301
|
const analyzer = new ProbabilityAnalyzer(engine);
|
|
244
302
|
server.registerTool('hoi4.probability_inspect', {
|
|
245
303
|
title: 'Inspect AI and MTTH weighted logic',
|
|
246
|
-
description: '
|
|
304
|
+
description: 'Discover weighted adapters, candidates, provenance, and required inputs.',
|
|
247
305
|
inputSchema: inspectInput,
|
|
248
306
|
outputSchema: inspectOutput,
|
|
249
307
|
annotations: readOnly,
|
|
@@ -252,7 +310,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
252
310
|
const base = await requestContext(engine, context, input.workspaceId, input.refresh, progress.signal);
|
|
253
311
|
try {
|
|
254
312
|
await progress.report(0, 2, 'Inspecting weighted source');
|
|
255
|
-
const inspected = await analyzer.inspect(base, input.adapter, input.source, input.candidatePool);
|
|
313
|
+
const inspected = await analyzer.inspect(base, input.adapter, input.source, input.candidatePool, input.customPoolManifest);
|
|
256
314
|
const result = emptyServiceResult(base.workspaceId, {
|
|
257
315
|
adapters: inspected.adapters.length,
|
|
258
316
|
...(inspected.surface === undefined
|
|
@@ -305,7 +363,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
305
363
|
});
|
|
306
364
|
server.registerTool('hoi4.probability_evaluate', {
|
|
307
365
|
title: 'Evaluate AI and MTTH scenarios',
|
|
308
|
-
description: 'Evaluate
|
|
366
|
+
description: 'Evaluate weights and probabilities under explicit scenarios.',
|
|
309
367
|
inputSchema: evaluateInput,
|
|
310
368
|
outputSchema: analysisOutput,
|
|
311
369
|
annotations: readOnly,
|
|
@@ -329,7 +387,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
329
387
|
});
|
|
330
388
|
server.registerTool('hoi4.probability_sweep', {
|
|
331
389
|
title: 'Sweep AI and MTTH parameters',
|
|
332
|
-
description: 'Evaluate declared ranges,
|
|
390
|
+
description: 'Evaluate declared ranges, breakpoints, and rank reversals.',
|
|
333
391
|
inputSchema: sweepInput,
|
|
334
392
|
outputSchema: analysisOutput,
|
|
335
393
|
annotations: readOnly,
|
|
@@ -353,7 +411,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
353
411
|
});
|
|
354
412
|
server.registerTool('hoi4.probability_simulate', {
|
|
355
413
|
title: 'Simulate uncertain weighted scenarios',
|
|
356
|
-
description: 'Run
|
|
414
|
+
description: 'Run seeded simulations with distributions and confidence intervals.',
|
|
357
415
|
inputSchema: simulateInput,
|
|
358
416
|
outputSchema: analysisOutput,
|
|
359
417
|
annotations: readOnly,
|
|
@@ -377,7 +435,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
377
435
|
});
|
|
378
436
|
server.registerTool('hoi4.probability_sequence', {
|
|
379
437
|
title: 'Analyze a declared weighted sequence',
|
|
380
|
-
description: 'Analyze
|
|
438
|
+
description: 'Analyze a manifest-declared dynamic weighted sequence.',
|
|
381
439
|
inputSchema: sequenceInput,
|
|
382
440
|
outputSchema: analysisOutput,
|
|
383
441
|
annotations: readOnly,
|
|
@@ -401,7 +459,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
401
459
|
});
|
|
402
460
|
server.registerTool('hoi4.probability_compare', {
|
|
403
461
|
title: 'Compare weighted source patches',
|
|
404
|
-
description: 'Compare
|
|
462
|
+
description: 'Compare sources or declared pools under identical scenarios.',
|
|
405
463
|
inputSchema: compareInput,
|
|
406
464
|
outputSchema: analysisOutput,
|
|
407
465
|
annotations: readOnly,
|
|
@@ -425,7 +483,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
425
483
|
});
|
|
426
484
|
server.registerTool('hoi4.probability_render', {
|
|
427
485
|
title: 'Render AI and MTTH analysis',
|
|
428
|
-
description: 'Render cached analysis
|
|
486
|
+
description: 'Render cached probability analysis as deterministic resources.',
|
|
429
487
|
inputSchema: renderInput,
|
|
430
488
|
outputSchema: analysisOutput,
|
|
431
489
|
annotations: readOnly,
|
|
@@ -465,22 +465,32 @@ function advance(state, manifest, selectedCandidates, unresolved) {
|
|
|
465
465
|
}
|
|
466
466
|
function initialState(manifest, scenario) {
|
|
467
467
|
const candidateIds = new Set();
|
|
468
|
+
const state = {
|
|
469
|
+
day: 0,
|
|
470
|
+
step: 0,
|
|
471
|
+
values: { ...(manifest.state ?? {}), ...scenario.state },
|
|
472
|
+
candidates: [],
|
|
473
|
+
timerMinDays: manifest.selection.timerMinDays ?? 0,
|
|
474
|
+
timerMaxDays: manifest.selection.timerMaxDays ?? manifest.selection.timerMinDays ?? 0,
|
|
475
|
+
terminal: false,
|
|
476
|
+
selectedIds: [],
|
|
477
|
+
selectionDays: {},
|
|
478
|
+
};
|
|
468
479
|
const candidates = manifest.candidates.map((candidate) => {
|
|
469
480
|
if (candidateIds.has(candidate.id))
|
|
470
481
|
throw new ServiceError('SEQUENCE_CANDIDATE_DUPLICATE', 'Custom pool candidate IDs must be unique', { id: candidate.id });
|
|
471
482
|
candidateIds.add(candidate.id);
|
|
472
|
-
const weight =
|
|
473
|
-
const cap = candidate.cap === undefined
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
throw new ServiceError('SEQUENCE_INITIAL_VALUE_INVALID', 'Initial custom pool weights and caps must resolve numerically', { id: candidate.id });
|
|
483
|
+
const weight = numberExpression(candidate.weight, state);
|
|
484
|
+
const cap = candidate.cap === undefined ? undefined : numberExpression(candidate.cap, state);
|
|
485
|
+
if (weight === undefined ||
|
|
486
|
+
!Number.isFinite(weight) ||
|
|
487
|
+
(cap !== undefined && !Number.isFinite(cap)))
|
|
488
|
+
throw new ServiceError('SEQUENCE_INITIAL_VALUE_INVALID', 'Initial custom pool weights and caps must resolve from manifest or scenario state', { id: candidate.id });
|
|
489
|
+
const boundedWeight = cap === undefined ? weight : Math.min(weight, cap);
|
|
480
490
|
return {
|
|
481
491
|
id: candidate.id,
|
|
482
492
|
...(candidate.category === undefined ? {} : { category: candidate.category }),
|
|
483
|
-
weight: rounded(
|
|
493
|
+
weight: rounded(boundedWeight, manifest.selection.rounding),
|
|
484
494
|
...(cap === undefined ? {} : { cap: rounded(cap, manifest.selection.rounding) }),
|
|
485
495
|
active: true,
|
|
486
496
|
oneTime: candidate.oneTime ?? false,
|
|
@@ -488,16 +498,31 @@ function initialState(manifest, scenario) {
|
|
|
488
498
|
cooldownUntil: 0,
|
|
489
499
|
};
|
|
490
500
|
});
|
|
501
|
+
state.candidates = candidates;
|
|
502
|
+
return state;
|
|
503
|
+
}
|
|
504
|
+
/** Evaluate one declared dynamic pool at the supplied scenario state before transitions run. */
|
|
505
|
+
export function evaluateCustomPoolPoint(manifest, scenario) {
|
|
506
|
+
const state = initialState(manifest, scenario);
|
|
507
|
+
const unresolved = [];
|
|
508
|
+
const eligible = eligibleCandidates(state, manifest, unresolved);
|
|
509
|
+
const eligibleIds = new Set(eligible.map(({ id }) => id));
|
|
510
|
+
const poolTotal = eligible.reduce((sum, candidate) => sum + candidate.weight, 0);
|
|
491
511
|
return {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
512
|
+
candidates: state.candidates.map((candidate) => ({
|
|
513
|
+
id: candidate.id,
|
|
514
|
+
weight: candidate.weight,
|
|
515
|
+
eligible: eligibleIds.has(candidate.id),
|
|
516
|
+
probability: !eligibleIds.has(candidate.id)
|
|
517
|
+
? 0
|
|
518
|
+
: manifest.selection.mode === 'categorical_weighted'
|
|
519
|
+
? poolTotal <= 0
|
|
520
|
+
? 0
|
|
521
|
+
: candidate.weight / poolTotal
|
|
522
|
+
: Math.max(0, Math.min(1, candidate.weight / 100)),
|
|
523
|
+
})),
|
|
524
|
+
poolTotal,
|
|
525
|
+
unresolved,
|
|
501
526
|
};
|
|
502
527
|
}
|
|
503
528
|
function stateKey(state) {
|