hoi4-agent-tools 3.0.9 → 3.2.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 (145) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +10 -2
  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/analysis-evidence.js +9 -0
  6. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +38 -0
  7. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  8. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  9. package/dist/hoi4_agent_tools/core/condition-evaluator.js +16 -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/impact-comparison.js +49 -0
  16. package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
  17. package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
  18. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  19. package/dist/hoi4_agent_tools/core/index.js +47 -4
  20. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  21. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  22. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  23. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  24. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  25. package/dist/hoi4_agent_tools/core/job-worker-host.js +202 -0
  26. package/dist/hoi4_agent_tools/core/job-worker.js +64 -0
  27. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  28. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  29. package/dist/hoi4_agent_tools/core/operation-tasks.js +387 -0
  30. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  31. package/dist/hoi4_agent_tools/core/result.js +1 -0
  32. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  33. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  34. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
  35. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  36. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  37. package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
  38. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  39. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  40. package/dist/hoi4_agent_tools/decision/ai.js +104 -0
  41. package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
  42. package/dist/hoi4_agent_tools/decision/cost.js +177 -0
  43. package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
  44. package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
  45. package/dist/hoi4_agent_tools/decision/service.js +172 -0
  46. package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
  47. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  48. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  49. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  50. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  51. package/dist/hoi4_agent_tools/event/service.js +67 -15
  52. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  53. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  54. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  55. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  56. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  57. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  58. package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
  59. package/dist/hoi4_agent_tools/impact/service.js +127 -0
  60. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  61. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  62. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  63. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  64. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  65. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  66. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  67. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  68. package/dist/hoi4_agent_tools/mcp/server/create.js +18 -6
  69. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  70. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  71. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  72. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  73. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  74. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +45 -0
  75. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  76. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  77. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +16 -0
  78. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  79. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  80. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +65 -0
  81. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  82. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  83. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  84. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  85. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  86. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  87. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  88. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  89. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  90. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  91. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  92. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  93. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  94. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  95. package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
  96. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  97. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  98. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  99. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  100. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  101. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  102. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  103. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  104. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  105. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  106. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  107. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  108. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  109. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  110. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  111. package/dist/hoi4_agent_tools/version.js +1 -1
  112. package/docs/README.md +3 -0
  113. package/docs/analysis.md +100 -0
  114. package/docs/development.md +2 -0
  115. package/docs/events.md +7 -2
  116. package/docs/helper-expansion.md +78 -0
  117. package/docs/http.md +2 -0
  118. package/docs/jobs.md +86 -0
  119. package/docs/technology.md +6 -2
  120. package/package.json +15 -6
  121. package/schemas/configuration.schema.json +1 -1
  122. package/schemas/continuous-focus-palette.schema.json +1 -1
  123. package/schemas/custom-weighted-pool.schema.json +1 -1
  124. package/schemas/decision-inspect-input.schema.json +290 -0
  125. package/schemas/event-feature-manifest.schema.json +1 -1
  126. package/schemas/focus-plan.schema.json +1 -1
  127. package/schemas/focus-planning-sidecar.schema.json +1 -1
  128. package/schemas/gui-animation-source.schema.json +1 -1
  129. package/schemas/gui-helper.schema.json +1 -1
  130. package/schemas/gui-scenario.schema.json +1 -1
  131. package/schemas/helper-expansion-request.schema.json +44 -0
  132. package/schemas/helper-expansion-summary.schema.json +70 -0
  133. package/schemas/impact-inspect-input.schema.json +203 -0
  134. package/schemas/map-operation.schema.json +1 -1
  135. package/schemas/operation-result.schema.json +1 -1
  136. package/schemas/probability-analysis-result.schema.json +1 -1
  137. package/schemas/probability-compare-input.schema.json +1 -1
  138. package/schemas/probability-evaluate-input.schema.json +1 -1
  139. package/schemas/probability-inspect-input.schema.json +1 -1
  140. package/schemas/probability-render-input.schema.json +1 -1
  141. package/schemas/probability-scenario-set.schema.json +1 -1
  142. package/schemas/probability-sequence-input.schema.json +1 -1
  143. package/schemas/probability-simulate-input.schema.json +1 -1
  144. package/schemas/probability-sweep-input.schema.json +1 -1
  145. package/server.json +3 -3
@@ -1,22 +1,10 @@
1
+ import { registerLegacyTaskTools } from '../server/legacy-task-tools.js';
1
2
  import { z } from 'zod/v4';
2
- import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
3
- import { compareCodeUnits, canonicalJson, hashCanonical } from '../../core/canonical.js';
4
- import { focusDomainScanPatterns } from '../../core/domain-scan-patterns.js';
5
- import { IdleCacheLifetime } from '../../core/idle-cache-lifetime.js';
6
- import { RenderBudget } from '../../core/render-budget.js';
7
- import { emptyServiceResult, ServiceError } from '../../core/result.js';
8
- import { parseClausewitz } from '../../core/source/index.js';
9
- import { readDependenciesFromScannedFiles } from '../../core/transactions.js';
10
- import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, assertCompactLayoutQuality, compactFocusTreePlanAsync, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
11
- import { continuousFocusPaletteSchema, focusLayoutMetricsSchema, focusLayoutSchema, focusTreePlanSchema, } from '../../schemas/focus.js';
12
- import { workspaceIdSchema, workspaceRelativePathSchema } from '../../schemas/common.js';
13
- import { PACKAGE_VERSION } from '../../version.js';
3
+ import { computeFocusVisualRevisions as computeCoreFocusVisualRevisions, } from '../../focus/tool-operations.js';
4
+ import { focusLayoutMetricsSchema } from '../../schemas/focus.js';
5
+ import { focusInspectRequestSchema, focusRenderRequestSchema, focusRewriteRequestSchema, } from '../../schemas/focus-requests.js';
14
6
  import { nonNegativeIntegerSchema, renderHashesSchema, sha256Schema, } from '../server/output-schemas.js';
15
- import { progressReporter } from '../server/progress.js';
16
- import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
17
- import { requireServerScope, resolveServerWorkspaceForSource, } from '../server/base-tools.js';
18
- import { compactValidatedInputSchema } from '../server/context-schemas.js';
19
- import { autonomousFailureContext, autonomousResultArtifacts, executePlannedTransaction, } from '../server/transaction-execution.js';
7
+ import { strictOperationResultSchema } from '../server/result.js';
20
8
  const focusInspectOutputSchema = strictOperationResultSchema(z
21
9
  .object({
22
10
  mode: z.enum(['national', 'continuous']),
@@ -79,7 +67,7 @@ const focusRenderOutputSchema = strictOperationResultSchema(z.discriminatedUnion
79
67
  })
80
68
  .strict(),
81
69
  ]));
82
- const focusDriftBaseOutputSchema = z
70
+ const focusDriftOutputSchema = z
83
71
  .object({
84
72
  status: z.enum([
85
73
  'clean',
@@ -95,8 +83,6 @@ const focusDriftBaseOutputSchema = z
95
83
  planChanged: z.boolean(),
96
84
  })
97
85
  .strict();
98
- const focusDriftOutputSchema = focusDriftBaseOutputSchema;
99
- const continuousFocusDriftOutputSchema = focusDriftBaseOutputSchema;
100
86
  const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('mode', [
101
87
  z
102
88
  .object({
@@ -114,7 +100,7 @@ const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('
114
100
  .object({
115
101
  mode: z.literal('continuous'),
116
102
  paletteId: z.string().max(256),
117
- drift: continuousFocusDriftOutputSchema,
103
+ drift: focusDriftOutputSchema,
118
104
  created: z.boolean(),
119
105
  execution: z.enum(['applied', 'blocked', 'unchanged']),
120
106
  fileCount: nonNegativeIntegerSchema,
@@ -122,733 +108,49 @@ const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('
122
108
  })
123
109
  .strict(),
124
110
  ]));
125
- const compactFocusPlanSchema = compactValidatedInputSchema(z.union([focusTreePlanSchema, continuousFocusPaletteSchema]), `Complete focus plan: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/focus.md`);
126
- const compactFocusLayoutSchema = compactValidatedInputSchema(focusLayoutSchema, 'Prior deterministic focus layout used to stabilize unchanged positions.');
127
- const focusInspectInput = z
128
- .object({
129
- mode: z.enum(['national', 'continuous']).optional(),
130
- workspaceId: workspaceIdSchema,
131
- relativePath: workspaceRelativePathSchema.optional(),
132
- treeId: z.string().min(1).max(256).optional(),
133
- paletteId: z.string().min(1).max(256).optional(),
134
- previous: compactFocusLayoutSchema.optional(),
135
- laneSpacing: z.number().int().min(1).max(100).optional(),
136
- nodeSpacing: z.number().int().min(1).max(100).optional(),
137
- })
138
- .strict()
139
- .superRefine((value, context) => {
140
- const mode = value.mode ?? 'national';
141
- if (mode === 'national' && value.paletteId !== undefined)
142
- context.addIssue({
143
- code: 'custom',
144
- path: ['paletteId'],
145
- message: 'paletteId is only valid in continuous mode',
146
- });
147
- if (mode === 'continuous' && value.treeId !== undefined)
148
- context.addIssue({
149
- code: 'custom',
150
- path: ['treeId'],
151
- message: 'treeId is only valid in national mode',
152
- });
153
- if (mode === 'continuous') {
154
- for (const field of ['previous', 'laneSpacing', 'nodeSpacing']) {
155
- if (value[field] !== undefined)
156
- context.addIssue({
157
- code: 'custom',
158
- path: [field],
159
- message: `${field} is only valid in national mode`,
160
- });
161
- }
162
- }
163
- if (value.previous !== undefined && value.treeId === undefined)
164
- context.addIssue({
165
- code: 'custom',
166
- path: ['previous'],
167
- message: 'previous requires treeId so it applies to exactly one tree',
168
- });
169
- });
170
- const focusRenderInput = z
171
- .object({
172
- mode: z.enum(['national', 'continuous']).optional(),
173
- workspaceId: workspaceIdSchema,
174
- relativePath: workspaceRelativePathSchema,
175
- treeId: z.string().min(1).max(256).optional(),
176
- paletteId: z.string().min(1).max(256).optional(),
177
- horizontalSpacing: z.number().int().min(80).max(1000).optional(),
178
- verticalSpacing: z.number().int().min(60).max(1000).optional(),
179
- reviewScale: z
180
- .number()
181
- .min(FOCUS_RENDER_MIN_OUTPUT_SCALE)
182
- .max(FOCUS_RENDER_MAX_OUTPUT_SCALE)
183
- .optional(),
184
- columns: z.number().int().min(1).max(12).optional(),
185
- padding: z.number().int().min(0).max(1000).optional(),
186
- })
187
- .strict()
188
- .superRefine((value, context) => {
189
- const mode = value.mode ?? 'national';
190
- const invalid = mode === 'national'
191
- ? [
192
- ['paletteId', value.paletteId],
193
- ['columns', value.columns],
194
- ]
195
- : [
196
- ['treeId', value.treeId],
197
- ['horizontalSpacing', value.horizontalSpacing],
198
- ['verticalSpacing', value.verticalSpacing],
199
- ['reviewScale', value.reviewScale],
200
- ];
201
- for (const [field, fieldValue] of invalid) {
202
- if (fieldValue !== undefined)
203
- context.addIssue({
204
- code: 'custom',
205
- path: [field],
206
- message: `${field} is not valid in ${mode} mode`,
207
- });
208
- }
209
- if (mode === 'continuous' && value.padding !== undefined && value.padding < 24)
210
- context.addIssue({
211
- code: 'custom',
212
- path: ['padding'],
213
- message: 'continuous focus padding must be at least 24 pixels',
214
- });
215
- });
216
- const focusPlanInput = z
217
- .object({
218
- mode: z.enum(['national', 'continuous']).optional(),
219
- workspaceId: workspaceIdSchema,
220
- relativePath: workspaceRelativePathSchema,
221
- treeId: z.string().min(1).max(256).optional(),
222
- layoutMode: z.enum(['authored', 'compact']).default('authored'),
223
- plan: compactFocusPlanSchema.optional(),
224
- createIfMissing: z.boolean().default(false),
225
- horizontalSpacing: z.number().int().min(80).max(1000).optional(),
226
- verticalSpacing: z.number().int().min(60).max(1000).optional(),
227
- padding: z.number().int().min(0).max(1000).optional(),
228
- reviewScale: z
229
- .number()
230
- .min(FOCUS_RENDER_MIN_OUTPUT_SCALE)
231
- .max(FOCUS_RENDER_MAX_OUTPUT_SCALE)
232
- .optional(),
233
- })
234
- .strict()
235
- .superRefine((value, context) => {
236
- const mode = value.mode ?? 'national';
237
- const schema = mode === 'continuous' ? continuousFocusPaletteSchema : focusTreePlanSchema;
238
- if (value.plan !== undefined && !schema.safeParse(value.plan).success)
239
- context.addIssue({
240
- code: 'custom',
241
- path: ['plan'],
242
- message: `plan must be a ${mode} focus plan`,
243
- });
244
- if (mode === 'continuous') {
245
- if (value.plan === undefined)
246
- context.addIssue({
247
- code: 'custom',
248
- path: ['plan'],
249
- message: 'plan is required in continuous mode',
250
- });
251
- if (value.treeId !== undefined)
252
- context.addIssue({
253
- code: 'custom',
254
- path: ['treeId'],
255
- message: 'treeId is only valid in national mode',
256
- });
257
- if (value.layoutMode !== 'authored')
258
- context.addIssue({
259
- code: 'custom',
260
- path: ['layoutMode'],
261
- message: 'compact layout mode is only valid in national mode',
262
- });
263
- for (const [field, fieldValue] of [
264
- ['horizontalSpacing', value.horizontalSpacing],
265
- ['verticalSpacing', value.verticalSpacing],
266
- ['padding', value.padding],
267
- ['reviewScale', value.reviewScale],
268
- ]) {
269
- if (fieldValue !== undefined)
270
- context.addIssue({
271
- code: 'custom',
272
- path: [field],
273
- message: `${field} is only valid for national focus review renders`,
274
- });
275
- }
276
- }
277
- else {
278
- if (value.plan === undefined && value.layoutMode !== 'compact')
279
- context.addIssue({
280
- code: 'custom',
281
- path: ['plan'],
282
- message: 'plan is required unless layoutMode is compact',
283
- });
284
- if (value.plan === undefined && value.treeId === undefined)
285
- context.addIssue({
286
- code: 'custom',
287
- path: ['treeId'],
288
- message: 'treeId is required for plan-free compact reflow',
289
- });
290
- if (value.plan === undefined && value.createIfMissing)
291
- context.addIssue({
292
- code: 'custom',
293
- path: ['createIfMissing'],
294
- message: 'plan-free compact reflow requires an existing focus source',
295
- });
296
- if (value.plan !== undefined &&
297
- value.treeId !== undefined &&
298
- 'id' in value.plan &&
299
- value.plan.id !== value.treeId)
300
- context.addIssue({
301
- code: 'custom',
302
- path: ['treeId'],
303
- message: 'treeId must match the supplied national focus plan',
304
- });
305
- }
306
- });
307
111
  const artifactProducing = {
308
112
  readOnlyHint: false,
309
113
  destructiveHint: false,
310
114
  idempotentHint: true,
311
115
  openWorldHint: false,
312
116
  };
313
- const LARGE_FOCUS_RENDER_NODE_THRESHOLD = 200;
314
- const LARGE_FOCUS_RENDER_SCALE = 0.5;
315
- function automaticFocusRenderScale(focusCount) {
316
- return focusCount >= LARGE_FOCUS_RENDER_NODE_THRESHOLD ? LARGE_FOCUS_RENDER_SCALE : 1;
317
- }
318
- export async function computeFocusVisualRevisions(engine, context, workspaceId, snapshot, selectors, signal) {
319
- const workspace = engine.resolver.get(workspaceId, context.principal);
320
- const paletteImport = continuousPalettes(snapshot);
321
- const revisions = [];
322
- for (const selector of selectors) {
323
- signal?.throwIfAborted();
324
- const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, selector.relativePath), paletteImport.palettes);
325
- const plan = selectPlan(imported, selector.treeId);
326
- const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
327
- const presentation = await resolveFocusPresentation({
328
- plans: [plan],
329
- palettes: linkedPalettes,
330
- files: snapshot.files,
331
- index: snapshot.index,
332
- scanner: engine.scanner,
333
- workspace,
334
- decodeIcons: false,
335
- budget: new RenderBudget(),
336
- ...(signal === undefined ? {} : { signal }),
337
- });
338
- const filesScanned = [
339
- ...new Set([plan.provenance.sourcePath, ...presentation.filesScanned]),
340
- ].sort(compareCodeUnits);
341
- revisions.push({
342
- relativePath: selector.relativePath,
343
- treeId: plan.id,
344
- revision: hashCanonical({
345
- planHash: focusPlanHash(plan),
346
- linkedPalettes,
347
- presentationSourceHashes: presentation.sourceHashes,
348
- }),
349
- filesScanned,
350
- });
351
- }
352
- return revisions;
353
- }
354
- async function executeFocusVisualTool(engine, workbench, context, input, extra, rasterize) {
355
- const { workspaceId: requestedWorkspaceId, relativePath } = input;
356
- const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
357
- const outputName = rasterize ? 'raster' : 'render';
358
- try {
359
- const progress = progressReporter(extra);
360
- await progress.report(0, 4, 'Importing and indexing focus source');
361
- const workspace = engine.resolver.get(workspaceId, context.principal);
362
- const snapshot = await engine.scan(workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, progress.signal);
363
- await progress.report(1, 4, 'Focus source index complete');
364
- const renderBudget = new RenderBudget();
365
- if (input.mode === 'continuous') {
366
- selectedContinuousFocusFile(snapshot, relativePath);
367
- const imported = await workbench.importContinuousPath(workspaceId, relativePath, context.principal);
368
- const plan = selectContinuousPlan(imported.result, input.paletteId);
369
- const presentation = await resolveFocusPresentation({
370
- plans: [],
371
- palettes: [plan],
372
- files: snapshot.files,
373
- index: snapshot.index,
374
- scanner: engine.scanner,
375
- workspace,
376
- decodeIcons: rasterize,
377
- budget: renderBudget,
378
- signal: progress.signal,
379
- });
380
- await progress.report(2, 4, `Producing continuous focus ${outputName} artifacts`);
381
- const rendered = await workbench.renderContinuousAndStore(workspaceId, plan, {
382
- ...(context.principal === undefined ? {} : { principal: context.principal }),
383
- presentation,
384
- sourceHashes: presentation.sourceHashes,
385
- ...(input.columns === undefined ? {} : { columns: input.columns }),
386
- ...(input.padding === undefined ? {} : { padding: input.padding }),
387
- renderProfile: { sourceRevision: snapshot.revision, output: outputName },
388
- rasterize,
389
- budget: renderBudget,
390
- signal: progress.signal,
391
- });
392
- const diagnostics = [...imported.result.diagnostics, ...rendered.diagnostics];
393
- const result = emptyServiceResult(workspaceId, {
394
- mode: 'continuous',
395
- paletteId: plan.id,
396
- focusCount: plan.focuses.length,
397
- hashes: rendered.bundle.hashes,
398
- width: rendered.bundle.width,
399
- height: rendered.bundle.height,
400
- });
401
- result.code = rasterize ? 'CONTINUOUS_FOCUS_RASTERIZED' : 'CONTINUOUS_FOCUS_RENDERED';
402
- setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
403
- result.diagnostics = diagnostics.slice(0, 100);
404
- result.artifacts = rendered.artifacts.map(publicArtifactLink);
405
- result.validation = validationFromDiagnostics(diagnostics);
406
- await progress.report(4, 4, `Continuous focus ${outputName} complete`);
407
- return toolResult(result);
408
- }
409
- const paletteImport = continuousPalettes(snapshot);
410
- const imported = importFocusFile(snapshot, selectedFocusFile(snapshot, workspace.registration.roots.focus, relativePath), paletteImport.palettes);
411
- const plan = selectPlan(imported, input.treeId);
412
- const linkedPalettes = paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id));
413
- const presentation = await resolveFocusPresentation({
414
- plans: [plan],
415
- palettes: linkedPalettes,
416
- files: snapshot.files,
417
- index: snapshot.index,
418
- scanner: engine.scanner,
419
- workspace,
420
- decodeIcons: rasterize,
421
- budget: renderBudget,
422
- signal: progress.signal,
423
- });
424
- await progress.report(2, 4, `Producing focus ${outputName} artifacts`);
425
- const effectiveReviewScale = input.reviewScale ?? automaticFocusRenderScale(plan.focuses.length);
426
- const rendered = await workbench.renderAndStore(workspaceId, plan, {
427
- ...(context.principal === undefined ? {} : { principal: context.principal }),
428
- index: snapshot.index,
429
- references: referenceCatalog(snapshot),
430
- presentation,
431
- sourceHashes: presentation.sourceHashes,
432
- ...(input.horizontalSpacing === undefined
433
- ? {}
434
- : { horizontalSpacing: input.horizontalSpacing }),
435
- ...(input.verticalSpacing === undefined ? {} : { verticalSpacing: input.verticalSpacing }),
436
- ...(input.padding === undefined ? {} : { padding: input.padding }),
437
- outputScale: effectiveReviewScale,
438
- rasterize,
439
- renderProfile: {
440
- sourceRevision: snapshot.revision,
441
- reviewScale: effectiveReviewScale,
442
- output: outputName,
443
- },
444
- budget: renderBudget,
445
- signal: progress.signal,
446
- });
447
- const diagnostics = [
448
- ...paletteImport.diagnostics,
449
- ...imported.diagnostics,
450
- ...rendered.diagnostics,
451
- ];
452
- const result = emptyServiceResult(workspaceId, {
453
- mode: 'national',
454
- treeId: plan.id,
455
- layoutHash: rendered.layout.layoutHash,
456
- hashes: rendered.bundle.hashes,
457
- width: rendered.bundle.width,
458
- height: rendered.bundle.height,
459
- });
460
- result.code = rasterize ? 'FOCUS_RASTERIZED' : 'FOCUS_RENDERED';
461
- setInlineFilesScanned(result, [...new Set([plan.provenance.sourcePath, ...presentation.filesScanned])].sort((left, right) => compareCodeUnits(left, right)));
462
- result.diagnostics = diagnostics.slice(0, 100);
463
- result.artifacts = rendered.artifacts.map(publicArtifactLink);
464
- result.validation = validationFromDiagnostics(diagnostics);
465
- await progress.report(4, 4, `Focus ${outputName} complete`);
466
- return toolResult(result);
467
- }
468
- catch (error) {
469
- return errorResult(error, workspaceId);
470
- }
471
- }
472
- function compactDrift(drift) {
473
- return {
474
- status: drift.status,
475
- sourceChanged: drift.sourceChanged,
476
- planChanged: drift.planChanged,
477
- };
478
- }
479
- function normalizedRoot(value) {
480
- return value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/+$/u, '').toLowerCase();
481
- }
482
- function underConfiguredRoot(relativePath, roots) {
483
- const candidate = relativePath.replaceAll('\\', '/').toLowerCase();
484
- return roots.some((root) => {
485
- const normalized = normalizedRoot(root);
486
- return candidate === normalized || candidate.startsWith(`${normalized}/`);
487
- });
488
- }
489
- function activeFocusFiles(snapshot, focusRoots) {
490
- return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
491
- underConfiguredRoot(relativePath, focusRoots) &&
492
- relativePath.toLowerCase().endsWith('.txt'));
493
- }
494
- function activeContinuousFocusFiles(snapshot) {
495
- return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
496
- relativePath.replaceAll('\\', '/').toLowerCase().startsWith('common/continuous_focus/') &&
497
- relativePath.toLowerCase().endsWith('.txt'));
498
- }
499
- function continuousPalettes(snapshot) {
500
- const palettes = [];
501
- const diagnostics = [];
502
- for (const file of activeContinuousFocusFiles(snapshot)) {
503
- const document = parseClausewitz(file.bytes, file.displayPath);
504
- const imported = importContinuousFocusPalettes(document);
505
- palettes.push(...imported.continuousFocusPalettes);
506
- diagnostics.push(...imported.diagnostics);
507
- }
508
- return { palettes, diagnostics };
509
- }
510
- function activeSidecar(snapshot, focusFile) {
511
- const relativePath = focusPlanningSidecarPath(focusFile.relativePath).toLowerCase();
512
- const file = snapshot.files.find((candidate) => candidate.shadowedBy === undefined && candidate.relativePath.toLowerCase() === relativePath);
513
- return file === undefined ? undefined : { file, sidecar: parseFocusPlanningSidecar(file.bytes) };
514
- }
515
- function importFocusFile(snapshot, file, palettes) {
516
- const document = parseClausewitz(file.bytes, file.displayPath);
517
- const imported = importFocusTrees(document, { references: referenceCatalog(snapshot) });
518
- const sidecar = activeSidecar(snapshot, file);
519
- imported.plans = imported.plans.map((plan) => {
520
- const enriched = sidecar === undefined
521
- ? { plan, diagnostics: [] }
522
- : enrichFocusPlanFromSidecar(plan, sidecar.sidecar);
523
- imported.diagnostics.push(...enriched.diagnostics);
524
- return linkContinuousFocusPalettes(enriched.plan, palettes);
525
- });
526
- return imported;
527
- }
528
- function selectedFocusFile(snapshot, focusRoots, relativePath) {
529
- const file = activeFocusFiles(snapshot, focusRoots).find((candidate) => candidate.relativePath === relativePath);
530
- if (file === undefined) {
531
- throw new ServiceError('FOCUS_SOURCE_NOT_FOUND', `Focus source was not found: ${relativePath}`);
532
- }
533
- return file;
534
- }
535
- function selectedContinuousFocusFile(snapshot, relativePath) {
536
- const file = activeContinuousFocusFiles(snapshot).find((candidate) => candidate.relativePath === relativePath);
537
- if (file === undefined) {
538
- throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_NOT_FOUND', `Continuous focus source was not found: ${relativePath}`);
539
- }
540
- return file;
541
- }
542
- function selectPlan(result, treeId) {
543
- if (result.plans.length === 0) {
544
- throw new ServiceError('FOCUS_TREE_NOT_FOUND', 'The source file contains no focus tree');
545
- }
546
- if (treeId === undefined) {
547
- if (result.plans.length !== 1) {
548
- throw new ServiceError('FOCUS_TREE_ID_REQUIRED', 'The source file contains multiple focus trees; provide treeId', { treeIds: result.plans.map(({ id }) => id) });
549
- }
550
- return result.plans[0];
551
- }
552
- const plan = result.plans.find(({ id }) => id === treeId);
553
- if (plan === undefined) {
554
- throw new ServiceError('FOCUS_TREE_NOT_FOUND', `Focus tree was not found: ${treeId}`);
555
- }
556
- return plan;
557
- }
558
- function selectContinuousPlan(result, paletteId) {
559
- if (result.continuousFocusPalettes.length === 0) {
560
- throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', 'The source file contains no continuous focus palette');
561
- }
562
- if (paletteId === undefined) {
563
- if (result.continuousFocusPalettes.length !== 1) {
564
- throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_ID_REQUIRED', 'The source file contains multiple continuous focus palettes; provide paletteId', { paletteIds: result.continuousFocusPalettes.map(({ id }) => id) });
565
- }
566
- return result.continuousFocusPalettes[0];
567
- }
568
- const plan = result.continuousFocusPalettes.find(({ id }) => id === paletteId);
569
- if (plan === undefined) {
570
- throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', `Continuous focus palette was not found: ${paletteId}`);
571
- }
572
- return plan;
573
- }
574
- function referenceCatalog(snapshot) {
575
- const identifiers = (kind) => [
576
- ...new Set(snapshot.index
577
- .findAll(kind)
578
- .filter(({ overridden }) => !overridden)
579
- .map(({ id }) => id)),
580
- ].sort((left, right) => compareCodeUnits(left, right));
581
- return {
582
- decision: identifiers('decision'),
583
- decision_category: identifiers('decision_category'),
584
- event: identifiers('event'),
585
- idea: identifiers('idea'),
586
- leader: identifiers('leader'),
587
- formable: identifiers('formable'),
588
- helper: identifiers('scripted_effect'),
589
- };
590
- }
591
- function validationFromDiagnostics(diagnostics) {
592
- const blocking = diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
593
- return {
594
- passed: blocking.length === 0,
595
- checks: [
596
- {
597
- id: 'focus-diagnostics',
598
- passed: blocking.length === 0,
599
- message: blocking.length === 0
600
- ? 'Focus source, references, and layout have no blocking diagnostics'
601
- : `${blocking.length} blocking focus diagnostics`,
602
- },
603
- ],
604
- };
605
- }
606
- export function registerFocusTools(server, engine, context) {
607
- const workbench = new FocusWorkbench(engine.resolver, engine.transactions, engine.artifacts);
608
- const cacheLifetime = new IdleCacheLifetime(() => engine.releaseScanCaches());
609
- server.registerTool('hoi4.focus_inspect', {
117
+ /** Compatibility wrapper retained for callers that supplied the signal separately. */
118
+ export function computeFocusVisualRevisions(engine, context, workspaceId, snapshot, selectors, signal) {
119
+ return computeCoreFocusVisualRevisions(engine, {
120
+ ...(context.principal === undefined ? {} : { principal: context.principal }),
121
+ ...(signal === undefined ? {} : { signal }),
122
+ }, workspaceId, snapshot, selectors);
123
+ }
124
+ export const focusTaskTools = [
125
+ {
126
+ name: 'hoi4.focus_inspect',
610
127
  title: 'Inspect focus trees',
611
128
  description: 'Inspect national trees or continuous palettes for creation and cleanup, including continuous-focus placement, complete plans, references, diagnostics, and stable layout decisions.',
612
- inputSchema: focusInspectInput,
129
+ inputSchema: focusInspectRequestSchema,
613
130
  outputSchema: focusInspectOutputSchema,
614
131
  annotations: artifactProducing,
615
- }, async (input, extra) => {
616
- const { workspaceId: requestedWorkspaceId, relativePath } = input;
617
- const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
618
- const releaseCaches = cacheLifetime.begin();
619
- try {
620
- const progress = progressReporter(extra);
621
- await progress.report(0, 3, 'Building shared workspace index');
622
- const workspace = engine.resolver.get(workspaceId, context.principal);
623
- const snapshot = await engine.scan(workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, progress.signal);
624
- if (input.mode === 'continuous') {
625
- const selectedFiles = activeContinuousFocusFiles(snapshot).filter((file) => relativePath === undefined || file.relativePath === relativePath);
626
- if (selectedFiles.length === 0) {
627
- throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_NOT_FOUND', 'No active continuous focus source matched the request');
628
- }
629
- const diagnostics = [];
630
- const importedPalettes = [];
631
- for (const file of selectedFiles) {
632
- progress.signal.throwIfAborted();
633
- const imported = importContinuousFocusPalettes(parseClausewitz(file.bytes, file.displayPath));
634
- importedPalettes.push(...imported.continuousFocusPalettes);
635
- diagnostics.push(...imported.diagnostics);
636
- }
637
- const palettes = importedPalettes.filter(({ id }) => input.paletteId === undefined || id === input.paletteId);
638
- if (palettes.length === 0) {
639
- throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', input.paletteId === undefined
640
- ? 'The selected source contains no continuous focus palette'
641
- : `Continuous focus palette was not found: ${input.paletteId}`);
642
- }
643
- const paletteDiagnosticCounts = new Map();
644
- for (const palette of palettes) {
645
- const lintDiagnostics = workbench.lintContinuous(palette);
646
- paletteDiagnosticCounts.set(palette.id, lintDiagnostics.length);
647
- diagnostics.push(...lintDiagnostics);
648
- }
649
- const presentation = await resolveFocusPresentation({
650
- plans: [],
651
- palettes,
652
- files: snapshot.files,
653
- index: snapshot.index,
654
- scanner: engine.scanner,
655
- workspace,
656
- decodeIcons: false,
657
- signal: progress.signal,
658
- });
659
- diagnostics.push(...presentation.diagnostics);
660
- await progress.report(2, 3, 'Writing continuous focus inspection');
661
- const completeSourceHashes = Object.fromEntries(Object.entries({
662
- ...Object.fromEntries(selectedFiles.map(({ displayPath, sha256 }) => [displayPath, sha256])),
663
- ...presentation.sourceHashes,
664
- }).sort(([left], [right]) => compareCodeUnits(left, right)));
665
- const sourceEvidence = boundedSourceHashEvidence(completeSourceHashes);
666
- const artifact = await engine.artifacts.putChunked(workspace, `focus-inspect.${snapshot.revision.slice(0, 16)}.json`, 'application/json', `${canonicalJson({
667
- schemaVersion: 1,
668
- mode: 'continuous',
669
- revision: snapshot.revision,
670
- continuousFocusPalettes: palettes,
671
- presentation: focusPresentationEvidence(presentation),
672
- sourceHashes: completeSourceHashes,
673
- diagnostics,
674
- })}\n`, {
675
- kind: 'focus-inspect',
676
- toolVersion: PACKAGE_VERSION,
677
- schemaVersion: 'focus-inspect.v1',
678
- sourceHashes: sourceEvidence.sourceHashes,
679
- metadata: { sourceHashInventory: sourceEvidence.inventory },
680
- }, 'Continuous focus plans and diagnostics', progress.signal);
681
- const result = emptyServiceResult(workspaceId, {
682
- mode: 'continuous',
683
- revision: snapshot.revision,
684
- treeCount: 0,
685
- paletteCount: palettes.length,
686
- trees: [],
687
- palettes: palettes.slice(0, 100).map((palette) => ({
688
- id: palette.id,
689
- sourcePath: palette.provenance.sourcePath,
690
- focusCount: palette.focuses.length,
691
- diagnosticCount: paletteDiagnosticCounts.get(palette.id) ?? 0,
692
- })),
693
- });
694
- result.code = 'FOCUS_INSPECTED';
695
- setInlineFilesScanned(result, [
696
- ...new Set([
697
- ...selectedFiles.map(({ displayPath }) => displayPath),
698
- ...presentation.filesScanned,
699
- ]),
700
- ].sort((left, right) => compareCodeUnits(left, right)));
701
- result.diagnostics = diagnostics.slice(0, 100);
702
- result.artifacts = [publicArtifactLink(artifact)];
703
- result.validation = validationFromDiagnostics(diagnostics);
704
- await progress.report(3, 3, 'Continuous focus inspection complete');
705
- return toolResult(result);
706
- }
707
- const selectedFiles = activeFocusFiles(snapshot, workspace.registration.roots.focus).filter((file) => relativePath === undefined || file.relativePath === relativePath);
708
- if (selectedFiles.length === 0) {
709
- throw new ServiceError('FOCUS_SOURCE_NOT_FOUND', 'No active focus source matched the request');
710
- }
711
- const selectedSidecarFiles = selectedFiles.flatMap((file) => {
712
- const resolved = activeSidecar(snapshot, file);
713
- return resolved === undefined ? [] : [resolved.file];
714
- });
715
- const importedPlans = [];
716
- const diagnostics = [];
717
- const paletteImport = continuousPalettes(snapshot);
718
- diagnostics.push(...paletteImport.diagnostics);
719
- for (const file of selectedFiles) {
720
- progress.signal.throwIfAborted();
721
- const imported = importFocusFile(snapshot, file, paletteImport.palettes);
722
- importedPlans.push(...imported.plans);
723
- diagnostics.push(...imported.diagnostics);
724
- }
725
- const plans = importedPlans.filter(({ id }) => input.treeId === undefined || id === input.treeId);
726
- if (plans.length === 0) {
727
- throw new ServiceError('FOCUS_TREE_NOT_FOUND', input.treeId === undefined
728
- ? 'The selected source contains no national focus tree'
729
- : `Focus tree was not found: ${input.treeId}`);
730
- }
731
- const inspectedPlans = [];
732
- for (const plan of plans) {
733
- const layout = await workbench.layoutAsync(plan, {
734
- ...(input.previous === undefined
735
- ? {}
736
- : { previous: input.previous }),
737
- ...(input.laneSpacing === undefined ? {} : { laneSpacing: input.laneSpacing }),
738
- ...(input.nodeSpacing === undefined ? {} : { nodeSpacing: input.nodeSpacing }),
739
- signal: progress.signal,
740
- });
741
- const lintDiagnostics = workbench.lint(plan, {
742
- index: snapshot.index,
743
- references: referenceCatalog(snapshot),
744
- layout,
745
- });
746
- diagnostics.push(...lintDiagnostics);
747
- inspectedPlans.push({ plan, layout, diagnosticCount: lintDiagnostics.length });
748
- }
749
- const presentation = await resolveFocusPresentation({
750
- plans,
751
- palettes: paletteImport.palettes,
752
- files: snapshot.files,
753
- index: snapshot.index,
754
- scanner: engine.scanner,
755
- workspace,
756
- decodeIcons: false,
757
- signal: progress.signal,
758
- });
759
- diagnostics.push(...presentation.diagnostics);
760
- await progress.report(2, 3, 'Writing focus plans, diagnostics, and layouts');
761
- const completeSourceHashes = Object.fromEntries(Object.entries({
762
- ...Object.fromEntries([
763
- ...selectedFiles,
764
- ...selectedSidecarFiles,
765
- ...activeContinuousFocusFiles(snapshot),
766
- ].map(({ displayPath, sha256 }) => [displayPath, sha256])),
767
- ...presentation.sourceHashes,
768
- }).sort(([left], [right]) => compareCodeUnits(left, right)));
769
- const sourceEvidence = boundedSourceHashEvidence(completeSourceHashes);
770
- const artifact = await engine.artifacts.putChunked(workspace, `focus-inspect.${snapshot.revision.slice(0, 16)}.json`, 'application/json', `${canonicalJson({
771
- schemaVersion: 1,
772
- revision: snapshot.revision,
773
- plans,
774
- layouts: inspectedPlans.map(({ plan, layout, diagnosticCount }) => ({
775
- treeId: plan.id,
776
- layout,
777
- diagnosticCount,
778
- })),
779
- continuousFocusPalettes: paletteImport.palettes,
780
- presentation: focusPresentationEvidence(presentation),
781
- sourceHashes: completeSourceHashes,
782
- diagnostics,
783
- })}\n`, {
784
- kind: 'focus-inspect',
785
- toolVersion: PACKAGE_VERSION,
786
- schemaVersion: 'focus-inspect.v1',
787
- sourceHashes: sourceEvidence.sourceHashes,
788
- metadata: { sourceHashInventory: sourceEvidence.inventory },
789
- }, 'Focus plans, diagnostics, and stable layout decisions', progress.signal);
790
- const result = emptyServiceResult(workspaceId, {
791
- mode: 'national',
792
- revision: snapshot.revision,
793
- treeCount: plans.length,
794
- paletteCount: paletteImport.palettes.length,
795
- trees: inspectedPlans.slice(0, 100).map(({ plan, layout, diagnosticCount }) => ({
796
- id: plan.id,
797
- sourcePath: plan.provenance.sourcePath,
798
- focusCount: plan.focuses.length,
799
- branchCount: plan.branchGroups.length,
800
- continuousPaletteCount: plan.continuousFocusPaletteIds.length,
801
- continuousFocusCount: plan.continuousFocusIds.length,
802
- continuousFocusPosition: plan.continuousFocusPosition ?? null,
803
- continuousFocusPaletteIds: plan.continuousFocusPaletteIds.slice(0, 100),
804
- resolvedTitleCount: plan.focuses.filter(({ id }) => presentation.entries[id]?.titleSourceLocation !== undefined).length,
805
- layoutHash: layout.layoutHash,
806
- layoutDecisionCount: layout.decisions.length,
807
- layoutMetrics: layout.metrics,
808
- diagnosticCount,
809
- })),
810
- palettes: [],
811
- });
812
- result.code = 'FOCUS_INSPECTED';
813
- setInlineFilesScanned(result, [
814
- ...new Set([
815
- ...selectedFiles.map(({ displayPath }) => displayPath),
816
- ...selectedSidecarFiles.map(({ displayPath }) => displayPath),
817
- ...activeContinuousFocusFiles(snapshot).map(({ displayPath }) => displayPath),
818
- ...presentation.filesScanned,
819
- ]),
820
- ].sort((left, right) => compareCodeUnits(left, right)));
821
- result.diagnostics = diagnostics.slice(0, 100);
822
- result.artifacts = [publicArtifactLink(artifact)];
823
- result.validation = validationFromDiagnostics(diagnostics);
824
- await progress.report(3, 3, 'Focus inspection complete');
825
- return toolResult(result);
826
- }
827
- catch (error) {
828
- return errorResult(error, workspaceId);
829
- }
830
- finally {
831
- releaseCaches();
832
- }
833
- });
834
- server.registerTool('hoi4.focus_render', {
132
+ },
133
+ {
134
+ name: 'hoi4.focus_render',
835
135
  title: 'Render focus review artifacts',
836
136
  description: 'Render a national tree or continuous palette as fast deterministic HTML, SVG, JSON, and source-map artifacts. Use hoi4.focus_raster when decoded icons and PNG output are needed.',
837
- inputSchema: focusRenderInput,
137
+ inputSchema: focusRenderRequestSchema,
838
138
  outputSchema: focusRenderOutputSchema,
839
139
  annotations: artifactProducing,
840
- }, (input, extra) => cacheLifetime.run(() => executeFocusVisualTool(engine, workbench, context, input, extra, false)));
841
- server.registerTool('hoi4.focus_raster', {
140
+ },
141
+ {
142
+ name: 'hoi4.focus_raster',
842
143
  title: 'Rasterize focus review artifacts',
843
144
  description: 'Produce the high-fidelity focus review with decoded source icons and deterministic PNG output. Use focus_render for the faster structural HTML, SVG, and JSON view.',
844
- inputSchema: focusRenderInput,
145
+ inputSchema: focusRenderRequestSchema,
845
146
  outputSchema: focusRenderOutputSchema,
846
147
  annotations: artifactProducing,
847
- }, (input, extra) => cacheLifetime.run(() => executeFocusVisualTool(engine, workbench, context, input, extra, true)));
848
- server.registerTool('hoi4.focus_rewrite', {
148
+ },
149
+ {
150
+ name: 'hoi4.focus_rewrite',
849
151
  title: 'Create or clean up focus content',
850
152
  description: 'Create or clean up a national tree or continuous palette, validate it, and apply it in one call. Supply a complete plan; set layoutMode compact for automatic arrangement. Existing national trees can omit the plan and use treeId plus layoutMode compact. Set createIfMissing for a new file.',
851
- inputSchema: focusPlanInput,
153
+ inputSchema: focusRewriteRequestSchema,
852
154
  outputSchema: focusPlanOutputSchema,
853
155
  annotations: {
854
156
  readOnlyHint: false,
@@ -856,315 +158,8 @@ export function registerFocusTools(server, engine, context) {
856
158
  idempotentHint: false,
857
159
  openWorldHint: false,
858
160
  },
859
- }, async (input, extra) => {
860
- const { workspaceId: requestedWorkspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
861
- const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
862
- const releaseCaches = cacheLifetime.begin();
863
- try {
864
- requireServerScope(context, 'hoi4:write');
865
- const progress = progressReporter(extra);
866
- await progress.report(0, 5, 'Validating shared index and current source');
867
- const workspace = engine.resolver.get(workspaceId, context.principal);
868
- const snapshot = await engine.scan(workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, progress.signal);
869
- if (input.mode === 'continuous') {
870
- const plan = input.plan;
871
- if (createIfMissing &&
872
- (!relativePath.toLowerCase().endsWith('.txt') ||
873
- !underConfiguredRoot(relativePath, ['common/continuous_focus'])))
874
- throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_PATH_INVALID', 'New continuous focus sources must be .txt files beneath common/continuous_focus', { relativePath });
875
- const sourceFile = activeContinuousFocusFiles(snapshot).find((candidate) => candidate.relativePath === relativePath);
876
- if (sourceFile === undefined && !createIfMissing)
877
- selectedContinuousFocusFile(snapshot, relativePath);
878
- if (sourceFile !== undefined && sourceFile.rootKind !== 'mod')
879
- throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_READ_ONLY', 'Continuous focus changes require a mod-owned source; create a new mod source path instead of shadowing a read-only file', { relativePath, rootKind: sourceFile.rootKind });
880
- const imported = sourceFile === undefined
881
- ? undefined
882
- : await workbench.importContinuousPath(workspaceId, relativePath, context.principal);
883
- const currentPlan = imported?.result.continuousFocusPalettes.find(({ id }) => id === plan.id);
884
- if (currentPlan === undefined && !createIfMissing) {
885
- if (imported === undefined)
886
- selectedContinuousFocusFile(snapshot, relativePath);
887
- else
888
- selectContinuousPlan(imported.result, plan.id);
889
- }
890
- if (currentPlan === undefined && createIfMissing && sourceFile !== undefined)
891
- throw new ServiceError('CONTINUOUS_FOCUS_CREATE_REQUIRES_NEW_SOURCE', 'Creating a continuous-focus palette requires a new mod source file so unrelated existing source is never repurposed', { relativePath, paletteId: plan.id });
892
- const renderBudget = new RenderBudget();
893
- const presentation = await resolveFocusPresentation({
894
- plans: [],
895
- palettes: [...(currentPlan === undefined ? [] : [currentPlan]), plan],
896
- files: snapshot.files,
897
- index: snapshot.index,
898
- scanner: engine.scanner,
899
- workspace,
900
- decodeIcons: false,
901
- budget: renderBudget,
902
- signal: progress.signal,
903
- });
904
- await progress.report(1, 5, 'Building proposed continuous focus review');
905
- const proposedPlanHash = focusPlanHash(plan);
906
- const reviewPlan = {
907
- ...plan,
908
- provenance: {
909
- sourcePath: `plan:${plan.id}`,
910
- sourceHash: proposedPlanHash,
911
- importedPlanHash: proposedPlanHash,
912
- },
913
- };
914
- const proposed = await workbench.renderContinuousAndStore(workspaceId, reviewPlan, {
915
- ...(context.principal === undefined ? {} : { principal: context.principal }),
916
- presentation,
917
- sourceHashes: presentation.sourceHashes,
918
- renderProfile: {
919
- sourceRevision: snapshot.revision,
920
- proposedPlanHash,
921
- output: 'vector',
922
- },
923
- rasterize: false,
924
- budget: renderBudget,
925
- signal: progress.signal,
926
- });
927
- const reviewArtifacts = proposed.artifacts.map(publicArtifactLink);
928
- await progress.report(3, 5, 'Preparing the continuous focus rewrite');
929
- const planned = await workbench.planContinuousChanges({
930
- workspaceId,
931
- relativePath,
932
- plan,
933
- createIfMissing,
934
- authority: 'plan',
935
- ...(context.principal === undefined ? {} : { principal: context.principal }),
936
- artifacts: reviewArtifacts,
937
- readDependencies: readDependenciesFromScannedFiles(snapshot.files),
938
- signal: progress.signal,
939
- });
940
- await progress.report(4, 5, 'Applying and validating the continuous focus rewrite');
941
- const execution = await executePlannedTransaction(engine, planned.transaction, context.principal, progress.signal);
942
- const transaction = execution.transaction;
943
- const result = emptyServiceResult(workspaceId, {
944
- mode: 'continuous',
945
- paletteId: plan.id,
946
- drift: compactDrift(planned.drift),
947
- created: planned.drift.status === 'target_missing',
948
- execution: execution.outcome,
949
- fileCount: transaction.files.length,
950
- artifactCount: transaction.artifacts.length,
951
- });
952
- result.status = transaction.validation.passed ? 'ok' : 'blocked';
953
- result.code =
954
- execution.outcome === 'applied'
955
- ? 'CONTINUOUS_FOCUS_CHANGES_APPLIED'
956
- : execution.outcome === 'unchanged'
957
- ? 'CONTINUOUS_FOCUS_CHANGES_UNCHANGED'
958
- : 'CONTINUOUS_FOCUS_CHANGES_BLOCKED';
959
- setInlineFilesScanned(result, [
960
- ...new Set([
961
- ...(sourceFile === undefined ? [] : [sourceFile.displayPath]),
962
- ...presentation.filesScanned,
963
- ]),
964
- ].sort((left, right) => compareCodeUnits(left, right)));
965
- result.proposedFiles = transaction.files
966
- .slice(0, 100)
967
- .map(({ relativePath: file }) => file);
968
- result.changedFiles = transaction.appliedFiles.slice(0, 100);
969
- result.diagnostics = transaction.diagnostics.slice(0, 100);
970
- result.artifacts = autonomousResultArtifacts(execution);
971
- result.validation = transaction.validation;
972
- result.blockers = transaction.diagnostics
973
- .filter(({ severity }) => severity === 'error' || severity === 'blocker')
974
- .map(({ code, message, details }) => ({
975
- code,
976
- message,
977
- ...(details === undefined ? {} : { details }),
978
- }));
979
- await progress.report(5, 5, execution.outcome === 'applied'
980
- ? 'Continuous focus rewrite complete'
981
- : execution.outcome === 'unchanged'
982
- ? 'Continuous focus content already satisfied the plan'
983
- : 'Continuous focus rewrite blocked');
984
- return toolResult(result);
985
- }
986
- const suppliedPlan = input.plan;
987
- if (createIfMissing &&
988
- (!relativePath.toLowerCase().endsWith('.txt') ||
989
- !underConfiguredRoot(relativePath, workspace.registration.roots.focus)))
990
- throw new ServiceError('FOCUS_SOURCE_PATH_INVALID', 'New national focus sources must be .txt files beneath a configured focus root', { relativePath });
991
- const focusFile = activeFocusFiles(snapshot, workspace.registration.roots.focus).find((candidate) => candidate.relativePath === relativePath);
992
- if (focusFile === undefined && !createIfMissing)
993
- selectedFocusFile(snapshot, workspace.registration.roots.focus, relativePath);
994
- if (focusFile !== undefined && focusFile.rootKind !== 'mod')
995
- throw new ServiceError('FOCUS_SOURCE_READ_ONLY', 'National focus changes require a mod-owned source; create a new mod source path instead of shadowing a read-only file', { relativePath, rootKind: focusFile.rootKind });
996
- const sidecar = focusFile === undefined ? undefined : activeSidecar(snapshot, focusFile);
997
- const catalog = referenceCatalog(snapshot);
998
- const imported = focusFile === undefined
999
- ? undefined
1000
- : await workbench.importPath(workspaceId, relativePath, context.principal, sidecar?.sidecar, catalog);
1001
- const paletteImport = continuousPalettes(snapshot);
1002
- if (imported !== undefined)
1003
- imported.result.plans = imported.result.plans.map((current) => linkContinuousFocusPalettes(current, paletteImport.palettes));
1004
- let compactPlanning;
1005
- let compactSourcePlan;
1006
- let plan;
1007
- if (suppliedPlan !== undefined) {
1008
- if (input.layoutMode === 'compact') {
1009
- compactSourcePlan = suppliedPlan;
1010
- compactPlanning = await compactFocusTreePlanAsync(suppliedPlan, {
1011
- signal: progress.signal,
1012
- });
1013
- plan = compactPlanning.plan;
1014
- }
1015
- else {
1016
- plan = suppliedPlan;
1017
- }
1018
- }
1019
- else {
1020
- if (imported === undefined)
1021
- throw new ServiceError('FOCUS_COMPACT_SOURCE_REQUIRED', 'Plan-free compact reflow requires an existing national focus source', { relativePath });
1022
- compactSourcePlan = selectPlan(imported.result, input.treeId);
1023
- compactPlanning = await compactFocusTreePlanAsync(compactSourcePlan, {
1024
- signal: progress.signal,
1025
- });
1026
- plan = compactPlanning.plan;
1027
- }
1028
- const currentPlan = imported?.result.plans.find(({ id }) => id === plan.id);
1029
- if (currentPlan === undefined && !createIfMissing) {
1030
- if (imported === undefined)
1031
- selectedFocusFile(snapshot, workspace.registration.roots.focus, relativePath);
1032
- else
1033
- selectPlan(imported.result, plan.id);
1034
- }
1035
- if (currentPlan === undefined && createIfMissing && focusFile !== undefined)
1036
- throw new ServiceError('FOCUS_CREATE_REQUIRES_NEW_SOURCE', 'Creating a national focus tree requires a new mod source file so an existing source and its planning sidecar cannot be repurposed', { relativePath, treeId: plan.id });
1037
- const renderBudget = new RenderBudget();
1038
- const effectiveReviewScale = reviewScale ?? automaticFocusRenderScale(plan.focuses.length);
1039
- const reviewRenderOptions = {
1040
- ...(horizontalSpacing === undefined ? {} : { horizontalSpacing }),
1041
- ...(verticalSpacing === undefined ? {} : { verticalSpacing }),
1042
- ...(padding === undefined ? {} : { padding }),
1043
- outputScale: effectiveReviewScale,
1044
- };
1045
- const reviewRenderProfile = {
1046
- ...(horizontalSpacing === undefined ? {} : { horizontalSpacing }),
1047
- ...(verticalSpacing === undefined ? {} : { verticalSpacing }),
1048
- ...(padding === undefined ? {} : { padding }),
1049
- reviewScale: effectiveReviewScale,
1050
- };
1051
- const presentation = await resolveFocusPresentation({
1052
- plans: [...(currentPlan === undefined ? [] : [currentPlan]), plan],
1053
- palettes: paletteImport.palettes.filter(({ id }) => plan.continuousFocusPaletteIds.includes(id)),
1054
- files: snapshot.files,
1055
- index: snapshot.index,
1056
- scanner: engine.scanner,
1057
- workspace,
1058
- decodeIcons: false,
1059
- budget: renderBudget,
1060
- signal: progress.signal,
1061
- });
1062
- const currentLayout = currentPlan === undefined
1063
- ? undefined
1064
- : currentPlan === compactSourcePlan && compactPlanning !== undefined
1065
- ? compactPlanning.currentLayout
1066
- : await workbench.layoutAsync(currentPlan, { signal: progress.signal });
1067
- await progress.report(1, 5, 'Building proposed focus review');
1068
- const proposedPlanHash = focusPlanHash(plan);
1069
- const reviewPlan = {
1070
- ...plan,
1071
- provenance: {
1072
- sourcePath: `plan:${plan.id}`,
1073
- sourceHash: proposedPlanHash,
1074
- importedPlanHash: proposedPlanHash,
1075
- },
1076
- };
1077
- const proposedLayout = compactPlanning?.proposedLayout ??
1078
- (await workbench.layoutAsync(reviewPlan, { signal: progress.signal }));
1079
- if (input.layoutMode === 'compact') {
1080
- assertCompactLayoutQuality(currentLayout, proposedLayout);
1081
- }
1082
- const proposed = await workbench.renderAndStore(workspaceId, reviewPlan, {
1083
- ...(context.principal === undefined ? {} : { principal: context.principal }),
1084
- index: snapshot.index,
1085
- references: catalog,
1086
- presentation,
1087
- sourceHashes: presentation.sourceHashes,
1088
- ...reviewRenderOptions,
1089
- rasterize: false,
1090
- renderProfile: {
1091
- sourceRevision: snapshot.revision,
1092
- proposedPlanHash,
1093
- reviewRenderProfile,
1094
- output: 'vector',
1095
- },
1096
- budget: renderBudget,
1097
- signal: progress.signal,
1098
- layout: proposedLayout,
1099
- });
1100
- const reviewArtifacts = proposed.artifacts.map(publicArtifactLink);
1101
- await progress.report(3, 5, 'Preparing the focus-tree rewrite');
1102
- const planned = await workbench.planChanges({
1103
- workspaceId,
1104
- relativePath,
1105
- plan,
1106
- createIfMissing,
1107
- authority: 'plan',
1108
- layout: proposed.layout,
1109
- ...(context.principal === undefined ? {} : { principal: context.principal }),
1110
- index: snapshot.index,
1111
- references: catalog,
1112
- artifacts: reviewArtifacts,
1113
- readDependencies: readDependenciesFromScannedFiles(snapshot.files),
1114
- signal: progress.signal,
1115
- });
1116
- await progress.report(4, 5, 'Applying and validating the focus-tree rewrite');
1117
- const execution = await executePlannedTransaction(engine, planned.transaction, context.principal, progress.signal);
1118
- const transaction = execution.transaction;
1119
- const result = emptyServiceResult(workspaceId, {
1120
- mode: 'national',
1121
- treeId: plan.id,
1122
- drift: compactDrift(planned.drift),
1123
- created: planned.drift.status === 'target_missing',
1124
- execution: execution.outcome,
1125
- layoutHash: planned.layout.layoutHash,
1126
- fileCount: transaction.files.length,
1127
- artifactCount: transaction.artifacts.length,
1128
- });
1129
- result.status = transaction.validation.passed ? 'ok' : 'blocked';
1130
- result.code =
1131
- execution.outcome === 'applied'
1132
- ? 'FOCUS_CHANGES_APPLIED'
1133
- : execution.outcome === 'unchanged'
1134
- ? 'FOCUS_CHANGES_UNCHANGED'
1135
- : 'FOCUS_CHANGES_BLOCKED';
1136
- setInlineFilesScanned(result, [
1137
- ...new Set([
1138
- ...(focusFile === undefined ? [] : [focusFile.displayPath]),
1139
- ...presentation.filesScanned,
1140
- ]),
1141
- ].sort((left, right) => compareCodeUnits(left, right)));
1142
- result.proposedFiles = transaction.files
1143
- .slice(0, 100)
1144
- .map(({ relativePath: file }) => file);
1145
- result.changedFiles = transaction.appliedFiles.slice(0, 100);
1146
- result.diagnostics = transaction.diagnostics.slice(0, 100);
1147
- result.artifacts = autonomousResultArtifacts(execution);
1148
- result.validation = transaction.validation;
1149
- result.blockers = transaction.diagnostics
1150
- .filter(({ severity }) => severity === 'error' || severity === 'blocker')
1151
- .map(({ code, message, details }) => ({
1152
- code,
1153
- message,
1154
- ...(details === undefined ? {} : { details }),
1155
- }));
1156
- await progress.report(5, 5, execution.outcome === 'applied'
1157
- ? 'Focus rewrite complete'
1158
- : execution.outcome === 'unchanged'
1159
- ? 'Focus content already satisfied the plan'
1160
- : 'Focus rewrite blocked');
1161
- return toolResult(result);
1162
- }
1163
- catch (error) {
1164
- return errorResult(error, workspaceId, autonomousFailureContext(error));
1165
- }
1166
- finally {
1167
- releaseCaches();
1168
- }
1169
- });
161
+ },
162
+ ];
163
+ export function registerFocusTools(server, _engine, _context) {
164
+ registerLegacyTaskTools(server, focusTaskTools);
1170
165
  }