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 +6 -0
- package/dist/hoi4_agent_tools/focus/compact.js +108 -139
- package/dist/hoi4_agent_tools/focus/layout.js +219 -29
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +2 -2
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,
|
|
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
|
-
|
|
260
|
-
|
|
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.
|
|
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.
|
|
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
|
|
316
|
-
*
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
|
417
|
-
export function assertCompactLayoutQuality(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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()]
|
|
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
|
-
|
|
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) &&
|
|
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
|
-
|
|
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 &&
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
+
"version": "2.3.12",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|