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.
Files changed (109) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +26 -18
  3. package/dist/hoi4_agent_tools/core/condition-evaluator.js +48 -2
  4. package/dist/hoi4_agent_tools/core/condition-schema.js +18 -0
  5. package/dist/hoi4_agent_tools/core/configuration.js +10 -0
  6. package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +9 -1
  7. package/dist/hoi4_agent_tools/core/impact-semantic.js +11 -4
  8. package/dist/hoi4_agent_tools/core/index.js +1 -0
  9. package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
  10. package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
  11. package/dist/hoi4_agent_tools/core/scanner.js +1 -1
  12. package/dist/hoi4_agent_tools/core/scenario-model.js +6 -0
  13. package/dist/hoi4_agent_tools/core/scenario-state.js +3 -0
  14. package/dist/hoi4_agent_tools/core/workspace.js +13 -1
  15. package/dist/hoi4_agent_tools/event/graph.js +8 -1
  16. package/dist/hoi4_agent_tools/event/operations.js +16 -10
  17. package/dist/hoi4_agent_tools/event/service.js +1 -0
  18. package/dist/hoi4_agent_tools/event/source-analysis.js +1 -1
  19. package/dist/hoi4_agent_tools/focus/render.js +10 -2
  20. package/dist/hoi4_agent_tools/gui/layout.js +58 -11
  21. package/dist/hoi4_agent_tools/gui/scenario.js +3 -1
  22. package/dist/hoi4_agent_tools/gui/source-graph.js +4 -1
  23. package/dist/hoi4_agent_tools/gui/studio.js +32 -10
  24. package/dist/hoi4_agent_tools/gui/validators.js +3 -0
  25. package/dist/hoi4_agent_tools/map/model.js +121 -12
  26. package/dist/hoi4_agent_tools/map/operations.js +80 -7
  27. package/dist/hoi4_agent_tools/map/service.js +1 -0
  28. package/dist/hoi4_agent_tools/map/validation.js +38 -1
  29. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +18 -1
  30. package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
  31. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  32. package/dist/hoi4_agent_tools/mcp/tools/reference.js +60 -0
  33. package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
  34. package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
  35. package/dist/hoi4_agent_tools/probability/service.js +2 -2
  36. package/dist/hoi4_agent_tools/probability/source-analysis.js +16 -0
  37. package/dist/hoi4_agent_tools/probability/value-evaluator.js +2 -1
  38. package/dist/hoi4_agent_tools/reference/lines.js +52 -0
  39. package/dist/hoi4_agent_tools/reference/operations.js +41 -0
  40. package/dist/hoi4_agent_tools/reference/service.js +427 -0
  41. package/dist/hoi4_agent_tools/reference/source-lookup.js +90 -0
  42. package/dist/hoi4_agent_tools/schemas/analysis.js +2 -0
  43. package/dist/hoi4_agent_tools/schemas/gui-requests.js +1 -1
  44. package/dist/hoi4_agent_tools/schemas/probability-requests.js +1 -1
  45. package/dist/hoi4_agent_tools/schemas/probability.js +26 -2
  46. package/dist/hoi4_agent_tools/schemas/reference.js +160 -0
  47. package/dist/hoi4_agent_tools/technology/operations.js +29 -1
  48. package/dist/hoi4_agent_tools/technology/render.js +11 -5
  49. package/dist/hoi4_agent_tools/technology/service.js +1 -0
  50. package/dist/hoi4_agent_tools/version.js +1 -1
  51. package/docs/README.md +3 -0
  52. package/docs/events.md +1 -1
  53. package/docs/gui.md +7 -0
  54. package/docs/jobs.md +1 -0
  55. package/docs/map.md +8 -0
  56. package/docs/mechanics.md +2 -1
  57. package/docs/probability.md +7 -0
  58. package/docs/reference.md +32 -0
  59. package/docs/research/reference-benchmark.md +28 -0
  60. package/docs/setup.md +6 -0
  61. package/docs/technology.md +5 -1
  62. package/examples/clients/codex-config.toml +2 -0
  63. package/package.json +4 -2
  64. package/schemas/configuration.schema.json +72 -58
  65. package/schemas/continuous-focus-palette.schema.json +1 -1
  66. package/schemas/custom-weighted-pool.schema.json +1 -1
  67. package/schemas/decision-inspect-input.schema.json +43 -26
  68. package/schemas/event-feature-manifest.schema.json +1 -1
  69. package/schemas/focus-plan.schema.json +1 -1
  70. package/schemas/focus-planning-sidecar.schema.json +1 -1
  71. package/schemas/gui-animation-source.schema.json +1 -1
  72. package/schemas/gui-helper.schema.json +1 -1
  73. package/schemas/gui-scenario.schema.json +102 -75
  74. package/schemas/helper-expansion-request.schema.json +1 -1
  75. package/schemas/helper-expansion-summary.schema.json +1 -1
  76. package/schemas/impact-inspect-input.schema.json +1 -1
  77. package/schemas/map-operation.schema.json +1 -1
  78. package/schemas/mechanic-test-input.schema.json +49 -32
  79. package/schemas/operation-result.schema.json +1 -1
  80. package/schemas/package-check-input.schema.json +1 -1
  81. package/schemas/package-manifest.schema.json +1 -1
  82. package/schemas/probability-analysis-result.schema.json +1 -1
  83. package/schemas/probability-compare-input.schema.json +160 -131
  84. package/schemas/probability-evaluate-input.schema.json +161 -132
  85. package/schemas/probability-inspect-input.schema.json +20 -11
  86. package/schemas/probability-render-input.schema.json +1 -1
  87. package/schemas/probability-scenario-set.schema.json +110 -90
  88. package/schemas/probability-sequence-input.schema.json +154 -134
  89. package/schemas/probability-simulate-input.schema.json +161 -132
  90. package/schemas/probability-sweep-input.schema.json +163 -134
  91. package/schemas/reference-context-input.schema.json +42 -0
  92. package/schemas/reference-read-input.schema.json +41 -0
  93. package/schemas/reference-search-input.schema.json +45 -0
  94. package/schemas/scenario-suite.schema.json +92 -75
  95. package/schemas/scenario-test-input.schema.json +103 -86
  96. package/schemas/shared-scenario.schema.json +39 -22
  97. package/schemas/source-lookup-input.schema.json +61 -0
  98. package/server.json +2 -2
  99. package/docs/images/readme/chaos-meter-ingame.png +0 -0
  100. package/docs/images/readme/chaos-meter-mcp.png +0 -0
  101. package/docs/images/readme/communist-insurgency-ingame.png +0 -0
  102. package/docs/images/readme/communist-insurgency-mcp.png +0 -0
  103. package/docs/images/readme/event-chain-mcp.png +0 -0
  104. package/docs/images/readme/focus-tree-mcp.png +0 -0
  105. package/docs/images/readme/map-mcp.png +0 -0
  106. package/docs/images/readme/probability-mcp.png +0 -0
  107. package/docs/images/readme/settings-advanced-mcp.png +0 -0
  108. package/docs/images/readme/settings-events-mcp.png +0 -0
  109. 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' || position.building === 'floating_harbor') {
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
- if ([
414
- 'set_variable',
415
- 'add_to_variable',
416
- 'subtract_from_variable',
417
- 'multiply_variable',
418
- 'divide_variable',
419
- 'clamp_variable',
420
- ].includes(key)) {
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
- if (variable === undefined || expression === undefined) {
430
- if (variable === undefined || short?.value.type !== 'block') {
431
- this.unknown(key, path, scope, 'Variable and operand must resolve');
432
- return;
433
- }
434
- }
435
- const value = expression === undefined
436
- ? this.mathExpression(short.value, path, scope)
437
- : this.number(expression, path, scope);
438
- const before = numeric(scenarioValue(this.state, variable, scope)) ?? 0;
439
- let after = value;
440
- if (value !== undefined && key === 'add_to_variable')
441
- after = before + value;
442
- if (value !== undefined && key === 'subtract_from_variable')
443
- after = before - value;
444
- if (value !== undefined && key === 'multiply_variable')
445
- after = before * value;
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, scope);
457
- this.note(key, path, scope, after === undefined ? 'unresolved' : 'applied', before, after, after === undefined ? 'Numeric operand is unknown or division by zero' : undefined, variable);
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 document = activeDocument(snapshot, symbol.path);
84
- const block = document === undefined ? undefined : assignments(document.root, symbol.id)[0]?.value;
85
- if (block?.type === 'block')
86
- result.set(symbol.id, { id: symbol.id, path: symbol.path, block });
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), scenario, candidate, definitions);
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
+ }