hoi4-agent-tools 2.8.11 → 3.0.1
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 +38 -11
- package/dist/hoi4_agent_tools/core/configuration.js +16 -3
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +10 -1
- package/dist/hoi4_agent_tools/core/engine.js +3 -0
- package/dist/hoi4_agent_tools/core/scanner.js +63 -7
- package/dist/hoi4_agent_tools/event/service.js +30 -23
- package/dist/hoi4_agent_tools/gui/assets.js +218 -0
- package/dist/hoi4_agent_tools/gui/layout.js +70 -9
- package/dist/hoi4_agent_tools/gui/renderer.js +80 -11
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +36 -0
- package/dist/hoi4_agent_tools/gui/studio.js +90 -25
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +9 -5
- package/dist/hoi4_agent_tools/probability/service.js +81 -14
- package/dist/hoi4_agent_tools/schemas/probability.js +2 -0
- package/dist/hoi4_agent_tools/technology/compare.js +6 -1
- package/dist/hoi4_agent_tools/technology/graph.js +67 -2
- package/dist/hoi4_agent_tools/technology/model.js +1 -1
- package/dist/hoi4_agent_tools/technology/queries.js +4 -0
- package/dist/hoi4_agent_tools/technology/render.js +73 -28
- package/dist/hoi4_agent_tools/technology/service.js +4 -1
- package/dist/hoi4_agent_tools/technology/source-analysis.js +102 -19
- 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/docs/probability.md +3 -1
- package/docs/technology.md +1 -1
- 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
|
@@ -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,
|
|
@@ -18,7 +18,7 @@ const candidatePool = z.array(z.string().min(1).max(512)).max(100_000).optional(
|
|
|
18
18
|
const outputs = z.array(probabilityOutputSchema).max(10).optional();
|
|
19
19
|
const commonShape = {
|
|
20
20
|
workspaceId: workspaceIdSchema,
|
|
21
|
-
adapter: probabilityAdapterIdSchema,
|
|
21
|
+
adapter: probabilityAdapterIdSchema.optional(),
|
|
22
22
|
scenarioSet: nestedScenarios,
|
|
23
23
|
candidatePool,
|
|
24
24
|
horizonDays: z.number().positive().max(1_000_000).optional(),
|
|
@@ -51,7 +51,9 @@ const inspectInput = z
|
|
|
51
51
|
code: 'custom',
|
|
52
52
|
message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
|
|
53
53
|
});
|
|
54
|
-
if (value.customPoolManifest !== undefined &&
|
|
54
|
+
if (value.customPoolManifest !== undefined &&
|
|
55
|
+
value.adapter !== undefined &&
|
|
56
|
+
value.adapter !== 'custom_weighted_pool')
|
|
55
57
|
context.addIssue({
|
|
56
58
|
code: 'custom',
|
|
57
59
|
path: ['adapter'],
|
|
@@ -71,7 +73,9 @@ const evaluateInput = z
|
|
|
71
73
|
code: 'custom',
|
|
72
74
|
message: 'Provide exactly one source or customPoolManifest',
|
|
73
75
|
});
|
|
74
|
-
if (value.customPoolManifest !== undefined &&
|
|
76
|
+
if (value.customPoolManifest !== undefined &&
|
|
77
|
+
value.adapter !== undefined &&
|
|
78
|
+
value.adapter !== 'custom_weighted_pool')
|
|
75
79
|
context.addIssue({
|
|
76
80
|
code: 'custom',
|
|
77
81
|
path: ['adapter'],
|
|
@@ -120,7 +124,7 @@ const sequenceInput = z
|
|
|
120
124
|
const compareInput = z
|
|
121
125
|
.object({
|
|
122
126
|
workspaceId: workspaceIdSchema,
|
|
123
|
-
adapter: probabilityAdapterIdSchema,
|
|
127
|
+
adapter: probabilityAdapterIdSchema.optional(),
|
|
124
128
|
before: nestedSource.optional(),
|
|
125
129
|
after: nestedSource.optional(),
|
|
126
130
|
beforeManifest: nestedManifest.optional(),
|
|
@@ -149,7 +153,7 @@ const compareInput = z
|
|
|
149
153
|
code: 'custom',
|
|
150
154
|
message: 'Custom-pool comparison requires beforeManifest and afterManifest',
|
|
151
155
|
});
|
|
152
|
-
if (manifestMode && value.adapter !== 'custom_weighted_pool')
|
|
156
|
+
if (manifestMode && value.adapter !== undefined && value.adapter !== 'custom_weighted_pool')
|
|
153
157
|
context.addIssue({
|
|
154
158
|
code: 'custom',
|
|
155
159
|
path: ['adapter'],
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
4
4
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
5
|
-
import { probabilityDomainScanPatterns } from '../core/domain-scan-patterns.js';
|
|
5
|
+
import { probabilityDomainScanPatterns, probabilitySharedDefinitionPatterns, } from '../core/domain-scan-patterns.js';
|
|
6
6
|
import { publicArtifactLink } from '../core/artifacts.js';
|
|
7
7
|
import { ServiceError } from '../core/result.js';
|
|
8
8
|
import { PACKAGE_VERSION } from '../version.js';
|
|
@@ -17,6 +17,43 @@ import { Rational } from './rational.js';
|
|
|
17
17
|
import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
|
|
18
18
|
import { sweepSurface } from './sweep.js';
|
|
19
19
|
const states = new WeakMap();
|
|
20
|
+
function resolveSourceBackedAdapter(snapshot, source, candidatePool, requested) {
|
|
21
|
+
if (requested !== undefined && requested !== 'custom_weighted_pool') {
|
|
22
|
+
try {
|
|
23
|
+
discoverWeightedSurface(snapshot, requested, source, candidatePool);
|
|
24
|
+
return requested;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!(error instanceof ServiceError) ||
|
|
28
|
+
(error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
|
|
29
|
+
error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
|
|
34
|
+
const viable = inventory.adapters
|
|
35
|
+
.filter(({ candidateCount, identifierMatchCount, candidatePoolMatchCount }) => candidateCount > 0 &&
|
|
36
|
+
(source.identifier === undefined || identifierMatchCount > 0) &&
|
|
37
|
+
(candidatePool.length === 0 || candidatePoolMatchCount > 0))
|
|
38
|
+
.sort((left, right) => right.identifierMatchCount - left.identifierMatchCount ||
|
|
39
|
+
right.candidatePoolMatchCount - left.candidatePoolMatchCount ||
|
|
40
|
+
right.candidateCount - left.candidateCount ||
|
|
41
|
+
compareCodeUnits(left.adapterId, right.adapterId));
|
|
42
|
+
if (viable.length === 1)
|
|
43
|
+
return viable[0].adapterId;
|
|
44
|
+
if (viable.length === 0)
|
|
45
|
+
throw new ServiceError('PROBABILITY_SURFACE_EMPTY', 'No weighted blocks matched this request', {
|
|
46
|
+
requestedAdapter: requested,
|
|
47
|
+
source,
|
|
48
|
+
candidatePool,
|
|
49
|
+
availableAdapters: inventory.adapters.filter(({ candidateCount }) => candidateCount > 0),
|
|
50
|
+
});
|
|
51
|
+
throw new ServiceError('PROBABILITY_ADAPTER_AMBIGUOUS', 'Several weighted adapters match this source; select one adapter or narrow the source by identifier or line', {
|
|
52
|
+
requestedAdapter: requested,
|
|
53
|
+
source,
|
|
54
|
+
matchingAdapters: viable,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
20
57
|
function analyzerState(engine) {
|
|
21
58
|
let state = states.get(engine);
|
|
22
59
|
if (state === undefined) {
|
|
@@ -649,12 +686,19 @@ export class ProbabilityAnalyzer {
|
|
|
649
686
|
this.engine = engine;
|
|
650
687
|
this.state = analyzerState(engine);
|
|
651
688
|
}
|
|
652
|
-
async scan(context, sourcePaths = []) {
|
|
689
|
+
async scan(context, sourcePaths = [], emptyScope = 'domain') {
|
|
653
690
|
if (context.refresh === true)
|
|
654
691
|
this.engine.invalidate(context.workspaceId);
|
|
655
692
|
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
693
|
+
const selectedSourcePaths = sourcePaths.filter((sourcePath) => sourcePath !== undefined);
|
|
656
694
|
return this.engine.scan(context.workspaceId, {
|
|
657
|
-
patterns:
|
|
695
|
+
patterns: selectedSourcePaths.length > 0
|
|
696
|
+
? probabilityDomainScanPatterns(workspace, selectedSourcePaths)
|
|
697
|
+
: emptyScope === 'domain'
|
|
698
|
+
? probabilityDomainScanPatterns(workspace)
|
|
699
|
+
: emptyScope === 'shared'
|
|
700
|
+
? probabilitySharedDefinitionPatterns()
|
|
701
|
+
: [],
|
|
658
702
|
}, context.principal, context.signal);
|
|
659
703
|
}
|
|
660
704
|
async verifyGameVersion(context, adapter) {
|
|
@@ -714,6 +758,14 @@ export class ProbabilityAnalyzer {
|
|
|
714
758
|
return {
|
|
715
759
|
workspaceId: context.workspaceId,
|
|
716
760
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
761
|
+
...(typeof extra.sourceScope === 'string'
|
|
762
|
+
? {
|
|
763
|
+
sourceScope: extra.sourceScope,
|
|
764
|
+
}
|
|
765
|
+
: {}),
|
|
766
|
+
...(Array.isArray(extra.sourcePaths) && extra.sourcePaths.length > 0
|
|
767
|
+
? { sourcePaths: extra.sourcePaths }
|
|
768
|
+
: {}),
|
|
717
769
|
sourceRevision: snapshot.revision,
|
|
718
770
|
sourceHash: surface.sourceHash,
|
|
719
771
|
scenarioHash,
|
|
@@ -838,9 +890,11 @@ export class ProbabilityAnalyzer {
|
|
|
838
890
|
return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
|
|
839
891
|
if (customPoolManifest !== undefined && source !== undefined)
|
|
840
892
|
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide either source or customPoolManifest for inspection');
|
|
841
|
-
if (customPoolManifest !== undefined &&
|
|
893
|
+
if (customPoolManifest !== undefined &&
|
|
894
|
+
adapter !== undefined &&
|
|
895
|
+
adapter !== 'custom_weighted_pool')
|
|
842
896
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest inspection requires adapter custom_weighted_pool');
|
|
843
|
-
const snapshot = await this.scan(context, [source?.path]);
|
|
897
|
+
const snapshot = await this.scan(context, [source?.path], source === undefined ? 'none' : source.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
844
898
|
if (customPoolManifest !== undefined) {
|
|
845
899
|
const surface = customSurface(customPoolManifest, snapshot.revision);
|
|
846
900
|
const gameVersionVerification = await this.verifyGameVersion(context, 'custom_weighted_pool');
|
|
@@ -1025,20 +1079,31 @@ export class ProbabilityAnalyzer {
|
|
|
1025
1079
|
const usingManifest = request.customPoolManifest !== undefined;
|
|
1026
1080
|
if (usingManifest !== (request.source === undefined))
|
|
1027
1081
|
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide exactly one source or customPoolManifest');
|
|
1028
|
-
if (usingManifest &&
|
|
1082
|
+
if (usingManifest &&
|
|
1083
|
+
request.adapter !== undefined &&
|
|
1084
|
+
request.adapter !== 'custom_weighted_pool')
|
|
1029
1085
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest requires the custom_weighted_pool adapter');
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1086
|
+
const snapshot = await this.scan(request, [request.source?.path], usingManifest ? 'none' : request.source?.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
1087
|
+
const resolvedAdapter = usingManifest
|
|
1088
|
+
? 'custom_weighted_pool'
|
|
1089
|
+
: resolveSourceBackedAdapter(snapshot, request.source, request.candidatePool ?? [], request.adapter);
|
|
1090
|
+
const gameVersionVerification = await this.verifyGameVersion(request, resolvedAdapter);
|
|
1034
1091
|
const surface = usingManifest
|
|
1035
1092
|
? customSurface(request.customPoolManifest, snapshot.revision)
|
|
1036
|
-
: discoverWeightedSurface(snapshot,
|
|
1093
|
+
: discoverWeightedSurface(snapshot, resolvedAdapter, request.source, request.candidatePool ?? []);
|
|
1037
1094
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
1038
1095
|
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, usingManifest
|
|
1039
1096
|
? request.customPoolManifest.candidates.map(({ id }) => id)
|
|
1040
1097
|
: (request.candidatePool ?? []), gameVersionVerification, {
|
|
1041
1098
|
operation,
|
|
1099
|
+
sourceScope: request.source?.path !== undefined
|
|
1100
|
+
? 'file'
|
|
1101
|
+
: request.source?.inlineClausewitz !== undefined
|
|
1102
|
+
? 'shared'
|
|
1103
|
+
: request.source !== undefined
|
|
1104
|
+
? 'domain'
|
|
1105
|
+
: 'none',
|
|
1106
|
+
sourcePaths: request.source?.path === undefined ? [] : [request.source.path],
|
|
1042
1107
|
horizonDays: request.horizonDays,
|
|
1043
1108
|
requestedMetrics: request.metrics,
|
|
1044
1109
|
acceptanceBands: request.acceptanceBands,
|
|
@@ -1167,7 +1232,7 @@ export class ProbabilityAnalyzer {
|
|
|
1167
1232
|
}
|
|
1168
1233
|
async sequence(request) {
|
|
1169
1234
|
request.signal?.throwIfAborted();
|
|
1170
|
-
const snapshot = await this.scan(request);
|
|
1235
|
+
const snapshot = await this.scan(request, [], 'none');
|
|
1171
1236
|
const surface = customSurface(request.customPoolManifest, snapshot.revision);
|
|
1172
1237
|
const gameVersionVerification = await this.verifyGameVersion(request, 'custom_weighted_pool');
|
|
1173
1238
|
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, request.customPoolManifest.candidates.map(({ id }) => id), gameVersionVerification, {
|
|
@@ -1334,8 +1399,10 @@ export class ProbabilityAnalyzer {
|
|
|
1334
1399
|
return this.store(stale, ['json'], false, request.signal, request.principal);
|
|
1335
1400
|
}
|
|
1336
1401
|
await this.verifyGameVersion(request, result.adapter.id);
|
|
1337
|
-
const snapshot =
|
|
1338
|
-
|
|
1402
|
+
const snapshot = result.metadata.sourceScope === undefined || result.metadata.sourceScope === 'none'
|
|
1403
|
+
? undefined
|
|
1404
|
+
: await this.scan(request, result.metadata.sourcePaths ?? [], result.metadata.sourceScope === 'shared' ? 'shared' : 'domain');
|
|
1405
|
+
if (snapshot !== undefined && snapshot.revision !== result.metadata.sourceRevision) {
|
|
1339
1406
|
const stale = {
|
|
1340
1407
|
...result,
|
|
1341
1408
|
status: 'stale',
|
|
@@ -737,6 +737,8 @@ const probabilityMetadataSchema = z
|
|
|
737
737
|
.object({
|
|
738
738
|
workspaceId: z.string(),
|
|
739
739
|
workspaceIdentity: z.string(),
|
|
740
|
+
sourceScope: z.enum(['file', 'shared', 'domain', 'none']).optional(),
|
|
741
|
+
sourcePaths: z.array(z.string()).max(100_000).optional(),
|
|
740
742
|
sourceRevision: z.string(),
|
|
741
743
|
sourceHash: z.string(),
|
|
742
744
|
scenarioHash: z.string(),
|
|
@@ -4,12 +4,17 @@ function byId(values) {
|
|
|
4
4
|
}
|
|
5
5
|
function placementSignature(values) {
|
|
6
6
|
return hashCanonical(values
|
|
7
|
-
.map(({ folderId, x, y, xExpression, yExpression }) => ({
|
|
7
|
+
.map(({ folderId, x, y, xExpression, yExpression, layoutSize, layoutWidth, layoutHeight, layoutOffsetX, layoutOffsetY, }) => ({
|
|
8
8
|
folderId,
|
|
9
9
|
x: x ?? null,
|
|
10
10
|
y: y ?? null,
|
|
11
11
|
xExpression: xExpression ?? null,
|
|
12
12
|
yExpression: yExpression ?? null,
|
|
13
|
+
layoutSize: layoutSize ?? null,
|
|
14
|
+
layoutWidth: layoutWidth ?? null,
|
|
15
|
+
layoutHeight: layoutHeight ?? null,
|
|
16
|
+
layoutOffsetX: layoutOffsetX ?? null,
|
|
17
|
+
layoutOffsetY: layoutOffsetY ?? null,
|
|
13
18
|
}))
|
|
14
19
|
.sort((left, right) => compareCodeUnits(left.folderId, right.folderId) ||
|
|
15
20
|
(left.x ?? 0) - (right.x ?? 0) ||
|
|
@@ -155,7 +155,51 @@ function activeGridboxes(values) {
|
|
|
155
155
|
.sort((left, right) => compareCodeUnits(left.folderId ?? '', right.folderId ?? '') ||
|
|
156
156
|
compareCodeUnits(left.name, right.name));
|
|
157
157
|
}
|
|
158
|
-
function
|
|
158
|
+
function activeItemLayouts(values) {
|
|
159
|
+
const groups = new Map();
|
|
160
|
+
for (const layout of values) {
|
|
161
|
+
const key = `${layout.folderId}\0${layout.layoutSize}`;
|
|
162
|
+
const group = groups.get(key) ?? [];
|
|
163
|
+
group.push(layout);
|
|
164
|
+
groups.set(key, group);
|
|
165
|
+
}
|
|
166
|
+
return [...groups.values()]
|
|
167
|
+
.map((group) => group.sort((left, right) => right.loadOrder - left.loadOrder ||
|
|
168
|
+
compareCodeUnits(left.sourcePath, right.sourcePath) ||
|
|
169
|
+
left.location.start.offset - right.location.start.offset)[0])
|
|
170
|
+
.sort((left, right) => compareCodeUnits(left.folderId, right.folderId) ||
|
|
171
|
+
compareCodeUnits(left.layoutSize, right.layoutSize));
|
|
172
|
+
}
|
|
173
|
+
function activeYearMarkers(values) {
|
|
174
|
+
const groups = new Map();
|
|
175
|
+
for (const marker of values) {
|
|
176
|
+
const key = `${marker.folderId}\0${marker.name}`;
|
|
177
|
+
const group = groups.get(key) ?? [];
|
|
178
|
+
group.push(marker);
|
|
179
|
+
groups.set(key, group);
|
|
180
|
+
}
|
|
181
|
+
return [...groups.values()]
|
|
182
|
+
.map((group) => group.sort((left, right) => right.loadOrder - left.loadOrder ||
|
|
183
|
+
compareCodeUnits(left.sourcePath, right.sourcePath) ||
|
|
184
|
+
left.location.start.offset - right.location.start.offset)[0])
|
|
185
|
+
.sort((left, right) => compareCodeUnits(left.folderId, right.folderId) ||
|
|
186
|
+
left.year - right.year ||
|
|
187
|
+
compareCodeUnits(left.name, right.name));
|
|
188
|
+
}
|
|
189
|
+
function enrichPlacements(placements, edges, gridboxes, technologies, unlocks, itemLayouts, signal) {
|
|
190
|
+
const technologyById = new Map(technologies.map((technology) => [technology.id, technology]));
|
|
191
|
+
const unlockKindsByTechnology = new Map();
|
|
192
|
+
for (const unlock of unlocks) {
|
|
193
|
+
const kinds = unlockKindsByTechnology.get(unlock.technologyId) ?? new Set();
|
|
194
|
+
kinds.add(unlock.kind);
|
|
195
|
+
unlockKindsByTechnology.set(unlock.technologyId, kinds);
|
|
196
|
+
}
|
|
197
|
+
const layoutsByFolder = new Map();
|
|
198
|
+
for (const layout of itemLayouts) {
|
|
199
|
+
const layouts = layoutsByFolder.get(layout.folderId) ?? new Map();
|
|
200
|
+
layouts.set(layout.layoutSize, layout);
|
|
201
|
+
layoutsByFolder.set(layout.folderId, layouts);
|
|
202
|
+
}
|
|
159
203
|
const placementsByTechnology = new Map();
|
|
160
204
|
for (const placement of placements) {
|
|
161
205
|
const group = placementsByTechnology.get(placement.technologyId) ?? [];
|
|
@@ -198,6 +242,15 @@ function enrichPlacements(placements, edges, gridboxes, signal) {
|
|
|
198
242
|
: gridboxes.filter(({ name, folderId }) => name === expectedGridbox &&
|
|
199
243
|
(folderId === undefined || folderId === placement.folderId));
|
|
200
244
|
const gridbox = candidates[0];
|
|
245
|
+
const technology = technologyById.get(placement.technologyId);
|
|
246
|
+
const folderLayouts = layoutsByFolder.get(placement.folderId);
|
|
247
|
+
const unlockKinds = unlockKindsByTechnology.get(placement.technologyId) ?? new Set();
|
|
248
|
+
const unlocksEquipment = unlockKinds.has('equipment') || unlockKinds.has('equipment_module');
|
|
249
|
+
const layoutSize = technology?.layoutSize === 'small' ||
|
|
250
|
+
(folderLayouts?.has('small') === true && !unlocksEquipment)
|
|
251
|
+
? 'small'
|
|
252
|
+
: 'large';
|
|
253
|
+
const itemLayout = folderLayouts?.get(layoutSize) ?? folderLayouts?.get('large');
|
|
201
254
|
let pixelX;
|
|
202
255
|
let pixelY;
|
|
203
256
|
if (gridbox !== undefined &&
|
|
@@ -223,6 +276,12 @@ function enrichPlacements(placements, edges, gridboxes, signal) {
|
|
|
223
276
|
...(gridbox === undefined ? {} : { gridboxId: gridbox.id }),
|
|
224
277
|
...(pixelX === undefined ? {} : { pixelX }),
|
|
225
278
|
...(pixelY === undefined ? {} : { pixelY }),
|
|
279
|
+
layoutSize,
|
|
280
|
+
...(itemLayout === undefined ? {} : { itemLayoutId: itemLayout.id }),
|
|
281
|
+
...(itemLayout?.size.width === undefined ? {} : { layoutWidth: itemLayout.size.width }),
|
|
282
|
+
...(itemLayout?.size.height === undefined ? {} : { layoutHeight: itemLayout.size.height }),
|
|
283
|
+
...(itemLayout?.position.x === undefined ? {} : { layoutOffsetX: itemLayout.position.x }),
|
|
284
|
+
...(itemLayout?.position.y === undefined ? {} : { layoutOffsetY: itemLayout.position.y }),
|
|
226
285
|
geometryStatus: pixelX !== undefined && pixelY !== undefined
|
|
227
286
|
? 'source_pixel'
|
|
228
287
|
: placement.x !== undefined && placement.y !== undefined
|
|
@@ -1093,6 +1152,8 @@ export function buildTechnologyGraph(snapshot, options) {
|
|
|
1093
1152
|
const selectedFolders = activeDefinitions(fragments.flatMap(({ folders }) => folders));
|
|
1094
1153
|
const folders = localiseFolders(selectedFolders.active, snapshot.index);
|
|
1095
1154
|
const gridboxes = activeGridboxes(fragments.flatMap(({ gridboxes }) => gridboxes));
|
|
1155
|
+
const itemLayouts = activeItemLayouts(fragments.flatMap(({ itemLayouts }) => itemLayouts));
|
|
1156
|
+
const yearMarkers = activeYearMarkers(fragments.flatMap(({ yearMarkers }) => yearMarkers));
|
|
1096
1157
|
const categories = activeCategories(fragments.flatMap(({ categories }) => categories));
|
|
1097
1158
|
const selectedDoctrines = activeDefinitions(fragments.flatMap(({ doctrineDefinitions }) => doctrineDefinitions));
|
|
1098
1159
|
const doctrineDefinitions = localiseDoctrines(selectedDoctrines.active, snapshot.index, assets);
|
|
@@ -1102,8 +1163,8 @@ export function buildTechnologyGraph(snapshot, options) {
|
|
|
1102
1163
|
const unlockTargets = selectedTargets.active;
|
|
1103
1164
|
const basePlacements = selectOwnedRecords(fragments.flatMap(({ placements }) => placements), ({ technologyId }) => technologyId, ({ location }) => location, technologyById).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1104
1165
|
const edges = selectOwnedRecords(fragments.flatMap(({ edges }) => edges), ({ from }) => from, ({ location }) => location, technologyById).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1105
|
-
const placements = enrichPlacements(basePlacements, edges, gridboxes, options.signal);
|
|
1106
1166
|
const unlocks = resolveUnlocks(selectOwnedRecords(fragments.flatMap(({ unlocks }) => unlocks), ({ technologyId }) => technologyId, ({ location }) => location, technologyById).sort((left, right) => compareCodeUnits(left.id, right.id)), unlockTargets, snapshot.complete);
|
|
1167
|
+
const placements = enrichPlacements(basePlacements, edges, gridboxes, technologies, unlocks, itemLayouts, options.signal);
|
|
1107
1168
|
const unresolved = fragments.flatMap(({ unresolved }) => unresolved);
|
|
1108
1169
|
const helperCalls = fragments.flatMap(({ helperCalls }) => helperCalls);
|
|
1109
1170
|
const externalReferences = expandHelperReferences(fragments.flatMap(({ externalReferences }) => externalReferences), helperCalls, unresolved, options.analysisMode ?? 'full', options.signal);
|
|
@@ -1167,6 +1228,8 @@ export function buildTechnologyGraph(snapshot, options) {
|
|
|
1167
1228
|
folders,
|
|
1168
1229
|
placements,
|
|
1169
1230
|
gridboxes,
|
|
1231
|
+
itemLayouts,
|
|
1232
|
+
yearMarkers,
|
|
1170
1233
|
edges,
|
|
1171
1234
|
categories,
|
|
1172
1235
|
doctrineDefinitions,
|
|
@@ -1183,6 +1246,8 @@ export function buildTechnologyGraph(snapshot, options) {
|
|
|
1183
1246
|
folderCount: folders.length,
|
|
1184
1247
|
placementCount: placements.length,
|
|
1185
1248
|
gridboxCount: gridboxes.length,
|
|
1249
|
+
itemLayoutCount: itemLayouts.length,
|
|
1250
|
+
yearMarkerCount: yearMarkers.length,
|
|
1186
1251
|
prerequisiteCount: edges.filter(({ kind }) => kind === 'prerequisite').length,
|
|
1187
1252
|
exclusiveCount: edges.filter(({ kind }) => kind === 'exclusive').length,
|
|
1188
1253
|
categoryCount: categories.length,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const TECHNOLOGY_GRAPH_SCHEMA_VERSION =
|
|
1
|
+
export const TECHNOLOGY_GRAPH_SCHEMA_VERSION = 2;
|
|
2
2
|
export const TECHNOLOGY_PARSER_VERSION = 'clausewitz-cst.v1';
|
|
@@ -15,6 +15,8 @@ function technologyRecordCount(graph) {
|
|
|
15
15
|
graph.categories.length +
|
|
16
16
|
graph.placements.length +
|
|
17
17
|
graph.gridboxes.length +
|
|
18
|
+
graph.itemLayouts.length +
|
|
19
|
+
graph.yearMarkers.length +
|
|
18
20
|
graph.edges.length +
|
|
19
21
|
graph.unlocks.length +
|
|
20
22
|
graph.unlockTargets.length +
|
|
@@ -62,6 +64,8 @@ export function technologyScanReport(graph, fullGraphRecordLimit = TECHNOLOGY_SC
|
|
|
62
64
|
categories: graph.categories.length,
|
|
63
65
|
placements: graph.placements.length,
|
|
64
66
|
gridboxes: graph.gridboxes.length,
|
|
67
|
+
itemLayouts: graph.itemLayouts.length,
|
|
68
|
+
yearMarkers: graph.yearMarkers.length,
|
|
65
69
|
edges: graph.edges.length,
|
|
66
70
|
unlocks: graph.unlocks.length,
|
|
67
71
|
unlockTargets: graph.unlockTargets.length,
|