hoi4-agent-tools 3.2.0 → 3.3.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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -0
  3. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
  4. package/dist/hoi4_agent_tools/core/condition-evaluator.js +5 -1
  5. package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
  6. package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
  7. package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
  8. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
  9. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  10. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
  11. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
  12. package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
  13. package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
  14. package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
  15. package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
  16. package/dist/hoi4_agent_tools/package-check/service.js +231 -0
  17. package/dist/hoi4_agent_tools/probability/evaluation.js +3 -1
  18. package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
  19. package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
  20. package/dist/hoi4_agent_tools/version.js +1 -1
  21. package/docs/README.md +1 -0
  22. package/docs/mechanics.md +70 -0
  23. package/package.json +2 -1
  24. package/schemas/configuration.schema.json +1 -1
  25. package/schemas/continuous-focus-palette.schema.json +1 -1
  26. package/schemas/custom-weighted-pool.schema.json +1 -1
  27. package/schemas/decision-inspect-input.schema.json +1 -1
  28. package/schemas/event-feature-manifest.schema.json +1 -1
  29. package/schemas/focus-plan.schema.json +1 -1
  30. package/schemas/focus-planning-sidecar.schema.json +1 -1
  31. package/schemas/gui-animation-source.schema.json +1 -1
  32. package/schemas/gui-helper.schema.json +1 -1
  33. package/schemas/gui-scenario.schema.json +1 -1
  34. package/schemas/helper-expansion-request.schema.json +1 -1
  35. package/schemas/helper-expansion-summary.schema.json +1 -1
  36. package/schemas/impact-inspect-input.schema.json +1 -1
  37. package/schemas/map-operation.schema.json +1 -1
  38. package/schemas/mechanic-test-input.schema.json +650 -0
  39. package/schemas/operation-result.schema.json +1 -1
  40. package/schemas/package-check-input.schema.json +164 -0
  41. package/schemas/package-manifest.schema.json +142 -0
  42. package/schemas/probability-analysis-result.schema.json +1 -1
  43. package/schemas/probability-compare-input.schema.json +1 -1
  44. package/schemas/probability-evaluate-input.schema.json +1 -1
  45. package/schemas/probability-inspect-input.schema.json +1 -1
  46. package/schemas/probability-render-input.schema.json +1 -1
  47. package/schemas/probability-scenario-set.schema.json +1 -1
  48. package/schemas/probability-sequence-input.schema.json +1 -1
  49. package/schemas/probability-simulate-input.schema.json +1 -1
  50. package/schemas/probability-sweep-input.schema.json +1 -1
  51. package/schemas/scenario-suite.schema.json +1004 -0
  52. package/schemas/scenario-test-input.schema.json +1057 -0
  53. package/schemas/shared-scenario.schema.json +237 -0
  54. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 3.3.0 - 2026-09-20
6
+
7
+ - Add `hoi4.mechanic_test` with copied versioned scenario state, bounded source effects, explicit time advancement, traces, and typed gameplay assertions.
8
+ - Add `hoi4.package_check` for declarative package definitions, calls, registrations, localisation, assets, and required suite-case references.
9
+ - Add `hoi4.scenario_test` for named inline or workspace-owned suites through fixed read-only operations, case artifacts, and authenticated revision-bound continuation.
10
+ - Share declared condition state across GUI and probability adapters while preserving their public inputs and specialized evaluation semantics.
11
+
5
12
  ## 3.2.0 - 2026-09-20
6
13
 
7
14
  - Add read-only `hoi4.impact_inspect` with active and overridden definitions, direct and transitive source consumers, state-key access roles, cycle and coverage evidence, and in-memory source comparisons.
package/README.md CHANGED
@@ -74,6 +74,9 @@ On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repo
74
74
  | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
75
75
  | `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
76
76
  | `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
77
+ | `hoi4.mechanic_test` | Execute bounded source effects on a copied declared scenario and check assertions. |
78
+ | `hoi4.package_check` | Check declarative package definitions, calls, registrations, assets, and case links. |
79
+ | `hoi4.scenario_test` | Run named source and domain cases in resumable, revision-bound batches. |
77
80
  | `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
78
81
  | `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
79
82
  | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
@@ -108,6 +111,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
108
111
  - Maps: "Render the whole map and find this state by name," "Create a state from these provinces," "Create a province inside this exact rectangle," or "Swap these state IDs and update connected references." See [Maps](docs/map.md).
109
112
  - Event chains: "Trace every route from this event and explain where its flags and variables change," or "Compare the workspace event graph with its previous revision and render the affected routes." See [Event chains](docs/events.md).
110
113
  - Cross-system impact and decisions: "Find every direct and transitive consumer of this idea," or "Compare who can take this decision, what they pay, and its mission end paths for these actor and target scenarios." See [Cross-system impact and decisions](docs/analysis.md).
114
+ - Mechanics and packages: "Test this transfer's conservation and payment," or "Check every declared package link and resume its named scenario suite." See [Mechanic tests and scenario suites](docs/mechanics.md).
111
115
  - Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
112
116
  - AI and MTTH: "Compare these focus weights across peace, defensive-war, and low-stability scenarios," or "Show when this MTTH event becomes likely and which unknown inputs control the result." See [AI and MTTH analysis](docs/probability.md).
113
117
 
@@ -1,14 +1,21 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { DecisionAnalyzer } from '../decision/service.js';
3
3
  import { ImpactAnalyzer } from '../impact/service.js';
4
+ import { MechanicAnalyzer } from '../mechanic/service.js';
5
+ import { PackageAnalyzer } from '../package-check/service.js';
6
+ import { ScenarioAnalyzer } from '../scenario-suite/service.js';
4
7
  import { decisionInspectRequestSchema, impactInspectRequestSchema } from '../schemas/analysis.js';
8
+ import { mechanicTestRequestSchema, packageCheckRequestSchema, scenarioTestRequestSchema, } from '../schemas/scenarios.js';
5
9
  import { errorResult, toolResult } from './operation-result.js';
6
10
  import { ServiceError } from './result.js';
7
11
  const wireResult = z.record(z.string(), z.json());
8
- /** Registers the two read-only analysis services for durable ordinary and native tasks. */
12
+ /** Registers read-only analysis services for durable ordinary and native tasks. */
9
13
  export function registerAnalysisJobs(operations, engine) {
10
14
  const impact = new ImpactAnalyzer(engine);
11
15
  const decision = new DecisionAnalyzer(engine);
16
+ const mechanic = new MechanicAnalyzer(engine);
17
+ const packages = new PackageAnalyzer(engine);
18
+ const scenarios = new ScenarioAnalyzer(engine);
12
19
  const register = (name, schema, execute, progressMessage) => {
13
20
  operations.registerRead(name, schema, async (input, context) => {
14
21
  try {
@@ -35,4 +42,7 @@ export function registerAnalysisJobs(operations, engine) {
35
42
  };
36
43
  register('hoi4.impact_inspect', impactInspectRequestSchema, (input) => impact.inspect(input), 'Tracing cross-system impact');
37
44
  register('hoi4.decision_inspect', decisionInspectRequestSchema, (input) => decision.inspect(input), 'Analyzing decisions and missions');
45
+ register('hoi4.mechanic_test', mechanicTestRequestSchema, (input) => mechanic.test(input), 'Testing declared mechanic steps');
46
+ register('hoi4.package_check', packageCheckRequestSchema, (input) => packages.check(input), 'Checking package connections');
47
+ register('hoi4.scenario_test', scenarioTestRequestSchema, (input) => scenarios.test(input), 'Running a bounded scenario suite');
38
48
  }
@@ -257,7 +257,11 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
257
257
  const explicit = bool(scopeStateValue(scenario, `flag.${right}`, scopeContext));
258
258
  if (explicit !== undefined)
259
259
  return { state: explicit ? 'true' : 'false', unresolved: [] };
260
- const flags = scopeContext.expression === 'ROOT' ? scenario.flags : scopeContext.binding.flags;
260
+ const flags = key === 'has_global_flag' && scenario.globalFlags !== undefined
261
+ ? scenario.globalFlags
262
+ : scopeContext.expression === 'ROOT'
263
+ ? scenario.flags
264
+ : scopeContext.binding.flags;
261
265
  if (scenario.closedFlags === false && flags?.includes(right) !== true)
262
266
  return unresolved(assignment, candidate, `Scenario does not declare flag ${right}`);
263
267
  return { state: flags?.includes(right) === true ? 'true' : 'false', unresolved: [] };
@@ -39,6 +39,9 @@ export class JobWorkerHost {
39
39
  if (![
40
40
  'hoi4.impact_inspect',
41
41
  'hoi4.decision_inspect',
42
+ 'hoi4.mechanic_test',
43
+ 'hoi4.package_check',
44
+ 'hoi4.scenario_test',
42
45
  'hoi4.event_inspect',
43
46
  'hoi4.event_render',
44
47
  'hoi4.event_compare',
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { decisionInspectRequestSchema, impactInspectRequestSchema } from '../schemas/analysis.js';
3
+ import { mechanicTestRequestSchema, packageCheckRequestSchema, scenarioTestRequestSchema, } from '../schemas/scenarios.js';
3
4
  import { eventCompareRequestSchema, eventInspectRequestSchema, eventRenderRequestSchema, } from '../schemas/event.js';
4
5
  import { focusInspectRequestSchema, focusRenderRequestSchema, focusRewriteRequestSchema, } from '../schemas/focus-requests.js';
5
6
  import { guiInspectRequestSchema, guiRenderRequestSchema, guiRewriteRequestSchema, } from '../schemas/gui-requests.js';
@@ -28,6 +29,9 @@ export const operationTaskCallSchema = z
28
29
  name: z.enum([
29
30
  'hoi4.impact_inspect',
30
31
  'hoi4.decision_inspect',
32
+ 'hoi4.mechanic_test',
33
+ 'hoi4.package_check',
34
+ 'hoi4.scenario_test',
31
35
  'hoi4.event_inspect',
32
36
  'hoi4.event_render',
33
37
  'hoi4.event_compare',
@@ -58,6 +62,9 @@ export const operationTaskCallSchema = z
58
62
  const taskArgumentSchemas = {
59
63
  'hoi4.impact_inspect': impactInspectRequestSchema,
60
64
  'hoi4.decision_inspect': decisionInspectRequestSchema,
65
+ 'hoi4.mechanic_test': mechanicTestRequestSchema,
66
+ 'hoi4.package_check': packageCheckRequestSchema,
67
+ 'hoi4.scenario_test': scenarioTestRequestSchema,
61
68
  'hoi4.event_inspect': eventInspectRequestSchema,
62
69
  'hoi4.event_render': eventRenderRequestSchema,
63
70
  'hoi4.event_compare': eventCompareRequestSchema,
@@ -0,0 +1,85 @@
1
+ export function copyScenario(scenario) {
2
+ return {
3
+ scenario: structuredClone(scenario),
4
+ unknown: new Set(),
5
+ day: 0,
6
+ };
7
+ }
8
+ export function scenarioValue(state, key, scope = 'ROOT') {
9
+ if (state.unknown.has('*') || state.unknown.has(`${scope}.${key}`))
10
+ return undefined;
11
+ if (scope === 'ROOT' || scope === 'THIS')
12
+ return state.scenario.state[key];
13
+ return state.scenario.scopes?.[scope]?.state[key];
14
+ }
15
+ export function setScenarioValue(state, key, value, scope = 'ROOT') {
16
+ const identity = `${scope}.${key}`;
17
+ if (value === undefined)
18
+ state.unknown.add(identity);
19
+ else {
20
+ state.unknown.delete(identity);
21
+ if (scope === 'ROOT' || scope === 'THIS')
22
+ state.scenario.state[key] = value;
23
+ else if (state.scenario.scopes?.[scope] !== undefined)
24
+ state.scenario.scopes[scope].state[key] = value;
25
+ }
26
+ }
27
+ /** Preserve the GUI's existing input precedence and exploratory flag semantics. */
28
+ export function scenarioFromGui(base, values = base.values) {
29
+ const state = {
30
+ ...base.stateValues,
31
+ ...base.variables,
32
+ ...values,
33
+ };
34
+ for (const [key, value] of Object.entries(base.flags))
35
+ state[`flag.${key}`] = value;
36
+ const scopes = structuredClone(base.scopes ?? {});
37
+ for (const [key, value] of Object.entries(state)) {
38
+ const match = /^(ROOT|THIS|FROM(?:\.FROM)*|PREV)\.(.+)$/u.exec(key);
39
+ if (match === null)
40
+ continue;
41
+ if (match[1] === 'ROOT' || match[1] === 'THIS')
42
+ state[match[2]] ??= value;
43
+ else {
44
+ const scope = (scopes[match[1]] ??= { id: match[1], state: {} });
45
+ scope.state[match[2]] ??= value;
46
+ }
47
+ }
48
+ const actor = base.country?.tag ?? base.country?.countryTag ?? base.country?.GetTag;
49
+ return {
50
+ schemaVersion: '1.0',
51
+ id: base.id,
52
+ state,
53
+ scopes,
54
+ closedFlags: false,
55
+ ...(typeof actor === 'string' ? { actor } : {}),
56
+ };
57
+ }
58
+ /** Probability retains pools, uncertainty, correlations and schedule in its own adapter. */
59
+ export function scenarioFromProbability(base) {
60
+ return {
61
+ schemaVersion: '1.0',
62
+ id: base.id,
63
+ ...(base.actor === undefined ? {} : { actor: base.actor }),
64
+ ...(base.date === undefined ? {} : { date: base.date }),
65
+ state: structuredClone(base.state),
66
+ ...(base.flags === undefined ? {} : { flags: [...base.flags] }),
67
+ ...(base.eventTargets === undefined ? {} : { eventTargets: { ...base.eventTargets } }),
68
+ ...(base.scopes === undefined ? {} : { scopes: structuredClone(base.scopes) }),
69
+ ...(base.candidateOverrides === undefined
70
+ ? {}
71
+ : { candidateOverrides: { ...base.candidateOverrides } }),
72
+ closedFlags: true,
73
+ ...(base.scopePools === undefined
74
+ ? {}
75
+ : {
76
+ scopeCatalogs: Object.fromEntries(base.scopePools.map((pool) => [
77
+ pool.id,
78
+ {
79
+ complete: pool.complete,
80
+ members: pool.candidates.map((candidate) => candidate.id),
81
+ },
82
+ ])),
83
+ }),
84
+ };
85
+ }
@@ -1,6 +1,7 @@
1
1
  import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
2
2
  import { hasBlockingDiagnostics } from '../core/diagnostics.js';
3
3
  import { evaluateTriggerBlock } from '../core/condition-evaluator.js';
4
+ import { scenarioFromGui } from '../core/scenario-model.js';
4
5
  import { resolveScopeContext, rootScopeContext } from '../core/scenario-state.js';
5
6
  import { assignments, firstScalar, parseClausewitz } from '../core/source/index.js';
6
7
  /** Keep declared scoped scalars out of exploratory value generation. */
@@ -30,29 +31,7 @@ function parsedCondition(expression) {
30
31
  };
31
32
  }
32
33
  export function guiConditionScenario(base, values = base.values) {
33
- const state = { ...base.stateValues, ...base.variables, ...values };
34
- for (const [key, value] of Object.entries(base.flags))
35
- state[`flag.${key}`] = value;
36
- const scopes = structuredClone(base.scopes ?? {});
37
- for (const [key, value] of Object.entries(state)) {
38
- const match = /^(ROOT|THIS|FROM(?:\.FROM)*|PREV)\.(.+)$/u.exec(key);
39
- if (match === null)
40
- continue;
41
- if (match[1] === 'ROOT' || match[1] === 'THIS')
42
- state[match[2]] ??= value;
43
- else {
44
- const scope = (scopes[match[1]] ??= { id: match[1], state: {} });
45
- scope.state[match[2]] ??= value;
46
- }
47
- }
48
- const actor = base.country?.tag ?? base.country?.countryTag ?? base.country?.GetTag;
49
- return {
50
- id: base.id,
51
- state,
52
- scopes,
53
- closedFlags: false,
54
- ...(typeof actor === 'string' ? { actor } : {}),
55
- };
34
+ return scenarioFromGui(base, values);
56
35
  }
57
36
  export function evaluateGuiCondition(expression, scenario, subject, definitions = new ClausewitzEvaluationDefinitions(), scope = 'ROOT') {
58
37
  const context = resolveScopeContext(scenario, scope, rootScopeContext(scenario));
@@ -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. Impact and decision 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 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.';
@@ -7,6 +7,9 @@ const controlTools = new Set(['hoi4.job_inspect', 'hoi4.job_cancel']);
7
7
  const backgroundTools = new Set([
8
8
  'hoi4.impact_inspect',
9
9
  'hoi4.decision_inspect',
10
+ 'hoi4.mechanic_test',
11
+ 'hoi4.package_check',
12
+ 'hoi4.scenario_test',
10
13
  'hoi4.event_inspect',
11
14
  'hoi4.event_render',
12
15
  'hoi4.event_compare',
@@ -1,5 +1,7 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { decisionInspectRequestSchema, impactInspectRequestSchema, } from '../../schemas/analysis.js';
3
+ import { mechanicCaseSchema, mechanicTestRequestSchema, packageManifestSchema, packageCheckRequestSchema, scenarioSuiteSchema, scenarioTestRequestSchema, } from '../../schemas/scenarios.js';
4
+ import { compactValidatedInputSchema } from '../server/context-schemas.js';
3
5
  import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
4
6
  import { strictOperationResultSchema } from '../server/result.js';
5
7
  import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
@@ -36,6 +38,70 @@ const decisionOutputSchema = strictOperationResultSchema(z
36
38
  unresolvedDefinitions: nonNegativeIntegerSchema,
37
39
  })
38
40
  .strict());
41
+ const mechanicOutputSchema = strictOperationResultSchema(z
42
+ .object({
43
+ sourceRevision: sha256Schema,
44
+ reportHash: sha256Schema,
45
+ status: z.enum(['passed', 'failed', 'unresolved']),
46
+ assertions: nonNegativeIntegerSchema,
47
+ passed: nonNegativeIntegerSchema,
48
+ failed: nonNegativeIntegerSchema,
49
+ unresolved: nonNegativeIntegerSchema,
50
+ steps: nonNegativeIntegerSchema,
51
+ })
52
+ .strict());
53
+ const packageOutputSchema = strictOperationResultSchema(z
54
+ .object({
55
+ sourceRevision: sha256Schema,
56
+ reportHash: sha256Schema,
57
+ complete: z.boolean(),
58
+ items: nonNegativeIntegerSchema,
59
+ present: nonNegativeIntegerSchema,
60
+ absent: nonNegativeIntegerSchema,
61
+ shadowed: nonNegativeIntegerSchema,
62
+ unresolved: nonNegativeIntegerSchema,
63
+ })
64
+ .strict());
65
+ const scenarioOutputSchema = strictOperationResultSchema(z
66
+ .object({
67
+ sourceRevision: sha256Schema,
68
+ suiteHash: sha256Schema,
69
+ reportHash: sha256Schema,
70
+ start: nonNegativeIntegerSchema,
71
+ end: nonNegativeIntegerSchema,
72
+ total: nonNegativeIntegerSchema,
73
+ completed: nonNegativeIntegerSchema,
74
+ failed: nonNegativeIntegerSchema,
75
+ unresolved: nonNegativeIntegerSchema,
76
+ pending: nonNegativeIntegerSchema,
77
+ continuation: z.string().optional(),
78
+ })
79
+ .strict());
80
+ const mechanicInputSchema = z
81
+ .object({
82
+ ...mechanicTestRequestSchema.shape,
83
+ test: compactValidatedInputSchema(mechanicCaseSchema, 'Versioned declared scenario, explicit effect steps, and typed assertions. See docs/mechanics.md.'),
84
+ })
85
+ .strict();
86
+ const packageInputSchema = z
87
+ .object({
88
+ ...packageCheckRequestSchema.shape,
89
+ manifest: compactValidatedInputSchema(packageManifestSchema, 'Declarative definitions, calls, registrations, localisation, assets, and required cases. See docs/mechanics.md.'),
90
+ })
91
+ .strict();
92
+ const scenarioInputSchema = z
93
+ .object({
94
+ ...scenarioTestRequestSchema.shape,
95
+ suite: compactValidatedInputSchema(scenarioSuiteSchema, 'Named mechanic, package, or fixed read-only tool cases. See docs/mechanics.md.').optional(),
96
+ })
97
+ .strict()
98
+ .superRefine((input, context) => {
99
+ if ((input.suite === undefined) === (input.suitePath === undefined))
100
+ context.addIssue({
101
+ code: 'custom',
102
+ message: 'Specify exactly one inline suite or suitePath',
103
+ });
104
+ });
39
105
  const readOnlyAnalysisTool = {
40
106
  readOnlyHint: true,
41
107
  destructiveHint: false,
@@ -59,6 +125,30 @@ export const analysisTaskTools = [
59
125
  outputSchema: decisionOutputSchema,
60
126
  annotations: readOnlyAnalysisTool,
61
127
  },
128
+ {
129
+ name: 'hoi4.mechanic_test',
130
+ title: 'Test declared mechanic steps',
131
+ description: 'Interpret bounded, source-backed effect steps on isolated declared state and check typed invariants. Unsupported effects remain unresolved; no game or mod source is changed.',
132
+ inputSchema: mechanicInputSchema,
133
+ outputSchema: mechanicOutputSchema,
134
+ annotations: readOnlyAnalysisTool,
135
+ },
136
+ {
137
+ name: 'hoi4.package_check',
138
+ title: 'Check package connections',
139
+ description: 'Check a declarative package manifest against active definitions, references, registrations, localisation, assets, and required suite cases.',
140
+ inputSchema: packageInputSchema,
141
+ outputSchema: packageOutputSchema,
142
+ annotations: readOnlyAnalysisTool,
143
+ },
144
+ {
145
+ name: 'hoi4.scenario_test',
146
+ title: 'Run scenario suite',
147
+ description: 'Run named inline or workspace-relative cases through bounded typed read-only domain services. Large suites return an authenticated continuation.',
148
+ inputSchema: scenarioInputSchema,
149
+ outputSchema: scenarioOutputSchema,
150
+ annotations: readOnlyAnalysisTool,
151
+ },
62
152
  ];
63
153
  export function registerAnalysisTools(server) {
64
154
  registerLegacyTaskTools(server, analysisTaskTools);
@@ -0,0 +1,112 @@
1
+ import { canonicalJson } from '../core/canonical.js';
2
+ import { scenarioValue } from '../core/scenario-model.js';
3
+ function value(state, path) {
4
+ return scenarioValue(state, path.key, path.scope);
5
+ }
6
+ function number(state, path) {
7
+ const found = value(state, path);
8
+ return typeof found === 'number' && Number.isFinite(found) ? found : undefined;
9
+ }
10
+ function result(assertion, status, observed, reason) {
11
+ return {
12
+ id: assertion.id,
13
+ kind: assertion.kind,
14
+ status,
15
+ ...(observed === undefined ? {} : { observed }),
16
+ ...(reason === undefined ? {} : { reason }),
17
+ };
18
+ }
19
+ export function evaluateMechanicAssertions(assertions, before, after, checkpoints, trace) {
20
+ return assertions.map((assertion) => {
21
+ if (before.unknown.has('*') || after.unknown.has('*'))
22
+ return result(assertion, 'unresolved', undefined, 'An unsupported effect may write this state');
23
+ if (assertion.kind === 'equals' || assertion.kind === 'end_state') {
24
+ const found = value(after, assertion.path);
25
+ if (found === undefined)
26
+ return result(assertion, 'unresolved', undefined, 'End-state value is unknown');
27
+ return result(assertion, found === assertion.expected ? 'passed' : 'failed', found);
28
+ }
29
+ if (assertion.kind === 'conservation') {
30
+ const starting = assertion.paths.map((path) => number(before, path));
31
+ const ending = assertion.paths.map((path) => number(after, path));
32
+ if ([...starting, ...ending].some((item) => item === undefined))
33
+ return result(assertion, 'unresolved', undefined, 'Conserved values must be declared numbers');
34
+ const initial = starting.reduce((sum, item) => sum + item, 0);
35
+ const final = ending.reduce((sum, item) => sum + item, 0);
36
+ return result(assertion, Math.abs(initial - final) <= assertion.tolerance ? 'passed' : 'failed', { initial, final, difference: final - initial });
37
+ }
38
+ if (assertion.kind === 'array_alignment') {
39
+ const arrays = assertion.paths.map((path) => value(after, path));
40
+ if (arrays.some((item) => !Array.isArray(item)))
41
+ return result(assertion, 'unresolved', undefined, 'All aligned values must be declared arrays');
42
+ const lengths = arrays.map((array) => array.length);
43
+ return result(assertion, lengths.every((length) => length === lengths[0]) ? 'passed' : 'failed', lengths);
44
+ }
45
+ if (assertion.kind === 'affordability') {
46
+ const available = number(before, assertion.balance);
47
+ return available === undefined
48
+ ? result(assertion, 'unresolved', undefined, 'Starting balance is unknown')
49
+ : result(assertion, available >= assertion.cost ? 'passed' : 'failed', {
50
+ available,
51
+ cost: assertion.cost,
52
+ });
53
+ }
54
+ if (assertion.kind === 'single_payment') {
55
+ const starting = number(before, assertion.balance);
56
+ const ending = number(after, assertion.balance);
57
+ const reductions = trace.flatMap((step) => step.status === 'applied' &&
58
+ step.scope === assertion.balance.scope &&
59
+ step.target === assertion.balance.key &&
60
+ typeof step.before === 'number' &&
61
+ typeof step.after === 'number' &&
62
+ step.before > step.after
63
+ ? [step.before - step.after]
64
+ : []);
65
+ return starting === undefined || ending === undefined
66
+ ? result(assertion, 'unresolved', undefined, 'Payment balance is unknown')
67
+ : result(assertion, reductions.length === 1 &&
68
+ reductions[0] === assertion.cost &&
69
+ starting - ending === assertion.cost
70
+ ? 'passed'
71
+ : 'failed', {
72
+ paid: starting - ending,
73
+ expected: assertion.cost,
74
+ deductions: reductions,
75
+ });
76
+ }
77
+ if (assertion.kind === 'repeated_setup') {
78
+ const first = checkpoints[assertion.firstStep];
79
+ const second = checkpoints[assertion.secondStep];
80
+ if (first === undefined ||
81
+ second === undefined ||
82
+ first.unknown.size > 0 ||
83
+ second.unknown.size > 0)
84
+ return result(assertion, 'unresolved', undefined, 'Repeated setup checkpoints are unavailable');
85
+ return result(assertion, canonicalJson(first.scenario) === canonicalJson(second.scenario) ? 'passed' : 'failed', { firstStep: assertion.firstStep, secondStep: assertion.secondStep });
86
+ }
87
+ if (assertion.kind === 'exclusive_flags') {
88
+ const flags = assertion.scope === 'GLOBAL'
89
+ ? after.scenario.globalFlags
90
+ : assertion.scope === 'ROOT'
91
+ ? after.scenario.flags
92
+ : after.scenario.scopes?.[assertion.scope]?.flags;
93
+ if (flags === undefined)
94
+ return result(assertion, 'unresolved', undefined, 'Flag scope is undeclared');
95
+ const active = assertion.flags.filter((flag) => flags.includes(flag));
96
+ return result(assertion, active.length <= 1 ? 'passed' : 'failed', active);
97
+ }
98
+ const flags = assertion.scope === 'GLOBAL'
99
+ ? after.scenario.globalFlags
100
+ : assertion.scope === 'ROOT'
101
+ ? after.scenario.flags
102
+ : after.scenario.scopes?.[assertion.scope]?.flags;
103
+ const targets = assertion.scope === 'ROOT'
104
+ ? after.scenario.eventTargets
105
+ : after.scenario.scopes?.[assertion.scope]?.eventTargets;
106
+ if (flags === undefined && assertion.flags.length > 0)
107
+ return result(assertion, 'unresolved', undefined, 'Cleanup flag scope is undeclared');
108
+ const lingeringFlags = assertion.flags.filter((flag) => flags?.includes(flag));
109
+ const lingeringTargets = assertion.targets.filter((target) => targets?.[target] !== undefined);
110
+ return result(assertion, lingeringFlags.length === 0 && lingeringTargets.length === 0 ? 'passed' : 'failed', { lingeringFlags, lingeringTargets });
111
+ });
112
+ }