hoi4-agent-tools 2.3.11 → 2.3.12

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.3.12 - 2026-08-05
4
+
5
+ - Made compact focus rewrites choose among preservation, row compression, linear-chain repair, sibling balancing, and combined strategies instead of forcing one aggressive transformation.
6
+ - Limited rewrite rejection to invalid geometry while retaining long paths, fake complexity, branch balance, and compactness as prioritized cleanup objectives.
7
+ - 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.
8
+
3
9
  ## 2.3.11 - 2026-08-05
4
10
 
5
11
  - 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
- ];
67
- }
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
45
  ];
99
46
  }
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
@@ -225,7 +167,8 @@ function straightenLinearChains(plan, preferredX, preferredY) {
225
167
  preferredY.set(focus.id, parentY + 1);
226
168
  }
227
169
  }
228
- function compactCandidate(plan, layout, scale, compressRows) {
170
+ function compactCandidate(plan, layout, strategy) {
171
+ const { scale, compressRows, mirrorSiblings, straightenChains } = strategy;
229
172
  const minimumX = Math.min(...layout.nodes.map(({ x }) => x));
230
173
  const maximumX = Math.max(...layout.nodes.map(({ x }) => x));
231
174
  const minimumY = Math.min(...layout.nodes.map(({ y }) => y));
@@ -256,8 +199,10 @@ function compactCandidate(plan, layout, scale, compressRows) {
256
199
  for (const [index, node] of row.entries())
257
200
  preferredX.set(node.id, (placed[index] ?? 0) + drift);
258
201
  }
259
- mirrorSiblingCohorts(plan, preferredX, preferredY);
260
- straightenLinearChains(plan, preferredX, preferredY);
202
+ if (mirrorSiblings)
203
+ mirrorSiblingCohorts(plan, preferredX, preferredY);
204
+ if (straightenChains)
205
+ straightenLinearChains(plan, preferredX, preferredY);
261
206
  const compacted = structuredClone(plan);
262
207
  compacted.laneGroups = compacted.laneGroups.map(({ id, label, order }) => ({
263
208
  id,
@@ -277,14 +222,24 @@ function compactCandidate(plan, layout, scale, compressRows) {
277
222
  }
278
223
  function compactScore(layout) {
279
224
  const metrics = requiredMetrics(layout);
225
+ const hardDiagnosticCount = layout.diagnostics.filter(({ code }) => COMPACT_HARD_DIAGNOSTICS.has(code)).length;
226
+ const aestheticDiagnosticCount = layout.diagnostics.filter(({ code }) => COMPACT_AESTHETIC_DIAGNOSTICS.has(code)).length;
280
227
  return [
228
+ hardDiagnosticCount,
229
+ metrics.connectors.crossingCount,
230
+ metrics.connectors.nodeIntersectionCount,
231
+ metrics.spacing.tooCloseSameRowPairCount,
232
+ aestheticDiagnosticCount,
281
233
  metrics.connectors.longConnectorCount,
282
- metrics.connectors.maximumHorizontalSpan,
234
+ metrics.symmetry.asymmetricSiblingCohortCount,
235
+ metrics.symmetry.offAnchorSiblingCohortCount,
236
+ metrics.symmetry.totalSiblingDeviation,
237
+ metrics.symmetry.totalSiblingAnchorDeviation,
238
+ metrics.bounds.rowCount,
283
239
  metrics.bounds.columnCount,
284
- metrics.connectors.nodeIntersectionCount,
240
+ metrics.connectors.maximumHorizontalSpan,
285
241
  metrics.connectors.totalHorizontalSpan,
286
242
  metrics.connectors.maximumManhattanSpan,
287
- metrics.bounds.rowCount,
288
243
  metrics.connectors.totalVerticalSpan,
289
244
  layout.layoutHash,
290
245
  ];
@@ -310,44 +265,60 @@ function compactScales(focusCount) {
310
265
  ? [0.55, 0.65, 0.75]
311
266
  : [0.65];
312
267
  }
268
+ function compactStrategies(focusCount) {
269
+ const preservationStrategies = [
270
+ { scale: 1, compressRows: false, mirrorSiblings: false, straightenChains: false },
271
+ { scale: 1, compressRows: true, mirrorSiblings: false, straightenChains: false },
272
+ { scale: 1, compressRows: false, mirrorSiblings: false, straightenChains: true },
273
+ { scale: 1, compressRows: false, mirrorSiblings: true, straightenChains: false },
274
+ ];
275
+ const compactStrategies = compactScales(focusCount).flatMap((scale) => [
276
+ { scale, compressRows: false, mirrorSiblings: true, straightenChains: true },
277
+ { scale, compressRows: true, mirrorSiblings: true, straightenChains: true },
278
+ ]);
279
+ return [...preservationStrategies, ...compactStrategies];
280
+ }
281
+ function needsPresentationNormalization(plan) {
282
+ return (plan.focuses.some(({ position }) => position.mode !== 'auto') ||
283
+ plan.laneGroups.some(({ minimumX, maximumX }) => minimumX !== undefined || maximumX !== undefined));
284
+ }
313
285
  /**
314
286
  * 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.
287
+ * repair and compression candidates are measured with the same layout engine.
288
+ * Invalid geometry cannot win; aesthetic tradeoffs are resolved by the score.
318
289
  */
319
290
  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);
291
+ const currentLayout = layoutFocusTree(plan, { aggressiveAestheticRepair: true });
292
+ const normalizedPlan = needsPresentationNormalization(plan)
293
+ ? compactCandidate(plan, currentLayout, {
294
+ scale: 1,
295
+ compressRows: false,
296
+ mirrorSiblings: false,
297
+ straightenChains: false,
298
+ })
299
+ : plan;
300
+ const normalizedLayout = normalizedPlan === plan
301
+ ? currentLayout
302
+ : layoutFocusTree(normalizedPlan, { aggressiveAestheticRepair: true });
327
303
  let selected;
328
304
  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)
305
+ for (const strategy of compactStrategies(plan.focuses.length)) {
306
+ const candidate = compactCandidate(normalizedPlan, normalizedLayout, strategy);
307
+ let layout;
308
+ try {
309
+ layout = layoutFocusTree(candidate, { aggressiveAestheticRepair: true });
310
+ }
311
+ catch (error) {
312
+ if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED')
346
313
  continue;
347
- if (selected === undefined ||
348
- betterScore(compactScore(layout), compactScore(selected.layout)))
349
- selected = { plan: candidate, layout };
314
+ throw error;
350
315
  }
316
+ if (fallback === undefined || betterScore(compactScore(layout), compactScore(fallback.layout)))
317
+ fallback = { plan: candidate, layout };
318
+ if (absoluteCompactRegressions(layout).length > 0)
319
+ continue;
320
+ if (selected === undefined || betterScore(compactScore(layout), compactScore(selected.layout)))
321
+ selected = { plan: candidate, layout };
351
322
  }
352
323
  if (selected !== undefined)
353
324
  return selected.plan;
@@ -365,42 +336,44 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
365
336
  const workBudget = new FocusLayoutWorkBudget(options.maximumWork ?? COMPACT_LAYOUT_WORK_MAX);
366
337
  const layoutOptions = {
367
338
  workBudget,
339
+ aggressiveAestheticRepair: true,
368
340
  ...(options.signal === undefined ? {} : { signal: options.signal }),
369
341
  };
370
342
  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
- }
343
+ const normalizedPlan = needsPresentationNormalization(plan)
344
+ ? compactCandidate(plan, currentLayout, {
345
+ scale: 1,
346
+ compressRows: false,
347
+ mirrorSiblings: false,
348
+ straightenChains: false,
349
+ })
350
+ : plan;
351
+ const normalizedLayout = normalizedPlan === plan
352
+ ? currentLayout
353
+ : await layoutFocusTreeAsync(normalizedPlan, layoutOptions);
376
354
  let selected;
377
355
  let fallback;
378
356
  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;
357
+ candidateSearch: for (const strategy of compactStrategies(plan.focuses.length)) {
358
+ options.signal?.throwIfAborted();
359
+ const candidate = compactCandidate(normalizedPlan, normalizedLayout, strategy);
360
+ let layout;
361
+ try {
362
+ layout = await layoutFocusTreeAsync(candidate, layoutOptions);
363
+ }
364
+ catch (error) {
365
+ if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED') {
366
+ exhausted = error;
367
+ break candidateSearch;
393
368
  }
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 };
369
+ throw error;
403
370
  }
371
+ if (fallback === undefined || betterScore(compactScore(layout), compactScore(fallback.layout)))
372
+ fallback = { plan: candidate, layout };
373
+ if (absoluteCompactRegressions(layout).length > 0)
374
+ continue;
375
+ if (selected === undefined || betterScore(compactScore(layout), compactScore(selected.layout)))
376
+ selected = { plan: candidate, layout };
404
377
  }
405
378
  if (selected !== undefined) {
406
379
  return { plan: selected.plan, currentLayout, proposedLayout: selected.layout };
@@ -413,19 +386,15 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
413
386
  }
414
387
  throw new ServiceError('FOCUS_COMPACT_LAYOUT_REQUIRED', 'Compact focus planning did not produce a measurable layout candidate', { treeId: plan.id });
415
388
  }
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);
389
+ /** Refuses compact rewrites that still contain invalid or unreadable geometry. */
390
+ export function assertCompactLayoutQuality(_current, proposed) {
391
+ const regressions = [...absoluteCompactRegressions(proposed)].filter((value, index, all) => all.indexOf(value) === index);
422
392
  if (regressions.length === 0)
423
393
  return;
424
- throw new ServiceError('FOCUS_COMPACT_QUALITY_BLOCKED', 'Compact focus reflow failed the absolute or relative layout-quality gate', {
394
+ throw new ServiceError('FOCUS_COMPACT_QUALITY_BLOCKED', 'Compact focus reflow failed the layout-correctness gate', {
425
395
  treeId: proposed.treeId,
426
396
  regressions,
427
- limits: compactLimits(proposed.nodes.length),
428
- ...(current === undefined ? {} : { before: requiredMetrics(current) }),
397
+ ...(_current === undefined ? {} : { before: requiredMetrics(_current) }),
429
398
  proposed: requiredMetrics(proposed),
430
399
  });
431
400
  }
@@ -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.12';
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. 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.12",
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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12/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.12",
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.12",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }