hoi4-agent-tools 2.8.10 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +35 -10
- package/dist/bin/stdio.js +0 -4
- package/dist/hoi4_agent_tools/core/configuration.js +16 -3
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +10 -13
- package/dist/hoi4_agent_tools/core/engine.js +3 -0
- package/dist/hoi4_agent_tools/core/scanner.js +63 -7
- package/dist/hoi4_agent_tools/core/source/limits.js +6 -7
- package/dist/hoi4_agent_tools/event/graph.js +3 -4
- package/dist/hoi4_agent_tools/event/limits.js +4 -4
- package/dist/hoi4_agent_tools/event/service.js +34 -39
- package/dist/hoi4_agent_tools/gui/assets.js +280 -12
- package/dist/hoi4_agent_tools/gui/layout.js +70 -9
- package/dist/hoi4_agent_tools/gui/renderer.js +81 -12
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +36 -0
- package/dist/hoi4_agent_tools/gui/studio.js +90 -25
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +72 -14
- package/dist/hoi4_agent_tools/probability/sequence.js +43 -18
- package/dist/hoi4_agent_tools/probability/service.js +187 -24
- package/dist/hoi4_agent_tools/schemas/probability.js +2 -0
- package/dist/hoi4_agent_tools/technology/service.js +41 -8
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/development.md +2 -0
- package/docs/gui.md +14 -3
- package/docs/images/readme/chaos-meter-ingame.png +0 -0
- package/docs/images/readme/chaos-meter-mcp.png +0 -0
- package/docs/images/readme/communist-insurgency-ingame.png +0 -0
- package/docs/images/readme/communist-insurgency-mcp.png +0 -0
- package/docs/images/readme/event-chain-mcp.png +0 -0
- package/docs/images/readme/focus-tree-mcp.png +0 -0
- package/docs/images/readme/map-mcp.png +0 -0
- package/docs/images/readme/probability-mcp.png +0 -0
- package/docs/images/readme/settings-advanced-mcp.png +0 -0
- package/docs/images/readme/settings-events-mcp.png +0 -0
- package/docs/images/readme/technology-tree-mcp.png +0 -0
- package/package.json +3 -1
- package/schemas/configuration.schema.json +3 -3
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +58 -39
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +2 -2
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +0 -483
|
@@ -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';
|
|
@@ -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;
|
|
@@ -591,12 +649,19 @@ export class ProbabilityAnalyzer {
|
|
|
591
649
|
this.engine = engine;
|
|
592
650
|
this.state = analyzerState(engine);
|
|
593
651
|
}
|
|
594
|
-
async scan(context, sourcePaths = []) {
|
|
652
|
+
async scan(context, sourcePaths = [], emptyScope = 'domain') {
|
|
595
653
|
if (context.refresh === true)
|
|
596
654
|
this.engine.invalidate(context.workspaceId);
|
|
597
655
|
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
656
|
+
const selectedSourcePaths = sourcePaths.filter((sourcePath) => sourcePath !== undefined);
|
|
598
657
|
return this.engine.scan(context.workspaceId, {
|
|
599
|
-
patterns:
|
|
658
|
+
patterns: selectedSourcePaths.length > 0
|
|
659
|
+
? probabilityDomainScanPatterns(workspace, selectedSourcePaths)
|
|
660
|
+
: emptyScope === 'domain'
|
|
661
|
+
? probabilityDomainScanPatterns(workspace)
|
|
662
|
+
: emptyScope === 'shared'
|
|
663
|
+
? probabilitySharedDefinitionPatterns()
|
|
664
|
+
: [],
|
|
600
665
|
}, context.principal, context.signal);
|
|
601
666
|
}
|
|
602
667
|
async verifyGameVersion(context, adapter) {
|
|
@@ -656,6 +721,14 @@ export class ProbabilityAnalyzer {
|
|
|
656
721
|
return {
|
|
657
722
|
workspaceId: context.workspaceId,
|
|
658
723
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
724
|
+
...(typeof extra.sourceScope === 'string'
|
|
725
|
+
? {
|
|
726
|
+
sourceScope: extra.sourceScope,
|
|
727
|
+
}
|
|
728
|
+
: {}),
|
|
729
|
+
...(Array.isArray(extra.sourcePaths) && extra.sourcePaths.length > 0
|
|
730
|
+
? { sourcePaths: extra.sourcePaths }
|
|
731
|
+
: {}),
|
|
659
732
|
sourceRevision: snapshot.revision,
|
|
660
733
|
sourceHash: surface.sourceHash,
|
|
661
734
|
scenarioHash,
|
|
@@ -775,13 +848,61 @@ export class ProbabilityAnalyzer {
|
|
|
775
848
|
}
|
|
776
849
|
return complete;
|
|
777
850
|
}
|
|
778
|
-
async inspect(context, adapter, source, candidatePool = []) {
|
|
779
|
-
if (source === undefined)
|
|
851
|
+
async inspect(context, adapter, source, candidatePool = [], customPoolManifest) {
|
|
852
|
+
if (source === undefined && customPoolManifest === undefined)
|
|
780
853
|
return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
|
|
781
|
-
|
|
854
|
+
if (customPoolManifest !== undefined && source !== undefined)
|
|
855
|
+
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide either source or customPoolManifest for inspection');
|
|
856
|
+
if (customPoolManifest !== undefined && adapter !== 'custom_weighted_pool')
|
|
857
|
+
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest inspection requires adapter custom_weighted_pool');
|
|
858
|
+
const snapshot = await this.scan(context, [source?.path], source === undefined ? 'none' : source.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
859
|
+
if (customPoolManifest !== undefined) {
|
|
860
|
+
const surface = customSurface(customPoolManifest, snapshot.revision);
|
|
861
|
+
const gameVersionVerification = await this.verifyGameVersion(context, 'custom_weighted_pool');
|
|
862
|
+
const report = {
|
|
863
|
+
schemaVersion: 'probability-inspection.v2',
|
|
864
|
+
workspaceId: context.workspaceId,
|
|
865
|
+
surface: {
|
|
866
|
+
id: surface.id,
|
|
867
|
+
adapter: surface.adapter,
|
|
868
|
+
poolComplete: true,
|
|
869
|
+
sourceRevision: surface.sourceRevision,
|
|
870
|
+
sourceHash: surface.sourceHash,
|
|
871
|
+
gameVersionVerification,
|
|
872
|
+
candidateCount: surface.candidates.length,
|
|
873
|
+
candidates: surface.candidates.map((candidate) => ({
|
|
874
|
+
id: candidate.id,
|
|
875
|
+
sourceKind: candidate.sourceKind,
|
|
876
|
+
provenance: candidate.provenance,
|
|
877
|
+
hasEligibility: customPoolManifest.candidates.find(({ id }) => id === candidate.id)?.eligibleWhen !==
|
|
878
|
+
undefined,
|
|
879
|
+
hasWeightBlock: true,
|
|
880
|
+
requiredInputs: [],
|
|
881
|
+
referencedProvenance: [],
|
|
882
|
+
analysisSupport: 'exact',
|
|
883
|
+
})),
|
|
884
|
+
requiredInputs: [],
|
|
885
|
+
unsupported: [],
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
889
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${surface.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
890
|
+
kind: 'probability-inspection',
|
|
891
|
+
toolVersion: PACKAGE_VERSION,
|
|
892
|
+
schemaVersion: 'probability-inspection.v2',
|
|
893
|
+
sourceHashes: { aggregate: surface.sourceHash },
|
|
894
|
+
}, 'Declared custom weighted-pool inventory', context.signal);
|
|
895
|
+
return {
|
|
896
|
+
adapters: probabilityAdapters(),
|
|
897
|
+
surface: report.surface,
|
|
898
|
+
artifacts: [publicArtifactLink(artifact)],
|
|
899
|
+
filesScanned: [],
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
const selectedSource = source;
|
|
782
903
|
const storeDiscovery = async (inventory, reason) => {
|
|
783
904
|
const availableAdapters = inventory.adapters.filter(({ candidateCount }) => candidateCount > 0);
|
|
784
|
-
const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) =>
|
|
905
|
+
const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) => selectedSource.identifier !== undefined && identifierMatchCount > 0);
|
|
785
906
|
const matchingPool = availableAdapters.filter(({ candidatePoolMatchCount }) => candidatePool.length > 0 && candidatePoolMatchCount > 0);
|
|
786
907
|
const requested = availableAdapters.find(({ adapterId }) => adapterId === adapter);
|
|
787
908
|
const ranked = [
|
|
@@ -826,7 +947,7 @@ export class ProbabilityAnalyzer {
|
|
|
826
947
|
};
|
|
827
948
|
};
|
|
828
949
|
if (adapter === undefined) {
|
|
829
|
-
const inventory = inventoryWeightedSurfaces(snapshot,
|
|
950
|
+
const inventory = inventoryWeightedSurfaces(snapshot, selectedSource, candidatePool);
|
|
830
951
|
return storeDiscovery(inventory, inventory.adapters.some(({ candidateCount }) => candidateCount > 0)
|
|
831
952
|
? 'source_inventory'
|
|
832
953
|
: 'no_weighted_surfaces');
|
|
@@ -834,14 +955,14 @@ export class ProbabilityAnalyzer {
|
|
|
834
955
|
const gameVersionVerification = await this.verifyGameVersion(context, adapter);
|
|
835
956
|
let surface;
|
|
836
957
|
try {
|
|
837
|
-
surface = discoverWeightedSurface(snapshot, adapter,
|
|
958
|
+
surface = discoverWeightedSurface(snapshot, adapter, selectedSource, candidatePool);
|
|
838
959
|
}
|
|
839
960
|
catch (error) {
|
|
840
961
|
if (!(error instanceof ServiceError) ||
|
|
841
962
|
(error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
|
|
842
963
|
error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
|
|
843
964
|
throw error;
|
|
844
|
-
const inventory = inventoryWeightedSurfaces(snapshot,
|
|
965
|
+
const inventory = inventoryWeightedSurfaces(snapshot, selectedSource, candidatePool);
|
|
845
966
|
const requested = inventory.adapters.find(({ adapterId }) => adapterId === adapter);
|
|
846
967
|
const reason = error.code === 'PROBABILITY_IDENTIFIER_NOT_FOUND'
|
|
847
968
|
? 'identifier_not_found'
|
|
@@ -916,12 +1037,31 @@ export class ProbabilityAnalyzer {
|
|
|
916
1037
|
if (request.scenarioSet.workspaceId !== undefined &&
|
|
917
1038
|
request.scenarioSet.workspaceId !== request.workspaceId)
|
|
918
1039
|
throw new ServiceError('PROBABILITY_SCENARIO_WORKSPACE_MISMATCH', 'Scenario set belongs to a different workspace');
|
|
1040
|
+
const usingManifest = request.customPoolManifest !== undefined;
|
|
1041
|
+
if (usingManifest !== (request.source === undefined))
|
|
1042
|
+
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide exactly one source or customPoolManifest');
|
|
1043
|
+
if (usingManifest && request.adapter !== 'custom_weighted_pool')
|
|
1044
|
+
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest requires the custom_weighted_pool adapter');
|
|
1045
|
+
if (!usingManifest && request.adapter === 'custom_weighted_pool')
|
|
1046
|
+
throw new ServiceError('PROBABILITY_MANIFEST_REQUIRED', 'custom_weighted_pool requires customPoolManifest');
|
|
919
1047
|
const gameVersionVerification = await this.verifyGameVersion(request, request.adapter);
|
|
920
|
-
const snapshot = await this.scan(request, [request.source
|
|
921
|
-
const surface =
|
|
1048
|
+
const snapshot = await this.scan(request, [request.source?.path], usingManifest ? 'none' : request.source?.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
1049
|
+
const surface = usingManifest
|
|
1050
|
+
? customSurface(request.customPoolManifest, snapshot.revision)
|
|
1051
|
+
: discoverWeightedSurface(snapshot, request.adapter, request.source, request.candidatePool ?? []);
|
|
922
1052
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
923
|
-
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet,
|
|
1053
|
+
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, usingManifest
|
|
1054
|
+
? request.customPoolManifest.candidates.map(({ id }) => id)
|
|
1055
|
+
: (request.candidatePool ?? []), gameVersionVerification, {
|
|
924
1056
|
operation,
|
|
1057
|
+
sourceScope: request.source?.path !== undefined
|
|
1058
|
+
? 'file'
|
|
1059
|
+
: request.source?.inlineClausewitz !== undefined
|
|
1060
|
+
? 'shared'
|
|
1061
|
+
: request.source !== undefined
|
|
1062
|
+
? 'domain'
|
|
1063
|
+
: 'none',
|
|
1064
|
+
sourcePaths: request.source?.path === undefined ? [] : [request.source.path],
|
|
925
1065
|
horizonDays: request.horizonDays,
|
|
926
1066
|
requestedMetrics: request.metrics,
|
|
927
1067
|
acceptanceBands: request.acceptanceBands,
|
|
@@ -931,7 +1071,9 @@ export class ProbabilityAnalyzer {
|
|
|
931
1071
|
const cached = this.state.byCacheKey.get(metadata.cacheKey);
|
|
932
1072
|
if (cached !== undefined)
|
|
933
1073
|
return { result: cached, surface, definitions, snapshot };
|
|
934
|
-
const scenarios =
|
|
1074
|
+
const scenarios = usingManifest
|
|
1075
|
+
? customPoolScenarios(surface, request.customPoolManifest, request.scenarioSet)
|
|
1076
|
+
: evaluateSurfaceScenarios(surface, request.scenarioSet.scenarios, definitions, request.horizonDays);
|
|
935
1077
|
return {
|
|
936
1078
|
result: this.buildResult(operation, metadata, surface, scenarios, {
|
|
937
1079
|
...(request.acceptanceBands === undefined
|
|
@@ -1048,7 +1190,7 @@ export class ProbabilityAnalyzer {
|
|
|
1048
1190
|
}
|
|
1049
1191
|
async sequence(request) {
|
|
1050
1192
|
request.signal?.throwIfAborted();
|
|
1051
|
-
const snapshot = await this.scan(request);
|
|
1193
|
+
const snapshot = await this.scan(request, [], 'none');
|
|
1052
1194
|
const surface = customSurface(request.customPoolManifest, snapshot.revision);
|
|
1053
1195
|
const gameVersionVerification = await this.verifyGameVersion(request, 'custom_weighted_pool');
|
|
1054
1196
|
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, request.customPoolManifest.candidates.map(({ id }) => id), gameVersionVerification, {
|
|
@@ -1137,12 +1279,31 @@ export class ProbabilityAnalyzer {
|
|
|
1137
1279
|
return this.store(result, request.outputs ?? ['json', 'sequence'], false, request.signal, request.principal);
|
|
1138
1280
|
}
|
|
1139
1281
|
async compare(request) {
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1282
|
+
const manifestMode = request.beforeManifest !== undefined || request.afterManifest !== undefined;
|
|
1283
|
+
if (manifestMode !== (request.before === undefined && request.after === undefined))
|
|
1284
|
+
throw new ServiceError('PROBABILITY_COMPARE_INPUT_AMBIGUOUS', 'Compare either before/after sources or beforeManifest/afterManifest');
|
|
1285
|
+
if (manifestMode &&
|
|
1286
|
+
(request.beforeManifest === undefined || request.afterManifest === undefined))
|
|
1287
|
+
throw new ServiceError('PROBABILITY_COMPARE_MANIFEST_REQUIRED', 'Custom-pool comparison requires both beforeManifest and afterManifest');
|
|
1288
|
+
if (!manifestMode && (request.before === undefined || request.after === undefined))
|
|
1289
|
+
throw new ServiceError('PROBABILITY_COMPARE_SOURCE_REQUIRED', 'Source comparison requires both before and after');
|
|
1290
|
+
const before = manifestMode
|
|
1291
|
+
? await this.analyzeUnstored('evaluate', {
|
|
1292
|
+
...request,
|
|
1293
|
+
customPoolManifest: request.beforeManifest,
|
|
1294
|
+
})
|
|
1295
|
+
: await this.analyzeUnstored('evaluate', { ...request, source: request.before });
|
|
1296
|
+
const after = manifestMode
|
|
1297
|
+
? await this.analyzeUnstored('evaluate', {
|
|
1298
|
+
...request,
|
|
1299
|
+
customPoolManifest: request.afterManifest,
|
|
1300
|
+
refresh: false,
|
|
1301
|
+
})
|
|
1302
|
+
: await this.analyzeUnstored('evaluate', {
|
|
1303
|
+
...request,
|
|
1304
|
+
source: request.after,
|
|
1305
|
+
refresh: false,
|
|
1306
|
+
});
|
|
1146
1307
|
const metadata = {
|
|
1147
1308
|
...after.result.metadata,
|
|
1148
1309
|
cacheKey: hashCanonical({
|
|
@@ -1196,8 +1357,10 @@ export class ProbabilityAnalyzer {
|
|
|
1196
1357
|
return this.store(stale, ['json'], false, request.signal, request.principal);
|
|
1197
1358
|
}
|
|
1198
1359
|
await this.verifyGameVersion(request, result.adapter.id);
|
|
1199
|
-
const snapshot =
|
|
1200
|
-
|
|
1360
|
+
const snapshot = result.metadata.sourceScope === undefined || result.metadata.sourceScope === 'none'
|
|
1361
|
+
? undefined
|
|
1362
|
+
: await this.scan(request, result.metadata.sourcePaths ?? [], result.metadata.sourceScope === 'shared' ? 'shared' : 'domain');
|
|
1363
|
+
if (snapshot !== undefined && snapshot.revision !== result.metadata.sourceRevision) {
|
|
1201
1364
|
const stale = {
|
|
1202
1365
|
...result,
|
|
1203
1366
|
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(),
|
|
@@ -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 = '
|
|
3
|
+
export const PACKAGE_VERSION = '3.0.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/development.md
CHANGED
|
@@ -35,4 +35,6 @@ npm run test:local
|
|
|
35
35
|
|
|
36
36
|
Add `HOI4_DEPENDENCY_ROOTS` as the platform path-delimited list when the external mod has dependencies. Local tests remain read-only.
|
|
37
37
|
|
|
38
|
+
The installed-game qualification also parses every shipped bitmap-font descriptor, resolves every atlas page, shapes native and enlarged glyphs, verifies deterministic raster output, and checks HOI4 localisation colours across base fonts and language overrides.
|
|
39
|
+
|
|
38
40
|
See the package [Security Policy](../SECURITY.md) for private vulnerability reporting.
|
package/docs/gui.md
CHANGED
|
@@ -31,13 +31,13 @@ Rewrite once, then inspect and render the result.
|
|
|
31
31
|
|
|
32
32
|
Inspection checks missing assets and localisation, invalid sizes, overlap, clipping, overflow, button-label centering, content crossing a background boundary, conflicting click regions, invisible blockers, broken parents or contexts, list-row cuts, state conflicts, trigger/effect gaps, and resolution drift. `hoi4.gui_render` returns a scripted-scenario gallery and JSON delta report alongside the generic state and resolution galleries.
|
|
33
33
|
|
|
34
|
-
The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
|
|
34
|
+
The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, country flags, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
|
|
35
35
|
|
|
36
36
|
Bitmap text uses the language-appropriate HOI4 font definition and its real glyph atlas. Header, serif, compact, typewriter, black, inverted, and other declared faces keep their own glyph shapes, native metrics, face colour, border colour, language override, and font-specific `textcolors` palette. The renderer reads BMFont channel roles, separates face and outline coverage, and recognizes both vanilla black-on-white masks and conventional white-on-transparent custom masks. HOI4 localisation colour runs such as `§Y` and `§R` therefore colour the glyph face while retaining its sharp antialiasing and declared border instead of flattening the glyph into a blurred monochrome mask or switching to the tool font. The default language uses the base font rather than an unrelated language override, and the renderer recognizes installed-game atlases whose filename omits a page suffix declared by the matching `.fnt` file.
|
|
37
37
|
|
|
38
38
|
Layout follows Clausewitz orientation and element-origin anchors, including `CENTER_LEFT`, `CENTER_RIGHT`, `CENTER_UP`, `CENTER_DOWN`, `origo`, `centerposition`, inherited container coordinate origins, local scale, and both `%` and `%%` relative dimensions. Text boxes use native font metrics together with `maxWidth`, `maxHeight`, alignment, wrapping, and `fixedsize` clipping. Cornered tile sprites use fixed nine-slice borders and optional center tiling; progress bars composite their filled and background textures; masked shields composite their background and mask textures.
|
|
39
39
|
|
|
40
|
-
Scripted-GUI composition follows `parent_scripted_gui` chains even when the linked child window is a top-level sibling in the `.gui` file. Literal `always = yes` and `always = no` visibility and click-enabled blocks are applied automatically. A scenario's `values` object supplies concrete runtime values in one place: variable names resolve numeric text, scripted-localisation names resolve dynamic labels, a progress-bar element name sets its fill value, and element keys ending in `.image`, `.frame`, `.x`, `.y`, `.visible`, or `.enabled` update that visual property. Scripted-GUI property expressions such as `image = "[GetMeterSprite]"` or `x = meter_fill_width` consume matching scenario values automatically. Dynamic lists instantiate their declared `entry_container` or `country_scope_entry_container`; each scenario row can select the country template with `countryScope`, choose an explicit `entryContainer`, and
|
|
40
|
+
Scripted-GUI composition follows `parent_scripted_gui` chains even when the linked child window is a top-level sibling in the `.gui` file. Literal `always = yes` and `always = no` visibility and click-enabled blocks are applied automatically. A scenario's `values` object supplies concrete runtime values in one place: variable names resolve numeric text, scripted-localisation names resolve dynamic labels, a progress-bar element name sets its fill value, and element keys ending in `.image`, `.frame`, `.x`, `.y`, `.visible`, or `.enabled` update that visual property. Scripted-GUI property expressions such as `image = "[GetMeterSprite]"` or `x = meter_fill_width` consume matching scenario values automatically. Dynamic text is parsed after scenario and scripted-localisation substitution, so a runtime string containing `£command_power` or `£[GetResourceIcon]` renders the resolved inline HOI4 text icon instead of literal marker text. Dynamic lists instantiate their declared `entry_container` or `country_scope_entry_container`; each scenario row can select the country template with `countryScope`, choose an explicit `entryContainer`, provide `countryTag` and `ideology` for `GetFlag`, and supply row-scoped text, image, frame, position, visibility, and enabled values. Masked country sprites use the actual mask and overlay dimensions, so custom flag sizes are discovered from GFX assets rather than assumed by the renderer.
|
|
41
41
|
|
|
42
42
|
`hoi4.gui_inspect` and `hoi4.gui_render` generate a plausible seeded runtime scenario by default. The generator follows the complete `parent_scripted_gui` surface, discovers numeric and text tokens in GUI localisation and scripted-GUI properties, chooses real scripted-localisation text and sprite candidates, applies simple source thresholds, fills meters within their declared ranges, populates dynamic lists with coherent rows, selects one overlapping tab pane, keeps active and idle controls mutually exclusive, and leaves values explicitly supplied by the caller unchanged. Normal controls and animation frames remain stable unless variation is explicitly requested. Every explicit `relatedScenarios` entry is rendered beside the generated view.
|
|
43
43
|
|
|
@@ -66,11 +66,22 @@ Preview scenarios can include exact layout expectations:
|
|
|
66
66
|
"values": {
|
|
67
67
|
"threat": 73,
|
|
68
68
|
"GetThreatLabel": "High",
|
|
69
|
+
"GetCostLine": "Cost £command_power 20",
|
|
69
70
|
"threat_meter": 73,
|
|
70
71
|
"confirm_button.enabled": true
|
|
71
72
|
},
|
|
72
73
|
"flags": { "panel_open": true },
|
|
73
|
-
"lists": {
|
|
74
|
+
"lists": {
|
|
75
|
+
"target_list": [
|
|
76
|
+
{
|
|
77
|
+
"id": 1,
|
|
78
|
+
"label": "France",
|
|
79
|
+
"countryScope": true,
|
|
80
|
+
"countryTag": "FRA",
|
|
81
|
+
"ideology": "democratic"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
74
85
|
"visibility": { "target_list": true, "confirm_button": true },
|
|
75
86
|
"expectations": {
|
|
76
87
|
"visible": ["target_list", "confirm_button"],
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"docs/map.md",
|
|
48
48
|
"docs/http.md",
|
|
49
49
|
"docs/development.md",
|
|
50
|
+
"docs/images/readme/",
|
|
50
51
|
"examples/clients/",
|
|
51
52
|
"examples/probability/",
|
|
52
53
|
"schemas/",
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
"test:local": "tsx scripts/run-local-tests.ts",
|
|
76
77
|
"test:coverage": "vitest run --coverage",
|
|
77
78
|
"benchmark": "node --expose-gc --import tsx scripts/benchmark.ts",
|
|
79
|
+
"docs:examples": "tsx scripts/generate-readme-examples.ts",
|
|
78
80
|
"fixtures:generate": "npm run fixtures:generate:focus && npm run fixtures:generate:gui && npm run fixtures:generate:map && npm run fixtures:generate:event && npm run fixtures:generate:technology && npm run fixtures:generate:probability",
|
|
79
81
|
"fixtures:generate:focus": "tsx scripts/generate-fixtures.ts",
|
|
80
82
|
"fixtures:generate:gui": "tsx scripts/generate-gui-fixture.ts",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/configuration.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"minLength": 1
|
|
61
61
|
},
|
|
62
62
|
"__schema2": {
|
|
63
|
-
"default":
|
|
63
|
+
"default": 100000,
|
|
64
64
|
"type": "integer",
|
|
65
65
|
"minimum": 1,
|
|
66
66
|
"maximum": 1000000
|
|
67
67
|
},
|
|
68
68
|
"__schema3": {
|
|
69
|
-
"default":
|
|
69
|
+
"default": 268435456,
|
|
70
70
|
"type": "integer",
|
|
71
71
|
"minimum": 1048576,
|
|
72
72
|
"maximum": 9007199254740991
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/continuous-focus-palette.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/custom-weighted-pool.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/event-feature-manifest.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/focus-plan.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/focus-planning-sidecar.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/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/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/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/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/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/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/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/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/operation-result.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|