hoi4-agent-tools 2.3.11 → 2.3.13

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.13 - 2026-08-06
4
+
5
+ - Repacked sparse sibling fan-outs around their structural parent instead of preserving avoidable long prerequisite connectors.
6
+ - Moved exclusive descendant branches with their compacted sibling root while keeping shared convergence focuses anchored.
7
+ - Added direct branch-packing regressions and reverified the public 1,024-focus MCP rewrite workflow.
8
+
9
+ ## 2.3.12 - 2026-08-05
10
+
11
+ - Made compact focus rewrites choose among preservation, row compression, linear-chain repair, sibling balancing, and combined strategies instead of forcing one aggressive transformation.
12
+ - Limited rewrite rejection to invalid geometry while retaining long paths, fake complexity, branch balance, and compactness as prioritized cleanup objectives.
13
+ - Added whole-cohort sibling refinement, collision-aware incremental placement, stable repeat rewrites, and regression coverage through the public Ireland and 1,024-focus MCP workflows.
14
+
3
15
  ## 2.3.11 - 2026-08-05
4
16
 
5
17
  - Made compact focus rewrites straighten offset linear chains and reject artificial staircase or zigzag geometry.
@@ -12,26 +12,21 @@ const COMPACT_HARD_DIAGNOSTICS = new Set([
12
12
  'FOCUS_LAYOUT_CONNECTOR_CROSSING_UNSATISFIED',
13
13
  'FOCUS_LAYOUT_CONNECTOR_THROUGH_NODE',
14
14
  'FOCUS_LAYOUT_COORDINATE_CONFLICT',
15
- 'FOCUS_LAYOUT_LINEAR_DETOUR',
16
15
  'FOCUS_LAYOUT_LANE_BOUNDS_UNSATISFIED',
17
16
  'FOCUS_LAYOUT_MUTUAL_EXCLUSION_SPACING_UNSATISFIED',
17
+ 'FOCUS_LAYOUT_PARENT_NOT_ABOVE',
18
18
  'FOCUS_LAYOUT_PARENT_ORDER_UNSATISFIED',
19
19
  'FOCUS_LAYOUT_SAME_ROW_SPACING_UNSATISFIED',
20
- 'FOCUS_LAYOUT_STAIRCASE_CHAIN',
21
20
  'FOCUS_LAYOUT_VISIBLE_OVERLAP',
21
+ ]);
22
+ const COMPACT_AESTHETIC_DIAGNOSTICS = new Set([
23
+ 'FOCUS_LAYOUT_LINEAR_DETOUR',
24
+ 'FOCUS_LAYOUT_LONG_CONNECTOR',
25
+ 'FOCUS_LAYOUT_SIBLING_ANCHOR_DEVIATION',
26
+ 'FOCUS_LAYOUT_SIBLING_ASYMMETRY',
27
+ 'FOCUS_LAYOUT_STAIRCASE_CHAIN',
22
28
  'FOCUS_LAYOUT_ZIGZAG_CHAIN',
23
29
  ]);
24
- function compactLimits(focusCount) {
25
- const graphScale = Math.sqrt(Math.max(1, focusCount));
26
- const maximumHorizontalSpan = Math.max(14, Math.ceil(graphScale * 3));
27
- const maximumVerticalSpan = Math.max(4, Math.ceil(Math.log2(Math.max(2, focusCount))));
28
- return {
29
- maximumColumns: Math.max(16, Math.ceil(graphScale * 5)),
30
- maximumHorizontalSpan,
31
- maximumVerticalSpan,
32
- maximumManhattanSpan: maximumHorizontalSpan + maximumVerticalSpan,
33
- };
34
- }
35
30
  function requiredMetrics(layout) {
36
31
  if (layout.metrics !== undefined)
37
32
  return layout.metrics;
@@ -39,7 +34,6 @@ function requiredMetrics(layout) {
39
34
  }
40
35
  function absoluteCompactRegressions(layout) {
41
36
  const metrics = requiredMetrics(layout);
42
- const limits = compactLimits(layout.nodes.length);
43
37
  return [
44
38
  ...(layout.diagnostics.some(({ code }) => COMPACT_HARD_DIAGNOSTICS.has(code))
45
39
  ? ['hardLayoutDiagnostics']
@@ -47,61 +41,9 @@ function absoluteCompactRegressions(layout) {
47
41
  ...(metrics.connectors.crossingCount > 0 ? ['connectorCrossingCount'] : []),
48
42
  ...(metrics.connectors.nodeIntersectionCount > 0 ? ['connectorNodeIntersections'] : []),
49
43
  ...(metrics.spacing.tooCloseSameRowPairCount > 0 ? ['sameRowSpacing'] : []),
50
- ...(metrics.symmetry.asymmetricSiblingCohortCount > 0 ? ['siblingMirrorDeviation'] : []),
51
- ...(metrics.symmetry.totalSiblingDeviation > 0 ? ['totalSiblingMirrorDeviation'] : []),
52
- ...(metrics.symmetry.offAnchorSiblingCohortCount > 0 ? ['siblingAnchorDeviation'] : []),
53
- ...(metrics.symmetry.totalSiblingAnchorDeviation > 0 ? ['totalSiblingAnchorDeviation'] : []),
54
44
  ...(metrics.symmetry.boundingCenterOffsetTwice > 1 ? ['boundingCenter'] : []),
55
- ...(metrics.bounds.columnCount > limits.maximumColumns ? ['columnBudget'] : []),
56
- ...(metrics.connectors.maximumHorizontalSpan > limits.maximumHorizontalSpan
57
- ? ['maximumHorizontalConnectorBudget']
58
- : []),
59
- ...(metrics.connectors.maximumVerticalSpan > limits.maximumVerticalSpan
60
- ? ['maximumVerticalConnectorBudget']
61
- : []),
62
- ...(metrics.connectors.maximumManhattanSpan > limits.maximumManhattanSpan
63
- ? ['maximumManhattanConnectorBudget']
64
- : []),
65
- ...(metrics.connectors.longConnectorCount > 0 ? ['longConnectors'] : []),
66
45
  ];
67
46
  }
68
- function relativeCompactRegressions(current, proposed) {
69
- const before = requiredMetrics(current);
70
- const after = requiredMetrics(proposed);
71
- return [
72
- ...(after.connectors.crossingCount > before.connectors.crossingCount
73
- ? ['connectorCrossingCount']
74
- : []),
75
- ...(after.connectors.nodeIntersectionCount > before.connectors.nodeIntersectionCount
76
- ? ['connectorNodeIntersectionCount']
77
- : []),
78
- ...(after.connectors.maximumHorizontalSpan > before.connectors.maximumHorizontalSpan
79
- ? ['maximumHorizontalConnectorSpan']
80
- : []),
81
- ...(after.connectors.maximumVerticalSpan > before.connectors.maximumVerticalSpan
82
- ? ['maximumVerticalConnectorSpan']
83
- : []),
84
- ...(after.connectors.maximumManhattanSpan > before.connectors.maximumManhattanSpan
85
- ? ['maximumManhattanConnectorSpan']
86
- : []),
87
- ...(after.connectors.longConnectorCount > before.connectors.longConnectorCount
88
- ? ['longConnectorCount']
89
- : []),
90
- ...(after.connectors.totalHorizontalSpan > before.connectors.totalHorizontalSpan
91
- ? ['totalHorizontalConnectorSpan']
92
- : []),
93
- ...(after.connectors.totalManhattanSpan > before.connectors.totalManhattanSpan
94
- ? ['totalManhattanConnectorSpan']
95
- : []),
96
- ...(after.bounds.columnCount > before.bounds.columnCount ? ['columnCount'] : []),
97
- ...(after.bounds.rowCount > before.bounds.rowCount ? ['rowCount'] : []),
98
- ];
99
- }
100
- function compactShape(plan, layout) {
101
- return (plan.focuses.every(({ position }) => position.mode === 'auto') &&
102
- plan.laneGroups.every(({ minimumX, maximumX }) => minimumX === undefined && maximumX === undefined) &&
103
- absoluteCompactRegressions(layout).length === 0);
104
- }
105
47
  function prerequisiteIds(plan, focusId) {
106
48
  const focus = plan.focuses.find(({ id }) => id === focusId);
107
49
  return (focus?.prerequisites.groups
@@ -151,8 +93,14 @@ function compactStructuralAnchorX(plan, focus, parents, preferredX, laneBases) {
151
93
  function mirrorSiblingCohorts(plan, preferredX, preferredY) {
152
94
  const cohorts = new Map();
153
95
  const laneBases = compactLaneBases(plan);
96
+ const childIds = new Map();
154
97
  for (const focus of plan.focuses) {
155
98
  const parents = prerequisiteIds(plan, focus.id);
99
+ for (const parentId of parents) {
100
+ const children = childIds.get(parentId) ?? [];
101
+ children.push(focus.id);
102
+ childIds.set(parentId, children);
103
+ }
156
104
  const key = JSON.stringify([
157
105
  parents.length === 0 ? focusLaneId(plan, focus) : null,
158
106
  parents,
@@ -166,36 +114,44 @@ function mirrorSiblingCohorts(plan, preferredX, preferredY) {
166
114
  if (cohort.length < 2)
167
115
  continue;
168
116
  cohort.sort((left, right) => (preferredX.get(left) ?? 0) - (preferredX.get(right) ?? 0) || compareCodeUnits(left, right));
169
- const coordinates = cohort.map((id) => preferredX.get(id) ?? 0);
170
- const pairCount = Math.floor(coordinates.length / 2);
171
117
  const focus = plan.focuses.find(({ id }) => id === cohort[0]);
172
118
  if (focus === undefined)
173
119
  continue;
174
- const mirroredAt = (centerTwice) => {
175
- const mirrored = [...coordinates];
176
- for (let index = 0; index < pairCount; index += 1) {
177
- const opposite = coordinates.length - 1 - index;
178
- const left = Math.round(((coordinates[index] ?? 0) + centerTwice - (coordinates[opposite] ?? 0)) / 2);
179
- mirrored[index] = left;
180
- mirrored[opposite] = centerTwice - left;
181
- }
182
- if (coordinates.length % 2 === 1)
183
- mirrored[pairCount] = Math.round(centerTwice / 2);
184
- let maximumLeft = centerTwice / 2 - (coordinates.length % 2 === 1 ? 2 : 1);
185
- for (let index = pairCount - 1; index >= 0; index -= 1) {
186
- const opposite = coordinates.length - 1 - index;
187
- const left = Math.min(mirrored[index] ?? maximumLeft, maximumLeft);
188
- mirrored[index] = left;
189
- mirrored[opposite] = centerTwice - left;
190
- maximumLeft = left - COMPACT_SAME_ROW_SPACING;
191
- }
192
- return mirrored;
193
- };
194
120
  const anchorCenterTwice = 2 *
195
121
  compactStructuralAnchorX(plan, focus, prerequisiteIds(plan, focus.id), preferredX, laneBases);
196
- const mirrored = mirroredAt(anchorCenterTwice);
197
- for (const [index, focusId] of cohort.entries())
198
- preferredX.set(focusId, mirrored[index] ?? 0);
122
+ const firstX = Math.round(anchorCenterTwice / 2 - ((cohort.length - 1) * COMPACT_SAME_ROW_SPACING) / 2);
123
+ const targetX = new Map(cohort.map((focusId, index) => [focusId, firstX + index * COMPACT_SAME_ROW_SPACING]));
124
+ const deltaX = new Map(cohort.map((focusId) => [
125
+ focusId,
126
+ (targetX.get(focusId) ?? 0) - (preferredX.get(focusId) ?? 0),
127
+ ]));
128
+ const reachableBy = new Map();
129
+ for (const rootId of cohort) {
130
+ const pending = [rootId];
131
+ const visited = new Set();
132
+ while (pending.length > 0) {
133
+ const focusId = pending.pop();
134
+ if (focusId === undefined || visited.has(focusId))
135
+ continue;
136
+ visited.add(focusId);
137
+ const owners = reachableBy.get(focusId) ?? new Set();
138
+ owners.add(rootId);
139
+ reachableBy.set(focusId, owners);
140
+ pending.push(...(childIds.get(focusId) ?? []));
141
+ }
142
+ }
143
+ for (const [focusId, owners] of reachableBy) {
144
+ if (owners.size !== 1)
145
+ continue;
146
+ const ownerId = [...owners][0];
147
+ if (ownerId === undefined)
148
+ continue;
149
+ const delta = deltaX.get(ownerId);
150
+ const current = preferredX.get(focusId);
151
+ if (delta === undefined || current === undefined)
152
+ continue;
153
+ preferredX.set(focusId, current + delta);
154
+ }
199
155
  }
200
156
  }
201
157
  function straightenLinearChains(plan, preferredX, preferredY) {
@@ -225,7 +181,8 @@ function straightenLinearChains(plan, preferredX, preferredY) {
225
181
  preferredY.set(focus.id, parentY + 1);
226
182
  }
227
183
  }
228
- function compactCandidate(plan, layout, scale, compressRows) {
184
+ function compactCandidate(plan, layout, strategy) {
185
+ const { scale, compressRows, mirrorSiblings, straightenChains } = strategy;
229
186
  const minimumX = Math.min(...layout.nodes.map(({ x }) => x));
230
187
  const maximumX = Math.max(...layout.nodes.map(({ x }) => x));
231
188
  const minimumY = Math.min(...layout.nodes.map(({ y }) => y));
@@ -256,8 +213,10 @@ function compactCandidate(plan, layout, scale, compressRows) {
256
213
  for (const [index, node] of row.entries())
257
214
  preferredX.set(node.id, (placed[index] ?? 0) + drift);
258
215
  }
259
- mirrorSiblingCohorts(plan, preferredX, preferredY);
260
- straightenLinearChains(plan, preferredX, preferredY);
216
+ if (mirrorSiblings)
217
+ mirrorSiblingCohorts(plan, preferredX, preferredY);
218
+ if (straightenChains)
219
+ straightenLinearChains(plan, preferredX, preferredY);
261
220
  const compacted = structuredClone(plan);
262
221
  compacted.laneGroups = compacted.laneGroups.map(({ id, label, order }) => ({
263
222
  id,
@@ -277,14 +236,24 @@ function compactCandidate(plan, layout, scale, compressRows) {
277
236
  }
278
237
  function compactScore(layout) {
279
238
  const metrics = requiredMetrics(layout);
239
+ const hardDiagnosticCount = layout.diagnostics.filter(({ code }) => COMPACT_HARD_DIAGNOSTICS.has(code)).length;
240
+ const aestheticDiagnosticCount = layout.diagnostics.filter(({ code }) => COMPACT_AESTHETIC_DIAGNOSTICS.has(code)).length;
280
241
  return [
242
+ hardDiagnosticCount,
243
+ metrics.connectors.crossingCount,
244
+ metrics.connectors.nodeIntersectionCount,
245
+ metrics.spacing.tooCloseSameRowPairCount,
246
+ aestheticDiagnosticCount,
281
247
  metrics.connectors.longConnectorCount,
282
- metrics.connectors.maximumHorizontalSpan,
248
+ metrics.symmetry.asymmetricSiblingCohortCount,
249
+ metrics.symmetry.offAnchorSiblingCohortCount,
250
+ metrics.symmetry.totalSiblingDeviation,
251
+ metrics.symmetry.totalSiblingAnchorDeviation,
252
+ metrics.bounds.rowCount,
283
253
  metrics.bounds.columnCount,
284
- metrics.connectors.nodeIntersectionCount,
254
+ metrics.connectors.maximumHorizontalSpan,
285
255
  metrics.connectors.totalHorizontalSpan,
286
256
  metrics.connectors.maximumManhattanSpan,
287
- metrics.bounds.rowCount,
288
257
  metrics.connectors.totalVerticalSpan,
289
258
  layout.layoutHash,
290
259
  ];
@@ -310,44 +279,60 @@ function compactScales(focusCount) {
310
279
  ? [0.55, 0.65, 0.75]
311
280
  : [0.65];
312
281
  }
282
+ function compactStrategies(focusCount) {
283
+ const preservationStrategies = [
284
+ { scale: 1, compressRows: false, mirrorSiblings: false, straightenChains: false },
285
+ { scale: 1, compressRows: true, mirrorSiblings: false, straightenChains: false },
286
+ { scale: 1, compressRows: false, mirrorSiblings: false, straightenChains: true },
287
+ { scale: 1, compressRows: false, mirrorSiblings: true, straightenChains: false },
288
+ ];
289
+ const compactStrategies = compactScales(focusCount).flatMap((scale) => [
290
+ { scale, compressRows: false, mirrorSiblings: true, straightenChains: true },
291
+ { scale, compressRows: true, mirrorSiblings: true, straightenChains: true },
292
+ ]);
293
+ return [...preservationStrategies, ...compactStrategies];
294
+ }
295
+ function needsPresentationNormalization(plan) {
296
+ return (plan.focuses.some(({ position }) => position.mode !== 'auto') ||
297
+ plan.laneGroups.some(({ minimumX, maximumX }) => minimumX !== undefined || maximumX !== undefined));
298
+ }
313
299
  /**
314
300
  * Produces a gameplay-neutral compact-reflow plan. Several deterministic
315
- * compression candidates are measured with the same layout engine; only a
316
- * centered, crossing-free, evenly spaced, branch-balanced candidate that
317
- * stays within absolute and source-relative connector budgets can win.
301
+ * repair and compression candidates are measured with the same layout engine.
302
+ * Invalid geometry cannot win; aesthetic tradeoffs are resolved by the score.
318
303
  */
319
304
  export function compactFocusTreePlan(plan) {
320
- const currentLayout = layoutFocusTree(plan);
321
- if (compactShape(plan, currentLayout)) {
322
- const stable = structuredClone(plan);
323
- stable.provenance.importedPlanHash = focusPlanHash(stable);
324
- return stable;
325
- }
326
- const scales = compactScales(plan.focuses.length);
305
+ const currentLayout = layoutFocusTree(plan, { aggressiveAestheticRepair: true });
306
+ const normalizedPlan = needsPresentationNormalization(plan)
307
+ ? compactCandidate(plan, currentLayout, {
308
+ scale: 1,
309
+ compressRows: false,
310
+ mirrorSiblings: false,
311
+ straightenChains: false,
312
+ })
313
+ : plan;
314
+ const normalizedLayout = normalizedPlan === plan
315
+ ? currentLayout
316
+ : layoutFocusTree(normalizedPlan, { aggressiveAestheticRepair: true });
327
317
  let selected;
328
318
  let fallback;
329
- for (const scale of scales) {
330
- for (const compressRows of [false, true]) {
331
- const candidate = compactCandidate(plan, currentLayout, scale, compressRows);
332
- let layout;
333
- try {
334
- layout = layoutFocusTree(candidate);
335
- }
336
- catch (error) {
337
- if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED')
338
- continue;
339
- throw error;
340
- }
341
- if (fallback === undefined ||
342
- betterScore(compactScore(layout), compactScore(fallback.layout)))
343
- fallback = { plan: candidate, layout };
344
- if (absoluteCompactRegressions(layout).length > 0 ||
345
- relativeCompactRegressions(currentLayout, layout).length > 0)
319
+ for (const strategy of compactStrategies(plan.focuses.length)) {
320
+ const candidate = compactCandidate(normalizedPlan, normalizedLayout, strategy);
321
+ let layout;
322
+ try {
323
+ layout = layoutFocusTree(candidate, { aggressiveAestheticRepair: true });
324
+ }
325
+ catch (error) {
326
+ if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED')
346
327
  continue;
347
- if (selected === undefined ||
348
- betterScore(compactScore(layout), compactScore(selected.layout)))
349
- selected = { plan: candidate, layout };
328
+ throw error;
350
329
  }
330
+ if (fallback === undefined || betterScore(compactScore(layout), compactScore(fallback.layout)))
331
+ fallback = { plan: candidate, layout };
332
+ if (absoluteCompactRegressions(layout).length > 0)
333
+ continue;
334
+ if (selected === undefined || betterScore(compactScore(layout), compactScore(selected.layout)))
335
+ selected = { plan: candidate, layout };
351
336
  }
352
337
  if (selected !== undefined)
353
338
  return selected.plan;
@@ -365,42 +350,44 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
365
350
  const workBudget = new FocusLayoutWorkBudget(options.maximumWork ?? COMPACT_LAYOUT_WORK_MAX);
366
351
  const layoutOptions = {
367
352
  workBudget,
353
+ aggressiveAestheticRepair: true,
368
354
  ...(options.signal === undefined ? {} : { signal: options.signal }),
369
355
  };
370
356
  const currentLayout = await layoutFocusTreeAsync(plan, layoutOptions);
371
- if (compactShape(plan, currentLayout)) {
372
- const stable = structuredClone(plan);
373
- stable.provenance.importedPlanHash = focusPlanHash(stable);
374
- return { plan: stable, currentLayout, proposedLayout: currentLayout };
375
- }
357
+ const normalizedPlan = needsPresentationNormalization(plan)
358
+ ? compactCandidate(plan, currentLayout, {
359
+ scale: 1,
360
+ compressRows: false,
361
+ mirrorSiblings: false,
362
+ straightenChains: false,
363
+ })
364
+ : plan;
365
+ const normalizedLayout = normalizedPlan === plan
366
+ ? currentLayout
367
+ : await layoutFocusTreeAsync(normalizedPlan, layoutOptions);
376
368
  let selected;
377
369
  let fallback;
378
370
  let exhausted;
379
- candidateSearch: for (const scale of compactScales(plan.focuses.length)) {
380
- for (const compressRows of [false, true]) {
381
- options.signal?.throwIfAborted();
382
- const candidate = compactCandidate(plan, currentLayout, scale, compressRows);
383
- let layout;
384
- try {
385
- layout = await layoutFocusTreeAsync(candidate, layoutOptions);
386
- }
387
- catch (error) {
388
- if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED') {
389
- exhausted = error;
390
- break candidateSearch;
391
- }
392
- throw error;
371
+ candidateSearch: for (const strategy of compactStrategies(plan.focuses.length)) {
372
+ options.signal?.throwIfAborted();
373
+ const candidate = compactCandidate(normalizedPlan, normalizedLayout, strategy);
374
+ let layout;
375
+ try {
376
+ layout = await layoutFocusTreeAsync(candidate, layoutOptions);
377
+ }
378
+ catch (error) {
379
+ if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED') {
380
+ exhausted = error;
381
+ break candidateSearch;
393
382
  }
394
- if (fallback === undefined ||
395
- betterScore(compactScore(layout), compactScore(fallback.layout)))
396
- fallback = { plan: candidate, layout };
397
- if (absoluteCompactRegressions(layout).length > 0 ||
398
- relativeCompactRegressions(currentLayout, layout).length > 0)
399
- continue;
400
- if (selected === undefined ||
401
- betterScore(compactScore(layout), compactScore(selected.layout)))
402
- selected = { plan: candidate, layout };
383
+ throw error;
403
384
  }
385
+ if (fallback === undefined || betterScore(compactScore(layout), compactScore(fallback.layout)))
386
+ fallback = { plan: candidate, layout };
387
+ if (absoluteCompactRegressions(layout).length > 0)
388
+ continue;
389
+ if (selected === undefined || betterScore(compactScore(layout), compactScore(selected.layout)))
390
+ selected = { plan: candidate, layout };
404
391
  }
405
392
  if (selected !== undefined) {
406
393
  return { plan: selected.plan, currentLayout, proposedLayout: selected.layout };
@@ -413,19 +400,15 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
413
400
  }
414
401
  throw new ServiceError('FOCUS_COMPACT_LAYOUT_REQUIRED', 'Compact focus planning did not produce a measurable layout candidate', { treeId: plan.id });
415
402
  }
416
- /** Refuses compact rewrites that are not clean or trade readability for a smaller canvas. */
417
- export function assertCompactLayoutQuality(current, proposed) {
418
- const regressions = [
419
- ...absoluteCompactRegressions(proposed),
420
- ...(current === undefined ? [] : relativeCompactRegressions(current, proposed)),
421
- ].filter((value, index, all) => all.indexOf(value) === index);
403
+ /** Refuses compact rewrites that still contain invalid or unreadable geometry. */
404
+ export function assertCompactLayoutQuality(_current, proposed) {
405
+ const regressions = [...absoluteCompactRegressions(proposed)].filter((value, index, all) => all.indexOf(value) === index);
422
406
  if (regressions.length === 0)
423
407
  return;
424
- throw new ServiceError('FOCUS_COMPACT_QUALITY_BLOCKED', 'Compact focus reflow failed the absolute or relative layout-quality gate', {
408
+ throw new ServiceError('FOCUS_COMPACT_QUALITY_BLOCKED', 'Compact focus reflow failed the layout-correctness gate', {
425
409
  treeId: proposed.treeId,
426
410
  regressions,
427
- limits: compactLimits(proposed.nodes.length),
428
- ...(current === undefined ? {} : { before: requiredMetrics(current) }),
411
+ ...(_current === undefined ? {} : { before: requiredMetrics(_current) }),
429
412
  proposed: requiredMetrics(proposed),
430
413
  });
431
414
  }
@@ -1258,26 +1258,60 @@ function longConnectorSpans(spans) {
1258
1258
  function longConnector(edge) {
1259
1259
  return longConnectorSpans(connectorSpans(edge));
1260
1260
  }
1261
- function betterConnectorQualityScore(candidate, current) {
1261
+ function linearDetourCount(edges) {
1262
+ const incomingCount = new Map();
1263
+ const outgoingCount = new Map();
1264
+ for (const edge of edges) {
1265
+ incomingCount.set(edge.childId, (incomingCount.get(edge.childId) ?? 0) + 1);
1266
+ outgoingCount.set(edge.parentId, (outgoingCount.get(edge.parentId) ?? 0) + 1);
1267
+ }
1268
+ return edges.filter((edge) => {
1269
+ if ((outgoingCount.get(edge.parentId) ?? 0) !== 1 ||
1270
+ (incomingCount.get(edge.childId) ?? 0) !== 1)
1271
+ return false;
1272
+ const spans = connectorSpans(edge);
1273
+ return spans.horizontal !== 0 || spans.vertical !== 1;
1274
+ }).length;
1275
+ }
1276
+ function betterConnectorQualityScore(candidate, current, aggressiveAestheticRepair = false) {
1262
1277
  if (candidate.longConnectorCount > current.longConnectorCount)
1263
1278
  return false;
1264
- const fields = [
1265
- 'crossingCount',
1266
- 'nodeIntersectionCount',
1267
- 'maximumManhattanSpan',
1268
- 'maximumHorizontalSpan',
1269
- 'longConnectorCount',
1270
- 'totalManhattanSpan',
1271
- 'totalHorizontalSpan',
1272
- 'asymmetricSiblingCohortCount',
1273
- 'maximumSiblingDeviation',
1274
- 'totalSiblingDeviation',
1275
- 'offAnchorSiblingCohortCount',
1276
- 'maximumSiblingAnchorDeviation',
1277
- 'totalSiblingAnchorDeviation',
1278
- 'columnCount',
1279
- 'boundingCenterOffsetTwice',
1280
- ];
1279
+ const fields = aggressiveAestheticRepair
1280
+ ? [
1281
+ 'crossingCount',
1282
+ 'nodeIntersectionCount',
1283
+ 'longConnectorCount',
1284
+ 'linearDetourCount',
1285
+ 'asymmetricSiblingCohortCount',
1286
+ 'maximumSiblingDeviation',
1287
+ 'totalSiblingDeviation',
1288
+ 'offAnchorSiblingCohortCount',
1289
+ 'maximumSiblingAnchorDeviation',
1290
+ 'totalSiblingAnchorDeviation',
1291
+ 'maximumManhattanSpan',
1292
+ 'maximumHorizontalSpan',
1293
+ 'totalManhattanSpan',
1294
+ 'totalHorizontalSpan',
1295
+ 'columnCount',
1296
+ 'boundingCenterOffsetTwice',
1297
+ ]
1298
+ : [
1299
+ 'crossingCount',
1300
+ 'nodeIntersectionCount',
1301
+ 'maximumManhattanSpan',
1302
+ 'maximumHorizontalSpan',
1303
+ 'longConnectorCount',
1304
+ 'totalManhattanSpan',
1305
+ 'totalHorizontalSpan',
1306
+ 'asymmetricSiblingCohortCount',
1307
+ 'maximumSiblingDeviation',
1308
+ 'totalSiblingDeviation',
1309
+ 'offAnchorSiblingCohortCount',
1310
+ 'maximumSiblingAnchorDeviation',
1311
+ 'totalSiblingAnchorDeviation',
1312
+ 'columnCount',
1313
+ 'boundingCenterOffsetTwice',
1314
+ ];
1281
1315
  for (const field of fields) {
1282
1316
  if (candidate[field] === current[field])
1283
1317
  continue;
@@ -1324,8 +1358,10 @@ function* affectedNodeIntersectionCount(context, edges, focusId, x) {
1324
1358
  }
1325
1359
  return count;
1326
1360
  }
1327
- function* connectorNodeIntersectionSummary(context, edges) {
1328
- const nodes = [...context.placed.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
1361
+ function* connectorNodeIntersectionSummary(context, edges, override) {
1362
+ const nodes = [...context.placed.values()]
1363
+ .map((node) => ({ ...node, x: override?.get(node.id) ?? node.x }))
1364
+ .sort((left, right) => compareCodeUnits(left.id, right.id));
1329
1365
  const involvedFocusIds = new Set();
1330
1366
  let count = 0;
1331
1367
  let comparisonIndex = 0;
@@ -1422,14 +1458,18 @@ function siblingQuality(context, override) {
1422
1458
  parentFocusIds,
1423
1459
  };
1424
1460
  }
1425
- function connectorQualityScore(context, edges, crossingCount, nodeIntersectionCount, symmetry) {
1461
+ function connectorQualityScore(context, edges, crossingCount, nodeIntersectionCount, symmetry, override) {
1426
1462
  const spans = edges.map(connectorSpans);
1427
- const nodes = [...context.placed.values()];
1463
+ const nodes = [...context.placed.values()].map((node) => ({
1464
+ ...node,
1465
+ x: override?.get(node.id) ?? node.x,
1466
+ }));
1428
1467
  const minimumX = Math.min(...nodes.map(({ x }) => x));
1429
1468
  const maximumX = Math.max(...nodes.map(({ x }) => x));
1430
1469
  return {
1431
1470
  crossingCount,
1432
1471
  nodeIntersectionCount,
1472
+ linearDetourCount: linearDetourCount(edges),
1433
1473
  asymmetricSiblingCohortCount: symmetry.asymmetricSiblingCohortCount,
1434
1474
  maximumSiblingDeviation: symmetry.maximumSiblingDeviation,
1435
1475
  totalSiblingDeviation: symmetry.totalSiblingDeviation,
@@ -1445,6 +1485,115 @@ function connectorQualityScore(context, edges, crossingCount, nodeIntersectionCo
1445
1485
  boundingCenterOffsetTwice: Math.abs(minimumX + maximumX),
1446
1486
  };
1447
1487
  }
1488
+ function edgesWithXOverride(edges, override) {
1489
+ return edges.map((edge) => ({
1490
+ ...edge,
1491
+ parent: { ...edge.parent, x: override.get(edge.parentId) ?? edge.parent.x },
1492
+ child: { ...edge.child, x: override.get(edge.childId) ?? edge.child.x },
1493
+ }));
1494
+ }
1495
+ function siblingBatchConflicts(context, cohort, override) {
1496
+ const moved = [...override].flatMap(([focusId, x]) => {
1497
+ const node = context.placed.get(focusId);
1498
+ return node === undefined ? [] : [{ ...node, x }];
1499
+ });
1500
+ for (const node of moved) {
1501
+ if (!coordinateWithinLane(context, node.id, node.x))
1502
+ return true;
1503
+ for (const other of context.placed.values()) {
1504
+ if (cohort.has(other.id) || other.y !== node.y)
1505
+ continue;
1506
+ if (Math.abs(other.x - node.x) < context.nodeSpacing)
1507
+ return true;
1508
+ }
1509
+ for (const excludedId of context.mutualExclusions.get(node.id) ?? []) {
1510
+ const other = context.placed.get(excludedId);
1511
+ if (other === undefined)
1512
+ continue;
1513
+ const otherX = override.get(excludedId) ?? other.x;
1514
+ if (Math.abs(otherX - node.x) < context.minimumMutualExclusionSpacing)
1515
+ return true;
1516
+ }
1517
+ }
1518
+ const ordered = [...moved].sort((left, right) => left.y - right.y || left.x - right.x || compareCodeUnits(left.id, right.id));
1519
+ for (let index = 1; index < ordered.length; index += 1) {
1520
+ const left = ordered[index - 1];
1521
+ const right = ordered[index];
1522
+ if (left?.y === right?.y &&
1523
+ left !== undefined &&
1524
+ right !== undefined &&
1525
+ Math.abs(right.x - left.x) < context.nodeSpacing)
1526
+ return true;
1527
+ }
1528
+ return false;
1529
+ }
1530
+ function* refineSiblingCohorts(context, edges, initial) {
1531
+ let current = initial;
1532
+ for (let pass = 0; pass < 2; pass += 1) {
1533
+ let accepted = false;
1534
+ for (const [cohortIndex, cohort] of context.siblingCohorts.entries()) {
1535
+ yield* cancellationCheckpoint(context.signal, cohortIndex, 8);
1536
+ if (cohort.some((focusId) => !canMoveForSoftConstraint(context, focusId)))
1537
+ continue;
1538
+ const nodes = cohort
1539
+ .flatMap((focusId) => {
1540
+ const node = context.placed.get(focusId);
1541
+ return node === undefined ? [] : [node];
1542
+ })
1543
+ .sort((left, right) => left.x - right.x || compareCodeUnits(left.id, right.id));
1544
+ if (nodes.length < 2)
1545
+ continue;
1546
+ const { anchorX } = siblingCohortAnchor(context, cohort);
1547
+ const currentSpan = (nodes.at(-1)?.x ?? 0) - (nodes[0]?.x ?? 0);
1548
+ const spacing = Math.max(context.nodeSpacing, Math.ceil(currentSpan / Math.max(1, nodes.length - 1)));
1549
+ const cohortIds = new Set(cohort);
1550
+ const internalCenter = ((nodes[0]?.x ?? 0) + (nodes.at(-1)?.x ?? 0)) / 2;
1551
+ let best;
1552
+ for (const center of [...new Set([internalCenter, anchorX])]) {
1553
+ const override = new Map();
1554
+ for (const [index, node] of nodes.entries()) {
1555
+ override.set(node.id, Math.round(center + (index - (nodes.length - 1) / 2) * spacing));
1556
+ }
1557
+ if (nodes.every((node) => override.get(node.id) === node.x))
1558
+ continue;
1559
+ if (siblingBatchConflicts(context, cohortIds, override))
1560
+ continue;
1561
+ const candidateEdges = edgesWithXOverride(edges, override);
1562
+ const crossingSummary = yield* connectorCrossings(candidateEdges, context);
1563
+ if (crossingSummary.count > current.crossingCount)
1564
+ continue;
1565
+ const intersectionSummary = yield* connectorNodeIntersectionSummary(context, candidateEdges, override);
1566
+ if (intersectionSummary.count > current.nodeIntersectionCount)
1567
+ continue;
1568
+ const symmetry = siblingQuality(context, override);
1569
+ const score = connectorQualityScore(context, candidateEdges, crossingSummary.count, intersectionSummary.count, symmetry, override);
1570
+ if (!betterConnectorQualityScore(score, current, true))
1571
+ continue;
1572
+ if (best !== undefined && !betterConnectorQualityScore(score, best.score, true))
1573
+ continue;
1574
+ best = { override, score };
1575
+ }
1576
+ if (best === undefined)
1577
+ continue;
1578
+ for (const node of nodes) {
1579
+ const x = best.override.get(node.id);
1580
+ if (x === undefined || x === node.x)
1581
+ continue;
1582
+ context.decisions.push({
1583
+ focusId: node.id,
1584
+ kind: 'moved_to_improve_connector_quality',
1585
+ message: `Sibling-balance refinement moved (${node.x}, ${node.y}) to (${x}, ${node.y})`,
1586
+ });
1587
+ movePlacedFocusX(context, node, x);
1588
+ }
1589
+ current = best.score;
1590
+ accepted = true;
1591
+ }
1592
+ if (!accepted)
1593
+ break;
1594
+ }
1595
+ return current;
1596
+ }
1448
1597
  function candidateQualityScore(context, edges, current, focusId, x, currentAffectedCrossings, candidateAffectedCrossings, currentAffectedNodeIntersections, candidateAffectedNodeIntersections) {
1449
1598
  const incidentEdges = edges.filter((edge) => edge.parentId === focusId || edge.childId === focusId);
1450
1599
  const nonIncidentEdges = edges.filter((edge) => edge.parentId !== focusId && edge.childId !== focusId);
@@ -1471,6 +1620,7 @@ function candidateQualityScore(context, edges, current, focusId, x, currentAffec
1471
1620
  nodeIntersectionCount: current.nodeIntersectionCount -
1472
1621
  currentAffectedNodeIntersections +
1473
1622
  candidateAffectedNodeIntersections,
1623
+ linearDetourCount: linearDetourCount([...nonIncidentEdges, ...candidateIncidentEdges]),
1474
1624
  asymmetricSiblingCohortCount: symmetry.asymmetricSiblingCohortCount,
1475
1625
  maximumSiblingDeviation: symmetry.maximumSiblingDeviation,
1476
1626
  totalSiblingDeviation: symmetry.totalSiblingDeviation,
@@ -1492,29 +1642,67 @@ function candidateQualityScore(context, edges, current, focusId, x, currentAffec
1492
1642
  }
1493
1643
  const CONNECTOR_REFINEMENT_WORK_MAX = 7_000_000;
1494
1644
  function* refineConnectorQualityWithinBudget(context) {
1495
- const maximumAcceptedMoves = 24;
1496
- const maximumCandidateNodes = 32;
1645
+ const maximumAcceptedMoves = context.aggressiveAestheticRepair ? 32 : 24;
1646
+ const maximumCandidateNodes = context.aggressiveAestheticRepair ? 48 : 32;
1497
1647
  const edges = yield* connectorEdges(context);
1498
1648
  const crossingSummary = yield* connectorCrossings(edges, context);
1499
1649
  const intersectionSummary = yield* connectorNodeIntersectionSummary(context, edges);
1500
1650
  const symmetry = siblingQuality(context);
1501
1651
  let current = connectorQualityScore(context, edges, crossingSummary.count, intersectionSummary.count, symmetry);
1502
- const siblingMembers = new Set(context.siblingCohorts.flat());
1652
+ if (context.aggressiveAestheticRepair)
1653
+ current = yield* refineSiblingCohorts(context, edges, current);
1503
1654
  const candidateIds = new Set(intersectionSummary.involvedFocusIds);
1655
+ const longFocusIds = new Set();
1504
1656
  for (const edge of edges.filter(longConnector)) {
1505
1657
  candidateIds.add(edge.parentId);
1506
1658
  candidateIds.add(edge.childId);
1659
+ longFocusIds.add(edge.parentId);
1660
+ longFocusIds.add(edge.childId);
1661
+ }
1662
+ const siblingFocusIds = new Set(context.siblingCohorts.flat());
1663
+ if (context.aggressiveAestheticRepair) {
1664
+ for (const cohort of context.siblingCohorts) {
1665
+ for (const focusId of cohort) {
1666
+ candidateIds.add(focusId);
1667
+ }
1668
+ }
1507
1669
  }
1508
1670
  for (const parentId of symmetry.parentFocusIds)
1509
1671
  candidateIds.add(parentId);
1672
+ const incomingCount = new Map();
1673
+ const outgoingCount = new Map();
1674
+ const linearFocusIds = new Set();
1675
+ for (const edge of context.aggressiveAestheticRepair ? edges : []) {
1676
+ incomingCount.set(edge.childId, (incomingCount.get(edge.childId) ?? 0) + 1);
1677
+ outgoingCount.set(edge.parentId, (outgoingCount.get(edge.parentId) ?? 0) + 1);
1678
+ }
1679
+ for (const edge of edges) {
1680
+ if ((outgoingCount.get(edge.parentId) ?? 0) !== 1 ||
1681
+ (incomingCount.get(edge.childId) ?? 0) !== 1)
1682
+ continue;
1683
+ const spans = connectorSpans(edge);
1684
+ if (spans.horizontal === 0 && spans.vertical === 1)
1685
+ continue;
1686
+ candidateIds.add(edge.parentId);
1687
+ candidateIds.add(edge.childId);
1688
+ linearFocusIds.add(edge.parentId);
1689
+ linearFocusIds.add(edge.childId);
1690
+ }
1510
1691
  const orderedCandidateIds = [...candidateIds]
1511
1692
  .filter((focusId) => {
1512
1693
  const degree = context.connectorDefinitionsByFocus.get(focusId)?.length ?? 0;
1513
- return (canMoveForSoftConstraint(context, focusId) && !siblingMembers.has(focusId) && degree !== 1);
1694
+ return (canMoveForSoftConstraint(context, focusId) &&
1695
+ (context.aggressiveAestheticRepair || (!siblingFocusIds.has(focusId) && degree !== 1)));
1514
1696
  })
1515
1697
  .sort((left, right) => {
1516
1698
  const degree = (focusId) => context.connectorDefinitionsByFocus.get(focusId)?.length ?? 0;
1517
- return degree(right) - degree(left) || compareCodeUnits(left, right);
1699
+ const priority = (focusId) => (intersectionSummary.involvedFocusIds.has(focusId) ? 8 : 0) +
1700
+ (longFocusIds.has(focusId) ? 4 : 0) +
1701
+ (linearFocusIds.has(focusId) ? 2 : 0) +
1702
+ (siblingFocusIds.has(focusId) ? 1 : 0);
1703
+ return (priority(right) - priority(left) ||
1704
+ degree(right) - degree(left) ||
1705
+ compareCodeUnits(left, right));
1518
1706
  })
1519
1707
  .slice(0, maximumCandidateNodes);
1520
1708
  for (let acceptedMoves = 0; acceptedMoves < maximumAcceptedMoves; acceptedMoves += 1) {
@@ -1551,9 +1739,10 @@ function* refineConnectorQualityWithinBudget(context) {
1551
1739
  continue;
1552
1740
  const candidateAffectedNodeIntersections = yield* affectedNodeIntersectionCount(context, edges, focusId, x);
1553
1741
  const score = candidateQualityScore(context, edges, current, focusId, x, currentAffectedCrossings, candidateAffectedCrossings, currentAffectedNodeIntersections, candidateAffectedNodeIntersections);
1554
- if (!betterConnectorQualityScore(score, current))
1742
+ if (!betterConnectorQualityScore(score, current, context.aggressiveAestheticRepair))
1555
1743
  continue;
1556
- if (best !== undefined && !betterConnectorQualityScore(score, best.score))
1744
+ if (best !== undefined &&
1745
+ !betterConnectorQualityScore(score, best.score, context.aggressiveAestheticRepair))
1557
1746
  continue;
1558
1747
  best = {
1559
1748
  focusId,
@@ -1955,6 +2144,7 @@ function* layoutFocusTreeSteps(plan, options = {}) {
1955
2144
  siblingCohorts: siblingLayout.cohorts,
1956
2145
  nodeSpacing,
1957
2146
  minimumMutualExclusionSpacing: nodeSpacing,
2147
+ aggressiveAestheticRepair: options.aggressiveAestheticRepair ?? false,
1958
2148
  decisions: [],
1959
2149
  diagnostics,
1960
2150
  work: options.workBudget ?? new FocusLayoutWorkBudget(),
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '2.3.11';
3
+ export const PACKAGE_VERSION = '2.3.13';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/focus.md CHANGED
@@ -30,7 +30,7 @@ For a layout-only cleanup of an existing national tree, call `hoi4.focus_rewrite
30
30
 
31
31
  Compact mode imports the tree, resets authored presentation coordinates and lane bounds, and performs a deterministic reflow in the same rewrite call. It keeps prerequisites, mutual exclusions, availability, bypasses, rewards, icons, localisation, AI behavior, and route/lane membership unchanged.
32
32
 
33
- The rewrite measures canvas bounds, same-row spacing, the same curved connector paths used by the renderer, connector lengths, connector-node intersections, branch balance, overall centering, and topology-aware fake-complexity patterns. Compact mode straightens mechanically linear chains and refuses a result with overlapping or cramped focuses, rendered crossings, connectors through unrelated focuses, long connectors, asymmetric or off-anchor sibling groups, offset linear detours, staircase or zigzag chains, an off-center canvas, or size metrics outside absolute and source-relative limits.
33
+ The rewrite measures canvas bounds, same-row spacing, the same curved connector paths used by the renderer, connector lengths, connector-node intersections, branch balance, overall centering, and topology-aware fake-complexity patterns. Compact mode evaluates preservation, row repair, linear-chain repair, sibling balancing, and combined compression strategies, then chooses the cleanest valid result. Sparse direct sibling fan-outs are packed around their parent at the minimum readable spacing, with each exclusive descendant branch moving as a unit and shared convergence focuses staying centered. Dense branches remain wider only when their contents need the room. It refuses invalid geometry such as overlapping or cramped focuses, bad parent order, rendered crossings, connectors through unrelated focuses, or an off-center canvas. Long connectors, fake detours, staircase or zigzag chains, and avoidable sibling imbalance are optimization priorities instead of independent rejection budgets.
34
34
 
35
35
  Use the default `layoutMode: "authored"` with a complete plan when deliberately designing coordinates or changing gameplay. Set `layoutMode: "compact"` with a complete plan when creating a large automatically arranged tree. Inspect and render calls remain useful before or after either rewrite mode, but a separate plan is not required for compact cleanup.
36
36
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
3
  "mcpName": "io.github.klimPaskov/hoi4-agent-tools",
4
- "version": "2.3.11",
4
+ "version": "2.3.13",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/configuration.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/continuous-focus-palette.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/custom-weighted-pool.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/event-feature-manifest.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/focus-plan.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/focus-planning-sidecar.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/gui-animation-source.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/gui-helper.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/gui-scenario.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/map-operation.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "oneOf": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/operation-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-analysis-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-compare-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-evaluate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-inspect-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-render-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-render-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-scenario-set.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-scenario-set.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-sequence-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-sequence-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-simulate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-simulate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.11/schemas/probability-sweep-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.3.13/schemas/probability-sweep-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.klimPaskov/hoi4-agent-tools",
4
4
  "title": "HOI4 Agent Tools",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
- "version": "2.3.11",
6
+ "version": "2.3.13",
7
7
  "repository": {
8
8
  "url": "https://github.com/klimPaskov/hoi4-agent-tools",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "hoi4-agent-tools",
16
- "version": "2.3.11",
16
+ "version": "2.3.13",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }