hoi4-agent-tools 3.0.8 → 3.1.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 (135) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +4 -0
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  7. package/dist/hoi4_agent_tools/core/condition-evaluator.js +399 -0
  8. package/dist/hoi4_agent_tools/core/condition-model.js +1 -0
  9. package/dist/hoi4_agent_tools/core/condition-schema.js +42 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  16. package/dist/hoi4_agent_tools/core/index.js +47 -4
  17. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  18. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  19. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  20. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  21. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  22. package/dist/hoi4_agent_tools/core/job-worker-host.js +200 -0
  23. package/dist/hoi4_agent_tools/core/job-worker.js +62 -0
  24. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  25. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  26. package/dist/hoi4_agent_tools/core/operation-tasks.js +382 -0
  27. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  28. package/dist/hoi4_agent_tools/core/result.js +1 -0
  29. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  30. package/dist/hoi4_agent_tools/core/scenario-state.js +97 -0
  31. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  32. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +74 -25
  33. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  34. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  35. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  36. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  37. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  38. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  39. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  40. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  41. package/dist/hoi4_agent_tools/event/service.js +67 -15
  42. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  43. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  44. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  45. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  46. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  47. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +119 -0
  48. package/dist/hoi4_agent_tools/gui/scenario-generator.js +59 -73
  49. package/dist/hoi4_agent_tools/gui/scenario.js +3 -0
  50. package/dist/hoi4_agent_tools/gui/studio.js +5 -2
  51. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  52. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  53. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  54. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  55. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  56. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  57. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  58. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  59. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  60. package/dist/hoi4_agent_tools/mcp/server/create.js +16 -6
  61. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  62. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  63. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  64. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  65. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  66. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +43 -0
  67. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  68. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  69. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +14 -0
  70. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  71. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  72. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  73. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  74. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  75. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  76. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  77. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  78. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  79. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  80. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  81. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  82. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  83. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  84. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  85. package/dist/hoi4_agent_tools/probability/scenario-state.js +2 -95
  86. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  87. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +1 -287
  88. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  89. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  90. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  91. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  92. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  93. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  94. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  95. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  96. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  97. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  98. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  99. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  100. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  101. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  102. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  103. package/dist/hoi4_agent_tools/version.js +1 -1
  104. package/docs/README.md +2 -0
  105. package/docs/development.md +2 -0
  106. package/docs/events.md +7 -2
  107. package/docs/gui.md +3 -1
  108. package/docs/helper-expansion.md +78 -0
  109. package/docs/http.md +2 -0
  110. package/docs/jobs.md +85 -0
  111. package/docs/technology.md +6 -2
  112. package/package.json +11 -5
  113. package/schemas/configuration.schema.json +1 -1
  114. package/schemas/continuous-focus-palette.schema.json +1 -1
  115. package/schemas/custom-weighted-pool.schema.json +1 -1
  116. package/schemas/event-feature-manifest.schema.json +1 -1
  117. package/schemas/focus-plan.schema.json +1 -1
  118. package/schemas/focus-planning-sidecar.schema.json +1 -1
  119. package/schemas/gui-animation-source.schema.json +1 -1
  120. package/schemas/gui-helper.schema.json +1 -1
  121. package/schemas/gui-scenario.schema.json +189 -33
  122. package/schemas/helper-expansion-request.schema.json +44 -0
  123. package/schemas/helper-expansion-summary.schema.json +70 -0
  124. package/schemas/map-operation.schema.json +1 -1
  125. package/schemas/operation-result.schema.json +1 -1
  126. package/schemas/probability-analysis-result.schema.json +1 -1
  127. package/schemas/probability-compare-input.schema.json +1 -1
  128. package/schemas/probability-evaluate-input.schema.json +1 -1
  129. package/schemas/probability-inspect-input.schema.json +1 -1
  130. package/schemas/probability-render-input.schema.json +1 -1
  131. package/schemas/probability-scenario-set.schema.json +1 -1
  132. package/schemas/probability-sequence-input.schema.json +1 -1
  133. package/schemas/probability-simulate-input.schema.json +1 -1
  134. package/schemas/probability-sweep-input.schema.json +1 -1
  135. package/server.json +2 -2
@@ -1,6 +1,8 @@
1
1
  import { canonicalJson, compareCodeUnits, deterministicId, hashCanonical, } from '../core/canonical.js';
2
2
  import { sortDiagnostics } from '../core/diagnostics.js';
3
3
  import { ServiceError } from '../core/result.js';
4
+ import { beginSemanticAnalysis, semanticFragment, SemanticCatalog, } from '../core/semantic-dependencies.js';
5
+ import { runDependencyAnalysis, runDependencyAnalysisAsync, } from '../core/dependency-walk.js';
4
6
  import { analyzeTechnologySource, technologyAnalysisDiagnostics, technologySourceFiles, technologySourceFragmentCacheKey, } from './source-analysis.js';
5
7
  import { TECHNOLOGY_GRAPH_SCHEMA_VERSION, TECHNOLOGY_PARSER_VERSION, } from './model.js';
6
8
  const MAX_TECHNOLOGIES = 100_000;
@@ -374,7 +376,24 @@ function stronglyConnectedComponents(nodeIds, edges, signal) {
374
376
  }
375
377
  return components.sort((left, right) => compareCodeUnits(left[0] ?? '', right[0] ?? ''));
376
378
  }
377
- function expandHelperReferences(references, calls, unresolved, analysisMode, signal) {
379
+ function technologyHelperWalk(calls, sourceRevision) {
380
+ return {
381
+ domain: 'technology-helper-references',
382
+ sourceRevision,
383
+ edges: calls.map(({ id, sourceKind, sourceId, helperId }) => ({
384
+ id,
385
+ from: `${sourceKind}:${sourceId}`,
386
+ to: `scripted_effect:${helperId}`,
387
+ })),
388
+ roots: calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect').map(({ id }) => id),
389
+ branches: [...new Set(calls.map(({ helperId }) => `scripted_effect:${helperId}`))].sort(compareCodeUnits),
390
+ maximumDepth: MAX_HELPER_DEPTH,
391
+ maximumSteps: 10_000_000,
392
+ visitPolicy: 'path',
393
+ reverseEdges: false,
394
+ };
395
+ }
396
+ function expandHelperReferences(references, calls, unresolved, analysisMode, walk, signal) {
378
397
  const direct = references.filter(({ sourceKind }) => sourceKind === 'scripted_effect');
379
398
  const publicReferences = references.filter(({ sourceKind }) => sourceKind !== 'scripted_effect');
380
399
  if (analysisMode === 'focused') {
@@ -404,82 +423,59 @@ function expandHelperReferences(references, calls, unresolved, analysisMode, sig
404
423
  group.push(reference);
405
424
  refsByHelper.set(reference.sourceId, group);
406
425
  }
407
- const callsBySource = new Map();
408
- for (const call of calls) {
409
- const key = `${call.sourceKind}:${call.sourceId}`;
410
- const group = callsBySource.get(key) ?? [];
411
- group.push(call);
412
- callsBySource.set(key, group);
413
- }
414
426
  const projected = [...publicReferences, ...direct];
415
427
  let projectionCount = 0;
416
- for (const rootCall of calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect')) {
417
- const stack = [
418
- {
419
- helperId: rootCall.helperId,
420
- helperStack: [rootCall.helperId],
421
- confidence: rootCall.confidence,
422
- },
423
- ];
424
- const visited = new Set();
425
- while (stack.length > 0) {
426
- signal?.throwIfAborted();
427
- const current = stack.pop();
428
- const visitKey = current.helperStack.join('\0');
429
- if (visited.has(visitKey))
430
- continue;
431
- visited.add(visitKey);
432
- if (current.helperStack.length > MAX_HELPER_DEPTH) {
433
- unresolved.push({
434
- id: deterministicId('tech-unresolved', { rootCall, helperStack: current.helperStack }),
435
- kind: 'unsupported_construct',
436
- expression: current.helperStack.join(' -> '),
437
- ownerId: rootCall.sourceId,
438
- location: rootCall.location,
439
- confidence: 'unresolved',
440
- blockers: [
441
- {
442
- code: 'TECH_HELPER_DEPTH_BLOCKED',
443
- message: `Scripted-effect expansion exceeds depth ${MAX_HELPER_DEPTH}`,
444
- location: rootCall.location,
445
- },
446
- ],
447
- });
448
- continue;
449
- }
450
- for (const reference of refsByHelper.get(current.helperId) ?? []) {
451
- projectionCount += 1;
452
- if (projectionCount > MAX_HELPER_PROJECTIONS)
453
- break;
454
- projected.push({
455
- ...reference,
456
- id: deterministicId('tech-external-projection', {
457
- root: rootCall.id,
458
- reference: reference.id,
459
- helperStack: current.helperStack,
460
- }),
461
- sourceKind: rootCall.sourceKind,
462
- sourceId: rootCall.sourceId,
463
- helperStack: [...current.helperStack],
464
- confidence: worseConfidence(current.confidence, reference.confidence),
465
- metadata: {
466
- ...reference.metadata,
467
- projectedFromScriptedEffect: reference.sourceId,
468
- callLocation: canonicalJson(rootCall.location),
428
+ if (walk === undefined)
429
+ throw new Error('Full technology helper analysis requires a dependency traversal');
430
+ const roots = calls.filter(({ sourceKind }) => sourceKind !== 'scripted_effect');
431
+ const callById = new Map(calls.map((call) => [call.id, call]));
432
+ for (const record of walk.state.records) {
433
+ signal?.throwIfAborted();
434
+ if (record.kind !== 'visit' && record.kind !== 'depth')
435
+ continue;
436
+ const rootCall = roots[record.root];
437
+ const path = walk.path(record.path).map(({ id }) => callById.get(id));
438
+ const helperStack = path.map(({ helperId }) => helperId);
439
+ if (record.kind === 'depth') {
440
+ unresolved.push({
441
+ id: deterministicId('tech-unresolved', { rootCall, helperStack }),
442
+ kind: 'unsupported_construct',
443
+ expression: helperStack.join(' -> '),
444
+ ownerId: rootCall.sourceId,
445
+ location: rootCall.location,
446
+ confidence: 'unresolved',
447
+ blockers: [
448
+ {
449
+ code: 'TECH_HELPER_DEPTH_BLOCKED',
450
+ message: `Scripted-effect expansion exceeds depth ${MAX_HELPER_DEPTH}`,
451
+ location: rootCall.location,
469
452
  },
470
- });
471
- }
453
+ ],
454
+ });
455
+ continue;
456
+ }
457
+ const confidence = path.reduce((value, call) => worseConfidence(value, call.confidence), rootCall.confidence);
458
+ for (const reference of refsByHelper.get(path.at(-1).helperId) ?? []) {
459
+ projectionCount++;
472
460
  if (projectionCount > MAX_HELPER_PROJECTIONS)
473
461
  break;
474
- for (const call of callsBySource.get(`scripted_effect:${current.helperId}`) ?? []) {
475
- if (current.helperStack.includes(call.helperId))
476
- continue;
477
- stack.push({
478
- helperId: call.helperId,
479
- helperStack: [...current.helperStack, call.helperId],
480
- confidence: worseConfidence(current.confidence, call.confidence),
481
- });
482
- }
462
+ projected.push({
463
+ ...reference,
464
+ id: deterministicId('tech-external-projection', {
465
+ root: rootCall.id,
466
+ reference: reference.id,
467
+ helperStack,
468
+ }),
469
+ sourceKind: rootCall.sourceKind,
470
+ sourceId: rootCall.sourceId,
471
+ helperStack,
472
+ confidence: worseConfidence(confidence, reference.confidence),
473
+ metadata: {
474
+ ...reference.metadata,
475
+ projectedFromScriptedEffect: reference.sourceId,
476
+ callLocation: canonicalJson(rootCall.location),
477
+ },
478
+ });
483
479
  }
484
480
  if (projectionCount > MAX_HELPER_PROJECTIONS)
485
481
  break;
@@ -1123,14 +1119,13 @@ function fragmentsFor(snapshot, options) {
1123
1119
  };
1124
1120
  const fingerprint = helperCatalogFingerprint(snapshot);
1125
1121
  const fragments = [];
1122
+ const dependencies = beginSemanticAnalysis(options.cache, snapshot, 'technology', options.workspaceIdentity);
1123
+ const reads = new SemanticCatalog();
1124
+ context.helperIds = reads.bind('helpers', 'scripted_effect', helperIds);
1126
1125
  for (const file of technologySourceFiles(snapshot.files)) {
1127
1126
  options.signal?.throwIfAborted();
1128
1127
  const key = technologySourceFragmentCacheKey(file, fingerprint);
1129
- let fragment = options.cache?.get(key);
1130
- if (fragment === undefined) {
1131
- fragment = analyzeTechnologySource(file, context);
1132
- options.cache?.set(key, fragment, file.bytes.length);
1133
- }
1128
+ const fragment = semanticFragment(dependencies, options.cache, file, key, reads, () => analyzeTechnologySource(file, context));
1134
1129
  fragments.push(fragment);
1135
1130
  }
1136
1131
  return fragments;
@@ -1143,6 +1138,12 @@ function resolveUnlocks(unlocks, targets, complete) {
1143
1138
  }));
1144
1139
  }
1145
1140
  export function buildTechnologyGraph(snapshot, options) {
1141
+ return runDependencyAnalysis(technologyGraphAnalysis(snapshot, options), options.signal);
1142
+ }
1143
+ export function buildTechnologyGraphAsync(snapshot, options) {
1144
+ return runDependencyAnalysisAsync(technologyGraphAnalysis(snapshot, options), options.signal);
1145
+ }
1146
+ function* technologyGraphAnalysis(snapshot, options) {
1146
1147
  options.signal?.throwIfAborted();
1147
1148
  const fragments = fragmentsFor(snapshot, options);
1148
1149
  const assets = assetMap(options.assetFiles ?? []);
@@ -1167,7 +1168,10 @@ export function buildTechnologyGraph(snapshot, options) {
1167
1168
  const placements = enrichPlacements(basePlacements, edges, gridboxes, technologies, unlocks, itemLayouts, options.signal);
1168
1169
  const unresolved = fragments.flatMap(({ unresolved }) => unresolved);
1169
1170
  const helperCalls = fragments.flatMap(({ helperCalls }) => helperCalls);
1170
- const externalReferences = expandHelperReferences(fragments.flatMap(({ externalReferences }) => externalReferences), helperCalls, unresolved, options.analysisMode ?? 'full', options.signal);
1171
+ const walk = options.analysisMode === 'focused'
1172
+ ? undefined
1173
+ : yield technologyHelperWalk(helperCalls, snapshot.revision);
1174
+ const externalReferences = expandHelperReferences(fragments.flatMap(({ externalReferences }) => externalReferences), helperCalls, unresolved, options.analysisMode ?? 'full', walk, options.signal);
1171
1175
  const issues = diagnose(technologies, folders, placements, gridboxes, edges, categories, doctrineDefinitions, unlocks, externalReferences, selectedTechnologies.duplicates.map((group) => group.map(({ iconSprite, effectSignatureFields, ...technology }) => ({
1172
1176
  ...technology,
1173
1177
  icon: iconStatus(iconSprite, snapshot.index, assets),
@@ -1199,6 +1203,7 @@ export function buildTechnologyGraph(snapshot, options) {
1199
1203
  revision,
1200
1204
  complete: snapshot.complete &&
1201
1205
  unresolved.every(({ kind }) => kind !== 'partial_source') &&
1206
+ !unresolved.some(({ blockers }) => blockers.some(({ code }) => code === 'TECH_HELPER_DEPTH_BLOCKED' || code === 'TECH_HELPER_PROJECTION_LIMIT')) &&
1202
1207
  !(options.analysisMode === 'focused' && helperCalls.length > 0),
1203
1208
  analysisBoundary: {
1204
1209
  staticAnalysis: true,
@@ -0,0 +1,33 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ export function technologyHelperInventory(graph, sourceComplete) {
3
+ return {
4
+ domain: 'technology',
5
+ sourceRevision: graph.revision,
6
+ sourceComplete,
7
+ sourceHashes: graph.sourceHashes,
8
+ unresolved: graph.unresolved.filter(({ id }) => id !== 'tech-unresolved-helper-expansion-deferred'),
9
+ edges: [
10
+ ...graph.helperCalls.map((call) => ({
11
+ id: call.id,
12
+ from: `${call.sourceKind}:${call.sourceId}`,
13
+ to: `scripted_effect:${call.helperId}`,
14
+ evidence: { kind: 'helper_call', call },
15
+ })),
16
+ ...graph.externalReferences
17
+ .filter(({ sourceKind }) => sourceKind === 'scripted_effect')
18
+ .map((reference) => ({
19
+ id: `reference:${reference.id}`,
20
+ from: `scripted_effect:${reference.sourceId}`,
21
+ to: `reference:${reference.id}`,
22
+ evidence: { kind: 'technology_reference', reference },
23
+ })),
24
+ ].sort((left, right) => compareCodeUnits(left.id, right.id)),
25
+ roots: graph.helperCalls
26
+ .filter(({ sourceKind }) => sourceKind !== 'scripted_effect')
27
+ .map(({ id }) => id)
28
+ .sort(compareCodeUnits),
29
+ branches: [
30
+ ...new Set(graph.helperCalls.map(({ helperId }) => `scripted_effect:${helperId}`)),
31
+ ].sort(compareCodeUnits),
32
+ };
33
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from 'zod/v4';
2
+ import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
3
+ import { errorResult, toolResult } from '../core/operation-result.js';
4
+ import { ServiceError } from '../core/result.js';
5
+ import { technologyCompareRequestSchema, technologyInspectRequestSchema, technologyRenderRequestSchema, } from '../schemas/technology.js';
6
+ import { compareTechnologies, inspectTechnologies, normalizeTechnologyCompareRequest, normalizeTechnologyInspectionRequest, normalizeTechnologyRenderRequest, renderTechnologies, } from './operations.js';
7
+ import { TechnologyTreeViewer } from './service.js';
8
+ const wireResult = z.record(z.string(), z.json());
9
+ /** Registers typed technology operations without invoking transport handlers. */
10
+ export function registerTechnologyJobs(operations, engine) {
11
+ const viewer = new TechnologyTreeViewer(engine);
12
+ const lifetime = new IdleCacheLifetime(() => viewer.clearCaches());
13
+ function register(name, schema, normalize, execute, message) {
14
+ operations.registerRead(name, schema, async (input, context) => {
15
+ const release = lifetime.begin();
16
+ try {
17
+ if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
18
+ throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
19
+ await context.progress({ completed: 0, total: 3, message });
20
+ const result = await execute(viewer, {
21
+ ...normalize(input),
22
+ workspaceId: context.workspaceId,
23
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
24
+ signal: context.signal,
25
+ });
26
+ await context.progress({
27
+ completed: 3,
28
+ total: 3,
29
+ message: 'Technology operation complete',
30
+ });
31
+ const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
32
+ const revision = result.data !== null &&
33
+ typeof result.data === 'object' &&
34
+ 'revision' in result.data &&
35
+ typeof result.data.revision === 'string'
36
+ ? result.data.revision
37
+ : result.data !== null &&
38
+ typeof result.data === 'object' &&
39
+ 'afterRevision' in result.data &&
40
+ typeof result.data.afterRevision === 'string'
41
+ ? result.data.afterRevision
42
+ : undefined;
43
+ if (revision !== undefined)
44
+ await context.stageResult(wire, { sourceRevision: revision });
45
+ return wire;
46
+ }
47
+ catch (error) {
48
+ if (context.signal.aborted)
49
+ throw error;
50
+ return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
51
+ }
52
+ finally {
53
+ release();
54
+ }
55
+ });
56
+ }
57
+ register('hoi4.tech_inspect', technologyInspectRequestSchema, normalizeTechnologyInspectionRequest, inspectTechnologies, 'Analyzing technology trees');
58
+ register('hoi4.tech_render', technologyRenderRequestSchema, normalizeTechnologyRenderRequest, renderTechnologies, 'Rendering technology-tree view');
59
+ register('hoi4.tech_compare', technologyCompareRequestSchema, normalizeTechnologyCompareRequest, compareTechnologies, 'Comparing technology-tree graphs');
60
+ }
@@ -0,0 +1,125 @@
1
+ import { publicArtifactLink } from '../core/artifacts.js';
2
+ import { helperExpansionValidation } from '../core/helper-expansion.js';
3
+ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
4
+ import { emptyServiceResult } from '../core/result.js';
5
+ import { setInlineFilesScanned } from '../core/operation-result.js';
6
+ import { technologyCompareRequestSchema, technologyInspectRequestSchema, technologyRenderRequestSchema, } from '../schemas/technology.js';
7
+ import { technologyDiagnostics } from './service.js';
8
+ export function normalizeTechnologyInspectionRequest(input) {
9
+ return JSON.parse(JSON.stringify(technologyInspectRequestSchema.parse(input)));
10
+ }
11
+ export function normalizeTechnologyRenderRequest(input) {
12
+ return JSON.parse(JSON.stringify(technologyRenderRequestSchema.parse(input)));
13
+ }
14
+ export function normalizeTechnologyCompareRequest(input) {
15
+ return JSON.parse(JSON.stringify(technologyCompareRequestSchema.parse(input)));
16
+ }
17
+ export function technologyGraphHash(graph) {
18
+ return hashCanonical({
19
+ schemaVersion: graph.schemaVersion,
20
+ analysisMode: graph.analysisMode ?? 'full',
21
+ workspaceIdentity: graph.workspaceIdentity,
22
+ revision: graph.revision,
23
+ statistics: graph.statistics,
24
+ sourceHashes: graph.sourceHashes,
25
+ });
26
+ }
27
+ export function technologyCounts(graph, artifacts) {
28
+ return {
29
+ technologies: graph.statistics.technologyCount,
30
+ legacyDoctrines: graph.statistics.legacyDoctrineCount,
31
+ folders: graph.statistics.folderCount,
32
+ placements: graph.statistics.placementCount,
33
+ edges: graph.edges.length,
34
+ unlocks: graph.statistics.unlockCount,
35
+ references: graph.statistics.externalReferenceCount,
36
+ issues: graph.statistics.issueCount,
37
+ unresolved: graph.statistics.unresolvedCount,
38
+ artifacts,
39
+ };
40
+ }
41
+ export function technologyValidation(graph) {
42
+ const blocking = technologyDiagnostics(graph).filter(({ severity }) => severity === 'error' || severity === 'blocker').length;
43
+ return {
44
+ passed: graph.complete && blocking === 0,
45
+ checks: [
46
+ {
47
+ id: 'technology-analysis',
48
+ passed: graph.complete && blocking === 0,
49
+ message: graph.complete
50
+ ? `${blocking} blocking technology diagnostics; full evidence is linked`
51
+ : graph.analysisMode === 'focused'
52
+ ? 'Helper projections were deferred for this large workspace; direct evidence is linked'
53
+ : graph.unresolved.some(({ blockers }) => blockers.some(({ code }) => code === 'TECH_HELPER_DEPTH_BLOCKED' ||
54
+ code === 'TECH_HELPER_PROJECTION_LIMIT'))
55
+ ? 'Helper expansion reached a depth or materialization boundary; helper_expansion mode provides bounded source-linked continuation'
56
+ : `${graph.skippedSourceCount} source(s) were skipped; full evidence is linked`,
57
+ },
58
+ ],
59
+ };
60
+ }
61
+ export async function inspectTechnologies(viewer, input) {
62
+ const output = await viewer.analyze(input);
63
+ const result = emptyServiceResult(input.workspaceId, {
64
+ mode: input.mode,
65
+ revision: output.graph.revision,
66
+ graphHash: technologyGraphHash(output.graph),
67
+ counts: technologyCounts(output.graph, output.artifacts.length),
68
+ ...(output.helperExpansion === undefined ? {} : { helperExpansion: output.helperExpansion }),
69
+ });
70
+ result.code =
71
+ (output.helperExpansion?.complete ?? output.graph.complete)
72
+ ? 'TECH_INSPECTED'
73
+ : 'TECH_INSPECTED_PARTIAL';
74
+ setInlineFilesScanned(result, output.graph.filesScanned);
75
+ result.artifacts = output.artifacts.map(publicArtifactLink);
76
+ result.validation = technologyValidation(output.graph);
77
+ if (output.helperExpansion !== undefined)
78
+ result.validation = helperExpansionValidation(output.helperExpansion);
79
+ return result;
80
+ }
81
+ export async function renderTechnologies(viewer, input) {
82
+ const output = await viewer.renderAndStore(input);
83
+ const result = emptyServiceResult(input.workspaceId, {
84
+ view: output.render.view,
85
+ revision: output.graph.revision,
86
+ graphHash: technologyGraphHash(output.graph),
87
+ hashes: output.render.hashes,
88
+ selectedNodes: output.render.selectedIds.length,
89
+ omittedNodes: output.render.omittedNodeCount,
90
+ focusedRenders: output.focused.length,
91
+ sourceAccurate: output.render.sourceAccurate,
92
+ });
93
+ result.code = output.graph.complete ? 'TECH_RENDERED' : 'TECH_RENDERED_PARTIAL';
94
+ setInlineFilesScanned(result, output.graph.filesScanned);
95
+ result.artifacts = output.artifacts.map(publicArtifactLink);
96
+ result.validation = technologyValidation(output.graph);
97
+ return result;
98
+ }
99
+ export async function compareTechnologies(viewer, input) {
100
+ const output = await viewer.compareAndStore(input);
101
+ const comparison = output.comparison;
102
+ const result = emptyServiceResult(input.workspaceId, {
103
+ beforeRevision: comparison.beforeRevision,
104
+ afterRevision: comparison.afterRevision,
105
+ added: comparison.technologies.added.length,
106
+ removed: comparison.technologies.removed.length,
107
+ renamed: comparison.technologies.renamed.length,
108
+ moved: comparison.technologies.moved.length,
109
+ regressions: comparison.regressions.length,
110
+ artifacts: output.artifacts.length,
111
+ ...(output.render === undefined ? {} : { renderHashes: output.render.hashes }),
112
+ });
113
+ result.code =
114
+ output.before.complete && output.after.complete ? 'TECH_COMPARED' : 'TECH_COMPARED_PARTIAL';
115
+ setInlineFilesScanned(result, [...new Set([...output.before.filesScanned, ...output.after.filesScanned])].sort(compareCodeUnits));
116
+ result.artifacts = output.artifacts.map(publicArtifactLink);
117
+ result.validation = {
118
+ passed: technologyValidation(output.before).passed && technologyValidation(output.after).passed,
119
+ checks: [
120
+ ...technologyValidation(output.before).checks,
121
+ ...technologyValidation(output.after).checks,
122
+ ],
123
+ };
124
+ return result;
125
+ }
@@ -1,4 +1,7 @@
1
1
  import path from 'node:path';
2
+ import { inspectHelperExpansion } from '../core/helper-expansion.js';
3
+ import { technologyHelperInventory } from './helper-expansion.js';
4
+ import { clearSemanticDependencies } from '../core/semantic-dependencies.js';
2
5
  import { canonicalJson, compareCodeUnits, hashCanonical, sha256Bytes } from '../core/canonical.js';
3
6
  import { boundedSourceHashEvidence, } from '../core/artifacts.js';
4
7
  import { RenderBudget } from '../core/render-budget.js';
@@ -9,7 +12,7 @@ import { PACKAGE_VERSION } from '../version.js';
9
12
  import { GuiAssetCatalog } from '../gui/assets.js';
10
13
  import { buildGuiSourceGraph } from '../gui/source-graph.js';
11
14
  import { compareTechnologyGraphs } from './compare.js';
12
- import { buildTechnologyGraph, technologyAssetPatterns } from './graph.js';
15
+ import { buildTechnologyGraph, buildTechnologyGraphAsync, technologyAssetPatterns, } from './graph.js';
13
16
  import { TECHNOLOGY_GRAPH_SCHEMA_VERSION } from './model.js';
14
17
  import { analyzeTechnologyImpact, discoverTechnologyFolders, explainTechnology, inspectTechnologyUnlocks, lintTechnologyGraph, technologyBonusCoverage, technologyScanReport, traceTechnology, } from './queries.js';
15
18
  import { renderTechnologyGraph, technologyRenderIconSprites, } from './render.js';
@@ -48,6 +51,7 @@ class BoundedTechnologyFragmentCache {
48
51
  }
49
52
  }
50
53
  clear() {
54
+ clearSemanticDependencies(this);
51
55
  this.#values.clear();
52
56
  this.#bytes = 0;
53
57
  }
@@ -445,16 +449,17 @@ export class TechnologyTreeViewer {
445
449
  const workspace = this.engine.resolver.get(workspaceId, options.principal);
446
450
  const generation = this.engine.generation(workspaceId);
447
451
  const cached = this.#state.current.get(workspaceId);
448
- if (options.refresh !== true && cached?.generation === generation)
449
- return cached.graph;
450
452
  const snapshot = await scanTechnologySources(this.engine, workspaceId, workspace, options.principal, options.signal);
451
453
  const analysisMode = 'full';
452
- const preliminary = buildTechnologyGraph(snapshot, {
453
- workspaceIdentity: workspace.workspaceIdentity,
454
- cache: this.#state.fragments,
455
- analysisMode,
456
- ...(options.signal === undefined ? {} : { signal: options.signal }),
457
- });
454
+ const unchangedSource = cached?.generation === generation && cached.snapshot.revision === snapshot.revision;
455
+ const preliminary = unchangedSource
456
+ ? cached.graph
457
+ : await buildTechnologyGraphAsync(snapshot, {
458
+ workspaceIdentity: workspace.workspaceIdentity,
459
+ cache: this.#state.fragments,
460
+ analysisMode,
461
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
462
+ });
458
463
  const patterns = technologyAssetPatterns(preliminary);
459
464
  const assetFiles = patterns.length === 0
460
465
  ? []
@@ -462,7 +467,15 @@ export class TechnologyTreeViewer {
462
467
  patterns,
463
468
  ...(options.signal === undefined ? {} : { signal: options.signal }),
464
469
  });
465
- const graph = buildTechnologyGraph(snapshot, {
470
+ const assetIdentity = (files) => hashCanonical(files.map(({ absolutePath, displayPath, loadOrder, sha256 }) => ({
471
+ absolutePath,
472
+ displayPath,
473
+ loadOrder,
474
+ sha256,
475
+ })));
476
+ if (unchangedSource && assetIdentity(assetFiles) === assetIdentity(cached.assetFiles))
477
+ return cached.graph;
478
+ const graph = await buildTechnologyGraphAsync(snapshot, {
466
479
  workspaceIdentity: workspace.workspaceIdentity,
467
480
  cache: this.#state.fragments,
468
481
  assetFiles,
@@ -474,6 +487,24 @@ export class TechnologyTreeViewer {
474
487
  return graph;
475
488
  }
476
489
  async analyze(input) {
490
+ if (input.mode === 'helper_expansion') {
491
+ const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
492
+ const snapshot = await scanTechnologySources(this.engine, input.workspaceId, workspace, input.principal, input.signal);
493
+ const graph = await buildTechnologyGraphAsync(snapshot, {
494
+ workspaceIdentity: workspace.workspaceIdentity,
495
+ cache: this.#state.fragments,
496
+ analysisMode: 'focused',
497
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
498
+ });
499
+ const expansion = await inspectHelperExpansion(this.engine, input.workspaceId, technologyHelperInventory(graph, snapshot.complete && graph.unresolved.every(({ kind }) => kind !== 'partial_source')), input.helperExpansion, input.principal, input.signal);
500
+ return {
501
+ graph,
502
+ report: expansion.report,
503
+ reportJson: expansion.reportJson,
504
+ artifacts: expansion.artifacts,
505
+ helperExpansion: expansion.summary,
506
+ };
507
+ }
477
508
  const graph = await this.scan(input.workspaceId, {
478
509
  refresh: input.refresh ?? input.mode === 'scan',
479
510
  ...(input.principal === undefined ? {} : { principal: input.principal }),
@@ -542,11 +573,37 @@ export class TechnologyTreeViewer {
542
573
  complete: graph.complete,
543
574
  analysisBoundary: graph.analysisBoundary,
544
575
  mode: input.mode,
576
+ resources: [
577
+ {
578
+ name: `${safeSlug(`technology-${input.mode}`)}-${graph.revision.slice(0, 12)}.json`,
579
+ mimeType: 'application/json',
580
+ },
581
+ ...(input.mode === 'scan'
582
+ ? [
583
+ {
584
+ name: `technology-graph-${graph.revision}.json`,
585
+ mimeType: 'application/json',
586
+ },
587
+ ]
588
+ : []),
589
+ ],
545
590
  report,
546
591
  })}\n`;
547
592
  const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
593
+ const graphHashValue = graphHash(graph);
548
594
  const artifact = await this.engine.artifacts.putChunked(workspace, `${safeSlug(`technology-${input.mode}`)}-${graph.revision.slice(0, 12)}.json`, 'application/json', reportJson, technologyProvenance(graph, `technology-${input.mode}`, { mode: input.mode }), `Authoritative Technology Tree Viewer ${input.mode.replaceAll('_', ' ')} report`, input.signal);
549
- return { graph, report, reportJson, artifacts: [artifact] };
595
+ const graphArtifact = input.mode === 'scan'
596
+ ? await this.engine.artifacts.putChunked(workspace, `technology-graph-${graph.revision}.json`, 'application/json', `${canonicalJson({ schemaVersion: 'technology-graph-snapshot.v1', graph })}\n`, technologyProvenance(graph, 'technology-graph-snapshot', {
597
+ revision: graph.revision,
598
+ graphHash: graphHashValue,
599
+ }), 'Revision-addressed Technology Tree Viewer graph snapshot', input.signal)
600
+ : undefined;
601
+ return {
602
+ graph,
603
+ report,
604
+ reportJson,
605
+ artifacts: [artifact, ...(graphArtifact === undefined ? [] : [graphArtifact])],
606
+ };
550
607
  }
551
608
  async renderAndStore(input) {
552
609
  const graph = await this.scan(input.workspaceId, {
@@ -747,10 +804,27 @@ export class TechnologyTreeViewer {
747
804
  return graph;
748
805
  }
749
806
  if (reference?.revision !== undefined) {
750
- const graph = this.#state.history.get(workspaceId)?.get(reference.revision);
751
- if (graph === undefined)
752
- throw new ServiceError('TECH_REVISION_NOT_CACHED', 'Requested technology revision is not cached');
753
- return graph;
807
+ const cached = this.#state.history.get(workspaceId)?.get(reference.revision);
808
+ if (cached !== undefined)
809
+ return cached;
810
+ const workspace = this.engine.resolver.get(workspaceId, principal);
811
+ const names = new Set([
812
+ `technology-graph-${reference.revision}.json`,
813
+ `technology-scan-${reference.revision.slice(0, 12)}.json`,
814
+ ]);
815
+ const candidates = (await this.engine.artifacts.list(workspace, signal)).filter(({ name }) => names.has(name));
816
+ for (const artifact of candidates) {
817
+ const selected = parseGraphArtifact(await readLogicalArtifact(this.engine, workspaceId, artifact.uri, principal, signal));
818
+ if (selected.revision === reference.revision &&
819
+ selected.workspaceId === workspaceId &&
820
+ selected.workspaceIdentity === current.workspaceIdentity) {
821
+ rememberGraph(this.#state, selected);
822
+ return selected;
823
+ }
824
+ }
825
+ if (candidates.length === 0)
826
+ throw new ServiceError('TECH_REVISION_NOT_CACHED', 'Requested technology revision is not cached or retained as a graph artifact');
827
+ throw new ServiceError('TECH_GRAPH_WORKSPACE_MISMATCH', 'Retained technology graph evidence does not match the requested revision and workspace topology');
754
828
  }
755
829
  return current;
756
830
  }
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { semanticSourceIdentity } from '../core/semantic-dependencies.js';
2
3
  import { compareCodeUnits, deterministicId } from '../core/canonical.js';
3
4
  import { assignments, childBlocks, firstScalar, nodeLocation, parseClausewitz, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
4
5
  const technologyStructuralFields = new Set([
@@ -1031,7 +1032,7 @@ export function analyzeTechnologySource(file, context) {
1031
1032
  return fragment;
1032
1033
  }
1033
1034
  export function technologySourceFragmentCacheKey(file, catalogFingerprint) {
1034
- return `${file.sha256}:${catalogFingerprint}`;
1035
+ return `${semanticSourceIdentity(file)}:${catalogFingerprint}`;
1035
1036
  }
1036
1037
  export function technologySourceFiles(files) {
1037
1038
  return files
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '3.0.8';
3
+ export const PACKAGE_VERSION = '3.1.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/README.md CHANGED
@@ -3,6 +3,8 @@
3
3
  - [Setup](setup.md): install the server, configure HOI4 paths, and connect an MCP client.
4
4
  - [Event chains](events.md): inspect, trace, lint, render, and compare event chains.
5
5
  - [Technology trees](technology.md): inspect, trace, lint, render, and compare technologies and doctrines.
6
+ - [Bounded helper expansion](helper-expansion.md): resume source-linked event and technology helper paths with exact coverage boundaries.
7
+ - [Persistent jobs](jobs.md): run, inspect, cancel, and recover long operations.
6
8
  - [AI and MTTH analysis](probability.md): evaluate weighted logic, timing, uncertainty, sequences, and patches.
7
9
  - [Focus trees](focus.md): inspect, create, render, and clean national or continuous focus content.
8
10
  - [Scripted GUIs](gui.md): inspect linked UI source, render states, and create or repair GUIs.
@@ -25,6 +25,8 @@ The technology acceptance fixture contains 1,040 project-owned technologies acro
25
25
 
26
26
  `npx vitest run tests/integration/mcp-concurrent-workloads.test.ts` tests simultaneous production calls across all six domains over six HTTP sessions and four independent stdio processes, including artifact retrieval and queued cancellation. Execution limits and connection behavior are described in [HTTP](http.md) and the [concurrency decision](https://github.com/klimPaskov/hoi4-agent-tools/blob/main/docs/adr/0026-concurrent-request-execution.md).
27
27
 
28
+ Persistent-job qualification covers authenticated record publication, request-key conflicts, read-result checkpoints, transaction-journal reconciliation, stopped-owner takeover, native task negotiation, ordinary-call parity, reconnect isolation, rewrite cancellation before source application, and fixed-entry worker execution. The public lifecycle contract is documented in [Persistent jobs and MCP tasks](jobs.md).
29
+
28
30
  The probability fixture contains more than 150 weighted blocks, 250 scenarios, and a declared stateful pool. Run `npm run benchmark` after analyzer changes. On the July 22, 2026 reference run, cold indexing took 28 ms, a cached block evaluation 29 ms, the 250-row matrix 85 ms, one million draws 9 ms, 3/12/25-candidate sequence cases 20/2,079/356 ms, and retrieval of a 1.84 MB result 3 ms. Review budgets are 250 ms, 150 ms, 750 ms, 100 ms, 200/10,000/3,000 ms, and 100 ms respectively; accuracy, method metadata, and omitted probability must not be weakened to meet them.
29
31
 
30
32
  Local opt-in tests can also read an installed game and an external mod without copying their sources: