hoi4-agent-tools 3.4.0 → 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 +27 -0
- package/README.md +26 -18
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +48 -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/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
- package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
- 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/operations.js +16 -10
- 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/gui/validators.js +3 -0
- 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/mechanic/interpreter.js +91 -30
- package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
- 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/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +11 -5
- package/dist/hoi4_agent_tools/technology/service.js +1 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/events.md +1 -1
- package/docs/gui.md +7 -0
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- 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/docs/technology.md +5 -1
- 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
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,22 @@ const numericEffects = new Map([
|
|
|
13
13
|
['add_war_support', { key: 'war_support', mode: 'add', min: 0, max: 1 }],
|
|
14
14
|
]);
|
|
15
15
|
const harmlessEffects = new Set(['hidden_effect', 'custom_effect_tooltip', 'log', 'tooltip']);
|
|
16
|
+
const variableEffects = new Map([
|
|
17
|
+
['set_variable', 'set'],
|
|
18
|
+
['set_temp_variable', 'set'],
|
|
19
|
+
['add_to_variable', 'add'],
|
|
20
|
+
['add_to_temp_variable', 'add'],
|
|
21
|
+
['subtract_from_variable', 'subtract'],
|
|
22
|
+
['subtract_from_temp_variable', 'subtract'],
|
|
23
|
+
['multiply_variable', 'multiply'],
|
|
24
|
+
['multiply_temp_variable', 'multiply'],
|
|
25
|
+
['divide_variable', 'divide'],
|
|
26
|
+
['divide_temp_variable', 'divide'],
|
|
27
|
+
['clamp_variable', 'clamp'],
|
|
28
|
+
['clamp_temp_variable', 'clamp'],
|
|
29
|
+
['round_variable', 'round'],
|
|
30
|
+
['round_temp_variable', 'round'],
|
|
31
|
+
]);
|
|
16
32
|
function numeric(value) {
|
|
17
33
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
18
34
|
return value;
|
|
@@ -175,6 +191,40 @@ export class MechanicInterpreter {
|
|
|
175
191
|
return (numeric(scenarioValue(this.state, key, targetScope)) ??
|
|
176
192
|
numeric(scenarioValue(this.state, `variable.${key}`, targetScope)));
|
|
177
193
|
}
|
|
194
|
+
scopeReference(expression, scope, stack) {
|
|
195
|
+
const scopeId = (name) => {
|
|
196
|
+
if (name === 'ROOT')
|
|
197
|
+
return this.state.scenario.actor ?? 'ROOT';
|
|
198
|
+
if (name === 'THIS')
|
|
199
|
+
return scope === 'ROOT'
|
|
200
|
+
? (this.state.scenario.actor ?? 'ROOT')
|
|
201
|
+
: this.state.scenario.scopes?.[scope]?.id;
|
|
202
|
+
if (name === 'PREV') {
|
|
203
|
+
const previous = stack.findLast((item) => item.startsWith('scope:'))?.slice(6);
|
|
204
|
+
return previous === undefined
|
|
205
|
+
? undefined
|
|
206
|
+
: previous === 'ROOT'
|
|
207
|
+
? (this.state.scenario.actor ?? 'ROOT')
|
|
208
|
+
: this.state.scenario.scopes?.[previous]?.id;
|
|
209
|
+
}
|
|
210
|
+
return this.state.scenario.scopes?.[name]?.id;
|
|
211
|
+
};
|
|
212
|
+
if (expression.startsWith('event_target:'))
|
|
213
|
+
return this.state.scenario.eventTargets?.[expression.slice(13)];
|
|
214
|
+
const identifier = /^([A-Za-z0-9_:]+)(?:\.id)?$/u.exec(expression)?.[1];
|
|
215
|
+
return identifier === undefined ? undefined : scopeId(identifier.toUpperCase());
|
|
216
|
+
}
|
|
217
|
+
value(expression, path, scope, stack) {
|
|
218
|
+
const numberValue = this.number(expression, path, scope);
|
|
219
|
+
if (numberValue !== undefined)
|
|
220
|
+
return numberValue;
|
|
221
|
+
const dotted = /^(ROOT|THIS|FROM|PREV)\.(.+)$/iu.exec(expression);
|
|
222
|
+
const targetScope = dotted?.[1]?.toUpperCase() === 'THIS' ? scope : (dotted?.[1]?.toUpperCase() ?? scope);
|
|
223
|
+
const key = dotted?.[2] ?? expression;
|
|
224
|
+
const existing = scenarioValue(this.state, key, targetScope) ??
|
|
225
|
+
scenarioValue(this.state, `variable.${key}`, targetScope);
|
|
226
|
+
return existing ?? this.scopeReference(expression, scope, stack);
|
|
227
|
+
}
|
|
178
228
|
mathExpression(block, path, scope) {
|
|
179
229
|
const entries = entriesOnly(block);
|
|
180
230
|
if (entries === undefined || entries[0]?.key.value !== 'value')
|
|
@@ -410,42 +460,53 @@ export class MechanicInterpreter {
|
|
|
410
460
|
}
|
|
411
461
|
return;
|
|
412
462
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
'
|
|
416
|
-
|
|
417
|
-
'
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
463
|
+
const variableMode = variableEffects.get(key);
|
|
464
|
+
if (variableMode !== undefined) {
|
|
465
|
+
const variableScope = key.includes('temp_variable') ? 'ROOT' : scope;
|
|
466
|
+
const scalarVariable = asText(entry.value);
|
|
467
|
+
if (variableMode === 'round') {
|
|
468
|
+
const variable = scalarVariable ?? (child === undefined ? undefined : firstScalar(child, 'var')?.value);
|
|
469
|
+
const before = variable === undefined
|
|
470
|
+
? undefined
|
|
471
|
+
: numeric(scenarioValue(this.state, variable, variableScope));
|
|
472
|
+
const after = before === undefined ? undefined : Math.round(before);
|
|
473
|
+
if (variable === undefined) {
|
|
474
|
+
this.unknown(key, path, scope, 'Variable must resolve');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
setScenarioValue(this.state, variable, after, variableScope);
|
|
478
|
+
this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied', before, after, after === undefined ? 'Variable value is not numeric' : undefined, variable);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
421
481
|
if (child === undefined) {
|
|
422
482
|
this.unknown(key, path, scope, 'Variable effect requires a block');
|
|
423
483
|
return;
|
|
424
484
|
}
|
|
425
485
|
const short = assignments(child).find((item) => !['var', 'value', 'min', 'max', 'tooltip'].includes(item.key.value));
|
|
426
486
|
const variable = firstScalar(child, 'var')?.value ?? short?.key.value;
|
|
487
|
+
if (variable === undefined) {
|
|
488
|
+
this.unknown(key, path, scope, 'Variable must resolve');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
427
491
|
const expression = firstScalar(child, 'value')?.value ??
|
|
428
492
|
(short === undefined ? undefined : asText(short.value));
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (value !== undefined && key === 'divide_variable')
|
|
447
|
-
after = value === 0 ? undefined : before / value;
|
|
448
|
-
if (value !== undefined && key === 'clamp_variable') {
|
|
493
|
+
const operand = expression === undefined
|
|
494
|
+
? variableMode === 'set' && short?.value.type === 'block'
|
|
495
|
+
? this.mathExpression(short.value, path, scope)
|
|
496
|
+
: undefined
|
|
497
|
+
: this.value(expression, path, scope, stack);
|
|
498
|
+
const previous = scenarioValue(this.state, variable, variableScope);
|
|
499
|
+
const before = numeric(previous) ?? 0;
|
|
500
|
+
let after = variableMode === 'set' ? operand : numeric(operand);
|
|
501
|
+
if (variableMode === 'add' && typeof after === 'number')
|
|
502
|
+
after = before + after;
|
|
503
|
+
if (variableMode === 'subtract' && typeof after === 'number')
|
|
504
|
+
after = before - after;
|
|
505
|
+
if (variableMode === 'multiply' && typeof after === 'number')
|
|
506
|
+
after = before * after;
|
|
507
|
+
if (variableMode === 'divide' && typeof after === 'number')
|
|
508
|
+
after = after === 0 ? undefined : before / after;
|
|
509
|
+
if (variableMode === 'clamp') {
|
|
449
510
|
const minimum = firstScalar(child, 'min')?.value;
|
|
450
511
|
const maximum = firstScalar(child, 'max')?.value;
|
|
451
512
|
const min = minimum === undefined ? -Infinity : this.number(minimum, path, scope);
|
|
@@ -453,8 +514,8 @@ export class MechanicInterpreter {
|
|
|
453
514
|
after =
|
|
454
515
|
min === undefined || max === undefined ? undefined : Math.max(min, Math.min(max, before));
|
|
455
516
|
}
|
|
456
|
-
setScenarioValue(this.state, variable, after,
|
|
457
|
-
this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied',
|
|
517
|
+
setScenarioValue(this.state, variable, after, variableScope);
|
|
518
|
+
this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied', previous, after, after === undefined ? 'Numeric operand is unknown or division by zero' : undefined, variable);
|
|
458
519
|
return;
|
|
459
520
|
}
|
|
460
521
|
if (key === 'set_country_flag' ||
|
|
@@ -77,13 +77,28 @@ export function selectMechanicSource(snapshot, selector) {
|
|
|
77
77
|
}
|
|
78
78
|
export function scriptedEffectSources(snapshot) {
|
|
79
79
|
const result = new Map();
|
|
80
|
+
const symbolsByPath = new Map();
|
|
80
81
|
for (const symbol of snapshot.index.symbols) {
|
|
81
82
|
if (symbol.kind !== 'scripted_effect' || symbol.overridden || symbol.sourceShadowed)
|
|
82
83
|
continue;
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const symbols = symbolsByPath.get(symbol.path) ?? [];
|
|
85
|
+
symbols.push(symbol);
|
|
86
|
+
symbolsByPath.set(symbol.path, symbols);
|
|
87
|
+
}
|
|
88
|
+
for (const [sourcePath, symbols] of symbolsByPath) {
|
|
89
|
+
const document = activeDocument(snapshot, sourcePath);
|
|
90
|
+
if (document === undefined)
|
|
91
|
+
continue;
|
|
92
|
+
const blocks = new Map();
|
|
93
|
+
for (const definition of assignments(document.root)) {
|
|
94
|
+
if (definition.value.type === 'block' && !blocks.has(definition.key.value))
|
|
95
|
+
blocks.set(definition.key.value, definition.value);
|
|
96
|
+
}
|
|
97
|
+
for (const symbol of symbols) {
|
|
98
|
+
const block = blocks.get(symbol.id);
|
|
99
|
+
if (block !== undefined)
|
|
100
|
+
result.set(symbol.id, { id: symbol.id, path: symbol.path, block });
|
|
101
|
+
}
|
|
87
102
|
}
|
|
88
103
|
return result;
|
|
89
104
|
}
|
|
@@ -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);
|
|
@@ -319,6 +319,22 @@ function candidatesFor(context, adapterId) {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
function sourceContexts(snapshot, source) {
|
|
322
|
+
if (source.snapshotPath !== undefined) {
|
|
323
|
+
if (source.path === undefined ||
|
|
324
|
+
source.expectedSourceHash === undefined ||
|
|
325
|
+
source.inlineClausewitz !== undefined ||
|
|
326
|
+
source.virtualPatch !== undefined)
|
|
327
|
+
throw new ServiceError('PROBABILITY_SNAPSHOT_CONTRACT_REQUIRED', 'Frozen source requires a logical path and exact snapshot hash');
|
|
328
|
+
const snapshotPath = normalized(source.snapshotPath);
|
|
329
|
+
const selected = snapshot.files.filter((file) => file.shadowedBy === undefined && normalized(file.relativePath) === snapshotPath);
|
|
330
|
+
if (selected.length !== 1)
|
|
331
|
+
throw new ServiceError('PROBABILITY_SNAPSHOT_NOT_FOUND', 'Frozen source must resolve to one active authorized file', { snapshotPath: source.snapshotPath });
|
|
332
|
+
const original = selected[0];
|
|
333
|
+
if (original.sha256 !== source.expectedSourceHash)
|
|
334
|
+
throw new ServiceError('PROBABILITY_SOURCE_STALE', 'Frozen probability source hash is stale', { expectedSourceHash: source.expectedSourceHash, actualSourceHashes: [original.sha256] });
|
|
335
|
+
const file = { ...original, relativePath: normalized(source.path) };
|
|
336
|
+
return [{ file, document: parseClausewitz(file.bytes, file.displayPath) }];
|
|
337
|
+
}
|
|
322
338
|
if (source.inlineClausewitz !== undefined || source.virtualPatch !== undefined) {
|
|
323
339
|
const text = source.virtualPatch ?? source.inlineClausewitz ?? '';
|
|
324
340
|
const bytes = Buffer.from(text, 'utf8');
|
|
@@ -198,6 +198,7 @@ export function evaluateValueBlock(block, candidate, scenario, definitions, init
|
|
|
198
198
|
const trace = [];
|
|
199
199
|
const unresolved = [];
|
|
200
200
|
const referencedProvenance = [];
|
|
201
|
+
const { candidateOverrides: _eligibilityOverrides, ...modifierScenario } = scenario;
|
|
201
202
|
if (block === undefined)
|
|
202
203
|
return { value: initial, interval: current, trace, unresolved };
|
|
203
204
|
for (const assignment of assignments(block)) {
|
|
@@ -257,7 +258,7 @@ export function evaluateValueBlock(block, candidate, scenario, definitions, init
|
|
|
257
258
|
if (assignment.key.value !== 'modifier')
|
|
258
259
|
continue;
|
|
259
260
|
const modifier = assignment.value;
|
|
260
|
-
const condition = evaluateTriggerBlock(conditionBlock(modifier),
|
|
261
|
+
const condition = evaluateTriggerBlock(conditionBlock(modifier), modifierScenario, candidate, definitions);
|
|
261
262
|
const conditionExpression = conditionSignature(conditionBlock(modifier));
|
|
262
263
|
unresolved.push(...condition.unresolved);
|
|
263
264
|
referencedProvenance.push(...(condition.helperProvenance ?? []));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ServiceError } from '../core/result.js';
|
|
2
|
+
/** Preserve source text while bounding one reply by lines and UTF-8 bytes. */
|
|
3
|
+
export function readBoundedLines(lines, firstLine, startLine, startColumn, maxLines, maxBytes) {
|
|
4
|
+
let lineIndex = startLine - firstLine;
|
|
5
|
+
let columnIndex = startColumn - 1;
|
|
6
|
+
if (lineIndex < 0 || lineIndex >= lines.length || columnIndex > (lines[lineIndex]?.length ?? 0)) {
|
|
7
|
+
throw new ServiceError('REFERENCE_POSITION_OUTSIDE_SECTION', 'Requested position is outside the selected section');
|
|
8
|
+
}
|
|
9
|
+
const first = lines[lineIndex];
|
|
10
|
+
if (columnIndex > 0 &&
|
|
11
|
+
/[\uD800-\uDBFF]/u.test(first[columnIndex - 1]) &&
|
|
12
|
+
/[\uDC00-\uDFFF]/u.test(first[columnIndex] ?? '')) {
|
|
13
|
+
throw new ServiceError('REFERENCE_POSITION_OUTSIDE_SECTION', 'Requested column splits a Unicode character');
|
|
14
|
+
}
|
|
15
|
+
const selected = [];
|
|
16
|
+
let bytes = 0;
|
|
17
|
+
let endLine = startLine;
|
|
18
|
+
while (lineIndex < lines.length && selected.length < maxLines) {
|
|
19
|
+
const line = lines[lineIndex];
|
|
20
|
+
const remainder = line.slice(columnIndex);
|
|
21
|
+
const separator = selected.length === 0 ? 0 : 1;
|
|
22
|
+
const allowance = maxBytes - bytes - separator;
|
|
23
|
+
if (allowance <= 0)
|
|
24
|
+
break;
|
|
25
|
+
let consumed = 0;
|
|
26
|
+
let used = 0;
|
|
27
|
+
for (const character of remainder) {
|
|
28
|
+
const characterBytes = Buffer.byteLength(character, 'utf8');
|
|
29
|
+
if (used + characterBytes > allowance)
|
|
30
|
+
break;
|
|
31
|
+
consumed += character.length;
|
|
32
|
+
used += characterBytes;
|
|
33
|
+
}
|
|
34
|
+
if (consumed === 0 && remainder.length > 0)
|
|
35
|
+
break;
|
|
36
|
+
selected.push(remainder.slice(0, consumed));
|
|
37
|
+
bytes += separator + used;
|
|
38
|
+
endLine = firstLine + lineIndex;
|
|
39
|
+
if (consumed < remainder.length) {
|
|
40
|
+
columnIndex += consumed;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
lineIndex++;
|
|
44
|
+
columnIndex = 0;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
text: selected.join('\n'),
|
|
48
|
+
endLine,
|
|
49
|
+
nextLine: lineIndex < lines.length ? firstLine + lineIndex : null,
|
|
50
|
+
nextColumn: lineIndex < lines.length ? columnIndex + 1 : null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { requireOperationScope, resolveOperationWorkspaceId, } from '../core/operation-context.js';
|
|
2
|
+
import { emptyServiceResult } from '../core/result.js';
|
|
3
|
+
import { toolResult } from '../core/operation-result.js';
|
|
4
|
+
import { referenceContextRequestSchema, referenceReadRequestSchema, referenceSearchRequestSchema, sourceLookupRequestSchema, } from '../schemas/reference.js';
|
|
5
|
+
import { ReferenceService } from './service.js';
|
|
6
|
+
import { sourceLookup } from './source-lookup.js';
|
|
7
|
+
const schemas = {
|
|
8
|
+
'hoi4.reference_search': referenceSearchRequestSchema,
|
|
9
|
+
'hoi4.reference_read': referenceReadRequestSchema,
|
|
10
|
+
'hoi4.reference_context': referenceContextRequestSchema,
|
|
11
|
+
'hoi4.source_lookup': sourceLookupRequestSchema,
|
|
12
|
+
};
|
|
13
|
+
export function isReferenceTool(name) {
|
|
14
|
+
return Object.hasOwn(schemas, name);
|
|
15
|
+
}
|
|
16
|
+
const sharedReferences = new WeakMap();
|
|
17
|
+
/** One authorization and execution contract for both MCP protocol adapters. */
|
|
18
|
+
export class ReferenceToolService {
|
|
19
|
+
engine;
|
|
20
|
+
references;
|
|
21
|
+
constructor(engine) {
|
|
22
|
+
this.engine = engine;
|
|
23
|
+
const cached = sharedReferences.get(engine) ?? new ReferenceService();
|
|
24
|
+
sharedReferences.set(engine, cached);
|
|
25
|
+
this.references = cached;
|
|
26
|
+
}
|
|
27
|
+
async call(name, input, context, signal) {
|
|
28
|
+
requireOperationScope(context, 'hoi4:read');
|
|
29
|
+
const parsed = schemas[name].parse(input);
|
|
30
|
+
const workspaceId = await resolveOperationWorkspaceId(this.engine, context, parsed.workspaceId, signal);
|
|
31
|
+
const workspace = this.engine.resolver.get(workspaceId, context.principal);
|
|
32
|
+
const data = name === 'hoi4.reference_search'
|
|
33
|
+
? await this.references.search(workspace, referenceSearchRequestSchema.parse(parsed), signal)
|
|
34
|
+
: name === 'hoi4.reference_read'
|
|
35
|
+
? await this.references.read(workspace, referenceReadRequestSchema.parse(parsed), signal)
|
|
36
|
+
: name === 'hoi4.reference_context'
|
|
37
|
+
? await this.references.context(workspace, referenceContextRequestSchema.parse(parsed), signal)
|
|
38
|
+
: await sourceLookup(this.engine, workspaceId, sourceLookupRequestSchema.parse(parsed), context.principal, signal);
|
|
39
|
+
return toolResult(emptyServiceResult(workspaceId, data));
|
|
40
|
+
}
|
|
41
|
+
}
|