hoi4-agent-tools 2.8.10 → 2.8.11
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 +7 -0
- 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 +4 -16
- package/dist/hoi4_agent_tools/gui/assets.js +62 -12
- package/dist/hoi4_agent_tools/gui/renderer.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 +156 -18
- package/dist/hoi4_agent_tools/technology/service.js +41 -8
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/package.json +1 -1
- 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 +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 +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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes.
|
|
6
6
|
|
|
7
|
+
## 2.8.11 - 2026-08-30
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- Keep large event and technology workspaces complete by separating game and mod scans, accepting large legitimate Clausewitz sources, retaining large state-access inventories, and removing automatic focused-analysis fallbacks.
|
|
11
|
+
- Inspect, evaluate, and compare complete declared dynamic weighted pools with scenario-resolved candidate weights, eligibility, exact probabilities, and complete candidate inventories.
|
|
12
|
+
- Authenticate GitHub release-state API checks so release publication does not fail on anonymous API limits.
|
|
13
|
+
|
|
7
14
|
## 2.8.10 - 2026-08-30
|
|
8
15
|
|
|
9
16
|
- Fix vanilla BMFont corruption by following declared face and outline channel coverage directly instead of guessing polarity independently for each glyph.
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
export const SOURCE_DIAGNOSTIC_LIMIT = 100;
|
|
2
2
|
export const SOURCE_MAX_NESTING = 256;
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const SOURCE_LINE_LIMIT = 250_000;
|
|
3
|
+
// This is a parser ceiling, not the aggregate workspace-scan or remote request ceiling. Large
|
|
4
|
+
// generated focus, event, and scripted-effect sources routinely exceed 8 MiB in real agentic mods.
|
|
5
|
+
export const SOURCE_MAX_BYTES = 33_554_432;
|
|
6
|
+
export const SOURCE_TOKEN_LIMIT = 4_000_000;
|
|
7
|
+
export const SOURCE_ENTRY_LIMIT = 1_000_000;
|
|
8
|
+
export const SOURCE_LINE_LIMIT = 2_000_000;
|
|
10
9
|
/**
|
|
11
10
|
* Parser ceilings that intentionally return no document or a structurally partial document.
|
|
12
11
|
* Broad inventories may skip these sources, but targeted reads and rewrites must continue to
|
|
@@ -1604,10 +1604,9 @@ export function buildEventGraph(snapshot, options = {}) {
|
|
|
1604
1604
|
assertGraphLimit(graph.unresolved.length, EVENT_GRAPH_MAX_UNRESOLVED, 'EVENT_UNRESOLVED_LIMIT', 'unresolved-analysis count');
|
|
1605
1605
|
budget.check();
|
|
1606
1606
|
const diagnostics = sortDiagnostics([...snapshot.diagnostics, ...partial.diagnostics]);
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
!graph.issues.some(({ classification, severity }) => classification === 'unresolved_analysis' || severity === 'blocker');
|
|
1607
|
+
// Coverage completeness is independent from defects discovered in the source. A full,
|
|
1608
|
+
// untruncated analysis remains complete when it correctly reports missing or dynamic links.
|
|
1609
|
+
const complete = !focused && snapshot.complete;
|
|
1611
1610
|
const nodes = sortNodes(graph.nodes);
|
|
1612
1611
|
const edges = stableUnique(graph.edges);
|
|
1613
1612
|
const stateAccesses = stableUnique(graph.stateAccesses);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ServiceError } from '../core/result.js';
|
|
2
2
|
export const EVENT_GRAPH_MAX_NODES = 250_000;
|
|
3
3
|
export const EVENT_GRAPH_MAX_EDGES = 500_000;
|
|
4
|
-
export const EVENT_GRAPH_MAX_STATE_ACCESSES =
|
|
5
|
-
export const EVENT_GRAPH_MAX_STATE_LINKS =
|
|
6
|
-
export const EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES =
|
|
4
|
+
export const EVENT_GRAPH_MAX_STATE_ACCESSES = 1_000_000;
|
|
5
|
+
export const EVENT_GRAPH_MAX_STATE_LINKS = 1_000_000;
|
|
6
|
+
export const EVENT_GRAPH_MAX_STATE_LINK_CANDIDATES = 2_000_000;
|
|
7
7
|
// Installed-game scans can legitimately produce more than twenty thousand source-linked
|
|
8
8
|
// findings before an agent narrows the query. Keep the graph bounded, but do not reject a
|
|
9
9
|
// complete large workspace merely because its diagnostics are numerous.
|
|
@@ -13,7 +13,7 @@ export const EVENT_GRAPH_MAX_HELPER_DEPTH = 64;
|
|
|
13
13
|
export const EVENT_GRAPH_MAX_HELPER_PROJECTIONS = 200_000;
|
|
14
14
|
export const EVENT_GRAPH_MAX_HELPER_STATE_PROJECTIONS = 50_000;
|
|
15
15
|
export const EVENT_GRAPH_MAX_CONDITION_TEXT = 16_384;
|
|
16
|
-
export const EVENT_GRAPH_WORK_LIMIT =
|
|
16
|
+
export const EVENT_GRAPH_WORK_LIMIT = 10_000_000;
|
|
17
17
|
export const EVENT_FRAGMENT_CACHE_MAX_ENTRIES = 10_000;
|
|
18
18
|
export const EVENT_FRAGMENT_CACHE_MAX_SOURCE_BYTES = 134_217_728;
|
|
19
19
|
export const EVENT_PROPOSED_SOURCE_MAX_FILES = 64;
|
|
@@ -45,16 +45,12 @@ class BoundedEventFragmentCache {
|
|
|
45
45
|
function eventGraphCacheKey(workspaceId, projectHelpers, analysisMode = 'full') {
|
|
46
46
|
return `${workspaceId}\0${projectHelpers ? 'expanded' : 'structural'}\0${analysisMode}`;
|
|
47
47
|
}
|
|
48
|
-
function isGameBackedWorkspace(workspace) {
|
|
49
|
-
return workspace.roots.some(({ kind }) => kind === 'game');
|
|
50
|
-
}
|
|
51
48
|
const serviceState = new WeakMap();
|
|
52
49
|
const EVENT_GRAPH_ARTIFACT_MAX_BYTES = 67_108_864;
|
|
53
50
|
const EVENT_GRAPH_ARTIFACT_MAX_CHUNKS = 1_024;
|
|
54
51
|
const EVENT_SCAN_FULL_GRAPH_RECORD_LIMIT = 100_000;
|
|
55
52
|
const EVENT_SCAN_SUMMARY_SAMPLE_LIMIT = 100;
|
|
56
53
|
const EVENT_FOCUSED_EVIDENCE_LIMIT = 2_000;
|
|
57
|
-
const EVENT_FOCUSED_GRAPH_FILE_THRESHOLD = 1_000;
|
|
58
54
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
|
|
59
55
|
const graphHashCache = new WeakMap();
|
|
60
56
|
function groupedCounts(values, key) {
|
|
@@ -704,8 +700,7 @@ export class EventChainViewer {
|
|
|
704
700
|
const generation = engineGeneration(this.engine, workspaceId);
|
|
705
701
|
const requestedProjectHelpers = options.projectHelpers ?? true;
|
|
706
702
|
const requestedAnalysisMode = options.analysisMode ?? 'full';
|
|
707
|
-
const
|
|
708
|
-
const cacheAnalysisMode = autoFocused ? 'focused' : requestedAnalysisMode;
|
|
703
|
+
const cacheAnalysisMode = requestedAnalysisMode;
|
|
709
704
|
const cacheProjectHelpers = cacheAnalysisMode === 'focused' ? false : requestedProjectHelpers;
|
|
710
705
|
const cacheKey = eventGraphCacheKey(workspaceId, cacheProjectHelpers, cacheAnalysisMode);
|
|
711
706
|
const cached = this.#state.current.get(cacheKey);
|
|
@@ -716,7 +711,7 @@ export class EventChainViewer {
|
|
|
716
711
|
this.#state.current.get(eventGraphCacheKey(workspaceId, requestedProjectHelpers, requestedAnalysisMode)) ??
|
|
717
712
|
this.#state.current.get(eventGraphCacheKey(workspaceId, !requestedProjectHelpers)) ??
|
|
718
713
|
this.#state.current.get(eventGraphCacheKey(workspaceId, requestedProjectHelpers));
|
|
719
|
-
const scanFocused =
|
|
714
|
+
const scanFocused = requestedAnalysisMode === 'focused';
|
|
720
715
|
const snapshot = options.refresh !== true && sibling?.generation === generation
|
|
721
716
|
? sibling.snapshot
|
|
722
717
|
: await scanEventSources(this.engine, workspaceId, workspace, scanFocused, options.principal, options.signal);
|
|
@@ -724,9 +719,7 @@ export class EventChainViewer {
|
|
|
724
719
|
if (cached?.snapshot.revision === snapshot.revision && cached.generation === generation) {
|
|
725
720
|
return cached.graph;
|
|
726
721
|
}
|
|
727
|
-
const buildAnalysisMode =
|
|
728
|
-
? 'focused'
|
|
729
|
-
: 'full';
|
|
722
|
+
const buildAnalysisMode = requestedAnalysisMode;
|
|
730
723
|
const projectHelpers = buildAnalysisMode === 'focused' ? false : requestedProjectHelpers;
|
|
731
724
|
const graph = buildEventGraph(snapshot, {
|
|
732
725
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
@@ -750,15 +743,10 @@ export class EventChainViewer {
|
|
|
750
743
|
input.mode !== 'roots' &&
|
|
751
744
|
input.mode !== 'trace' &&
|
|
752
745
|
input.mode !== 'explain_path',
|
|
753
|
-
...(input.mode === 'trace' || input.mode === 'explain_path'
|
|
754
|
-
? { analysisMode: 'focused' }
|
|
755
|
-
: {}),
|
|
756
746
|
...(input.principal === undefined ? {} : { principal: input.principal }),
|
|
757
747
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
758
748
|
});
|
|
759
|
-
|
|
760
|
-
// reusing its structural cache and avoiding a second large graph build.
|
|
761
|
-
const graph = input.mode === 'trace' || input.mode === 'explain_path' ? { ...scannedGraph } : scannedGraph;
|
|
749
|
+
const graph = scannedGraph;
|
|
762
750
|
const maxDepth = input.maxDepth ?? 8;
|
|
763
751
|
const maxNodes = input.maxNodes ?? 500;
|
|
764
752
|
const maxEdges = input.maxEdges ?? 2_000;
|
|
@@ -110,6 +110,7 @@ export function parseBmFont(text) {
|
|
|
110
110
|
? fields.width
|
|
111
111
|
: 0,
|
|
112
112
|
page: typeof fields.page === 'number' ? fields.page : 0,
|
|
113
|
+
channelMask: typeof fields.chnl === 'number' ? fields.chnl : 15,
|
|
113
114
|
});
|
|
114
115
|
}
|
|
115
116
|
recognised = true;
|
|
@@ -471,7 +472,9 @@ export class GuiAssetCatalog {
|
|
|
471
472
|
if (previous !== undefined)
|
|
472
473
|
penX += (entry.bmfont.kerning.get(`${previous}:${codePoint}`) ?? 0) * scale;
|
|
473
474
|
const pagePath = entry.bmfont.pages[metric.page];
|
|
474
|
-
|
|
475
|
+
// Older shipped HOI4 descriptors omit the page record entirely. Page zero still uses the
|
|
476
|
+
// descriptor stem (for example garamond_12.fnt -> garamond_12.dds/tga).
|
|
477
|
+
const pageFile = this.resolveBmFontPage(entry, pagePath ?? '', metric.page);
|
|
475
478
|
const raster = pageFile === undefined
|
|
476
479
|
? undefined
|
|
477
480
|
: await this.loadBmFontGlyph(entry, pageFile, metric, codePoint);
|
|
@@ -619,26 +622,73 @@ export class GuiAssetCatalog {
|
|
|
619
622
|
.raw()
|
|
620
623
|
.toBuffer({ resolveWithObject: true });
|
|
621
624
|
const channelRoles = entry.bmfont?.channelRoles;
|
|
622
|
-
|
|
625
|
+
const channelRanges = [0, 1, 2, 3].map((channel) => {
|
|
626
|
+
let minimum = 255;
|
|
627
|
+
let maximum = 0;
|
|
628
|
+
for (let offset = channel; offset < extracted.data.length; offset += 4) {
|
|
629
|
+
const sample = extracted.data[offset] ?? 0;
|
|
630
|
+
minimum = Math.min(minimum, sample);
|
|
631
|
+
maximum = Math.max(maximum, sample);
|
|
632
|
+
}
|
|
633
|
+
return maximum - minimum;
|
|
634
|
+
});
|
|
635
|
+
const channelSelected = (channel) => {
|
|
636
|
+
if (metric.channelMask === 15)
|
|
637
|
+
return true;
|
|
638
|
+
const maskForChannel = [4, 2, 1, 8][channel] ?? 0;
|
|
639
|
+
return (metric.channelMask & maskForChannel) !== 0;
|
|
640
|
+
};
|
|
641
|
+
const informativeChannels = (role) => channelRoles === undefined
|
|
642
|
+
? []
|
|
643
|
+
: channelRoles
|
|
644
|
+
.map((candidate, channel) => ({ candidate, channel }))
|
|
645
|
+
.filter(({ candidate, channel }) => candidate === role &&
|
|
646
|
+
channelSelected(channel) &&
|
|
647
|
+
(channelRanges[channel] ?? 0) > 0)
|
|
648
|
+
.map(({ channel }) => channel);
|
|
649
|
+
const alphaIsInformative = (channelRanges[3] ?? 0) > 0;
|
|
650
|
+
let faceChannels = informativeChannels(0);
|
|
651
|
+
let borderChannels = informativeChannels(1);
|
|
652
|
+
if (faceChannels.length === 0) {
|
|
653
|
+
if (borderChannels.length > 0) {
|
|
654
|
+
// Some vanilla fonts declare their only useful alpha bitmap as the outline
|
|
655
|
+
// channel even though the atlas contains no separate face channel. In that case
|
|
656
|
+
// the alpha bitmap is the face; treating constant-white RGB as coverage produces
|
|
657
|
+
// an opaque rectangle for every glyph.
|
|
658
|
+
faceChannels = borderChannels;
|
|
659
|
+
borderChannels = [];
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
faceChannels = alphaIsInformative
|
|
663
|
+
? [3]
|
|
664
|
+
: channelRanges
|
|
665
|
+
.map((range, channel) => ({ range, channel }))
|
|
666
|
+
.filter(({ range }) => range > 0)
|
|
667
|
+
.map(({ channel }) => channel);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const facePixels = Buffer.alloc(extracted.data.length);
|
|
623
671
|
let borderPixels;
|
|
624
|
-
if (
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
? Buffer.alloc(extracted.data.length)
|
|
628
|
-
: undefined;
|
|
672
|
+
if (faceChannels.length > 0) {
|
|
673
|
+
borderPixels =
|
|
674
|
+
borderChannels.length > 0 ? Buffer.alloc(extracted.data.length) : undefined;
|
|
629
675
|
// BMFont role 0 is coverage, not a per-glyph light/dark bitmap. Vanilla atlases may
|
|
630
676
|
// contain glyphs with either more filled or more empty pixels, so polarity inference
|
|
631
677
|
// from an individual crop corrupts neighboring letters inconsistently.
|
|
632
678
|
for (let offset = 0; offset < extracted.data.length; offset += 4) {
|
|
633
679
|
let faceCoverage = 0;
|
|
634
680
|
let borderCoverage = 0;
|
|
635
|
-
for (
|
|
681
|
+
for (const channel of faceChannels) {
|
|
636
682
|
const sample = extracted.data[offset + channel] ?? 0;
|
|
637
|
-
|
|
638
|
-
faceCoverage = Math.max(faceCoverage, sample);
|
|
639
|
-
if (channelRoles[channel] === 1)
|
|
640
|
-
borderCoverage = Math.max(borderCoverage, sample);
|
|
683
|
+
faceCoverage = Math.max(faceCoverage, sample);
|
|
641
684
|
}
|
|
685
|
+
for (const channel of borderChannels)
|
|
686
|
+
borderCoverage = Math.max(borderCoverage, extracted.data[offset + channel] ?? 0);
|
|
687
|
+
// Vanilla's compressed multi-channel atlases can bleed a neighboring glyph into
|
|
688
|
+
// an RGB face crop while the alpha silhouette remains correctly bounded. The
|
|
689
|
+
// face is the intersection, not the union, of those declared layers.
|
|
690
|
+
if (borderChannels.length > 0)
|
|
691
|
+
faceCoverage = Math.min(faceCoverage, borderCoverage);
|
|
642
692
|
facePixels[offset] = 255;
|
|
643
693
|
facePixels[offset + 1] = 255;
|
|
644
694
|
facePixels[offset + 2] = 255;
|
|
@@ -60,7 +60,7 @@ function sceneGlyphDefinitions(scene) {
|
|
|
60
60
|
.map(([key, glyphPath]) => `<path id="${outlineDefinitionId(key)}" d="${glyphPath}"/>`),
|
|
61
61
|
...[...bitmaps.entries()]
|
|
62
62
|
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
63
|
-
.map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none"/>${glyph.borderDataUri === undefined ? '' : `<image id="${bitmapDefinitionId(key, 'border')}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.borderDataUri}" preserveAspectRatio="none"/>`}`),
|
|
63
|
+
.map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none" image-rendering="optimizeSpeed"/>${glyph.borderDataUri === undefined ? '' : `<image id="${bitmapDefinitionId(key, 'border')}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.borderDataUri}" preserveAspectRatio="none" image-rendering="optimizeSpeed"/>`}`),
|
|
64
64
|
].join('');
|
|
65
65
|
}
|
|
66
66
|
function bitmapTintMarkup(id, colour, glyphs, rect) {
|
|
@@ -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) {
|
|
@@ -11,8 +11,9 @@ import { compareProbabilityResults } from './compare.js';
|
|
|
11
11
|
import { evaluateExactCandidates, evaluateSurfaceScenarios } from './evaluation.js';
|
|
12
12
|
import { VERIFIED_GAME_CHECKSUM, VERIFIED_GAME_RAW_VERSION, VERIFIED_GAME_VERSION, } from './model.js';
|
|
13
13
|
import { renderProbabilityResult } from './render.js';
|
|
14
|
-
import { analyzeSequence } from './sequence.js';
|
|
14
|
+
import { analyzeSequence, evaluateCustomPoolPoint } from './sequence.js';
|
|
15
15
|
import { simulateSurface } from './simulation.js';
|
|
16
|
+
import { Rational } from './rational.js';
|
|
16
17
|
import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
|
|
17
18
|
import { sweepSurface } from './sweep.js';
|
|
18
19
|
const states = new WeakMap();
|
|
@@ -584,6 +585,63 @@ function customSurface(manifest, sourceRevision) {
|
|
|
584
585
|
unsupported: [],
|
|
585
586
|
};
|
|
586
587
|
}
|
|
588
|
+
function customPoolScenarios(surface, manifest, scenarioSet) {
|
|
589
|
+
return scenarioSet.scenarios.map((scenario) => {
|
|
590
|
+
const point = evaluateCustomPoolPoint(manifest, scenario);
|
|
591
|
+
const ordered = [...point.candidates].sort((left, right) => right.probability - left.probability || compareCodeUnits(left.id, right.id));
|
|
592
|
+
const rank = new Map(ordered.map((candidate, index) => [candidate.id, index + 1]));
|
|
593
|
+
const candidates = point.candidates.map((candidate) => {
|
|
594
|
+
const exactWeight = Rational.parse(candidate.weight);
|
|
595
|
+
const exactPoolTotal = Rational.parse(point.poolTotal);
|
|
596
|
+
const exactProbability = !candidate.eligible
|
|
597
|
+
? Rational.zero
|
|
598
|
+
: manifest.selection.mode === 'categorical_weighted'
|
|
599
|
+
? exactWeight !== undefined && exactPoolTotal !== undefined && !exactPoolTotal.isZero()
|
|
600
|
+
? exactWeight.divide(exactPoolTotal)
|
|
601
|
+
: undefined
|
|
602
|
+
: exactWeight?.divide(new Rational(100n)).max(Rational.zero).min(Rational.one);
|
|
603
|
+
const unresolved = point.unresolved.filter(({ candidateId }) => candidateId === undefined || candidateId === candidate.id);
|
|
604
|
+
return {
|
|
605
|
+
id: candidate.id,
|
|
606
|
+
eligibility: candidate.eligible ? 'true' : 'false',
|
|
607
|
+
supportLevel: unresolved.length === 0 ? 'exact' : 'unsupported',
|
|
608
|
+
rawValue: exactWeight?.toJSON() ?? null,
|
|
609
|
+
conditionalProbability: candidate.probability,
|
|
610
|
+
...(exactProbability === undefined
|
|
611
|
+
? {}
|
|
612
|
+
: { exactConditionalProbability: exactProbability.toJSON() }),
|
|
613
|
+
rank: candidate.eligible ? (rank.get(candidate.id) ?? null) : null,
|
|
614
|
+
trace: [],
|
|
615
|
+
provenance: surface.candidates.find(({ id }) => id === candidate.id)?.provenance ?? [],
|
|
616
|
+
unresolved,
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
const eligible = ordered.filter(({ eligible }) => eligible);
|
|
620
|
+
return {
|
|
621
|
+
id: scenario.id,
|
|
622
|
+
...(scenario.label === undefined ? {} : { label: scenario.label }),
|
|
623
|
+
...(scenario.prevalence === undefined ? {} : { prevalence: scenario.prevalence }),
|
|
624
|
+
poolComplete: true,
|
|
625
|
+
supportLevel: point.unresolved.length === 0 ? 'exact' : 'unsupported',
|
|
626
|
+
candidates,
|
|
627
|
+
...(Rational.parse(point.poolTotal) === undefined
|
|
628
|
+
? {}
|
|
629
|
+
: { poolTotal: Rational.parse(point.poolTotal).toJSON() }),
|
|
630
|
+
summary: {
|
|
631
|
+
topOutcomes: eligible.slice(0, 3).map(({ id }) => id),
|
|
632
|
+
bottomEligibleOutcomes: eligible.slice(-3).map(({ id }) => id),
|
|
633
|
+
impossibleOutcomes: ordered
|
|
634
|
+
.filter(({ probability }) => probability === 0)
|
|
635
|
+
.map(({ id }) => id),
|
|
636
|
+
unresolvedOutcomes: candidates
|
|
637
|
+
.filter(({ unresolved }) => unresolved.length > 0)
|
|
638
|
+
.map(({ id }) => id),
|
|
639
|
+
dominantFactors: [],
|
|
640
|
+
},
|
|
641
|
+
unresolved: point.unresolved,
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}
|
|
587
645
|
export class ProbabilityAnalyzer {
|
|
588
646
|
engine;
|
|
589
647
|
state;
|
|
@@ -775,13 +833,61 @@ export class ProbabilityAnalyzer {
|
|
|
775
833
|
}
|
|
776
834
|
return complete;
|
|
777
835
|
}
|
|
778
|
-
async inspect(context, adapter, source, candidatePool = []) {
|
|
779
|
-
if (source === undefined)
|
|
836
|
+
async inspect(context, adapter, source, candidatePool = [], customPoolManifest) {
|
|
837
|
+
if (source === undefined && customPoolManifest === undefined)
|
|
780
838
|
return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
|
|
781
|
-
|
|
839
|
+
if (customPoolManifest !== undefined && source !== undefined)
|
|
840
|
+
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide either source or customPoolManifest for inspection');
|
|
841
|
+
if (customPoolManifest !== undefined && adapter !== 'custom_weighted_pool')
|
|
842
|
+
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest inspection requires adapter custom_weighted_pool');
|
|
843
|
+
const snapshot = await this.scan(context, [source?.path]);
|
|
844
|
+
if (customPoolManifest !== undefined) {
|
|
845
|
+
const surface = customSurface(customPoolManifest, snapshot.revision);
|
|
846
|
+
const gameVersionVerification = await this.verifyGameVersion(context, 'custom_weighted_pool');
|
|
847
|
+
const report = {
|
|
848
|
+
schemaVersion: 'probability-inspection.v2',
|
|
849
|
+
workspaceId: context.workspaceId,
|
|
850
|
+
surface: {
|
|
851
|
+
id: surface.id,
|
|
852
|
+
adapter: surface.adapter,
|
|
853
|
+
poolComplete: true,
|
|
854
|
+
sourceRevision: surface.sourceRevision,
|
|
855
|
+
sourceHash: surface.sourceHash,
|
|
856
|
+
gameVersionVerification,
|
|
857
|
+
candidateCount: surface.candidates.length,
|
|
858
|
+
candidates: surface.candidates.map((candidate) => ({
|
|
859
|
+
id: candidate.id,
|
|
860
|
+
sourceKind: candidate.sourceKind,
|
|
861
|
+
provenance: candidate.provenance,
|
|
862
|
+
hasEligibility: customPoolManifest.candidates.find(({ id }) => id === candidate.id)?.eligibleWhen !==
|
|
863
|
+
undefined,
|
|
864
|
+
hasWeightBlock: true,
|
|
865
|
+
requiredInputs: [],
|
|
866
|
+
referencedProvenance: [],
|
|
867
|
+
analysisSupport: 'exact',
|
|
868
|
+
})),
|
|
869
|
+
requiredInputs: [],
|
|
870
|
+
unsupported: [],
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
874
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${surface.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
875
|
+
kind: 'probability-inspection',
|
|
876
|
+
toolVersion: PACKAGE_VERSION,
|
|
877
|
+
schemaVersion: 'probability-inspection.v2',
|
|
878
|
+
sourceHashes: { aggregate: surface.sourceHash },
|
|
879
|
+
}, 'Declared custom weighted-pool inventory', context.signal);
|
|
880
|
+
return {
|
|
881
|
+
adapters: probabilityAdapters(),
|
|
882
|
+
surface: report.surface,
|
|
883
|
+
artifacts: [publicArtifactLink(artifact)],
|
|
884
|
+
filesScanned: [],
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const selectedSource = source;
|
|
782
888
|
const storeDiscovery = async (inventory, reason) => {
|
|
783
889
|
const availableAdapters = inventory.adapters.filter(({ candidateCount }) => candidateCount > 0);
|
|
784
|
-
const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) =>
|
|
890
|
+
const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) => selectedSource.identifier !== undefined && identifierMatchCount > 0);
|
|
785
891
|
const matchingPool = availableAdapters.filter(({ candidatePoolMatchCount }) => candidatePool.length > 0 && candidatePoolMatchCount > 0);
|
|
786
892
|
const requested = availableAdapters.find(({ adapterId }) => adapterId === adapter);
|
|
787
893
|
const ranked = [
|
|
@@ -826,7 +932,7 @@ export class ProbabilityAnalyzer {
|
|
|
826
932
|
};
|
|
827
933
|
};
|
|
828
934
|
if (adapter === undefined) {
|
|
829
|
-
const inventory = inventoryWeightedSurfaces(snapshot,
|
|
935
|
+
const inventory = inventoryWeightedSurfaces(snapshot, selectedSource, candidatePool);
|
|
830
936
|
return storeDiscovery(inventory, inventory.adapters.some(({ candidateCount }) => candidateCount > 0)
|
|
831
937
|
? 'source_inventory'
|
|
832
938
|
: 'no_weighted_surfaces');
|
|
@@ -834,14 +940,14 @@ export class ProbabilityAnalyzer {
|
|
|
834
940
|
const gameVersionVerification = await this.verifyGameVersion(context, adapter);
|
|
835
941
|
let surface;
|
|
836
942
|
try {
|
|
837
|
-
surface = discoverWeightedSurface(snapshot, adapter,
|
|
943
|
+
surface = discoverWeightedSurface(snapshot, adapter, selectedSource, candidatePool);
|
|
838
944
|
}
|
|
839
945
|
catch (error) {
|
|
840
946
|
if (!(error instanceof ServiceError) ||
|
|
841
947
|
(error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
|
|
842
948
|
error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
|
|
843
949
|
throw error;
|
|
844
|
-
const inventory = inventoryWeightedSurfaces(snapshot,
|
|
950
|
+
const inventory = inventoryWeightedSurfaces(snapshot, selectedSource, candidatePool);
|
|
845
951
|
const requested = inventory.adapters.find(({ adapterId }) => adapterId === adapter);
|
|
846
952
|
const reason = error.code === 'PROBABILITY_IDENTIFIER_NOT_FOUND'
|
|
847
953
|
? 'identifier_not_found'
|
|
@@ -916,11 +1022,22 @@ export class ProbabilityAnalyzer {
|
|
|
916
1022
|
if (request.scenarioSet.workspaceId !== undefined &&
|
|
917
1023
|
request.scenarioSet.workspaceId !== request.workspaceId)
|
|
918
1024
|
throw new ServiceError('PROBABILITY_SCENARIO_WORKSPACE_MISMATCH', 'Scenario set belongs to a different workspace');
|
|
1025
|
+
const usingManifest = request.customPoolManifest !== undefined;
|
|
1026
|
+
if (usingManifest !== (request.source === undefined))
|
|
1027
|
+
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide exactly one source or customPoolManifest');
|
|
1028
|
+
if (usingManifest && request.adapter !== 'custom_weighted_pool')
|
|
1029
|
+
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest requires the custom_weighted_pool adapter');
|
|
1030
|
+
if (!usingManifest && request.adapter === 'custom_weighted_pool')
|
|
1031
|
+
throw new ServiceError('PROBABILITY_MANIFEST_REQUIRED', 'custom_weighted_pool requires customPoolManifest');
|
|
919
1032
|
const gameVersionVerification = await this.verifyGameVersion(request, request.adapter);
|
|
920
|
-
const snapshot = await this.scan(request, [request.source
|
|
921
|
-
const surface =
|
|
1033
|
+
const snapshot = await this.scan(request, [request.source?.path]);
|
|
1034
|
+
const surface = usingManifest
|
|
1035
|
+
? customSurface(request.customPoolManifest, snapshot.revision)
|
|
1036
|
+
: discoverWeightedSurface(snapshot, request.adapter, request.source, request.candidatePool ?? []);
|
|
922
1037
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
923
|
-
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet,
|
|
1038
|
+
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, usingManifest
|
|
1039
|
+
? request.customPoolManifest.candidates.map(({ id }) => id)
|
|
1040
|
+
: (request.candidatePool ?? []), gameVersionVerification, {
|
|
924
1041
|
operation,
|
|
925
1042
|
horizonDays: request.horizonDays,
|
|
926
1043
|
requestedMetrics: request.metrics,
|
|
@@ -931,7 +1048,9 @@ export class ProbabilityAnalyzer {
|
|
|
931
1048
|
const cached = this.state.byCacheKey.get(metadata.cacheKey);
|
|
932
1049
|
if (cached !== undefined)
|
|
933
1050
|
return { result: cached, surface, definitions, snapshot };
|
|
934
|
-
const scenarios =
|
|
1051
|
+
const scenarios = usingManifest
|
|
1052
|
+
? customPoolScenarios(surface, request.customPoolManifest, request.scenarioSet)
|
|
1053
|
+
: evaluateSurfaceScenarios(surface, request.scenarioSet.scenarios, definitions, request.horizonDays);
|
|
935
1054
|
return {
|
|
936
1055
|
result: this.buildResult(operation, metadata, surface, scenarios, {
|
|
937
1056
|
...(request.acceptanceBands === undefined
|
|
@@ -1137,12 +1256,31 @@ export class ProbabilityAnalyzer {
|
|
|
1137
1256
|
return this.store(result, request.outputs ?? ['json', 'sequence'], false, request.signal, request.principal);
|
|
1138
1257
|
}
|
|
1139
1258
|
async compare(request) {
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1259
|
+
const manifestMode = request.beforeManifest !== undefined || request.afterManifest !== undefined;
|
|
1260
|
+
if (manifestMode !== (request.before === undefined && request.after === undefined))
|
|
1261
|
+
throw new ServiceError('PROBABILITY_COMPARE_INPUT_AMBIGUOUS', 'Compare either before/after sources or beforeManifest/afterManifest');
|
|
1262
|
+
if (manifestMode &&
|
|
1263
|
+
(request.beforeManifest === undefined || request.afterManifest === undefined))
|
|
1264
|
+
throw new ServiceError('PROBABILITY_COMPARE_MANIFEST_REQUIRED', 'Custom-pool comparison requires both beforeManifest and afterManifest');
|
|
1265
|
+
if (!manifestMode && (request.before === undefined || request.after === undefined))
|
|
1266
|
+
throw new ServiceError('PROBABILITY_COMPARE_SOURCE_REQUIRED', 'Source comparison requires both before and after');
|
|
1267
|
+
const before = manifestMode
|
|
1268
|
+
? await this.analyzeUnstored('evaluate', {
|
|
1269
|
+
...request,
|
|
1270
|
+
customPoolManifest: request.beforeManifest,
|
|
1271
|
+
})
|
|
1272
|
+
: await this.analyzeUnstored('evaluate', { ...request, source: request.before });
|
|
1273
|
+
const after = manifestMode
|
|
1274
|
+
? await this.analyzeUnstored('evaluate', {
|
|
1275
|
+
...request,
|
|
1276
|
+
customPoolManifest: request.afterManifest,
|
|
1277
|
+
refresh: false,
|
|
1278
|
+
})
|
|
1279
|
+
: await this.analyzeUnstored('evaluate', {
|
|
1280
|
+
...request,
|
|
1281
|
+
source: request.after,
|
|
1282
|
+
refresh: false,
|
|
1283
|
+
});
|
|
1146
1284
|
const metadata = {
|
|
1147
1285
|
...after.result.metadata,
|
|
1148
1286
|
cacheKey: hashCanonical({
|
|
@@ -20,7 +20,6 @@ const GRAPH_ARTIFACT_BYTES = 134_217_728;
|
|
|
20
20
|
const GRAPH_ARTIFACT_CHUNKS = 2_048;
|
|
21
21
|
const HISTORY_ENTRIES = 12;
|
|
22
22
|
const FOCUSED_RENDER_LIMIT = 32;
|
|
23
|
-
const FOCUSED_GRAPH_FILE_THRESHOLD = 1_000;
|
|
24
23
|
class BoundedTechnologyFragmentCache {
|
|
25
24
|
#values = new Map();
|
|
26
25
|
#bytes = 0;
|
|
@@ -88,6 +87,45 @@ function technologyScanPatterns(workspace) {
|
|
|
88
87
|
...under(roots.localisation, '*.{yml,yaml}'),
|
|
89
88
|
].sort(compareCodeUnits);
|
|
90
89
|
}
|
|
90
|
+
function recomputeTechnologyShadowing(files) {
|
|
91
|
+
const groups = new Map();
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
delete file.shadowedBy;
|
|
94
|
+
const key = file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
95
|
+
const group = groups.get(key) ?? [];
|
|
96
|
+
group.push(file);
|
|
97
|
+
groups.set(key, group);
|
|
98
|
+
}
|
|
99
|
+
for (const group of groups.values()) {
|
|
100
|
+
group.sort((left, right) => right.loadOrder - left.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
101
|
+
const active = group[0];
|
|
102
|
+
for (const shadowed of group.slice(1))
|
|
103
|
+
shadowed.shadowedBy = active.displayPath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function scanTechnologySources(engine, workspaceId, workspace, principal, signal) {
|
|
107
|
+
const patterns = technologyScanPatterns(workspace);
|
|
108
|
+
const scans = await Promise.all([
|
|
109
|
+
engine.scan(workspaceId, { patterns, rootKinds: ['mod', 'dependency', 'fixture'] }, principal, signal),
|
|
110
|
+
...(workspace.roots.some(({ kind }) => kind === 'game')
|
|
111
|
+
? [engine.scan(workspaceId, { patterns, rootKinds: ['game'] }, principal, signal)]
|
|
112
|
+
: []),
|
|
113
|
+
]);
|
|
114
|
+
const files = scans.flatMap(({ files }) => files.map((file) => ({ ...file })));
|
|
115
|
+
recomputeTechnologyShadowing(files);
|
|
116
|
+
files.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
117
|
+
const index = engine.indexFiles(files);
|
|
118
|
+
return {
|
|
119
|
+
workspaceId,
|
|
120
|
+
revision: hashCanonical(files.map(({ displayPath, loadOrder, sha256 }) => ({ displayPath, loadOrder, sha256 }))),
|
|
121
|
+
files,
|
|
122
|
+
index,
|
|
123
|
+
complete: scans.every(({ complete }) => complete) && index.complete,
|
|
124
|
+
skippedSourceCount: index.skippedSourceCount,
|
|
125
|
+
skippedSources: index.skippedSources,
|
|
126
|
+
diagnostics: [...scans.flatMap(({ diagnostics }) => diagnostics), ...index.diagnostics],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
91
129
|
function activeTechnologySprite(spriteName, spritePath, sprites) {
|
|
92
130
|
return sprites.find((sprite) => sprite.name === spriteName && (spritePath === undefined || sprite.sourcePath === spritePath));
|
|
93
131
|
}
|
|
@@ -130,11 +168,6 @@ async function resolveTechnologyIconDataUris(cached, requestedSprites, budget, s
|
|
|
130
168
|
}));
|
|
131
169
|
return Object.fromEntries(Object.entries(dataUris).sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
132
170
|
}
|
|
133
|
-
function technologyAnalysisMode(workspace, snapshot) {
|
|
134
|
-
return workspace.gameRoot !== undefined || snapshot.files.length > FOCUSED_GRAPH_FILE_THRESHOLD
|
|
135
|
-
? 'focused'
|
|
136
|
-
: 'full';
|
|
137
|
-
}
|
|
138
171
|
function rememberGraph(state, graph) {
|
|
139
172
|
const history = state.history.get(graph.workspaceId) ?? new Map();
|
|
140
173
|
history.delete(graph.revision);
|
|
@@ -400,8 +433,8 @@ export class TechnologyTreeViewer {
|
|
|
400
433
|
const cached = this.#state.current.get(workspaceId);
|
|
401
434
|
if (options.refresh !== true && cached?.generation === generation)
|
|
402
435
|
return cached.graph;
|
|
403
|
-
const snapshot = await this.engine
|
|
404
|
-
const analysisMode =
|
|
436
|
+
const snapshot = await scanTechnologySources(this.engine, workspaceId, workspace, options.principal, options.signal);
|
|
437
|
+
const analysisMode = 'full';
|
|
405
438
|
const preliminary = buildTechnologyGraph(snapshot, {
|
|
406
439
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
407
440
|
cache: this.#state.fragments,
|
|
@@ -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.8.
|
|
3
|
+
export const PACKAGE_VERSION = '2.8.11';
|
|
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/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.8.
|
|
4
|
+
"version": "2.8.11",
|
|
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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/focus-planning-sidecar.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/gui-animation-source.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/gui-helper.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/gui-scenario.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/map-operation.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"oneOf": [
|
|
5
5
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/operation-result.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-analysis-result.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-compare-input.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-evaluate-input.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-inspect-input.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-render-input.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-scenario-set.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-sequence-input.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-simulate-input.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.8.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.11/schemas/probability-sweep-input.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.klimPaskov/hoi4-agent-tools",
|
|
4
4
|
"title": "HOI4 Agent Tools",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
|
-
"version": "2.8.
|
|
6
|
+
"version": "2.8.11",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/klimPaskov/hoi4-agent-tools",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "hoi4-agent-tools",
|
|
16
|
-
"version": "2.8.
|
|
16
|
+
"version": "2.8.11",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|