hoi4-agent-tools 3.10.0 → 4.0.1

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 (163) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +91 -147
  3. package/dist/bin/http.js +10 -0
  4. package/dist/bin/setup.js +3 -2
  5. package/dist/bin/stdio.js +10 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +40 -5
  7. package/dist/hoi4_agent_tools/core/canonical.js +18 -4
  8. package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -0
  9. package/dist/hoi4_agent_tools/core/configuration.js +7 -0
  10. package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
  11. package/dist/hoi4_agent_tools/core/domain-tools.js +65 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +5 -2
  13. package/dist/hoi4_agent_tools/core/engine.js +29 -2
  14. package/dist/hoi4_agent_tools/core/index.js +2 -2
  15. package/dist/hoi4_agent_tools/core/job-executor.js +14 -1
  16. package/dist/hoi4_agent_tools/core/job-operation-registration.js +15 -7
  17. package/dist/hoi4_agent_tools/core/job-store.js +78 -5
  18. package/dist/hoi4_agent_tools/core/job-transactions.js +31 -2
  19. package/dist/hoi4_agent_tools/core/job-worker-host.js +314 -127
  20. package/dist/hoi4_agent_tools/core/job-worker.js +96 -37
  21. package/dist/hoi4_agent_tools/core/operation-result.js +123 -10
  22. package/dist/hoi4_agent_tools/core/operation-tasks.js +49 -89
  23. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +25 -21
  24. package/dist/hoi4_agent_tools/core/server-registry.js +22 -5
  25. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
  26. package/dist/hoi4_agent_tools/core/source/cache.js +20 -10
  27. package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
  28. package/dist/hoi4_agent_tools/core/temporary-directories.js +293 -0
  29. package/dist/hoi4_agent_tools/core/transactions.js +114 -23
  30. package/dist/hoi4_agent_tools/core/worker-exit.js +80 -0
  31. package/dist/hoi4_agent_tools/core/workspace.js +84 -8
  32. package/dist/hoi4_agent_tools/decision/ai.js +1 -1
  33. package/dist/hoi4_agent_tools/decision/service.js +28 -0
  34. package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
  35. package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
  36. package/dist/hoi4_agent_tools/event/operations.js +14 -1
  37. package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
  38. package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
  39. package/dist/hoi4_agent_tools/gui/assets.js +3 -0
  40. package/dist/hoi4_agent_tools/gui/dds.js +8 -4
  41. package/dist/hoi4_agent_tools/gui/layout.js +45 -5
  42. package/dist/hoi4_agent_tools/gui/native-state.js +244 -0
  43. package/dist/hoi4_agent_tools/gui/renderer.js +12 -7
  44. package/dist/hoi4_agent_tools/gui/source-graph.js +17 -4
  45. package/dist/hoi4_agent_tools/gui/studio.js +33 -21
  46. package/dist/hoi4_agent_tools/gui/validators.js +8 -6
  47. package/dist/hoi4_agent_tools/map/bmp.js +1 -1
  48. package/dist/hoi4_agent_tools/map/derived.js +748 -0
  49. package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
  50. package/dist/hoi4_agent_tools/map/model.js +103 -0
  51. package/dist/hoi4_agent_tools/map/operations.js +427 -58
  52. package/dist/hoi4_agent_tools/map/references.js +483 -0
  53. package/dist/hoi4_agent_tools/map/service.js +136 -61
  54. package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
  55. package/dist/hoi4_agent_tools/map/validation.js +3 -0
  56. package/dist/hoi4_agent_tools/map/world.js +1280 -0
  57. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +2 -2
  58. package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
  59. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
  60. package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
  61. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  62. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
  63. package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
  64. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +28 -8
  65. package/dist/hoi4_agent_tools/mcp/tools/event.js +19 -30
  66. package/dist/hoi4_agent_tools/mcp/tools/focus.js +5 -13
  67. package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -6
  68. package/dist/hoi4_agent_tools/mcp/tools/job.js +4 -4
  69. package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -18
  70. package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
  71. package/dist/hoi4_agent_tools/mcp/tools/reference.js +26 -9
  72. package/dist/hoi4_agent_tools/mcp/tools/technology.js +17 -30
  73. package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
  74. package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +31 -8
  75. package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
  76. package/dist/hoi4_agent_tools/probability/operations.js +175 -13
  77. package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
  78. package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
  79. package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
  80. package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
  81. package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
  82. package/dist/hoi4_agent_tools/reference/operations.js +88 -19
  83. package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
  84. package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
  85. package/dist/hoi4_agent_tools/reference/service.js +6 -0
  86. package/dist/hoi4_agent_tools/runtime.js +14 -1
  87. package/dist/hoi4_agent_tools/schemas/event.js +78 -19
  88. package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
  89. package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
  90. package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
  91. package/dist/hoi4_agent_tools/schemas/map.js +122 -3
  92. package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
  93. package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
  94. package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
  95. package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
  96. package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
  97. package/dist/hoi4_agent_tools/schemas/technology.js +79 -21
  98. package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
  99. package/dist/hoi4_agent_tools/technology/operations.js +14 -1
  100. package/dist/hoi4_agent_tools/version.js +1 -1
  101. package/docs/README.md +7 -4
  102. package/docs/analysis.md +15 -2
  103. package/docs/configuration.md +281 -0
  104. package/docs/development.md +26 -2
  105. package/docs/events.md +29 -6
  106. package/docs/examples.md +13 -28
  107. package/docs/focus.md +127 -5
  108. package/docs/gui.md +105 -21
  109. package/docs/helper-expansion.md +9 -9
  110. package/docs/http.md +3 -1
  111. package/docs/jobs.md +23 -14
  112. package/docs/map.md +199 -7
  113. package/docs/mechanics.md +6 -2
  114. package/docs/probability.md +46 -16
  115. package/docs/reference.md +73 -5
  116. package/docs/setup.md +45 -14
  117. package/docs/technology.md +28 -4
  118. package/docs/third-party-notices.md +11 -0
  119. package/docs/tools.md +150 -42
  120. package/docs/troubleshooting.md +61 -0
  121. package/examples/clients/codex-config.toml +5 -2
  122. package/examples/clients/generic.json +2 -2
  123. package/examples/config/config.json +7 -0
  124. package/npm-shrinkwrap.json +151 -156
  125. package/package.json +13 -8
  126. package/schemas/configuration.schema.json +144 -128
  127. package/schemas/continuous-focus-palette.schema.json +1 -1
  128. package/schemas/custom-weighted-pool.schema.json +1 -1
  129. package/schemas/decision-inspect-input.schema.json +1 -1
  130. package/schemas/error-log-input.schema.json +55 -0
  131. package/schemas/event-feature-manifest.schema.json +1 -1
  132. package/schemas/focus-plan.schema.json +1 -1
  133. package/schemas/focus-planning-sidecar.schema.json +1 -1
  134. package/schemas/gui-animation-source.schema.json +1 -1
  135. package/schemas/gui-helper.schema.json +1 -1
  136. package/schemas/gui-scenario.schema.json +1 -1
  137. package/schemas/helper-expansion-request.schema.json +1 -1
  138. package/schemas/helper-expansion-summary.schema.json +1 -1
  139. package/schemas/impact-inspect-input.schema.json +1 -1
  140. package/schemas/map-operation.schema.json +597 -126
  141. package/schemas/mechanic-test-input.schema.json +1 -1
  142. package/schemas/mod-index-input.schema.json +51 -0
  143. package/schemas/operation-result.schema.json +2 -2
  144. package/schemas/package-check-input.schema.json +1 -1
  145. package/schemas/package-manifest.schema.json +1 -1
  146. package/schemas/probability-analysis-result.schema.json +1 -1
  147. package/schemas/probability-compare-input.schema.json +1 -1
  148. package/schemas/probability-evaluate-input.schema.json +1 -1
  149. package/schemas/probability-inspect-input.schema.json +1 -1
  150. package/schemas/probability-render-input.schema.json +1 -1
  151. package/schemas/probability-scenario-set.schema.json +1 -1
  152. package/schemas/probability-sequence-input.schema.json +1 -1
  153. package/schemas/probability-simulate-input.schema.json +1 -1
  154. package/schemas/probability-sweep-input.schema.json +1 -1
  155. package/schemas/reference-context-input.schema.json +1 -1
  156. package/schemas/reference-read-input.schema.json +1 -1
  157. package/schemas/reference-search-input.schema.json +1 -1
  158. package/schemas/scenario-suite.schema.json +3 -5
  159. package/schemas/scenario-test-input.schema.json +3 -5
  160. package/schemas/script-validate-input.schema.json +1 -1
  161. package/schemas/shared-scenario.schema.json +1 -1
  162. package/schemas/source-lookup-input.schema.json +1 -1
  163. package/server.json +3 -3
@@ -8,6 +8,9 @@ import { ReferenceService } from './service.js';
8
8
  import { sourceLookup } from './source-lookup.js';
9
9
  import { scriptValidateDataSchema, scriptValidateRequestSchema, } from '../schemas/script-validation.js';
10
10
  import { validateScript } from './script-validation.js';
11
+ import { readErrorLog } from './error-log.js';
12
+ import { modIndex } from './mod-index.js';
13
+ import { errorLogDataSchema, errorLogRequestSchema, modIndexDataSchema, modIndexRequestSchema, } from '../schemas/mod-tools.js';
11
14
  import { eventPopupDefinitions } from './event-popups.js';
12
15
  import { locatePatterns, selectFile } from './source-locate.js';
13
16
  /**
@@ -43,6 +46,8 @@ const schemas = {
43
46
  'hoi4.reference_context': referenceContextRequestSchema,
44
47
  'hoi4.source_lookup': sourceLookupRequestSchema,
45
48
  'hoi4.script_validate': scriptValidateRequestSchema,
49
+ 'hoi4.error_log': errorLogRequestSchema,
50
+ 'hoi4.mod_index': modIndexRequestSchema,
46
51
  };
47
52
  export function isReferenceTool(name) {
48
53
  return Object.hasOwn(schemas, name);
@@ -69,10 +74,34 @@ export class ReferenceToolService {
69
74
  ? await this.references.read(workspace, referenceReadRequestSchema.parse(parsed), signal)
70
75
  : name === 'hoi4.reference_context'
71
76
  ? await this.references.context(workspace, referenceContextRequestSchema.parse(parsed), signal)
72
- : name === 'hoi4.script_validate'
73
- ? await this.validateScript(workspaceId, workspace, scriptValidateRequestSchema.parse(parsed), context.principal, signal)
74
- : await sourceLookup(this.engine, workspaceId, sourceLookupRequestSchema.parse(parsed), context.principal, signal);
77
+ : name === 'hoi4.error_log'
78
+ ? await readErrorLog(workspace, errorLogRequestSchema.parse(parsed), this.engine.resolver.config().gameUserRoot, signal)
79
+ : name === 'hoi4.mod_index'
80
+ ? await modIndex(this.engine, workspaceId, modIndexRequestSchema.parse(parsed), context.principal, signal)
81
+ : name === 'hoi4.script_validate'
82
+ ? await this.validateScript(workspaceId, workspace, scriptValidateRequestSchema.parse(parsed), context.principal, signal)
83
+ : await sourceLookup(this.engine, workspaceId, sourceLookupRequestSchema.parse(parsed), context.principal, signal);
75
84
  const result = emptyServiceResult(workspaceId, data);
85
+ if (name === 'hoi4.error_log') {
86
+ const log = errorLogDataSchema.parse(data);
87
+ result.code = 'ERROR_LOG_READ';
88
+ result.validation = {
89
+ passed: log.modEntries === 0,
90
+ checks: [
91
+ {
92
+ id: 'error-log-mod-entries',
93
+ passed: log.modEntries === 0,
94
+ message: log.modEntries === 0
95
+ ? 'The last game launch logged no errors located in this mod'
96
+ : `The last game launch logged ${log.modEntries} distinct errors located in this mod`,
97
+ },
98
+ ],
99
+ };
100
+ }
101
+ if (name === 'hoi4.mod_index') {
102
+ const index = modIndexDataSchema.parse(data);
103
+ result.code = index.complete ? 'MOD_INDEXED' : 'MOD_INDEXED_PARTIAL';
104
+ }
76
105
  if (name === 'hoi4.script_validate') {
77
106
  const checked = scriptValidateDataSchema.parse(data);
78
107
  result.code = checked.valid === null ? 'SCRIPT_CHECK_PARTIAL' : 'SCRIPT_CHECKED';
@@ -117,20 +146,32 @@ export class ReferenceToolService {
117
146
  #playerText = new Map();
118
147
  /** File mode reads the scanned file and the mod's scripted helper names from one scan. */
119
148
  async validateScript(workspaceId, workspace, request, principal, signal) {
120
- if (request.path === undefined)
121
- return validateScript(this.references, workspace, request, signal);
122
- const snapshot = await this.engine.scan(workspaceId, {
123
- patterns: [
124
- ...(locatePatterns(this.engine, workspaceId, request.path, principal) ?? []),
125
- 'common/scripted_effects/**/*.txt',
126
- 'common/scripted_triggers/**/*.txt',
127
- ...Object.keys(implicitTriggerSources).map((folder) => `common/${folder}/*.txt`),
128
- // One scan serves every check: the engine keeps one snapshot per workspace.
129
- ...playerTextPatterns,
130
- 'events/**/*.txt',
131
- ],
132
- }, principal, signal);
133
- const file = selectFile(snapshot, request.path);
149
+ if (request.path === undefined) {
150
+ if (request.kind !== undefined)
151
+ return validateScript(this.references, workspace, request, signal);
152
+ // A snippet without a kind is checked as an effect and as a trigger; the reading with
153
+ // fewer errors and unresolved commands is the one the author meant.
154
+ const effect = await validateScript(this.references, workspace, { ...request, kind: 'effect' }, signal);
155
+ const trigger = await validateScript(this.references, workspace, { ...request, kind: 'trigger' }, signal);
156
+ const misses = (checked) => checked.findings.filter(({ status }) => status === 'error').length * 100_000 +
157
+ checked.unresolvedCount;
158
+ return misses(trigger) < misses(effect) ? trigger : effect;
159
+ }
160
+ // Every file check of the supported families shares one scan, so checking files across
161
+ // folders reuses one snapshot and its incremental index; other folders add their own.
162
+ const scan = (extra) => this.engine.scan(workspaceId, { patterns: [...fileCheckPatterns, ...extra] }, principal, signal);
163
+ let snapshot = await scan([]);
164
+ let file;
165
+ try {
166
+ file = selectFile(snapshot, request.path);
167
+ }
168
+ catch (error) {
169
+ const extra = locatePatterns(this.engine, workspaceId, request.path, principal);
170
+ if (extra === undefined)
171
+ throw error;
172
+ snapshot = await scan(extra);
173
+ file = selectFile(snapshot, request.path);
174
+ }
134
175
  const playerText = playerTextFamily(file.relativePath)
135
176
  ? this.playerTextContext(workspaceId, snapshot)
136
177
  : undefined;
@@ -143,8 +184,26 @@ export class ReferenceToolService {
143
184
  implicitTriggers: implicitTriggerNames(snapshot),
144
185
  ...(playerText === undefined ? {} : { playerText }),
145
186
  eventPopups: this.eventPopupContext(workspaceId, snapshot),
187
+ knownMistakes: { eventNamespaces: this.eventNamespaces(workspaceId, snapshot) },
146
188
  });
147
189
  }
190
+ #eventNamespaces = new Map();
191
+ /** Namespaces declared with `add_namespace` in every active events file, cached by hash. */
192
+ eventNamespaces(workspaceId, snapshot) {
193
+ const sources = [...snapshot.index.files.values()].filter(({ relativePath, shadowedBy }) => shadowedBy === undefined && relativePath.startsWith('events/'));
194
+ const cacheKey = hashCanonical(sources.map(({ displayPath, sha256 }) => [displayPath, sha256]));
195
+ const cached = this.#eventNamespaces.get(workspaceId);
196
+ if (cached?.key === cacheKey)
197
+ return cached.namespaces;
198
+ const namespaces = new Set();
199
+ for (const file of sources)
200
+ for (const match of file.bytes
201
+ .toString('utf8')
202
+ .matchAll(/^\s*add_namespace\s*=\s*"?([\w.]+)"?/gmu))
203
+ namespaces.add(match[1]);
204
+ this.#eventNamespaces.set(workspaceId, { key: cacheKey, namespaces });
205
+ return namespaces;
206
+ }
148
207
  /** Every active event definition, cached by the hashes of the events files. */
149
208
  eventPopupContext(workspaceId, snapshot) {
150
209
  const sources = [...snapshot.index.files.values()]
@@ -170,7 +229,9 @@ export class ReferenceToolService {
170
229
  return cached.context;
171
230
  const localisation = new Map();
172
231
  for (const symbol of snapshot.index.symbols) {
173
- if (symbol.kind !== 'localisation' || !symbol.id.startsWith('l_english:'))
232
+ if (symbol.kind !== 'localisation' ||
233
+ symbol.sourceShadowed ||
234
+ !symbol.id.startsWith('l_english:'))
174
235
  continue;
175
236
  const key = symbol.id.slice('l_english:'.length);
176
237
  const previous = localisation.get(key);
@@ -216,7 +277,15 @@ export class ReferenceToolService {
216
277
  return context;
217
278
  }
218
279
  }
219
- const playerTextPatterns = [
280
+ /** The families file mode checks, the helpers and databases it resolves, and localisation. */
281
+ const fileCheckPatterns = [
282
+ 'events/**/*.txt',
283
+ 'common/decisions/**/*.txt',
284
+ 'common/national_focus/**/*.txt',
285
+ 'common/on_actions/**/*.txt',
286
+ 'common/scripted_effects/**/*.txt',
287
+ 'common/scripted_triggers/**/*.txt',
288
+ ...Object.keys(implicitTriggerSources).map((folder) => `common/${folder}/*.txt`),
220
289
  'localisation/**/*_l_english.yml',
221
290
  'common/scripted_localisation/**/*.txt',
222
291
  ];
@@ -7,7 +7,8 @@ const tooltipTriggerBlocks = {
7
7
  };
8
8
  /** Words that describe the mod's development rather than the game world. */
9
9
  const implementationWording = /\b(?:TODO|FIXME|TBD|hard-?coded|newly added|(?:has been|was|been) reworked|placeholder)\b/iu;
10
- const colourCode = /[§£]/u;
10
+ /** Colour codes; text icons (£) are legible on parchment and stay allowed. */
11
+ const colourCode = '§';
11
12
  function blockOf(entry) {
12
13
  return entry.value.type === 'block' ? entry.value : undefined;
13
14
  }
@@ -61,14 +62,14 @@ export function playerTextFindings(family, document, context) {
61
62
  add(node, 'SCRIPT_TEXT_IMPLEMENTATION_WORDING', `${key} (${where(text)}) describes development rather than the game: "${implementationWording.exec(text.value)[0]}"`);
62
63
  if (!options.colourFree)
63
64
  return;
64
- if (colourCode.test(text.value)) {
65
+ if (text.value.includes(colourCode)) {
65
66
  add(node, 'SCRIPT_NEWS_TEXT_COLOUR_CODE', `${key} (${where(text)}) uses a colour code in news or report event text`);
66
67
  return;
67
68
  }
68
69
  for (const name of scriptedTokens(text.value, context))
69
70
  for (const branchKey of context.scriptedLocalisation.get(name) ?? []) {
70
71
  const branch = context.localisation.get(branchKey);
71
- if (branch !== undefined && colourCode.test(branch.value)) {
72
+ if (branch?.value.includes(colourCode) === true) {
72
73
  add(node, 'SCRIPT_NEWS_TEXT_COLOUR_CODE', `${key} (${where(text)}) shows [${name}], whose branch ${branchKey} (${where(branch)}) uses a colour code in news or report event text`);
73
74
  return;
74
75
  }
@@ -4,6 +4,7 @@ import { ServiceError } from '../core/result.js';
4
4
  import { locationFor, parseClausewitz, } from '../core/source/index.js';
5
5
  import { repeatedPopupFindings } from './event-popups.js';
6
6
  import { playerTextFindings, playerTextPosition } from './player-text.js';
7
+ import { knownMistakeFindings } from './known-mistakes.js';
7
8
  import { editDistance } from './suggestions.js';
8
9
  import { publicReferenceSection } from './service.js';
9
10
  const MAX_COMMANDS = 256;
@@ -487,10 +488,26 @@ export async function validateScript(references, workspace, input, signal, file)
487
488
  scope: UNKNOWN_SCOPE,
488
489
  line: position.line,
489
490
  column: position.column,
490
- message: finding.message,
491
+ message: finding.message.slice(0, 500),
491
492
  suggestions: [],
492
493
  });
493
494
  }
495
+ const knownMistakes = plan.family === 'snippet' || plan.family === 'unsupported' ? undefined : file?.knownMistakes;
496
+ if (knownMistakes !== undefined)
497
+ for (const finding of knownMistakeFindings(plan.family, document, knownMistakes)) {
498
+ const position = playerTextPosition(documentPath, document.lineIndex, finding);
499
+ findings.push({
500
+ code: finding.code,
501
+ status: finding.rule.severity,
502
+ command: finding.command.slice(0, 256),
503
+ kind: 'definition',
504
+ scope: UNKNOWN_SCOPE,
505
+ line: position.line,
506
+ column: position.column,
507
+ message: finding.message.slice(0, 500),
508
+ suggestions: [],
509
+ });
510
+ }
494
511
  const unresolvedCount = findings.filter(({ status }) => status === 'unresolved').length;
495
512
  const invalid = findings.some(({ status }) => status === 'error');
496
513
  const truncated = visited > commandLimit;
@@ -513,6 +530,7 @@ export async function validateScript(references, workspace, input, signal, file)
513
530
  'declared_scope',
514
531
  ...(playerTextChecked ? ['player_text'] : []),
515
532
  ...(eventPopups === undefined ? [] : ['event_popups']),
533
+ ...(knownMistakes === undefined ? [] : ['known_mistakes']),
516
534
  ],
517
535
  parametersChecked: false,
518
536
  documentation: {
@@ -6,6 +6,7 @@ import { ServiceError } from '../core/result.js';
6
6
  import { isWithin } from '../core/workspace.js';
7
7
  import { prepareSource, verifySource, readBoundedFile, } from '../core/scanner.js';
8
8
  import { decodeSource } from '../core/source/encoding.js';
9
+ import { activeKnownMistakes } from './known-mistakes.js';
9
10
  import { readBoundedJsonLines } from './lines.js';
10
11
  import { RankingIndex, sectionTerms } from './ranking.js';
11
12
  const MAX_FILES = 512;
@@ -544,6 +545,11 @@ export class ReferenceService {
544
545
  missing,
545
546
  coverage: inventory.coverage,
546
547
  skipped: inventory.skipped,
548
+ knownMistakes: activeKnownMistakes(input.surface).map(({ id, title, fix }) => ({
549
+ id,
550
+ title,
551
+ fix,
552
+ })),
547
553
  };
548
554
  }
549
555
  publicSection(section) {
@@ -1,11 +1,12 @@
1
1
  import { lstat, readdir, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { homedir } from 'node:os';
3
+ import { homedir, tmpdir } from 'node:os';
4
4
  import { loadConfiguration, serverConfigurationSchema, } from './core/configuration.js';
5
5
  import { CoreEngine } from './core/engine.js';
6
6
  import { ServiceError } from './core/result.js';
7
7
  import { WorkspaceResolver } from './core/workspace.js';
8
8
  import { CONFIG_VERSION } from './version.js';
9
+ import { sweepTemporaryDirectories } from './core/temporary-directories.js';
9
10
  export function defaultConfigurationPath() {
10
11
  const configRoot = process.env.APPDATA ?? process.env.XDG_CONFIG_HOME ?? path.join(homedir(), '.config');
11
12
  return path.join(configRoot, 'hoi4-agent-tools', 'config.json');
@@ -236,6 +237,18 @@ export async function createEngine(configPath = configurationPath()) {
236
237
  configuration = await automaticConfiguration();
237
238
  }
238
239
  const resolver = await WorkspaceResolver.create(configuration);
240
+ await sweepTemporaryDirectories(tmpdir(), {
241
+ protectedRoots: [
242
+ ...resolver
243
+ .list()
244
+ .flatMap((workspace) => [
245
+ ...workspace.roots.map((root) => root.path),
246
+ workspace.artifactRoot,
247
+ workspace.cacheRoot,
248
+ ]),
249
+ ...(resolver.serverState() === undefined ? [] : [resolver.serverState().root]),
250
+ ],
251
+ }).catch(() => undefined);
239
252
  const engine = new CoreEngine(resolver);
240
253
  await engine.initialize();
241
254
  return engine;
@@ -112,25 +112,6 @@ export function validateEventInspectRequest(value, context) {
112
112
  message: 'Impact analysis requires impactSubject',
113
113
  });
114
114
  }
115
- export const eventInspectRequestSchema = z
116
- .object({
117
- workspaceId: workspaceIdSchema,
118
- mode: eventInspectModeSchema,
119
- selector: eventSelectorSchema.optional(),
120
- from: eventSelectorSchema.optional(),
121
- to: eventSelectorSchema.optional(),
122
- direction: eventDirectionSchema.optional(),
123
- maxDepth: z.number().int().min(1).max(64).optional(),
124
- maxNodes: z.number().int().min(1).max(5_000).optional(),
125
- maxEdges: z.number().int().min(1).max(20_000).optional(),
126
- expandHelpers: z.boolean().optional(),
127
- stateSubject: eventStateSubjectSchema.optional(),
128
- impactSubject: eventImpactSubjectSchema.optional(),
129
- helperExpansion: helperExpansionRequestSchema.optional(),
130
- refresh: z.boolean().optional(),
131
- })
132
- .strict()
133
- .superRefine(validateEventInspectRequest);
134
115
  export const eventRenderRequestSchema = z
135
116
  .object({
136
117
  workspaceId: workspaceIdSchema,
@@ -172,3 +153,81 @@ export const eventCompareRequestSchema = z
172
153
  })
173
154
  .strict()
174
155
  .superRefine(validateEventCompareRequest);
156
+ export const eventInspectToolModeSchema = z.enum([...eventInspectModeSchema.options, 'compare']);
157
+ const compareOnlyFields = [
158
+ 'before',
159
+ 'after',
160
+ 'proposedSources',
161
+ 'maxChainNodes',
162
+ 'render',
163
+ 'maxRenderNodes',
164
+ ];
165
+ const inspectOnlyFields = [
166
+ 'from',
167
+ 'to',
168
+ 'direction',
169
+ 'maxDepth',
170
+ 'maxNodes',
171
+ 'maxEdges',
172
+ 'expandHelpers',
173
+ 'stateSubject',
174
+ 'impactSubject',
175
+ 'helperExpansion',
176
+ ];
177
+ /**
178
+ * Inspection and comparison share one tool: mode compare diffs a cached, artifact or proposed
179
+ * event graph against another, every other mode analyzes the current graph.
180
+ */
181
+ export const eventInspectRequestSchema = z
182
+ .object({
183
+ workspaceId: workspaceIdSchema,
184
+ mode: eventInspectToolModeSchema,
185
+ selector: eventSelectorSchema.optional(),
186
+ from: eventSelectorSchema.optional(),
187
+ to: eventSelectorSchema.optional(),
188
+ direction: eventDirectionSchema.optional(),
189
+ maxDepth: z.number().int().min(1).max(64).optional(),
190
+ maxNodes: z.number().int().min(1).max(5_000).optional(),
191
+ maxEdges: z.number().int().min(1).max(20_000).optional(),
192
+ expandHelpers: z.boolean().optional(),
193
+ stateSubject: eventStateSubjectSchema.optional(),
194
+ impactSubject: eventImpactSubjectSchema.optional(),
195
+ helperExpansion: helperExpansionRequestSchema.optional(),
196
+ before: eventGraphReferenceSchema.optional(),
197
+ after: eventGraphReferenceSchema.optional(),
198
+ proposedSources: z.array(eventProposedSourceSchema).min(1).max(64).optional(),
199
+ maxChainNodes: z.number().int().min(1).max(5_000).optional(),
200
+ render: z.boolean().optional(),
201
+ maxRenderNodes: z.number().int().min(1).max(240).optional(),
202
+ refresh: z.boolean().optional(),
203
+ })
204
+ .strict()
205
+ .superRefine(validateEventInspectToolRequest);
206
+ export function validateEventInspectToolRequest(value, context) {
207
+ const compare = value.mode === 'compare';
208
+ for (const field of compare ? inspectOnlyFields : compareOnlyFields)
209
+ if (value[field] !== undefined)
210
+ context.addIssue({
211
+ code: 'custom',
212
+ path: [field],
213
+ message: compare
214
+ ? `${field} is not used by mode compare`
215
+ : `${field} is used only by mode compare`,
216
+ });
217
+ if (compare)
218
+ validateEventCompareRequest(value, context);
219
+ else
220
+ validateEventInspectRequest(value, context);
221
+ }
222
+ /** Split an inspect request into a comparison or an analysis of the current graph. */
223
+ export function splitEventInspectRequest(input) {
224
+ const { mode, ...rest } = input;
225
+ if (mode === 'compare') {
226
+ const request = { workspaceId: rest.workspaceId };
227
+ for (const field of [...compareOnlyFields, 'selector', 'refresh'])
228
+ if (rest[field] !== undefined)
229
+ request[field] = rest[field];
230
+ return { compare: true, request: eventCompareRequestSchema.parse(request) };
231
+ }
232
+ return { compare: false, request: { ...rest, mode } };
233
+ }
@@ -77,10 +77,20 @@ export const focusRenderRequestSchema = z
77
77
  cropFocusIds: z.array(z.string().min(1).max(256)).max(16).optional(),
78
78
  columns: z.number().int().min(1).max(12).optional(),
79
79
  padding: z.number().int().min(0).max(1000).optional(),
80
+ png: z
81
+ .boolean()
82
+ .optional()
83
+ .describe('Default true: add a PNG with decoded source icons. False returns only the faster HTML, SVG and JSON.'),
80
84
  })
81
85
  .strict()
82
86
  .superRefine((value, context) => {
83
87
  const mode = value.mode ?? 'national';
88
+ if (value.png === false && (value.cropFocusIds?.length ?? 0) > 0)
89
+ context.addIssue({
90
+ code: 'custom',
91
+ path: ['cropFocusIds'],
92
+ message: 'cropFocusIds produce PNG crops; omit png or set it to true',
93
+ });
84
94
  const invalid = mode === 'national'
85
95
  ? [
86
96
  ['paletteId', value.paletteId],