hoi4-agent-tools 2.3.0 → 2.3.4

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 (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/bin/stdio.js +4 -0
  3. package/dist/hoi4_agent_tools/event/algorithms.js +178 -1
  4. package/dist/hoi4_agent_tools/event/graph.js +38 -9
  5. package/dist/hoi4_agent_tools/event/limits.js +4 -1
  6. package/dist/hoi4_agent_tools/event/service.js +200 -46
  7. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +480 -0
  8. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -3
  9. package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
  10. package/dist/hoi4_agent_tools/runtime.js +109 -7
  11. package/dist/hoi4_agent_tools/technology/graph.js +30 -3
  12. package/dist/hoi4_agent_tools/technology/queries.js +70 -3
  13. package/dist/hoi4_agent_tools/technology/service.js +38 -2
  14. package/dist/hoi4_agent_tools/version.js +1 -1
  15. package/docs/events.md +2 -2
  16. package/docs/setup.md +2 -0
  17. package/docs/technology.md +4 -2
  18. package/package.json +3 -1
  19. package/schemas/configuration.schema.json +1 -1
  20. package/schemas/continuous-focus-palette.schema.json +1 -1
  21. package/schemas/custom-weighted-pool.schema.json +1 -1
  22. package/schemas/event-feature-manifest.schema.json +1 -1
  23. package/schemas/focus-plan.schema.json +1 -1
  24. package/schemas/focus-planning-sidecar.schema.json +1 -1
  25. package/schemas/gui-animation-source.schema.json +1 -1
  26. package/schemas/gui-helper.schema.json +1 -1
  27. package/schemas/gui-scenario.schema.json +1 -1
  28. package/schemas/map-operation.schema.json +1 -1
  29. package/schemas/operation-result.schema.json +1 -1
  30. package/schemas/probability-analysis-result.schema.json +1 -1
  31. package/schemas/probability-compare-input.schema.json +1 -1
  32. package/schemas/probability-evaluate-input.schema.json +1 -1
  33. package/schemas/probability-inspect-input.schema.json +1 -1
  34. package/schemas/probability-render-input.schema.json +1 -1
  35. package/schemas/probability-scenario-set.schema.json +1 -1
  36. package/schemas/probability-sequence-input.schema.json +1 -1
  37. package/schemas/probability-simulate-input.schema.json +1 -1
  38. package/schemas/probability-sweep-input.schema.json +1 -1
  39. package/server.json +2 -2
@@ -42,14 +42,19 @@ class BoundedEventFragmentCache {
42
42
  }
43
43
  }
44
44
  }
45
- function eventGraphCacheKey(workspaceId, projectHelpers) {
46
- return `${workspaceId}\0${projectHelpers ? 'expanded' : 'structural'}`;
45
+ function eventGraphCacheKey(workspaceId, projectHelpers, analysisMode = 'full') {
46
+ return `${workspaceId}\0${projectHelpers ? 'expanded' : 'structural'}\0${analysisMode}`;
47
+ }
48
+ function isGameBackedWorkspace(workspace) {
49
+ return workspace.roots.some(({ kind }) => kind === 'game');
47
50
  }
48
51
  const serviceState = new WeakMap();
49
52
  const EVENT_GRAPH_ARTIFACT_MAX_BYTES = 67_108_864;
50
53
  const EVENT_GRAPH_ARTIFACT_MAX_CHUNKS = 1_024;
51
54
  const EVENT_SCAN_FULL_GRAPH_RECORD_LIMIT = 100_000;
52
55
  const EVENT_SCAN_SUMMARY_SAMPLE_LIMIT = 100;
56
+ const EVENT_FOCUSED_EVIDENCE_LIMIT = 2_000;
57
+ const EVENT_FOCUSED_GRAPH_FILE_THRESHOLD = 1_000;
53
58
  const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
54
59
  const graphHashCache = new WeakMap();
55
60
  function groupedCounts(values, key) {
@@ -75,6 +80,7 @@ export function eventScanReport(graph, fullGraphRecordLimit = EVENT_SCAN_FULL_GR
75
80
  graphSummary: {
76
81
  schemaVersion: graph.schemaVersion,
77
82
  parserVersion: graph.parserVersion,
83
+ analysisMode: graph.analysisMode ?? 'full',
78
84
  workspaceId: graph.workspaceId,
79
85
  workspaceIdentity: graph.workspaceIdentity,
80
86
  revision: graph.revision,
@@ -116,7 +122,17 @@ function cachedEventGraphHash(graph, signal) {
116
122
  const cached = graphHashCache.get(graph);
117
123
  if (cached !== undefined)
118
124
  return cached;
119
- const computed = eventGraphHash(graph, signal);
125
+ const computed = graph.analysisMode === 'focused'
126
+ ? hashCanonical({
127
+ schemaVersion: graph.schemaVersion,
128
+ parserVersion: graph.parserVersion,
129
+ analysisMode: graph.analysisMode ?? 'full',
130
+ workspaceIdentity: graph.workspaceIdentity,
131
+ revision: graph.revision,
132
+ statistics: graph.statistics,
133
+ sourceHashes: graph.sourceHashes,
134
+ })
135
+ : eventGraphHash(graph, signal);
120
136
  graphHashCache.set(graph, computed);
121
137
  return computed;
122
138
  }
@@ -135,6 +151,61 @@ function sharedState(engine) {
135
151
  function engineGeneration(engine, workspaceId) {
136
152
  return engine.generation(workspaceId);
137
153
  }
154
+ function eventScanPatterns(workspace, focused = false) {
155
+ const roots = workspace.registration.roots;
156
+ const under = (values, glob) => values.map((root) => `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/${glob}`);
157
+ if (focused) {
158
+ return ['events/**/*.txt', 'common/on_actions/**/*.txt'].sort(compareCodeUnits);
159
+ }
160
+ return [
161
+ 'events/**/*.txt',
162
+ 'history/countries/**/*.txt',
163
+ ...under(roots.states, '**/*.txt'),
164
+ 'common/continuous_focus/**/*.txt',
165
+ 'common/decisions/**/*.txt',
166
+ 'common/ideas/**/*.txt',
167
+ 'common/characters/**/*.txt',
168
+ 'common/operations/**/*.txt',
169
+ 'common/raids/**/*.txt',
170
+ 'common/bop/**/*.txt',
171
+ 'common/resistance_compliance_modifiers/**/*.txt',
172
+ 'common/abilities/**/*.txt',
173
+ 'common/on_actions/**/*.txt',
174
+ 'common/special_projects/**/*.txt',
175
+ 'common/scripted_effects/**/*.txt',
176
+ ...under(roots.focus, '**/*.txt'),
177
+ ...under(roots.scriptedGui, '**/*.txt'),
178
+ ...under(roots.localisation, 'english/**/*.{yml,yaml}'),
179
+ ...under(roots.localisation, '*.{yml,yaml}'),
180
+ ].sort(compareCodeUnits);
181
+ }
182
+ async function scanEventSources(engine, workspaceId, workspace, focused = false, principal, signal) {
183
+ const patterns = eventScanPatterns(workspace, focused);
184
+ const gamePatterns = eventScanPatterns(workspace, focused);
185
+ const sourceKinds = ['mod', 'dependency', 'fixture'];
186
+ const scans = await Promise.all([
187
+ engine.scan(workspaceId, { patterns, rootKinds: sourceKinds }, principal, signal),
188
+ ...(workspace.roots.some(({ kind }) => kind === 'game')
189
+ ? [
190
+ engine.scan(workspaceId, { patterns: gamePatterns, rootKinds: ['game'] }, principal, signal),
191
+ ]
192
+ : []),
193
+ ]);
194
+ const files = scans.flatMap(({ files }) => files.map((file) => ({ ...file })));
195
+ recomputeShadowing(files);
196
+ files.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
197
+ const index = engine.indexFiles(files);
198
+ return {
199
+ workspaceId,
200
+ revision: snapshotRevision(files),
201
+ files,
202
+ index,
203
+ complete: scans.every(({ complete }) => complete) && index.complete,
204
+ skippedSourceCount: index.skippedSourceCount,
205
+ skippedSources: index.skippedSources,
206
+ diagnostics: [...scans.flatMap(({ diagnostics }) => diagnostics), ...index.diagnostics],
207
+ };
208
+ }
138
209
  function rememberGraph(state, workspaceId, graph) {
139
210
  const history = state.history.get(workspaceId) ?? new Map();
140
211
  history.delete(graph.revision);
@@ -163,6 +234,20 @@ function eventIssueDiagnostic(issue) {
163
234
  },
164
235
  };
165
236
  }
237
+ function boundedEvidence(values, limit = EVENT_FOCUSED_EVIDENCE_LIMIT) {
238
+ if (values.length <= limit)
239
+ return { values: [...values] };
240
+ return {
241
+ values: values.slice(0, limit),
242
+ inventory: { count: values.length, retainedCount: limit, truncated: true },
243
+ };
244
+ }
245
+ function boundedUnresolved(graph, selectedNodeIds) {
246
+ const relevant = selectedNodeIds === undefined
247
+ ? graph.unresolved
248
+ : graph.unresolved.filter(({ id, ownerId }) => selectedNodeIds.has(id) || (ownerId !== undefined && selectedNodeIds.has(ownerId)));
249
+ return boundedEvidence(relevant);
250
+ }
166
251
  export function eventDiagnostics(graph) {
167
252
  const existing = new Set(graph.diagnostics.map(({ code, location }) => `${code}:${location?.path ?? ''}:${location?.start.offset ?? -1}`));
168
253
  return [
@@ -617,38 +702,63 @@ export class EventChainViewer {
617
702
  // principal bypass the resolver check for a later caller.
618
703
  const workspace = this.engine.resolver.get(workspaceId, options.principal);
619
704
  const generation = engineGeneration(this.engine, workspaceId);
620
- const projectHelpers = options.projectHelpers ?? true;
621
- const cacheKey = eventGraphCacheKey(workspaceId, projectHelpers);
705
+ const requestedProjectHelpers = options.projectHelpers ?? true;
706
+ const requestedAnalysisMode = options.analysisMode ?? 'full';
707
+ const autoFocused = isGameBackedWorkspace(workspace) && requestedAnalysisMode === 'full';
708
+ const cacheAnalysisMode = autoFocused ? 'focused' : requestedAnalysisMode;
709
+ const cacheProjectHelpers = cacheAnalysisMode === 'focused' ? false : requestedProjectHelpers;
710
+ const cacheKey = eventGraphCacheKey(workspaceId, cacheProjectHelpers, cacheAnalysisMode);
622
711
  const cached = this.#state.current.get(cacheKey);
623
712
  if (options.refresh !== true && cached?.generation === generation) {
624
713
  return cached.graph;
625
714
  }
626
- const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, !projectHelpers));
715
+ const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, !cacheProjectHelpers, cacheAnalysisMode)) ??
716
+ this.#state.current.get(eventGraphCacheKey(workspaceId, requestedProjectHelpers, requestedAnalysisMode)) ??
717
+ this.#state.current.get(eventGraphCacheKey(workspaceId, !requestedProjectHelpers)) ??
718
+ this.#state.current.get(eventGraphCacheKey(workspaceId, requestedProjectHelpers));
719
+ const scanFocused = autoFocused || requestedAnalysisMode === 'focused';
627
720
  const snapshot = options.refresh !== true && sibling?.generation === generation
628
721
  ? sibling.snapshot
629
- : await this.engine.scan(workspaceId, {}, options.principal, options.signal);
722
+ : await scanEventSources(this.engine, workspaceId, workspace, scanFocused, options.principal, options.signal);
630
723
  options.signal?.throwIfAborted();
631
724
  if (cached?.snapshot.revision === snapshot.revision && cached.generation === generation) {
632
725
  return cached.graph;
633
726
  }
727
+ const buildAnalysisMode = autoFocused || snapshot.files.length > EVENT_FOCUSED_GRAPH_FILE_THRESHOLD
728
+ ? 'focused'
729
+ : 'full';
730
+ const projectHelpers = buildAnalysisMode === 'focused' ? false : requestedProjectHelpers;
634
731
  const graph = buildEventGraph(snapshot, {
635
732
  ...(options.signal === undefined ? {} : { signal: options.signal }),
636
733
  cache: this.#state.fragments,
637
734
  workspaceIdentity: workspace.workspaceIdentity,
638
735
  projectHelpers,
736
+ analysisMode: buildAnalysisMode,
639
737
  });
640
738
  this.#state.current.set(cacheKey, { generation, snapshot, graph });
641
- rememberGraph(this.#state, workspaceId, graph);
739
+ if (buildAnalysisMode === 'full')
740
+ rememberGraph(this.#state, workspaceId, graph);
642
741
  return graph;
643
742
  }
644
743
  async inspect(input) {
645
744
  input.signal?.throwIfAborted();
646
- const graph = await this.scan(input.workspaceId, {
745
+ const scannedGraph = await this.scan(input.workspaceId, {
647
746
  refresh: input.refresh ?? input.mode === 'scan',
648
- projectHelpers: input.mode !== 'scan' && input.mode !== 'roots',
747
+ // Focused routes traverse helper nodes directly and collapse only the
748
+ // selected path. Avoid materialising the full workspace projection first.
749
+ projectHelpers: input.mode !== 'scan' &&
750
+ input.mode !== 'roots' &&
751
+ input.mode !== 'trace' &&
752
+ input.mode !== 'explain_path',
753
+ ...(input.mode === 'trace' || input.mode === 'explain_path'
754
+ ? { analysisMode: 'focused' }
755
+ : {}),
649
756
  ...(input.principal === undefined ? {} : { principal: input.principal }),
650
757
  ...(input.signal === undefined ? {} : { signal: input.signal }),
651
758
  });
759
+ // Keep the service result's graph identity distinct from a prior scan while
760
+ // reusing its structural cache and avoiding a second large graph build.
761
+ const graph = input.mode === 'trace' || input.mode === 'explain_path' ? { ...scannedGraph } : scannedGraph;
652
762
  const maxDepth = input.maxDepth ?? 8;
653
763
  const maxNodes = input.maxNodes ?? 500;
654
764
  const maxEdges = input.maxEdges ?? 2_000;
@@ -671,28 +781,41 @@ export class EventChainViewer {
671
781
  report = eventScanReport(graph);
672
782
  break;
673
783
  case 'roots':
674
- report = {
675
- graphRevision: graph.revision,
676
- sourceHashes: graph.sourceHashes,
677
- roots: discoverEventRoots(graph, input.signal),
678
- unresolved: graph.unresolved,
679
- };
784
+ {
785
+ const unresolved = boundedUnresolved(graph);
786
+ report = {
787
+ graphRevision: graph.revision,
788
+ sourceHashes: graph.sourceHashes,
789
+ roots: discoverEventRoots(graph, input.signal),
790
+ unresolved: unresolved.values,
791
+ ...(unresolved.inventory === undefined
792
+ ? {}
793
+ : { unresolvedInventory: unresolved.inventory }),
794
+ };
795
+ }
680
796
  break;
681
797
  case 'trace':
682
798
  if (input.selector === undefined)
683
799
  throw new ServiceError('EVENT_SELECTOR_REQUIRED', 'Trace requires a selector');
684
- report = {
685
- graphRevision: graph.revision,
686
- sourceHashes: graph.sourceHashes,
687
- trace: traceSelectedEvents(graph, input.selector, {
800
+ {
801
+ const trace = traceSelectedEvents(graph, input.selector, {
688
802
  maxDepth,
689
803
  maxNodes,
690
804
  maxEdges,
691
805
  direction: input.direction ?? 'both',
692
806
  expandHelpers: input.expandHelpers ?? false,
693
- }, input.signal),
694
- unresolved: graph.unresolved,
695
- };
807
+ }, input.signal);
808
+ const unresolved = boundedUnresolved(graph, new Set(trace.nodes.map(({ id }) => id)));
809
+ report = {
810
+ graphRevision: graph.revision,
811
+ sourceHashes: graph.sourceHashes,
812
+ trace,
813
+ unresolved: unresolved.values,
814
+ ...(unresolved.inventory === undefined
815
+ ? {}
816
+ : { unresolvedInventory: unresolved.inventory }),
817
+ };
818
+ }
696
819
  break;
697
820
  case 'explain_path':
698
821
  if (input.from === undefined || input.to === undefined)
@@ -704,31 +827,54 @@ export class EventChainViewer {
704
827
  };
705
828
  break;
706
829
  case 'state_flow':
707
- report = {
708
- graphRevision: graph.revision,
709
- sourceHashes: graph.sourceHashes,
710
- flow: inspectEventStateFlow(graph, input.selector, input.stateSubject, input.signal),
711
- issues: lintEventGraph(graph, input.selector, input.signal).filter(({ code }) => /(?:STATE|FLAG|VARIABLE|ARRAY|TARGET|SCOPE)/u.test(code)),
712
- };
830
+ {
831
+ const unresolved = boundedUnresolved(graph);
832
+ const issues = boundedEvidence(lintEventGraph(graph, input.selector, input.signal).filter(({ code }) => /(?:STATE|FLAG|VARIABLE|ARRAY|TARGET|SCOPE)/u.test(code)));
833
+ report = {
834
+ graphRevision: graph.revision,
835
+ sourceHashes: graph.sourceHashes,
836
+ flow: inspectEventStateFlow(graph, input.selector, input.stateSubject, input.signal),
837
+ issues: issues.values,
838
+ unresolved: unresolved.values,
839
+ ...(issues.inventory === undefined ? {} : { issueInventory: issues.inventory }),
840
+ ...(unresolved.inventory === undefined
841
+ ? {}
842
+ : { unresolvedInventory: unresolved.inventory }),
843
+ };
844
+ }
713
845
  break;
714
846
  case 'lint':
715
- report = {
716
- graphRevision: graph.revision,
717
- sourceHashes: graph.sourceHashes,
718
- issues: lintEventGraph(graph, input.selector, input.signal),
719
- complete: graph.complete,
720
- unresolved: graph.unresolved,
721
- };
847
+ {
848
+ const issues = boundedEvidence(lintEventGraph(graph, input.selector, input.signal));
849
+ const unresolved = boundedUnresolved(graph);
850
+ report = {
851
+ graphRevision: graph.revision,
852
+ sourceHashes: graph.sourceHashes,
853
+ issues: issues.values,
854
+ complete: graph.complete,
855
+ unresolved: unresolved.values,
856
+ ...(issues.inventory === undefined ? {} : { issueInventory: issues.inventory }),
857
+ ...(unresolved.inventory === undefined
858
+ ? {}
859
+ : { unresolvedInventory: unresolved.inventory }),
860
+ };
861
+ }
722
862
  break;
723
863
  case 'impact':
724
864
  if (input.impactSubject === undefined)
725
865
  throw new ServiceError('EVENT_IMPACT_SUBJECT_REQUIRED', 'Impact analysis requires a subject');
726
- report = {
727
- graphRevision: graph.revision,
728
- sourceHashes: graph.sourceHashes,
729
- impact: analyzeEventImpact(graph, input.impactSubject, input.signal),
730
- unresolved: graph.unresolved,
731
- };
866
+ {
867
+ const unresolved = boundedUnresolved(graph);
868
+ report = {
869
+ graphRevision: graph.revision,
870
+ sourceHashes: graph.sourceHashes,
871
+ impact: analyzeEventImpact(graph, input.impactSubject, input.signal),
872
+ unresolved: unresolved.values,
873
+ ...(unresolved.inventory === undefined
874
+ ? {}
875
+ : { unresolvedInventory: unresolved.inventory }),
876
+ };
877
+ }
732
878
  break;
733
879
  }
734
880
  const graphHash = cachedEventGraphHash(graph, input.signal);
@@ -742,7 +888,7 @@ export class EventChainViewer {
742
888
  : undefined;
743
889
  const artifactUnresolved = projectedScan
744
890
  ? graph.unresolved.slice(0, EVENT_SCAN_SUMMARY_SAMPLE_LIMIT)
745
- : graph.unresolved;
891
+ : boundedUnresolved(graph).values;
746
892
  const reportJson = `${canonicalJson({
747
893
  schemaVersion: 'event-analysis.v1',
748
894
  graphSchemaVersion: graph.schemaVersion,
@@ -759,12 +905,12 @@ export class EventChainViewer {
759
905
  complete: graph.complete,
760
906
  filters: analysisFilters,
761
907
  unresolved: artifactUnresolved,
762
- ...(projectedScan
908
+ ...(artifactUnresolved.length !== graph.unresolved.length
763
909
  ? {
764
910
  unresolvedInventory: {
765
911
  count: graph.unresolved.length,
766
912
  retainedCount: artifactUnresolved.length,
767
- truncated: artifactUnresolved.length !== graph.unresolved.length,
913
+ truncated: true,
768
914
  },
769
915
  }
770
916
  : {}),
@@ -950,7 +1096,15 @@ export class EventChainViewer {
950
1096
  includeHtml: input.includeHtml ?? false,
951
1097
  refresh: input.refresh ?? false,
952
1098
  },
953
- unresolved: graph.unresolved,
1099
+ ...(() => {
1100
+ const unresolved = boundedUnresolved(graph, new Set(render.selectedNodeIds));
1101
+ return {
1102
+ unresolved: unresolved.values,
1103
+ ...(unresolved.inventory === undefined
1104
+ ? {}
1105
+ : { unresolvedInventory: unresolved.inventory }),
1106
+ };
1107
+ })(),
954
1108
  resources: { ...resourceNames, artifacts: resourceLinks },
955
1109
  overview: {
956
1110
  selectedNodeIds: render.selectedNodeIds,