hoi4-agent-tools 2.2.0 → 2.3.3

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 (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +30 -21
  3. package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
  4. package/dist/hoi4_agent_tools/core/engine.js +5 -0
  5. package/dist/hoi4_agent_tools/core/index.js +57 -1
  6. package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
  7. package/dist/hoi4_agent_tools/core/source/index.js +1 -0
  8. package/dist/hoi4_agent_tools/event/algorithms.js +178 -1
  9. package/dist/hoi4_agent_tools/event/graph.js +24 -9
  10. package/dist/hoi4_agent_tools/event/limits.js +5 -2
  11. package/dist/hoi4_agent_tools/event/service.js +182 -45
  12. package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
  13. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +13 -63
  14. package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
  15. package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
  16. package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
  17. package/dist/hoi4_agent_tools/probability/compare.js +178 -0
  18. package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
  19. package/dist/hoi4_agent_tools/probability/index.js +13 -0
  20. package/dist/hoi4_agent_tools/probability/model.js +5 -0
  21. package/dist/hoi4_agent_tools/probability/rational.js +145 -0
  22. package/dist/hoi4_agent_tools/probability/render.js +307 -0
  23. package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
  24. package/dist/hoi4_agent_tools/probability/service.js +1168 -0
  25. package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
  26. package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
  27. package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
  28. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
  29. package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
  30. package/dist/hoi4_agent_tools/runtime.js +109 -7
  31. package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
  32. package/dist/hoi4_agent_tools/technology/graph.js +30 -3
  33. package/dist/hoi4_agent_tools/technology/queries.js +70 -3
  34. package/dist/hoi4_agent_tools/technology/service.js +38 -2
  35. package/dist/hoi4_agent_tools/version.js +1 -1
  36. package/docs/README.md +1 -0
  37. package/docs/development.md +2 -0
  38. package/docs/events.md +2 -2
  39. package/docs/probability.md +80 -0
  40. package/docs/research/probability-adapter-evidence.md +64 -0
  41. package/docs/setup.md +2 -0
  42. package/docs/technology.md +4 -2
  43. package/examples/clients/codex-config.toml +3 -0
  44. package/examples/clients/generic.json +8 -0
  45. package/examples/probability/adaptive-event-pool.json +92 -0
  46. package/examples/probability/adaptive-event-sequence-input.json +57 -0
  47. package/examples/probability/event-mtth-scenarios.json +57 -0
  48. package/examples/probability/event-mtth-simulate-input.json +37 -0
  49. package/examples/probability/focus-route-scenarios.json +61 -0
  50. package/examples/probability/focus-route-sweep-input.json +36 -0
  51. package/package.json +19 -8
  52. package/schemas/configuration.schema.json +747 -0
  53. package/schemas/continuous-focus-palette.schema.json +325 -0
  54. package/schemas/custom-weighted-pool.schema.json +298 -0
  55. package/schemas/event-feature-manifest.schema.json +63 -0
  56. package/schemas/focus-plan.schema.json +927 -0
  57. package/schemas/focus-planning-sidecar.schema.json +407 -0
  58. package/schemas/gui-animation-source.schema.json +176 -0
  59. package/schemas/gui-helper.schema.json +301 -0
  60. package/schemas/gui-scenario.schema.json +298 -0
  61. package/schemas/map-operation.schema.json +2284 -0
  62. package/schemas/operation-result.schema.json +327 -0
  63. package/schemas/probability-analysis-result.schema.json +2211 -0
  64. package/schemas/probability-compare-input.schema.json +664 -0
  65. package/schemas/probability-evaluate-input.schema.json +665 -0
  66. package/schemas/probability-inspect-input.schema.json +97 -0
  67. package/schemas/probability-render-input.schema.json +141 -0
  68. package/schemas/probability-scenario-set.schema.json +430 -0
  69. package/schemas/probability-sequence-input.schema.json +886 -0
  70. package/schemas/probability-simulate-input.schema.json +691 -0
  71. package/schemas/probability-sweep-input.schema.json +702 -0
  72. package/server.json +3 -3
@@ -42,14 +42,16 @@ 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
47
  }
48
48
  const serviceState = new WeakMap();
49
49
  const EVENT_GRAPH_ARTIFACT_MAX_BYTES = 67_108_864;
50
50
  const EVENT_GRAPH_ARTIFACT_MAX_CHUNKS = 1_024;
51
51
  const EVENT_SCAN_FULL_GRAPH_RECORD_LIMIT = 100_000;
52
52
  const EVENT_SCAN_SUMMARY_SAMPLE_LIMIT = 100;
53
+ const EVENT_FOCUSED_EVIDENCE_LIMIT = 2_000;
54
+ const EVENT_FOCUSED_GRAPH_FILE_THRESHOLD = 1_000;
53
55
  const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
54
56
  const graphHashCache = new WeakMap();
55
57
  function groupedCounts(values, key) {
@@ -116,7 +118,16 @@ function cachedEventGraphHash(graph, signal) {
116
118
  const cached = graphHashCache.get(graph);
117
119
  if (cached !== undefined)
118
120
  return cached;
119
- const computed = eventGraphHash(graph, signal);
121
+ const computed = graph.analysisMode === 'focused'
122
+ ? hashCanonical({
123
+ schemaVersion: graph.schemaVersion,
124
+ parserVersion: graph.parserVersion,
125
+ workspaceIdentity: graph.workspaceIdentity,
126
+ revision: graph.revision,
127
+ statistics: graph.statistics,
128
+ sourceHashes: graph.sourceHashes,
129
+ })
130
+ : eventGraphHash(graph, signal);
120
131
  graphHashCache.set(graph, computed);
121
132
  return computed;
122
133
  }
@@ -135,6 +146,55 @@ function sharedState(engine) {
135
146
  function engineGeneration(engine, workspaceId) {
136
147
  return engine.generation(workspaceId);
137
148
  }
149
+ function eventScanPatterns(workspace) {
150
+ const roots = workspace.registration.roots;
151
+ const under = (values, glob) => values.map((root) => `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/${glob}`);
152
+ return [
153
+ 'events/**/*.txt',
154
+ 'history/countries/**/*.txt',
155
+ ...under(roots.states, '**/*.txt'),
156
+ 'common/continuous_focus/**/*.txt',
157
+ 'common/decisions/**/*.txt',
158
+ 'common/ideas/**/*.txt',
159
+ 'common/characters/**/*.txt',
160
+ 'common/operations/**/*.txt',
161
+ 'common/raids/**/*.txt',
162
+ 'common/bop/**/*.txt',
163
+ 'common/resistance_compliance_modifiers/**/*.txt',
164
+ 'common/abilities/**/*.txt',
165
+ 'common/on_actions/**/*.txt',
166
+ 'common/special_projects/**/*.txt',
167
+ 'common/scripted_effects/**/*.txt',
168
+ ...under(roots.focus, '**/*.txt'),
169
+ ...under(roots.scriptedGui, '**/*.txt'),
170
+ ...under(roots.localisation, 'english/**/*.{yml,yaml}'),
171
+ ...under(roots.localisation, '*.{yml,yaml}'),
172
+ ].sort(compareCodeUnits);
173
+ }
174
+ async function scanEventSources(engine, workspaceId, workspace, principal, signal) {
175
+ const patterns = eventScanPatterns(workspace);
176
+ const sourceKinds = ['mod', 'dependency', 'fixture'];
177
+ const scans = await Promise.all([
178
+ engine.scan(workspaceId, { patterns, rootKinds: sourceKinds }, principal, signal),
179
+ ...(workspace.roots.some(({ kind }) => kind === 'game')
180
+ ? [engine.scan(workspaceId, { patterns, rootKinds: ['game'] }, principal, signal)]
181
+ : []),
182
+ ]);
183
+ const files = scans.flatMap(({ files }) => files.map((file) => ({ ...file })));
184
+ recomputeShadowing(files);
185
+ files.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
186
+ const index = engine.indexFiles(files);
187
+ return {
188
+ workspaceId,
189
+ revision: snapshotRevision(files),
190
+ files,
191
+ index,
192
+ complete: scans.every(({ complete }) => complete) && index.complete,
193
+ skippedSourceCount: index.skippedSourceCount,
194
+ skippedSources: index.skippedSources,
195
+ diagnostics: [...scans.flatMap(({ diagnostics }) => diagnostics), ...index.diagnostics],
196
+ };
197
+ }
138
198
  function rememberGraph(state, workspaceId, graph) {
139
199
  const history = state.history.get(workspaceId) ?? new Map();
140
200
  history.delete(graph.revision);
@@ -163,6 +223,20 @@ function eventIssueDiagnostic(issue) {
163
223
  },
164
224
  };
165
225
  }
226
+ function boundedEvidence(values, limit = EVENT_FOCUSED_EVIDENCE_LIMIT) {
227
+ if (values.length <= limit)
228
+ return { values: [...values] };
229
+ return {
230
+ values: values.slice(0, limit),
231
+ inventory: { count: values.length, retainedCount: limit, truncated: true },
232
+ };
233
+ }
234
+ function boundedUnresolved(graph, selectedNodeIds) {
235
+ const relevant = selectedNodeIds === undefined
236
+ ? graph.unresolved
237
+ : graph.unresolved.filter(({ id, ownerId }) => selectedNodeIds.has(id) || (ownerId !== undefined && selectedNodeIds.has(ownerId)));
238
+ return boundedEvidence(relevant);
239
+ }
166
240
  export function eventDiagnostics(graph) {
167
241
  const existing = new Set(graph.diagnostics.map(({ code, location }) => `${code}:${location?.path ?? ''}:${location?.start.offset ?? -1}`));
168
242
  return [
@@ -618,37 +692,56 @@ export class EventChainViewer {
618
692
  const workspace = this.engine.resolver.get(workspaceId, options.principal);
619
693
  const generation = engineGeneration(this.engine, workspaceId);
620
694
  const projectHelpers = options.projectHelpers ?? true;
621
- const cacheKey = eventGraphCacheKey(workspaceId, projectHelpers);
695
+ const analysisMode = options.analysisMode ?? 'full';
696
+ const cacheKey = eventGraphCacheKey(workspaceId, projectHelpers, analysisMode);
622
697
  const cached = this.#state.current.get(cacheKey);
623
698
  if (options.refresh !== true && cached?.generation === generation) {
624
699
  return cached.graph;
625
700
  }
626
- const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, !projectHelpers));
701
+ const sibling = this.#state.current.get(eventGraphCacheKey(workspaceId, !projectHelpers, analysisMode)) ??
702
+ this.#state.current.get(eventGraphCacheKey(workspaceId, !projectHelpers)) ??
703
+ this.#state.current.get(eventGraphCacheKey(workspaceId, projectHelpers));
627
704
  const snapshot = options.refresh !== true && sibling?.generation === generation
628
705
  ? sibling.snapshot
629
- : await this.engine.scan(workspaceId, {}, options.principal, options.signal);
706
+ : await scanEventSources(this.engine, workspaceId, workspace, options.principal, options.signal);
630
707
  options.signal?.throwIfAborted();
631
708
  if (cached?.snapshot.revision === snapshot.revision && cached.generation === generation) {
632
709
  return cached.graph;
633
710
  }
711
+ const buildAnalysisMode = analysisMode === 'focused' && snapshot.files.length > EVENT_FOCUSED_GRAPH_FILE_THRESHOLD
712
+ ? 'focused'
713
+ : 'full';
634
714
  const graph = buildEventGraph(snapshot, {
635
715
  ...(options.signal === undefined ? {} : { signal: options.signal }),
636
716
  cache: this.#state.fragments,
637
717
  workspaceIdentity: workspace.workspaceIdentity,
638
718
  projectHelpers,
719
+ analysisMode: buildAnalysisMode,
639
720
  });
640
721
  this.#state.current.set(cacheKey, { generation, snapshot, graph });
641
- rememberGraph(this.#state, workspaceId, graph);
722
+ if (analysisMode === 'full')
723
+ rememberGraph(this.#state, workspaceId, graph);
642
724
  return graph;
643
725
  }
644
726
  async inspect(input) {
645
727
  input.signal?.throwIfAborted();
646
- const graph = await this.scan(input.workspaceId, {
728
+ const scannedGraph = await this.scan(input.workspaceId, {
647
729
  refresh: input.refresh ?? input.mode === 'scan',
648
- projectHelpers: input.mode !== 'scan' && input.mode !== 'roots',
730
+ // Focused routes traverse helper nodes directly and collapse only the
731
+ // selected path. Avoid materialising the full workspace projection first.
732
+ projectHelpers: input.mode !== 'scan' &&
733
+ input.mode !== 'roots' &&
734
+ input.mode !== 'trace' &&
735
+ input.mode !== 'explain_path',
736
+ ...(input.mode === 'trace' || input.mode === 'explain_path'
737
+ ? { analysisMode: 'focused' }
738
+ : {}),
649
739
  ...(input.principal === undefined ? {} : { principal: input.principal }),
650
740
  ...(input.signal === undefined ? {} : { signal: input.signal }),
651
741
  });
742
+ // Keep the service result's graph identity distinct from a prior scan while
743
+ // reusing its structural cache and avoiding a second large graph build.
744
+ const graph = input.mode === 'trace' || input.mode === 'explain_path' ? { ...scannedGraph } : scannedGraph;
652
745
  const maxDepth = input.maxDepth ?? 8;
653
746
  const maxNodes = input.maxNodes ?? 500;
654
747
  const maxEdges = input.maxEdges ?? 2_000;
@@ -671,28 +764,41 @@ export class EventChainViewer {
671
764
  report = eventScanReport(graph);
672
765
  break;
673
766
  case 'roots':
674
- report = {
675
- graphRevision: graph.revision,
676
- sourceHashes: graph.sourceHashes,
677
- roots: discoverEventRoots(graph, input.signal),
678
- unresolved: graph.unresolved,
679
- };
767
+ {
768
+ const unresolved = boundedUnresolved(graph);
769
+ report = {
770
+ graphRevision: graph.revision,
771
+ sourceHashes: graph.sourceHashes,
772
+ roots: discoverEventRoots(graph, input.signal),
773
+ unresolved: unresolved.values,
774
+ ...(unresolved.inventory === undefined
775
+ ? {}
776
+ : { unresolvedInventory: unresolved.inventory }),
777
+ };
778
+ }
680
779
  break;
681
780
  case 'trace':
682
781
  if (input.selector === undefined)
683
782
  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, {
783
+ {
784
+ const trace = traceSelectedEvents(graph, input.selector, {
688
785
  maxDepth,
689
786
  maxNodes,
690
787
  maxEdges,
691
788
  direction: input.direction ?? 'both',
692
789
  expandHelpers: input.expandHelpers ?? false,
693
- }, input.signal),
694
- unresolved: graph.unresolved,
695
- };
790
+ }, input.signal);
791
+ const unresolved = boundedUnresolved(graph, new Set(trace.nodes.map(({ id }) => id)));
792
+ report = {
793
+ graphRevision: graph.revision,
794
+ sourceHashes: graph.sourceHashes,
795
+ trace,
796
+ unresolved: unresolved.values,
797
+ ...(unresolved.inventory === undefined
798
+ ? {}
799
+ : { unresolvedInventory: unresolved.inventory }),
800
+ };
801
+ }
696
802
  break;
697
803
  case 'explain_path':
698
804
  if (input.from === undefined || input.to === undefined)
@@ -704,31 +810,54 @@ export class EventChainViewer {
704
810
  };
705
811
  break;
706
812
  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
- };
813
+ {
814
+ const unresolved = boundedUnresolved(graph);
815
+ const issues = boundedEvidence(lintEventGraph(graph, input.selector, input.signal).filter(({ code }) => /(?:STATE|FLAG|VARIABLE|ARRAY|TARGET|SCOPE)/u.test(code)));
816
+ report = {
817
+ graphRevision: graph.revision,
818
+ sourceHashes: graph.sourceHashes,
819
+ flow: inspectEventStateFlow(graph, input.selector, input.stateSubject, input.signal),
820
+ issues: issues.values,
821
+ unresolved: unresolved.values,
822
+ ...(issues.inventory === undefined ? {} : { issueInventory: issues.inventory }),
823
+ ...(unresolved.inventory === undefined
824
+ ? {}
825
+ : { unresolvedInventory: unresolved.inventory }),
826
+ };
827
+ }
713
828
  break;
714
829
  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
- };
830
+ {
831
+ const issues = boundedEvidence(lintEventGraph(graph, input.selector, input.signal));
832
+ const unresolved = boundedUnresolved(graph);
833
+ report = {
834
+ graphRevision: graph.revision,
835
+ sourceHashes: graph.sourceHashes,
836
+ issues: issues.values,
837
+ complete: graph.complete,
838
+ unresolved: unresolved.values,
839
+ ...(issues.inventory === undefined ? {} : { issueInventory: issues.inventory }),
840
+ ...(unresolved.inventory === undefined
841
+ ? {}
842
+ : { unresolvedInventory: unresolved.inventory }),
843
+ };
844
+ }
722
845
  break;
723
846
  case 'impact':
724
847
  if (input.impactSubject === undefined)
725
848
  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
- };
849
+ {
850
+ const unresolved = boundedUnresolved(graph);
851
+ report = {
852
+ graphRevision: graph.revision,
853
+ sourceHashes: graph.sourceHashes,
854
+ impact: analyzeEventImpact(graph, input.impactSubject, input.signal),
855
+ unresolved: unresolved.values,
856
+ ...(unresolved.inventory === undefined
857
+ ? {}
858
+ : { unresolvedInventory: unresolved.inventory }),
859
+ };
860
+ }
732
861
  break;
733
862
  }
734
863
  const graphHash = cachedEventGraphHash(graph, input.signal);
@@ -742,7 +871,7 @@ export class EventChainViewer {
742
871
  : undefined;
743
872
  const artifactUnresolved = projectedScan
744
873
  ? graph.unresolved.slice(0, EVENT_SCAN_SUMMARY_SAMPLE_LIMIT)
745
- : graph.unresolved;
874
+ : boundedUnresolved(graph).values;
746
875
  const reportJson = `${canonicalJson({
747
876
  schemaVersion: 'event-analysis.v1',
748
877
  graphSchemaVersion: graph.schemaVersion,
@@ -759,12 +888,12 @@ export class EventChainViewer {
759
888
  complete: graph.complete,
760
889
  filters: analysisFilters,
761
890
  unresolved: artifactUnresolved,
762
- ...(projectedScan
891
+ ...(artifactUnresolved.length !== graph.unresolved.length
763
892
  ? {
764
893
  unresolvedInventory: {
765
894
  count: graph.unresolved.length,
766
895
  retainedCount: artifactUnresolved.length,
767
- truncated: artifactUnresolved.length !== graph.unresolved.length,
896
+ truncated: true,
768
897
  },
769
898
  }
770
899
  : {}),
@@ -950,7 +1079,15 @@ export class EventChainViewer {
950
1079
  includeHtml: input.includeHtml ?? false,
951
1080
  refresh: input.refresh ?? false,
952
1081
  },
953
- unresolved: graph.unresolved,
1082
+ ...(() => {
1083
+ const unresolved = boundedUnresolved(graph, new Set(render.selectedNodeIds));
1084
+ return {
1085
+ unresolved: unresolved.values,
1086
+ ...(unresolved.inventory === undefined
1087
+ ? {}
1088
+ : { unresolvedInventory: unresolved.inventory }),
1089
+ };
1090
+ })(),
954
1091
  resources: { ...resourceNames, artifacts: resourceLinks },
955
1092
  overview: {
956
1093
  selectedNodeIds: render.selectedNodeIds,
@@ -5,8 +5,9 @@ import { registerGuiTools } from '../tools/gui.js';
5
5
  import { registerMapTools } from '../tools/map.js';
6
6
  import { registerEventTools } from '../tools/event.js';
7
7
  import { registerTechnologyTools } from '../tools/technology.js';
8
+ import { registerProbabilityTools } from '../tools/probability.js';
8
9
  import { registerMcpResources } from '../resources/register.js';
9
- export const SERVER_INSTRUCTIONS = 'Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Use hoi4.event_inspect, hoi4.event_render, and hoi4.event_compare for event chains. Use hoi4.tech_inspect, hoi4.tech_render, and hoi4.tech_compare for technology and doctrines. Event tools are read-only. Technology tools are read-only. Large evidence is linked as resources.';
10
+ 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 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. Large evidence is linked as resources.';
10
11
  export function createMcpServer(engine, context = {}) {
11
12
  const server = new McpServer({
12
13
  name: PACKAGE_NAME,
@@ -21,6 +22,7 @@ export function createMcpServer(engine, context = {}) {
21
22
  registerMapTools(server, engine, context);
22
23
  registerEventTools(server, engine, context);
23
24
  registerTechnologyTools(server, engine, context);
25
+ registerProbabilityTools(server, engine, context);
24
26
  registerMcpResources(server, engine, context);
25
27
  return server;
26
28
  }
@@ -14,7 +14,6 @@ import { compactValidatedInputSchema } from '../server/context-schemas.js';
14
14
  import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
15
15
  import { progressReporter } from '../server/progress.js';
16
16
  import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
17
- import { computeFocusVisualRevisions } from './focus.js';
18
17
  const countryTagSchema = z
19
18
  .string()
20
19
  .regex(/^[A-Z0-9]{3}$/u)
@@ -43,12 +42,6 @@ const chaosxCountryAssetsOutput = strictOperationResultSchema(z
43
42
  artifactCount: nonNegativeIntegerSchema,
44
43
  })
45
44
  .strict());
46
- const visualFocusSelectorSchema = z
47
- .object({
48
- relativePath: z.string().min(1).max(1024),
49
- treeId: z.string().min(1).max(256),
50
- })
51
- .strict();
52
45
  const visualGuiSelectorSchema = z
53
46
  .object({
54
47
  windowName: z.string().min(1).max(256),
@@ -58,27 +51,20 @@ const visualGuiSelectorSchema = z
58
51
  const chaosxVisualRevisionInput = compactValidatedInputSchema(z
59
52
  .object({
60
53
  workspaceId: workspaceIdSchema,
61
- focusTrees: z.array(visualFocusSelectorSchema).max(3).default([]),
62
- guiWindows: z.array(visualGuiSelectorSchema).max(3).default([]),
63
- countryTags: z.array(countryTagSchema).max(12).default([]),
64
- eventId: z.number().int().min(0).max(999).optional(),
54
+ guiWindows: z.array(visualGuiSelectorSchema).min(1).max(3),
65
55
  })
66
- .strict(), 'ChaosX-only selectors for fast visual cache revision checks');
56
+ .strict(), 'ChaosX-only scripted-GUI selectors for fast cache revision checks');
67
57
  const visualRevisionEntrySchema = z
68
58
  .object({
69
- relativePath: z.string().min(1).max(1024).optional(),
70
- treeId: z.string().min(1).max(256).optional(),
71
- windowName: z.string().min(1).max(256).optional(),
72
- guiId: z.string().min(1).max(256).optional(),
59
+ windowName: z.string().min(1).max(256),
60
+ guiId: z.string().min(1).max(256),
73
61
  revision: sha256Schema,
74
62
  })
75
63
  .strict();
76
64
  const chaosxVisualRevisionOutput = strictOperationResultSchema(z
77
65
  .object({
78
66
  workspaceRevision: sha256Schema,
79
- focusRevisions: z.array(visualRevisionEntrySchema).max(3),
80
67
  guiRevisions: z.array(visualRevisionEntrySchema).max(3),
81
- countryRevision: sha256Schema,
82
68
  dependencyFileCount: nonNegativeIntegerSchema,
83
69
  })
84
70
  .strict());
@@ -344,35 +330,12 @@ async function resolveGuiRevision(engine, context, studio, cache, workspaceId, s
344
330
  filesScanned: Object.keys(sourceHashes).sort(compareCodeUnits),
345
331
  };
346
332
  }
347
- function countryRevision(snapshot, selections, catalog) {
348
- const sourceHashes = {};
349
- for (const selection of selections) {
350
- for (const sourcePath of [selection.discoverySourcePath, selection.spriteSourcePath]) {
351
- if (sourcePath === undefined)
352
- continue;
353
- const source = snapshot.files.find(({ displayPath }) => displayPath === sourcePath);
354
- if (source !== undefined)
355
- sourceHashes[source.displayPath] = source.sha256;
356
- }
357
- for (const assetPath of [selection.flagPath, selection.leaderTexturePath]) {
358
- if (assetPath === undefined)
359
- continue;
360
- const source = catalog.resolveFile(assetPath);
361
- if (source !== undefined)
362
- sourceHashes[source.displayPath] = source.sha256;
363
- }
364
- }
365
- return {
366
- revision: hashCanonical({ selections, sourceHashes }),
367
- filesScanned: Object.keys(sourceHashes).sort(compareCodeUnits),
368
- };
369
- }
370
333
  export function registerChaosxTools(server, engine, context) {
371
334
  const guiStudio = new ScriptedGuiStudio(engine);
372
335
  const guiRevisionCache = new Map();
373
336
  server.registerTool('chaosx.visual_revision', {
374
337
  title: 'Check ChaosX visual revisions',
375
- description: 'Private ChaosX cache-coherency endpoint. Computes exact source revisions for event, focus, GUI, flag, and leader visuals without rendering PNG artifacts.',
338
+ description: 'Private ChaosX cache-coherency endpoint. Computes exact scripted-GUI source revisions without rendering PNG artifacts.',
376
339
  inputSchema: chaosxVisualRevisionInput,
377
340
  outputSchema: chaosxVisualRevisionOutput,
378
341
  annotations: readOnly,
@@ -380,41 +343,28 @@ export function registerChaosxTools(server, engine, context) {
380
343
  const workspaceId = engine.resolver.resolveWorkspaceId(input.workspaceId, context.principal);
381
344
  try {
382
345
  const progress = progressReporter(extra);
383
- await progress.report(0, 4, 'Scanning ChaosX visual sources');
384
- const snapshot = await engine.scan(workspaceId, {}, context.principal, progress.signal);
385
- const focusRevisions = await computeFocusVisualRevisions(engine, context, workspaceId, snapshot, input.focusTrees, progress.signal);
386
- await progress.report(1, 4, 'Checking ChaosX GUI sources');
346
+ await progress.report(0, 2, 'Checking ChaosX scripted-GUI sources');
387
347
  const resolvedGuiRevisions = [];
388
348
  for (const selector of input.guiWindows)
389
349
  resolvedGuiRevisions.push(await resolveGuiRevision(engine, context, guiStudio, guiRevisionCache, workspaceId, selector, progress.signal));
350
+ const filesScanned = new Set(resolvedGuiRevisions.flatMap(({ filesScanned }) => filesScanned));
390
351
  const guiRevisions = resolvedGuiRevisions.map(({ windowName, guiId, revision }) => ({
391
352
  windowName,
392
353
  guiId,
393
354
  revision,
394
355
  }));
395
- await progress.report(2, 4, 'Checking ChaosX country artwork');
396
- const tags = [...new Set(input.countryTags)];
397
- const { selections, catalog } = await selectCountryAssets(engine, snapshot, workspaceId, tags, input.eventId, context, progress.signal);
398
- const countries = countryRevision(snapshot, selections, catalog);
399
- const filesScanned = new Set([
400
- ...focusRevisions.flatMap(({ filesScanned }) => filesScanned),
401
- ...resolvedGuiRevisions.flatMap(({ filesScanned }) => filesScanned),
402
- ...countries.filesScanned,
403
- ]);
356
+ const workspaceRevision = hashCanonical(resolvedGuiRevisions.map(({ windowName, workspaceRevision: revision }) => ({
357
+ windowName,
358
+ revision,
359
+ })));
404
360
  const result = emptyServiceResult(workspaceId, {
405
- workspaceRevision: snapshot.revision,
406
- focusRevisions: focusRevisions.map(({ relativePath, treeId, revision }) => ({
407
- relativePath,
408
- treeId,
409
- revision,
410
- })),
361
+ workspaceRevision,
411
362
  guiRevisions,
412
- countryRevision: countries.revision,
413
363
  dependencyFileCount: filesScanned.size,
414
364
  });
415
365
  result.code = 'CHAOSX_VISUAL_REVISION_CHECKED';
416
366
  setInlineFilesScanned(result, [...filesScanned].sort(compareCodeUnits));
417
- await progress.report(4, 4, 'ChaosX visual revisions complete');
367
+ await progress.report(2, 2, 'ChaosX scripted-GUI revisions complete');
418
368
  return toolResult(result);
419
369
  }
420
370
  catch (error) {