hoi4-agent-tools 3.4.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +26 -18
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +41 -2
- package/dist/hoi4_agent_tools/core/condition-schema.js +18 -0
- package/dist/hoi4_agent_tools/core/configuration.js +10 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +9 -1
- package/dist/hoi4_agent_tools/core/impact-semantic.js +11 -4
- package/dist/hoi4_agent_tools/core/scanner.js +1 -1
- package/dist/hoi4_agent_tools/core/scenario-model.js +6 -0
- package/dist/hoi4_agent_tools/core/scenario-state.js +3 -0
- package/dist/hoi4_agent_tools/core/workspace.js +13 -1
- package/dist/hoi4_agent_tools/event/graph.js +8 -1
- package/dist/hoi4_agent_tools/event/service.js +1 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +10 -2
- package/dist/hoi4_agent_tools/gui/layout.js +58 -11
- package/dist/hoi4_agent_tools/gui/scenario.js +3 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +4 -1
- package/dist/hoi4_agent_tools/gui/studio.js +32 -10
- package/dist/hoi4_agent_tools/map/model.js +121 -12
- package/dist/hoi4_agent_tools/map/operations.js +80 -7
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/map/validation.js +38 -1
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +18 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +60 -0
- package/dist/hoi4_agent_tools/probability/service.js +2 -2
- package/dist/hoi4_agent_tools/probability/source-analysis.js +16 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +2 -1
- package/dist/hoi4_agent_tools/reference/lines.js +52 -0
- package/dist/hoi4_agent_tools/reference/operations.js +41 -0
- package/dist/hoi4_agent_tools/reference/service.js +427 -0
- package/dist/hoi4_agent_tools/reference/source-lookup.js +90 -0
- package/dist/hoi4_agent_tools/schemas/analysis.js +2 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability.js +26 -2
- package/dist/hoi4_agent_tools/schemas/reference.js +160 -0
- package/dist/hoi4_agent_tools/technology/render.js +9 -4
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/gui.md +6 -0
- package/docs/map.md +8 -0
- package/docs/probability.md +7 -0
- package/docs/reference.md +32 -0
- package/docs/research/reference-benchmark.md +28 -0
- package/docs/setup.md +6 -0
- package/examples/clients/codex-config.toml +2 -0
- package/package.json +4 -2
- package/schemas/configuration.schema.json +72 -58
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +43 -26
- 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 +102 -75
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +49 -32
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +160 -131
- package/schemas/probability-evaluate-input.schema.json +161 -132
- package/schemas/probability-inspect-input.schema.json +20 -11
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +110 -90
- package/schemas/probability-sequence-input.schema.json +154 -134
- package/schemas/probability-simulate-input.schema.json +161 -132
- package/schemas/probability-sweep-input.schema.json +163 -134
- package/schemas/reference-context-input.schema.json +42 -0
- package/schemas/reference-read-input.schema.json +41 -0
- package/schemas/reference-search-input.schema.json +45 -0
- package/schemas/scenario-suite.schema.json +92 -75
- package/schemas/scenario-test-input.schema.json +103 -86
- package/schemas/shared-scenario.schema.json +39 -22
- package/schemas/source-lookup-input.schema.json +61 -0
- package/server.json +2 -2
- 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
|
@@ -498,6 +498,24 @@ function scenarioTemplateNames(scenarios) {
|
|
|
498
498
|
...new Set(scenarios.flatMap(({ lists }) => Object.values(lists).flatMap((rows) => rows.flatMap(({ entryContainer }) => typeof entryContainer === 'string' && entryContainer.length > 0 ? [entryContainer] : [])))),
|
|
499
499
|
].sort(compareCodeUnits);
|
|
500
500
|
}
|
|
501
|
+
function scenarioLocalisationKeys(scenarios) {
|
|
502
|
+
const keys = new Set();
|
|
503
|
+
const pending = [...scenarios];
|
|
504
|
+
while (pending.length > 0) {
|
|
505
|
+
const value = pending.pop();
|
|
506
|
+
if (typeof value === 'string') {
|
|
507
|
+
if (/^[A-Za-z0-9_.-]{1,512}$/u.test(value))
|
|
508
|
+
keys.add(value);
|
|
509
|
+
for (const match of value.matchAll(/\$([A-Za-z0-9_.-]+)\$/gu))
|
|
510
|
+
keys.add(match[1]);
|
|
511
|
+
}
|
|
512
|
+
else if (Array.isArray(value))
|
|
513
|
+
pending.push(...value);
|
|
514
|
+
else if (value !== null && typeof value === 'object')
|
|
515
|
+
pending.push(...Object.values(value));
|
|
516
|
+
}
|
|
517
|
+
return [...keys].sort(compareCodeUnits);
|
|
518
|
+
}
|
|
501
519
|
/** Native row bindings admit only bounded country tokens, never caller-supplied paths or globs. */
|
|
502
520
|
export function scenarioCountryFlagPatterns(scenarios) {
|
|
503
521
|
const tags = new Set();
|
|
@@ -861,8 +879,8 @@ export class ScriptedGuiStudio {
|
|
|
861
879
|
this.scanner = this.engine.scanner;
|
|
862
880
|
this.artifacts = this.engine.artifacts;
|
|
863
881
|
}
|
|
864
|
-
graphForFiles(files, scope) {
|
|
865
|
-
const key = `${scope}:${hashCanonical(files.map(({ displayPath, sha256, shadowedBy }) => ({
|
|
882
|
+
graphForFiles(files, scope, additionalLocalisationKeys = []) {
|
|
883
|
+
const key = `${scope}:${hashCanonical(additionalLocalisationKeys)}:${hashCanonical(files.map(({ displayPath, sha256, shadowedBy }) => ({
|
|
866
884
|
displayPath,
|
|
867
885
|
sha256,
|
|
868
886
|
...(shadowedBy === undefined ? {} : { shadowedBy }),
|
|
@@ -876,7 +894,7 @@ export class ScriptedGuiStudio {
|
|
|
876
894
|
const retainedFiles = [...files];
|
|
877
895
|
const scanned = {
|
|
878
896
|
files: retainedFiles,
|
|
879
|
-
graph: buildGuiSourceGraph(retainedFiles, this.engine.indexFiles(retainedFiles)),
|
|
897
|
+
graph: buildGuiSourceGraph(retainedFiles, this.engine.indexFiles(retainedFiles), additionalLocalisationKeys),
|
|
880
898
|
};
|
|
881
899
|
this.#graphCache.set(key, scanned);
|
|
882
900
|
while (this.#graphCache.size > 1) {
|
|
@@ -906,7 +924,7 @@ export class ScriptedGuiStudio {
|
|
|
906
924
|
const files = mergeScannedFiles(snapshot.files, localisationFiles);
|
|
907
925
|
return this.graphForFiles(files, `workspace:${workspaceId}`);
|
|
908
926
|
}
|
|
909
|
-
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = []) {
|
|
927
|
+
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = [], additionalLocalisationKeys = []) {
|
|
910
928
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
911
929
|
const layoutPatterns = guiLayoutPatterns(workspace);
|
|
912
930
|
const overlayLayouts = await this.engine.scan(workspaceId, {
|
|
@@ -934,7 +952,7 @@ export class ScriptedGuiStudio {
|
|
|
934
952
|
this.engine.scan(workspaceId, { patterns: guiLocalisationPatterns(workspace, languages) }, principal, signal),
|
|
935
953
|
]);
|
|
936
954
|
const definitionFiles = mergeScannedFiles(layoutFiles, linkedDefinitions.files, localisation.files);
|
|
937
|
-
const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}
|
|
955
|
+
const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}`, additionalLocalisationKeys);
|
|
938
956
|
assertTargetWindowAvailable(definitions.graph, windowName);
|
|
939
957
|
const referencedPatterns = [
|
|
940
958
|
...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames),
|
|
@@ -973,7 +991,7 @@ export class ScriptedGuiStudio {
|
|
|
973
991
|
const fontPages = mergeScannedFiles(exactFontPages, fallbackFontPages);
|
|
974
992
|
signal?.throwIfAborted();
|
|
975
993
|
const files = mergeScannedFiles(definitions.files, referenced, fontPages);
|
|
976
|
-
return this.graphForFiles(files, `window:${workspaceId}:${windowName}
|
|
994
|
+
return this.graphForFiles(files, `window:${workspaceId}:${windowName}`, additionalLocalisationKeys);
|
|
977
995
|
}
|
|
978
996
|
async lint(input) {
|
|
979
997
|
input.signal?.throwIfAborted();
|
|
@@ -982,7 +1000,7 @@ export class ScriptedGuiStudio {
|
|
|
982
1000
|
const generatedOptions = input.generatedScenarios === undefined
|
|
983
1001
|
? undefined
|
|
984
1002
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
985
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]));
|
|
1003
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]));
|
|
986
1004
|
const generatedScenarios = generatedOptions === undefined
|
|
987
1005
|
? []
|
|
988
1006
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
|
|
@@ -1012,7 +1030,7 @@ export class ScriptedGuiStudio {
|
|
|
1012
1030
|
const budget = new RenderBudget();
|
|
1013
1031
|
const beforeScenario = parsePreviewScenario(input.before);
|
|
1014
1032
|
const afterScenario = parsePreviewScenario(input.after);
|
|
1015
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]));
|
|
1033
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]), scenarioLocalisationKeys([beforeScenario, afterScenario]));
|
|
1016
1034
|
const beforeCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, beforeScenario.language);
|
|
1017
1035
|
const afterCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, afterScenario.language);
|
|
1018
1036
|
const before = await buildGuiScene(scanned.graph, scanned.files, input.windowName, beforeScenario, beforeCatalog);
|
|
@@ -1069,6 +1087,10 @@ export class ScriptedGuiStudio {
|
|
|
1069
1087
|
placeholderScenario,
|
|
1070
1088
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1071
1089
|
...explicitRelatedScenarios,
|
|
1090
|
+
]), scenarioLocalisationKeys([
|
|
1091
|
+
placeholderScenario,
|
|
1092
|
+
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1093
|
+
...explicitRelatedScenarios,
|
|
1072
1094
|
]));
|
|
1073
1095
|
const generatedScenarios = generatedOptions === undefined
|
|
1074
1096
|
? []
|
|
@@ -1177,7 +1199,7 @@ export class ScriptedGuiStudio {
|
|
|
1177
1199
|
loadOrder: original.loadOrder,
|
|
1178
1200
|
},
|
|
1179
1201
|
]);
|
|
1180
|
-
const baselineGraph = this.graphForFiles(baselineFiles, `source-baseline:${input.workspaceId}
|
|
1202
|
+
const baselineGraph = this.graphForFiles(baselineFiles, `source-baseline:${input.workspaceId}`, scenarioLocalisationKeys([scenario])).graph;
|
|
1181
1203
|
if (!baselineGraph.elements.some(({ name }) => name === input.windowName))
|
|
1182
1204
|
throw new ServiceError('GUI_SOURCE_BASELINE_WINDOW_MISSING', 'Source baseline does not define the requested GUI window', { relativePath, windowName: input.windowName });
|
|
1183
1205
|
sourceBaselineScene = await buildGuiScene(baselineGraph, baselineFiles, input.windowName, scenario, new GuiAssetCatalog(baselineGraph, baselineFiles, budget, scenario.language));
|
|
@@ -1416,7 +1438,7 @@ export class ScriptedGuiStudio {
|
|
|
1416
1438
|
const previewScenario = input.scenario === undefined ? undefined : parsePreviewScenario(input.scenario);
|
|
1417
1439
|
const initiallyScanned = input.windowName === undefined
|
|
1418
1440
|
? await this.scan(input.workspaceId, input.principal, input.signal)
|
|
1419
|
-
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]));
|
|
1441
|
+
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]), scenarioLocalisationKeys([previewScenario]));
|
|
1420
1442
|
const exactTargets = await this.scanner.scan(workspace, {
|
|
1421
1443
|
patterns: prepared.map(({ relativePath }) => relativePath),
|
|
1422
1444
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
@@ -169,6 +169,39 @@ function parseNumber(value) {
|
|
|
169
169
|
const parsed = Number(value);
|
|
170
170
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
171
171
|
}
|
|
172
|
+
export function parseMapDate(value) {
|
|
173
|
+
const match = /^(\d{1,4})\.(\d{1,2})\.(\d{1,2})(?:\.(\d{1,2}))?$/u.exec(value);
|
|
174
|
+
if (match === null)
|
|
175
|
+
return undefined;
|
|
176
|
+
const year = Number(match[1]);
|
|
177
|
+
const month = Number(match[2]);
|
|
178
|
+
const day = Number(match[3]);
|
|
179
|
+
const hour = match[4] === undefined ? 0 : Number(match[4]);
|
|
180
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23)
|
|
181
|
+
return undefined;
|
|
182
|
+
return year * 1_000_000 + month * 10_000 + day * 100 + hour;
|
|
183
|
+
}
|
|
184
|
+
function earliestBookmarkDate(files) {
|
|
185
|
+
let earliest;
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
if (!/^common\/bookmarks\/.*\.txt$/iu.test(normalizedPath(file.relativePath)))
|
|
188
|
+
continue;
|
|
189
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
190
|
+
for (const root of childBlocks(document.root, 'bookmarks')) {
|
|
191
|
+
for (const bookmark of childBlocks(root, 'bookmark')) {
|
|
192
|
+
const value = firstScalar(bookmark, 'date')?.value;
|
|
193
|
+
if (value === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
const ordinal = parseMapDate(value);
|
|
196
|
+
if (ordinal === undefined)
|
|
197
|
+
continue;
|
|
198
|
+
if (earliest === undefined || ordinal < earliest.ordinal)
|
|
199
|
+
earliest = { value, ordinal, sourcePath: file.displayPath };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return earliest;
|
|
204
|
+
}
|
|
172
205
|
function requiredNumber(values, index, label) {
|
|
173
206
|
const value = values[index];
|
|
174
207
|
if (value === undefined) {
|
|
@@ -260,7 +293,7 @@ export function parseDefinitions(file, diagnostics) {
|
|
|
260
293
|
}
|
|
261
294
|
return definitions;
|
|
262
295
|
}
|
|
263
|
-
export function parseState(file, diagnostics) {
|
|
296
|
+
export function parseState(file, diagnostics, earliestBookmark) {
|
|
264
297
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
265
298
|
addMapDiagnostics(diagnostics, document.diagnostics);
|
|
266
299
|
const assignment = firstTopLevelAssignment(document, 'state');
|
|
@@ -318,8 +351,85 @@ export function parseState(file, diagnostics) {
|
|
|
318
351
|
}
|
|
319
352
|
}
|
|
320
353
|
}
|
|
321
|
-
|
|
322
|
-
|
|
354
|
+
let owner = firstScalar(historyBlock ?? block, 'owner')?.value;
|
|
355
|
+
let controller = firstScalar(historyBlock ?? block, 'controller')?.value;
|
|
356
|
+
let cores = historyBlock === undefined
|
|
357
|
+
? []
|
|
358
|
+
: assignments(historyBlock, 'add_core_of').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []);
|
|
359
|
+
let claims = historyBlock === undefined
|
|
360
|
+
? []
|
|
361
|
+
: assignments(historyBlock, 'add_claim_by').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []);
|
|
362
|
+
if (historyBlock !== undefined) {
|
|
363
|
+
const dated = assignments(historyBlock)
|
|
364
|
+
.flatMap((entry) => {
|
|
365
|
+
if (entry.value.type !== 'block')
|
|
366
|
+
return [];
|
|
367
|
+
const date = parseMapDate(entry.key.value);
|
|
368
|
+
return date === undefined ? [] : [{ entry, date }];
|
|
369
|
+
})
|
|
370
|
+
.sort((left, right) => left.date - right.date || left.entry.start - right.entry.start);
|
|
371
|
+
if (dated.length > 0 && earliestBookmark === undefined) {
|
|
372
|
+
addMapDiagnostic(diagnostics, {
|
|
373
|
+
code: 'MAP_STATE_DATED_HISTORY_UNRESOLVED',
|
|
374
|
+
severity: 'warning',
|
|
375
|
+
category: 'map',
|
|
376
|
+
message: `State ${id} has dated history but no active bookmark date was found`,
|
|
377
|
+
location: nodeLocation(document, dated[0].entry),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
for (const { entry, date } of dated) {
|
|
381
|
+
if (earliestBookmark === undefined || date >= earliestBookmark.ordinal)
|
|
382
|
+
continue;
|
|
383
|
+
if (entry.value.type !== 'block')
|
|
384
|
+
continue;
|
|
385
|
+
for (const command of assignments(entry.value)) {
|
|
386
|
+
const value = command.value;
|
|
387
|
+
if (value.type === 'scalar') {
|
|
388
|
+
if (command.key.value === 'owner')
|
|
389
|
+
owner = value.value;
|
|
390
|
+
else if (command.key.value === 'controller')
|
|
391
|
+
controller = value.value;
|
|
392
|
+
else if (command.key.value === 'add_core_of' && !cores.includes(value.value))
|
|
393
|
+
cores.push(value.value);
|
|
394
|
+
else if (command.key.value === 'remove_core_of')
|
|
395
|
+
cores = cores.filter((tag) => tag !== value.value);
|
|
396
|
+
else if (command.key.value === 'add_claim_by' && !claims.includes(value.value))
|
|
397
|
+
claims.push(value.value);
|
|
398
|
+
else if (command.key.value === 'remove_claim_by')
|
|
399
|
+
claims = claims.filter((tag) => tag !== value.value);
|
|
400
|
+
}
|
|
401
|
+
else if (command.key.value === 'victory_points') {
|
|
402
|
+
const values = scalarList(value);
|
|
403
|
+
const provinceId = parseInteger(values[0]);
|
|
404
|
+
const amount = parseNumber(values[1]);
|
|
405
|
+
if (provinceId === undefined || amount === undefined)
|
|
406
|
+
continue;
|
|
407
|
+
const existing = victoryPoints.findIndex((point) => point.provinceId === provinceId);
|
|
408
|
+
if (existing >= 0)
|
|
409
|
+
victoryPoints.splice(existing, 1);
|
|
410
|
+
victoryPoints.push({ provinceId, value: amount, assignment: command });
|
|
411
|
+
}
|
|
412
|
+
else if (command.key.value === 'buildings') {
|
|
413
|
+
for (const building of assignments(value)) {
|
|
414
|
+
if (building.value.type === 'block') {
|
|
415
|
+
const provinceId = parseInteger(building.key.value);
|
|
416
|
+
if (provinceId === undefined)
|
|
417
|
+
continue;
|
|
418
|
+
const merged = new Map(provinceBuildings.get(provinceId) ?? []);
|
|
419
|
+
for (const [key, level] of numericMap(building.value))
|
|
420
|
+
merged.set(key, level);
|
|
421
|
+
provinceBuildings.set(provinceId, merged);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
const level = parseNumber(building.value.value);
|
|
425
|
+
if (level !== undefined)
|
|
426
|
+
stateBuildings.set(building.key.value, level);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
323
433
|
const capital = derivedStateCapital(victoryPoints);
|
|
324
434
|
return {
|
|
325
435
|
id,
|
|
@@ -331,12 +441,8 @@ export function parseState(file, diagnostics) {
|
|
|
331
441
|
resources: numericMap(resourcesBlock),
|
|
332
442
|
...(owner === undefined ? {} : { owner }),
|
|
333
443
|
...(controller === undefined ? {} : { controller }),
|
|
334
|
-
cores
|
|
335
|
-
|
|
336
|
-
: assignments(historyBlock, 'add_core_of').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []),
|
|
337
|
-
claims: historyBlock === undefined
|
|
338
|
-
? []
|
|
339
|
-
: assignments(historyBlock, 'add_claim_by').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []),
|
|
444
|
+
cores,
|
|
445
|
+
claims,
|
|
340
446
|
victoryPoints,
|
|
341
447
|
provinceBuildings,
|
|
342
448
|
stateBuildings,
|
|
@@ -825,6 +931,7 @@ export class MapWorkspaceIndex {
|
|
|
825
931
|
/** Shared cross-domain symbol/reference authority for these exact source bytes. */
|
|
826
932
|
sharedIndex;
|
|
827
933
|
activeFiles;
|
|
934
|
+
earliestBookmark;
|
|
828
935
|
diagnostics = [];
|
|
829
936
|
definitions;
|
|
830
937
|
definitionsAcrossRoots;
|
|
@@ -875,6 +982,7 @@ export class MapWorkspaceIndex {
|
|
|
875
982
|
localisation: sourceRoots.localisation.map(normalizeConfiguredRoot),
|
|
876
983
|
};
|
|
877
984
|
this.activeFiles = selectActiveFiles(files);
|
|
985
|
+
this.earliestBookmark = earliestBookmarkDate(this.activeFiles.all);
|
|
878
986
|
const modelBudget = new MapModelBudget();
|
|
879
987
|
modelBudget.assertScriptFiles(this.sourceFiles.filter((file) => inConfiguredRoot(file.relativePath, this.sourceRoots.states) ||
|
|
880
988
|
this.sourceRoots.map.some((root) => normalizedPath(file.relativePath).startsWith(`${root}/strategicregions/`)) ||
|
|
@@ -935,7 +1043,9 @@ export class MapWorkspaceIndex {
|
|
|
935
1043
|
const cached = parsedStates.get(file.displayPath);
|
|
936
1044
|
if (cached !== undefined)
|
|
937
1045
|
return cached ?? undefined;
|
|
938
|
-
const state = parseState(file, activeStatePaths.has(file.displayPath) ? this.diagnostics : []);
|
|
1046
|
+
const state = parseState(file, activeStatePaths.has(file.displayPath) ? this.diagnostics : [], this.earliestBookmark);
|
|
1047
|
+
if (state !== undefined && this.earliestBookmark !== undefined)
|
|
1048
|
+
state.earliestBookmark = this.earliestBookmark;
|
|
939
1049
|
parsedStates.set(file.displayPath, state ?? null);
|
|
940
1050
|
if (state !== undefined) {
|
|
941
1051
|
modelBudget.addDocument(state.document);
|
|
@@ -1075,9 +1185,8 @@ export class MapWorkspaceIndex {
|
|
|
1075
1185
|
this.coastalProvinceIds = this.raster?.coastalProvinceIds ?? new Set();
|
|
1076
1186
|
const portPositionsByStateProvince = new Map();
|
|
1077
1187
|
for (const position of this.buildingPositions) {
|
|
1078
|
-
if (position.building !== 'naval_base_spawn'
|
|
1188
|
+
if (position.building !== 'naval_base_spawn')
|
|
1079
1189
|
continue;
|
|
1080
|
-
}
|
|
1081
1190
|
const provinceId = this.provinceAtMapCoordinate(position.x, position.z);
|
|
1082
1191
|
if (provinceId === undefined)
|
|
1083
1192
|
continue;
|
|
@@ -5,7 +5,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
5
5
|
import { applyReplacements, assignments, encodeSource, parseLocalisation, parseClausewitz, } from '../core/source/index.js';
|
|
6
6
|
import { rgbKey } from './bmp.js';
|
|
7
7
|
import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES, MAP_SELECTED_PIXEL_LIMIT, } from './limits.js';
|
|
8
|
-
import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseTextDocument, } from './model.js';
|
|
8
|
+
import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
|
|
9
9
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
10
10
|
if (iteration % stride !== 0)
|
|
11
11
|
return;
|
|
@@ -1034,8 +1034,74 @@ function stateMembershipId(index, provinceId) {
|
|
|
1034
1034
|
return states[0]?.id;
|
|
1035
1035
|
}
|
|
1036
1036
|
function commitState(changes, state, data, fields, operationId) {
|
|
1037
|
+
assertDatedStateHistorySafe(state, fields, operationId);
|
|
1037
1038
|
addChange(changes, state.file.relativePath, patchState(state, data, fields), operationId, 'text/plain');
|
|
1038
1039
|
}
|
|
1040
|
+
function assertDatedStateHistorySafe(state, fields, operationId) {
|
|
1041
|
+
if (state.historyBlock === undefined)
|
|
1042
|
+
return;
|
|
1043
|
+
for (const entry of assignments(state.historyBlock)) {
|
|
1044
|
+
if (entry.value.type !== 'block')
|
|
1045
|
+
continue;
|
|
1046
|
+
const datedAt = parseMapDate(entry.key.value);
|
|
1047
|
+
if (datedAt === undefined)
|
|
1048
|
+
continue;
|
|
1049
|
+
const keys = new Set(assignments(entry.value).map(({ key }) => key.value));
|
|
1050
|
+
const applicable = state.earliestBookmark === undefined || datedAt <= state.earliestBookmark.ordinal;
|
|
1051
|
+
const affected = [
|
|
1052
|
+
['owner', fields.owner === true && keys.has('owner') && applicable],
|
|
1053
|
+
['controller', fields.controller === true && keys.has('controller') && applicable],
|
|
1054
|
+
[
|
|
1055
|
+
'cores',
|
|
1056
|
+
fields.cores === true &&
|
|
1057
|
+
(keys.has('add_core_of') || keys.has('remove_core_of')) &&
|
|
1058
|
+
applicable,
|
|
1059
|
+
],
|
|
1060
|
+
[
|
|
1061
|
+
'claims',
|
|
1062
|
+
fields.claims === true &&
|
|
1063
|
+
(keys.has('add_claim_by') || keys.has('remove_claim_by')) &&
|
|
1064
|
+
applicable,
|
|
1065
|
+
],
|
|
1066
|
+
[
|
|
1067
|
+
'victoryPoints',
|
|
1068
|
+
((fields.victoryPoints === true && applicable) || fields.provinces === true) &&
|
|
1069
|
+
keys.has('victory_points'),
|
|
1070
|
+
],
|
|
1071
|
+
[
|
|
1072
|
+
'buildings',
|
|
1073
|
+
((fields.buildings === true && applicable) || fields.provinces === true) &&
|
|
1074
|
+
keys.has('buildings'),
|
|
1075
|
+
],
|
|
1076
|
+
];
|
|
1077
|
+
const conflict = affected.find(([, selected]) => selected);
|
|
1078
|
+
if (conflict === undefined)
|
|
1079
|
+
continue;
|
|
1080
|
+
throw new ServiceError('MAP_DATED_STATE_HISTORY_CONFLICT', `State ${state.id} has a dated ${conflict[0]} change that this map operation cannot safely rewrite`, {
|
|
1081
|
+
operationId,
|
|
1082
|
+
stateId: state.id,
|
|
1083
|
+
field: conflict[0],
|
|
1084
|
+
datedAt: entry.key.value,
|
|
1085
|
+
earliestBookmark: state.earliestBookmark?.value ?? null,
|
|
1086
|
+
sourcePath: state.file.displayPath,
|
|
1087
|
+
sourceOffset: entry.start,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function assertNoDatedStateHistory(state, operationId) {
|
|
1092
|
+
if (state.historyBlock === undefined)
|
|
1093
|
+
return;
|
|
1094
|
+
const dated = assignments(state.historyBlock).find((entry) => entry.value.type === 'block' && parseMapDate(entry.key.value) !== undefined);
|
|
1095
|
+
if (dated === undefined)
|
|
1096
|
+
return;
|
|
1097
|
+
throw new ServiceError('MAP_DATED_STATE_DISTRIBUTION_UNSUPPORTED', `State ${state.id} has dated history that cannot be distributed to a new or merged state`, {
|
|
1098
|
+
operationId,
|
|
1099
|
+
stateId: state.id,
|
|
1100
|
+
datedAt: dated.key.value,
|
|
1101
|
+
sourcePath: state.file.displayPath,
|
|
1102
|
+
sourceOffset: dated.start,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1039
1105
|
function commitRegion(changes, region, provinces, operationId) {
|
|
1040
1106
|
addChange(changes, region.file.relativePath, patchRegion(region, provinces), operationId, 'text/plain');
|
|
1041
1107
|
}
|
|
@@ -1219,6 +1285,7 @@ function* applySplitState(base, changes, allocations, operation, signal) {
|
|
|
1219
1285
|
const source = index.statesById.get(sourceStateId);
|
|
1220
1286
|
if (source === undefined)
|
|
1221
1287
|
throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${sourceStateId} does not exist`);
|
|
1288
|
+
assertNoDatedStateHistory(source, operation.id);
|
|
1222
1289
|
const selectedSet = new Set(selected);
|
|
1223
1290
|
if (selected.length === 0 || selected.some((id) => !source.provinces.includes(id))) {
|
|
1224
1291
|
throw new ServiceError('MAP_STATE_PROVINCE_SELECTION_INVALID', 'Every new-state province must belong to the source state');
|
|
@@ -1336,6 +1403,8 @@ function applyMergeStates(base, changes, operation) {
|
|
|
1336
1403
|
if (target === undefined || sourceIds.length === 0 || sources.length !== sourceIds.length) {
|
|
1337
1404
|
throw new ServiceError('MAP_STATE_SELECTION_INVALID', 'Merge target and source states must exist');
|
|
1338
1405
|
}
|
|
1406
|
+
for (const source of sources)
|
|
1407
|
+
assertNoDatedStateHistory(source, operation.id);
|
|
1339
1408
|
const regionIds = new Set([...target.provinces, ...sources.flatMap((state) => state.provinces)].map((id) => regionMembershipId(index, id)));
|
|
1340
1409
|
if (regionIds.size !== 1)
|
|
1341
1410
|
throw new ServiceError('MAP_STATE_REGION_DISTRIBUTION_CONFLICT', 'Merged states must share one strategic region');
|
|
@@ -1964,12 +2033,16 @@ function* applyUpdateProvinceDefinition(base, changes, operation, signal) {
|
|
|
1964
2033
|
if (policy.ports === 'retain-if-valid') {
|
|
1965
2034
|
for (const position of dependentPortPositions) {
|
|
1966
2035
|
const locatedProvince = index.provinceAtMapCoordinate(position.x, position.z);
|
|
1967
|
-
|
|
1968
|
-
(targetType !== '
|
|
1969
|
-
position.
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2036
|
+
const incompatible = position.building === 'floating_harbor'
|
|
2037
|
+
? (locatedProvince === source.id && targetType !== 'sea') ||
|
|
2038
|
+
(position.adjacentSeaProvince === source.id && targetType !== 'land')
|
|
2039
|
+
: (locatedProvince === source.id &&
|
|
2040
|
+
(targetType !== 'land' ||
|
|
2041
|
+
position.stateId !== projectedStateId ||
|
|
2042
|
+
!updated.coastal ||
|
|
2043
|
+
index.definitionsById.get(position.adjacentSeaProvince)?.type !== 'sea')) ||
|
|
2044
|
+
(position.adjacentSeaProvince === source.id && targetType !== 'sea');
|
|
2045
|
+
if (incompatible)
|
|
1973
2046
|
provinceTypeDistributionConflict(source, targetType, 'ports', 'A retained port placement would no longer identify compatible land and sea provinces', {
|
|
1974
2047
|
stateId: position.stateId,
|
|
1975
2048
|
locatedProvince: locatedProvince ?? null,
|
|
@@ -379,6 +379,7 @@ function defaultMapPatterns(roots) {
|
|
|
379
379
|
}
|
|
380
380
|
function sourceTextPatterns(roots) {
|
|
381
381
|
return [
|
|
382
|
+
'common/bookmarks/**/*.txt',
|
|
382
383
|
...roots.map.flatMap((root) => {
|
|
383
384
|
const normalized = normalizeSourceRoot(root);
|
|
384
385
|
return ['map', 'csv', 'txt'].map((extension) => `${normalized}/**/*.${extension}`);
|
|
@@ -960,6 +960,43 @@ function* validatePositions(index, diagnostics, options) {
|
|
|
960
960
|
const location = lineLocation(position.document, position.line);
|
|
961
961
|
const state = index.statesById.get(position.stateId);
|
|
962
962
|
const province = index.provinceAtMapCoordinate(position.x, position.z);
|
|
963
|
+
if (position.building === 'floating_harbor') {
|
|
964
|
+
const sea = province === undefined ? undefined : index.definitionsById.get(province);
|
|
965
|
+
const land = index.definitionsById.get(position.adjacentSeaProvince);
|
|
966
|
+
if (sea?.type !== 'sea') {
|
|
967
|
+
addDiagnostic(diagnostics, options, {
|
|
968
|
+
code: 'MAP_FLOATING_HARBOR_POSITION_REVIEW',
|
|
969
|
+
severity: 'warning',
|
|
970
|
+
category: 'map',
|
|
971
|
+
message: 'Floating-harbor position resolves outside a sea province; review the installed-map precedent',
|
|
972
|
+
...(location === undefined ? {} : { location }),
|
|
973
|
+
details: { resolvedProvince: province },
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
if (land?.type !== 'land') {
|
|
977
|
+
addDiagnostic(diagnostics, options, {
|
|
978
|
+
code: 'MAP_FLOATING_HARBOR_LAND_INVALID',
|
|
979
|
+
severity: 'error',
|
|
980
|
+
category: 'map',
|
|
981
|
+
message: 'Floating-harbor final field must name an existing land province',
|
|
982
|
+
...(location === undefined ? {} : { location }),
|
|
983
|
+
details: { landingProvince: position.adjacentSeaProvince, stateId: position.stateId },
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
else if (state === undefined ||
|
|
987
|
+
!state.provinces.includes(position.adjacentSeaProvince) ||
|
|
988
|
+
!index.coastalProvinceIds.has(position.adjacentSeaProvince)) {
|
|
989
|
+
addDiagnostic(diagnostics, options, {
|
|
990
|
+
code: 'MAP_FLOATING_HARBOR_ASSOCIATION_REVIEW',
|
|
991
|
+
severity: 'warning',
|
|
992
|
+
category: 'map',
|
|
993
|
+
message: 'Floating-harbor target differs from its state or computed coastal membership',
|
|
994
|
+
...(location === undefined ? {} : { location }),
|
|
995
|
+
details: { landingProvince: position.adjacentSeaProvince, stateId: position.stateId },
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
963
1000
|
if (state === undefined || province === undefined || !state.provinces.includes(province)) {
|
|
964
1001
|
addDiagnostic(diagnostics, options, {
|
|
965
1002
|
code: 'MAP_BUILDING_POSITION_INVALID',
|
|
@@ -970,7 +1007,7 @@ function* validatePositions(index, diagnostics, options) {
|
|
|
970
1007
|
details: { resolvedProvince: province },
|
|
971
1008
|
});
|
|
972
1009
|
}
|
|
973
|
-
if (position.building === 'naval_base_spawn'
|
|
1010
|
+
if (position.building === 'naval_base_spawn') {
|
|
974
1011
|
if (province !== undefined)
|
|
975
1012
|
navalLocatorProvinces.add(province);
|
|
976
1013
|
const adjacent = index.definitionsById.get(position.adjacentSeaProvince);
|
|
@@ -10,6 +10,8 @@ import { MODERN_TASK_ROUTES, ModernTaskRoutingServer } from '../transports/moder
|
|
|
10
10
|
import { registerModernResources } from '../resources/register-modern.js';
|
|
11
11
|
import { registerModernPrompts } from '../prompts/register-modern.js';
|
|
12
12
|
import { jobControlTools } from '../tools/job.js';
|
|
13
|
+
import { referenceTools } from '../tools/reference.js';
|
|
14
|
+
import { isReferenceTool, ReferenceToolService } from '../../reference/operations.js';
|
|
13
15
|
import { ModernTaskAdapter, TASKS_EXTENSION } from './modern-tasks.js';
|
|
14
16
|
import { SERVER_INSTRUCTIONS } from './instructions.js';
|
|
15
17
|
import { modernProgressReporter, withProgressHeartbeat } from './progress.js';
|
|
@@ -39,9 +41,13 @@ function clientCapabilities(envelope) {
|
|
|
39
41
|
export function createModernOperationServer(engine, context = {}, tasks = new OperationTaskService(engine), options = {}) {
|
|
40
42
|
const adapter = new ModernTaskAdapter(tasks);
|
|
41
43
|
const controls = new JobControlService(engine);
|
|
44
|
+
const references = new ReferenceToolService(engine);
|
|
42
45
|
const privateOperations = options.createPrivateTools?.(engine, context);
|
|
46
|
+
const mapEnd = taskToolCatalog.findIndex(({ name }) => name === 'hoi4.map_rewrite') + 1;
|
|
43
47
|
const definitions = [
|
|
44
|
-
...taskToolCatalog,
|
|
48
|
+
...taskToolCatalog.slice(0, mapEnd),
|
|
49
|
+
...referenceTools,
|
|
50
|
+
...taskToolCatalog.slice(mapEnd),
|
|
45
51
|
...jobControlTools,
|
|
46
52
|
...(privateOperations === undefined
|
|
47
53
|
? []
|
|
@@ -109,6 +115,17 @@ export function createModernOperationServer(engine, context = {}, tasks = new Op
|
|
|
109
115
|
};
|
|
110
116
|
}
|
|
111
117
|
try {
|
|
118
|
+
if (isReferenceTool(definition.name)) {
|
|
119
|
+
const name = definition.name;
|
|
120
|
+
const result = await withProgressHeartbeat(() => engine.requests.run(references, Buffer.byteLength(JSON.stringify(arguments_)), extra.mcpReq.signal, () => engine.sharedRequests.run(extra.mcpReq.signal, () => references.call(name, arguments_, operationContext, extra.mcpReq.signal))), modernProgressReporter(extra));
|
|
121
|
+
definition.outputSchema?.parse(result.structuredContent);
|
|
122
|
+
return CallToolResultSchema.parse({
|
|
123
|
+
...result,
|
|
124
|
+
resultType: 'complete',
|
|
125
|
+
ttlMs: 0,
|
|
126
|
+
cacheScope: 'private',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
112
129
|
if (privateOperations !== undefined) {
|
|
113
130
|
const operations = operationContext === context || options.createPrivateTools === undefined
|
|
114
131
|
? privateOperations
|
|
@@ -4,6 +4,7 @@ import { PACKAGE_NAME, PACKAGE_VERSION } from '../../version.js';
|
|
|
4
4
|
import { registerFocusTools } from '../tools/focus.js';
|
|
5
5
|
import { registerGuiTools } from '../tools/gui.js';
|
|
6
6
|
import { registerMapTools } from '../tools/map.js';
|
|
7
|
+
import { registerReferenceTools } from '../tools/reference.js';
|
|
7
8
|
import { registerEventTools } from '../tools/event.js';
|
|
8
9
|
import { registerAnalysisTools } from '../tools/analysis.js';
|
|
9
10
|
import { registerTechnologyTools } from '../tools/technology.js';
|
|
@@ -53,6 +54,7 @@ export function createMcpServer(engine, context = {}) {
|
|
|
53
54
|
registerFocusTools(server, engine, serverContext);
|
|
54
55
|
registerGuiTools(server, engine, serverContext);
|
|
55
56
|
registerMapTools(server, engine, serverContext);
|
|
57
|
+
registerReferenceTools(server, engine, serverContext);
|
|
56
58
|
registerEventTools(server);
|
|
57
59
|
registerAnalysisTools(server);
|
|
58
60
|
registerTechnologyTools(server);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Shared agent-facing guidance for both MCP protocol generations. */
|
|
2
|
-
export const SERVER_INSTRUCTIONS = 'Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Start unfamiliar event chains with hoi4.event_inspect. Event tools are read-only. Start cross-system source review with hoi4.impact_inspect and decision or mission scenarios with hoi4.decision_inspect. Test declared mechanic steps with hoi4.mechanic_test, package connections with hoi4.package_check, and named suites with hoi4.scenario_test. These analysis tools are read-only. Start technology and doctrine work with hoi4.tech_inspect. Technology tools are read-only. Start weighted AI, MTTH, random, and declared-pool analysis with hoi4.probability_inspect. Probability tools are read-only. Concurrent calls queue automatically and emit periodic progress when the client requests it. Long operations support negotiated MCP tasks; native background rewrites require a stable requestKey. Large evidence is linked as resources.';
|
|
2
|
+
export const SERVER_INSTRUCTIONS = 'Use hoi4.reference_context for local wiki and installed documentation citations, hoi4.reference_search and hoi4.reference_read for exact sections, and hoi4.source_lookup for indexed source examples. Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Start unfamiliar event chains with hoi4.event_inspect. Event tools are read-only. Start cross-system source review with hoi4.impact_inspect and decision or mission scenarios with hoi4.decision_inspect. Test declared mechanic steps with hoi4.mechanic_test, package connections with hoi4.package_check, and named suites with hoi4.scenario_test. These analysis tools are read-only. Start technology and doctrine work with hoi4.tech_inspect. Technology tools are read-only. Start weighted AI, MTTH, random, and declared-pool analysis with hoi4.probability_inspect. Probability tools are read-only. Concurrent calls queue automatically and emit periodic progress when the client requests it. Long operations support negotiated MCP tasks; native background rewrites require a stable requestKey. Large evidence is linked as resources.';
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { errorResult, strictOperationResultSchema } from '../../core/operation-result.js';
|
|
2
|
+
import { ReferenceToolService } from '../../reference/operations.js';
|
|
3
|
+
import { referenceContextRequestSchema, referenceReadRequestSchema, referenceSearchRequestSchema, sourceLookupRequestSchema, referenceContextDataSchema, referenceReadDataSchema, referenceSearchDataSchema, sourceLookupDataSchema, } from '../../schemas/reference.js';
|
|
4
|
+
const readOnly = {
|
|
5
|
+
readOnlyHint: true,
|
|
6
|
+
destructiveHint: false,
|
|
7
|
+
idempotentHint: true,
|
|
8
|
+
openWorldHint: false,
|
|
9
|
+
};
|
|
10
|
+
export const referenceTools = [
|
|
11
|
+
{
|
|
12
|
+
name: 'hoi4.reference_search',
|
|
13
|
+
title: 'Search local HOI4 references',
|
|
14
|
+
description: 'Search bounded, cited sections of the configured offline wiki and installed game documentation.',
|
|
15
|
+
inputSchema: referenceSearchRequestSchema,
|
|
16
|
+
outputSchema: strictOperationResultSchema(referenceSearchDataSchema),
|
|
17
|
+
annotations: readOnly,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'hoi4.reference_read',
|
|
21
|
+
title: 'Read cited HOI4 reference section',
|
|
22
|
+
description: 'Read at most 80 lines from one revision-bound local documentation section.',
|
|
23
|
+
inputSchema: referenceReadRequestSchema,
|
|
24
|
+
outputSchema: strictOperationResultSchema(referenceReadDataSchema),
|
|
25
|
+
annotations: readOnly,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'hoi4.reference_context',
|
|
29
|
+
title: 'Get HOI4 task references',
|
|
30
|
+
description: 'Return a compact bundle of offline wiki and installed game documentation citations for a modding surface.',
|
|
31
|
+
inputSchema: referenceContextRequestSchema,
|
|
32
|
+
outputSchema: strictOperationResultSchema(referenceContextDataSchema),
|
|
33
|
+
annotations: readOnly,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'hoi4.source_lookup',
|
|
37
|
+
title: 'Look up HOI4 source symbol',
|
|
38
|
+
description: 'Find exact Clausewitz definitions, override status, a bounded source block, and indexed usages in the configured game and mod.',
|
|
39
|
+
inputSchema: sourceLookupRequestSchema,
|
|
40
|
+
outputSchema: strictOperationResultSchema(sourceLookupDataSchema),
|
|
41
|
+
annotations: readOnly,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
export function registerReferenceTools(server, engine, context) {
|
|
45
|
+
const service = new ReferenceToolService(engine);
|
|
46
|
+
for (const entry of referenceTools) {
|
|
47
|
+
const { name: _name, ...definition } = entry;
|
|
48
|
+
const name = entry.name;
|
|
49
|
+
server.registerTool(name, definition, async (input, extra) => {
|
|
50
|
+
try {
|
|
51
|
+
return await service.call(name, input, context, extra.signal);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (extra.signal.aborted)
|
|
55
|
+
throw error;
|
|
56
|
+
return errorResult(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -962,7 +962,7 @@ export class ProbabilityAnalyzer {
|
|
|
962
962
|
adapter !== undefined &&
|
|
963
963
|
adapter !== 'custom_weighted_pool')
|
|
964
964
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest inspection requires adapter custom_weighted_pool');
|
|
965
|
-
const snapshot = await this.scan(context, [source?.path], source === undefined ? 'none' : source.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
965
|
+
const snapshot = await this.scan(context, [source?.path, source?.snapshotPath], source === undefined ? 'none' : source.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
966
966
|
if (customPoolManifest !== undefined) {
|
|
967
967
|
const surface = customSurface(customPoolManifest, snapshot.revision);
|
|
968
968
|
const gameVersionVerification = await this.verifyGameVersion(context, 'custom_weighted_pool');
|
|
@@ -1150,7 +1150,7 @@ export class ProbabilityAnalyzer {
|
|
|
1150
1150
|
request.adapter !== undefined &&
|
|
1151
1151
|
request.adapter !== 'custom_weighted_pool')
|
|
1152
1152
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest requires the custom_weighted_pool adapter');
|
|
1153
|
-
const snapshot = await this.scan(request, [request.source?.path], usingManifest ? 'none' : request.source?.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
1153
|
+
const snapshot = await this.scan(request, [request.source?.path, request.source?.snapshotPath], usingManifest ? 'none' : request.source?.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
1154
1154
|
const resolvedAdapter = usingManifest
|
|
1155
1155
|
? 'custom_weighted_pool'
|
|
1156
1156
|
: resolveSourceBackedAdapter(snapshot, request.source, request.candidatePool ?? [], request.adapter);
|