hoi4-agent-tools 2.3.10 → 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 +12 -0
- package/dist/hoi4_agent_tools/focus/compact.js +139 -171
- package/dist/hoi4_agent_tools/focus/layout.js +290 -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,17 @@
|
|
|
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
|
+
|
|
9
|
+
## 2.3.11 - 2026-08-05
|
|
10
|
+
|
|
11
|
+
- Made compact focus rewrites straighten offset linear chains and reject artificial staircase or zigzag geometry.
|
|
12
|
+
- Tightened compact layout acceptance so long connectors, connector paths through focuses, crossings, asymmetric sibling groups, and off-anchor branches cannot survive a successful cleanup.
|
|
13
|
+
- Kept the stricter rewrite deterministic and verified it through the public 1,024-focus MCP workflow.
|
|
14
|
+
|
|
3
15
|
## 2.3.10 - 2026-08-04
|
|
4
16
|
|
|
5
17
|
- Routed focus operations to the unique discovered mod containing a requested source when a stale client root points at another mod.
|
|
@@ -10,28 +10,23 @@ const LARGE_COMPACT_SCALES = [0.55, 0.6, 0.7, 0.8];
|
|
|
10
10
|
const COMPACT_LAYOUT_WORK_MAX = FOCUS_LAYOUT_WORK_MAX * 10;
|
|
11
11
|
const COMPACT_HARD_DIAGNOSTICS = new Set([
|
|
12
12
|
'FOCUS_LAYOUT_CONNECTOR_CROSSING_UNSATISFIED',
|
|
13
|
+
'FOCUS_LAYOUT_CONNECTOR_THROUGH_NODE',
|
|
13
14
|
'FOCUS_LAYOUT_COORDINATE_CONFLICT',
|
|
14
15
|
'FOCUS_LAYOUT_LANE_BOUNDS_UNSATISFIED',
|
|
15
16
|
'FOCUS_LAYOUT_MUTUAL_EXCLUSION_SPACING_UNSATISFIED',
|
|
17
|
+
'FOCUS_LAYOUT_PARENT_NOT_ABOVE',
|
|
16
18
|
'FOCUS_LAYOUT_PARENT_ORDER_UNSATISFIED',
|
|
17
19
|
'FOCUS_LAYOUT_SAME_ROW_SPACING_UNSATISFIED',
|
|
18
20
|
'FOCUS_LAYOUT_VISIBLE_OVERLAP',
|
|
19
21
|
]);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
maximumManhattanSpan: maximumHorizontalSpan + maximumVerticalSpan,
|
|
29
|
-
maximumLongConnectors: Math.ceil(connectorCount * 0.05),
|
|
30
|
-
maximumNodeIntersections: Math.ceil(connectorCount * 0.12),
|
|
31
|
-
maximumSiblingDeviation: Math.max(2, Math.ceil(graphScale * 2)),
|
|
32
|
-
maximumTotalSiblingDeviation: Math.max(4, Math.ceil(focusCount * 0.5)),
|
|
33
|
-
};
|
|
34
|
-
}
|
|
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',
|
|
28
|
+
'FOCUS_LAYOUT_ZIGZAG_CHAIN',
|
|
29
|
+
]);
|
|
35
30
|
function requiredMetrics(layout) {
|
|
36
31
|
if (layout.metrics !== undefined)
|
|
37
32
|
return layout.metrics;
|
|
@@ -39,79 +34,16 @@ function requiredMetrics(layout) {
|
|
|
39
34
|
}
|
|
40
35
|
function absoluteCompactRegressions(layout) {
|
|
41
36
|
const metrics = requiredMetrics(layout);
|
|
42
|
-
const limits = compactLimits(layout.nodes.length, metrics.connectors.count);
|
|
43
37
|
return [
|
|
44
38
|
...(layout.diagnostics.some(({ code }) => COMPACT_HARD_DIAGNOSTICS.has(code))
|
|
45
39
|
? ['hardLayoutDiagnostics']
|
|
46
40
|
: []),
|
|
47
41
|
...(metrics.connectors.crossingCount > 0 ? ['connectorCrossingCount'] : []),
|
|
48
|
-
...(metrics.connectors.nodeIntersectionCount >
|
|
49
|
-
? ['connectorNodeIntersectionBudget']
|
|
50
|
-
: []),
|
|
42
|
+
...(metrics.connectors.nodeIntersectionCount > 0 ? ['connectorNodeIntersections'] : []),
|
|
51
43
|
...(metrics.spacing.tooCloseSameRowPairCount > 0 ? ['sameRowSpacing'] : []),
|
|
52
|
-
...(metrics.symmetry.maximumSiblingDeviation > 1 ? ['siblingMirrorDeviation'] : []),
|
|
53
|
-
...(metrics.symmetry.totalSiblingDeviation > metrics.symmetry.siblingCohortCount
|
|
54
|
-
? ['totalSiblingMirrorDeviation']
|
|
55
|
-
: []),
|
|
56
|
-
...(metrics.symmetry.maximumSiblingAnchorDeviation > limits.maximumSiblingDeviation
|
|
57
|
-
? ['maximumSiblingDeviationBudget']
|
|
58
|
-
: []),
|
|
59
|
-
...(metrics.symmetry.totalSiblingAnchorDeviation > limits.maximumTotalSiblingDeviation
|
|
60
|
-
? ['totalSiblingDeviationBudget']
|
|
61
|
-
: []),
|
|
62
44
|
...(metrics.symmetry.boundingCenterOffsetTwice > 1 ? ['boundingCenter'] : []),
|
|
63
|
-
...(metrics.bounds.columnCount > limits.maximumColumns ? ['columnBudget'] : []),
|
|
64
|
-
...(metrics.connectors.maximumHorizontalSpan > limits.maximumHorizontalSpan
|
|
65
|
-
? ['maximumHorizontalConnectorBudget']
|
|
66
|
-
: []),
|
|
67
|
-
...(metrics.connectors.maximumVerticalSpan > limits.maximumVerticalSpan
|
|
68
|
-
? ['maximumVerticalConnectorBudget']
|
|
69
|
-
: []),
|
|
70
|
-
...(metrics.connectors.maximumManhattanSpan > limits.maximumManhattanSpan
|
|
71
|
-
? ['maximumManhattanConnectorBudget']
|
|
72
|
-
: []),
|
|
73
|
-
...(metrics.connectors.longConnectorCount > limits.maximumLongConnectors
|
|
74
|
-
? ['longConnectorBudget']
|
|
75
|
-
: []),
|
|
76
|
-
];
|
|
77
|
-
}
|
|
78
|
-
function relativeCompactRegressions(current, proposed) {
|
|
79
|
-
const before = requiredMetrics(current);
|
|
80
|
-
const after = requiredMetrics(proposed);
|
|
81
|
-
return [
|
|
82
|
-
...(after.connectors.crossingCount > before.connectors.crossingCount
|
|
83
|
-
? ['connectorCrossingCount']
|
|
84
|
-
: []),
|
|
85
|
-
...(after.connectors.nodeIntersectionCount > before.connectors.nodeIntersectionCount
|
|
86
|
-
? ['connectorNodeIntersectionCount']
|
|
87
|
-
: []),
|
|
88
|
-
...(after.connectors.maximumHorizontalSpan > before.connectors.maximumHorizontalSpan
|
|
89
|
-
? ['maximumHorizontalConnectorSpan']
|
|
90
|
-
: []),
|
|
91
|
-
...(after.connectors.maximumVerticalSpan > before.connectors.maximumVerticalSpan
|
|
92
|
-
? ['maximumVerticalConnectorSpan']
|
|
93
|
-
: []),
|
|
94
|
-
...(after.connectors.maximumManhattanSpan > before.connectors.maximumManhattanSpan
|
|
95
|
-
? ['maximumManhattanConnectorSpan']
|
|
96
|
-
: []),
|
|
97
|
-
...(after.connectors.longConnectorCount > before.connectors.longConnectorCount
|
|
98
|
-
? ['longConnectorCount']
|
|
99
|
-
: []),
|
|
100
|
-
...(after.connectors.totalHorizontalSpan > before.connectors.totalHorizontalSpan
|
|
101
|
-
? ['totalHorizontalConnectorSpan']
|
|
102
|
-
: []),
|
|
103
|
-
...(after.connectors.totalManhattanSpan > before.connectors.totalManhattanSpan
|
|
104
|
-
? ['totalManhattanConnectorSpan']
|
|
105
|
-
: []),
|
|
106
|
-
...(after.bounds.columnCount > before.bounds.columnCount ? ['columnCount'] : []),
|
|
107
|
-
...(after.bounds.rowCount > before.bounds.rowCount ? ['rowCount'] : []),
|
|
108
45
|
];
|
|
109
46
|
}
|
|
110
|
-
function compactShape(plan, layout) {
|
|
111
|
-
return (plan.focuses.every(({ position }) => position.mode === 'auto') &&
|
|
112
|
-
plan.laneGroups.every(({ minimumX, maximumX }) => minimumX === undefined && maximumX === undefined) &&
|
|
113
|
-
absoluteCompactRegressions(layout).length === 0);
|
|
114
|
-
}
|
|
115
47
|
function prerequisiteIds(plan, focusId) {
|
|
116
48
|
const focus = plan.focuses.find(({ id }) => id === focusId);
|
|
117
49
|
return (focus?.prerequisites.groups
|
|
@@ -161,11 +93,8 @@ function compactStructuralAnchorX(plan, focus, parents, preferredX, laneBases) {
|
|
|
161
93
|
function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
162
94
|
const cohorts = new Map();
|
|
163
95
|
const laneBases = compactLaneBases(plan);
|
|
164
|
-
const childCounts = new Map();
|
|
165
96
|
for (const focus of plan.focuses) {
|
|
166
97
|
const parents = prerequisiteIds(plan, focus.id);
|
|
167
|
-
for (const parentId of parents)
|
|
168
|
-
childCounts.set(parentId, (childCounts.get(parentId) ?? 0) + 1);
|
|
169
98
|
const key = JSON.stringify([
|
|
170
99
|
parents.length === 0 ? focusLaneId(plan, focus) : null,
|
|
171
100
|
parents,
|
|
@@ -184,11 +113,6 @@ function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
|
184
113
|
const focus = plan.focuses.find(({ id }) => id === cohort[0]);
|
|
185
114
|
if (focus === undefined)
|
|
186
115
|
continue;
|
|
187
|
-
// A leaf fan can be mirrored directly around its structural parent. For
|
|
188
|
-
// rooted subtrees, moving only the entry focuses would push the long
|
|
189
|
-
// connectors into their descendants; keep the subtree envelope's center
|
|
190
|
-
// until the bounded gateway refinement can move the common parent.
|
|
191
|
-
const leafFan = cohort.every((focusId) => (childCounts.get(focusId) ?? 0) === 0);
|
|
192
116
|
const mirroredAt = (centerTwice) => {
|
|
193
117
|
const mirrored = [...coordinates];
|
|
194
118
|
for (let index = 0; index < pairCount; index += 1) {
|
|
@@ -209,25 +133,42 @@ function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
|
209
133
|
}
|
|
210
134
|
return mirrored;
|
|
211
135
|
};
|
|
212
|
-
const selfCenterTwice = (coordinates[0] ?? 0) + (coordinates.at(-1) ?? 0);
|
|
213
136
|
const anchorCenterTwice = 2 *
|
|
214
137
|
compactStructuralAnchorX(plan, focus, prerequisiteIds(plan, focus.id), preferredX, laneBases);
|
|
215
|
-
|
|
216
|
-
if (leafFan) {
|
|
217
|
-
const cohortIds = new Set(cohort);
|
|
218
|
-
const y = preferredY.get(cohort[0] ?? '');
|
|
219
|
-
const otherCoordinates = plan.focuses.flatMap((candidate) => !cohortIds.has(candidate.id) && preferredY.get(candidate.id) === y
|
|
220
|
-
? [preferredX.get(candidate.id) ?? 0]
|
|
221
|
-
: []);
|
|
222
|
-
if (mirrored.some((coordinate) => otherCoordinates.some((otherCoordinate) => Math.abs(coordinate - otherCoordinate) < COMPACT_SAME_ROW_SPACING))) {
|
|
223
|
-
mirrored = mirroredAt(selfCenterTwice);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
138
|
+
const mirrored = mirroredAt(anchorCenterTwice);
|
|
226
139
|
for (const [index, focusId] of cohort.entries())
|
|
227
140
|
preferredX.set(focusId, mirrored[index] ?? 0);
|
|
228
141
|
}
|
|
229
142
|
}
|
|
230
|
-
function
|
|
143
|
+
function straightenLinearChains(plan, preferredX, preferredY) {
|
|
144
|
+
const parentIds = new Map(plan.focuses.map((focus) => [focus.id, prerequisiteIds(plan, focus.id)]));
|
|
145
|
+
const childIds = new Map();
|
|
146
|
+
for (const [focusId, parents] of parentIds) {
|
|
147
|
+
for (const parentId of parents) {
|
|
148
|
+
const children = childIds.get(parentId) ?? [];
|
|
149
|
+
children.push(focusId);
|
|
150
|
+
childIds.set(parentId, children);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const ordered = [...plan.focuses].sort((left, right) => (preferredY.get(left.id) ?? 0) - (preferredY.get(right.id) ?? 0) ||
|
|
154
|
+
compareCodeUnits(left.id, right.id));
|
|
155
|
+
for (const focus of ordered) {
|
|
156
|
+
const parents = parentIds.get(focus.id) ?? [];
|
|
157
|
+
if (parents.length !== 1)
|
|
158
|
+
continue;
|
|
159
|
+
const parentId = parents[0];
|
|
160
|
+
if (parentId === undefined || (childIds.get(parentId)?.length ?? 0) !== 1)
|
|
161
|
+
continue;
|
|
162
|
+
const parentX = preferredX.get(parentId);
|
|
163
|
+
const parentY = preferredY.get(parentId);
|
|
164
|
+
if (parentX === undefined || parentY === undefined)
|
|
165
|
+
continue;
|
|
166
|
+
preferredX.set(focus.id, parentX);
|
|
167
|
+
preferredY.set(focus.id, parentY + 1);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function compactCandidate(plan, layout, strategy) {
|
|
171
|
+
const { scale, compressRows, mirrorSiblings, straightenChains } = strategy;
|
|
231
172
|
const minimumX = Math.min(...layout.nodes.map(({ x }) => x));
|
|
232
173
|
const maximumX = Math.max(...layout.nodes.map(({ x }) => x));
|
|
233
174
|
const minimumY = Math.min(...layout.nodes.map(({ y }) => y));
|
|
@@ -258,7 +199,10 @@ function compactCandidate(plan, layout, scale, compressRows) {
|
|
|
258
199
|
for (const [index, node] of row.entries())
|
|
259
200
|
preferredX.set(node.id, (placed[index] ?? 0) + drift);
|
|
260
201
|
}
|
|
261
|
-
|
|
202
|
+
if (mirrorSiblings)
|
|
203
|
+
mirrorSiblingCohorts(plan, preferredX, preferredY);
|
|
204
|
+
if (straightenChains)
|
|
205
|
+
straightenLinearChains(plan, preferredX, preferredY);
|
|
262
206
|
const compacted = structuredClone(plan);
|
|
263
207
|
compacted.laneGroups = compacted.laneGroups.map(({ id, label, order }) => ({
|
|
264
208
|
id,
|
|
@@ -278,14 +222,24 @@ function compactCandidate(plan, layout, scale, compressRows) {
|
|
|
278
222
|
}
|
|
279
223
|
function compactScore(layout) {
|
|
280
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;
|
|
281
227
|
return [
|
|
228
|
+
hardDiagnosticCount,
|
|
229
|
+
metrics.connectors.crossingCount,
|
|
230
|
+
metrics.connectors.nodeIntersectionCount,
|
|
231
|
+
metrics.spacing.tooCloseSameRowPairCount,
|
|
232
|
+
aestheticDiagnosticCount,
|
|
282
233
|
metrics.connectors.longConnectorCount,
|
|
283
|
-
metrics.
|
|
234
|
+
metrics.symmetry.asymmetricSiblingCohortCount,
|
|
235
|
+
metrics.symmetry.offAnchorSiblingCohortCount,
|
|
236
|
+
metrics.symmetry.totalSiblingDeviation,
|
|
237
|
+
metrics.symmetry.totalSiblingAnchorDeviation,
|
|
238
|
+
metrics.bounds.rowCount,
|
|
284
239
|
metrics.bounds.columnCount,
|
|
285
|
-
metrics.connectors.
|
|
240
|
+
metrics.connectors.maximumHorizontalSpan,
|
|
286
241
|
metrics.connectors.totalHorizontalSpan,
|
|
287
242
|
metrics.connectors.maximumManhattanSpan,
|
|
288
|
-
metrics.bounds.rowCount,
|
|
289
243
|
metrics.connectors.totalVerticalSpan,
|
|
290
244
|
layout.layoutHash,
|
|
291
245
|
];
|
|
@@ -311,44 +265,60 @@ function compactScales(focusCount) {
|
|
|
311
265
|
? [0.55, 0.65, 0.75]
|
|
312
266
|
: [0.65];
|
|
313
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
|
+
}
|
|
314
285
|
/**
|
|
315
286
|
* Produces a gameplay-neutral compact-reflow plan. Several deterministic
|
|
316
|
-
* compression candidates are measured with the same layout engine
|
|
317
|
-
*
|
|
318
|
-
* 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.
|
|
319
289
|
*/
|
|
320
290
|
export function compactFocusTreePlan(plan) {
|
|
321
|
-
const currentLayout = layoutFocusTree(plan);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 });
|
|
328
303
|
let selected;
|
|
329
304
|
let fallback;
|
|
330
|
-
for (const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
if (error instanceof ServiceError && error.code === 'FOCUS_LAYOUT_WORK_BUDGET_BLOCKED')
|
|
339
|
-
continue;
|
|
340
|
-
throw error;
|
|
341
|
-
}
|
|
342
|
-
if (fallback === undefined ||
|
|
343
|
-
betterScore(compactScore(layout), compactScore(fallback.layout)))
|
|
344
|
-
fallback = { plan: candidate, layout };
|
|
345
|
-
if (absoluteCompactRegressions(layout).length > 0 ||
|
|
346
|
-
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')
|
|
347
313
|
continue;
|
|
348
|
-
|
|
349
|
-
betterScore(compactScore(layout), compactScore(selected.layout)))
|
|
350
|
-
selected = { plan: candidate, layout };
|
|
314
|
+
throw error;
|
|
351
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 };
|
|
352
322
|
}
|
|
353
323
|
if (selected !== undefined)
|
|
354
324
|
return selected.plan;
|
|
@@ -366,42 +336,44 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
|
|
|
366
336
|
const workBudget = new FocusLayoutWorkBudget(options.maximumWork ?? COMPACT_LAYOUT_WORK_MAX);
|
|
367
337
|
const layoutOptions = {
|
|
368
338
|
workBudget,
|
|
339
|
+
aggressiveAestheticRepair: true,
|
|
369
340
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
370
341
|
};
|
|
371
342
|
const currentLayout = await layoutFocusTreeAsync(plan, layoutOptions);
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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);
|
|
377
354
|
let selected;
|
|
378
355
|
let fallback;
|
|
379
356
|
let exhausted;
|
|
380
|
-
candidateSearch: for (const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
break candidateSearch;
|
|
392
|
-
}
|
|
393
|
-
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;
|
|
394
368
|
}
|
|
395
|
-
|
|
396
|
-
betterScore(compactScore(layout), compactScore(fallback.layout)))
|
|
397
|
-
fallback = { plan: candidate, layout };
|
|
398
|
-
if (absoluteCompactRegressions(layout).length > 0 ||
|
|
399
|
-
relativeCompactRegressions(currentLayout, layout).length > 0)
|
|
400
|
-
continue;
|
|
401
|
-
if (selected === undefined ||
|
|
402
|
-
betterScore(compactScore(layout), compactScore(selected.layout)))
|
|
403
|
-
selected = { plan: candidate, layout };
|
|
369
|
+
throw error;
|
|
404
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 };
|
|
405
377
|
}
|
|
406
378
|
if (selected !== undefined) {
|
|
407
379
|
return { plan: selected.plan, currentLayout, proposedLayout: selected.layout };
|
|
@@ -414,19 +386,15 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
|
|
|
414
386
|
}
|
|
415
387
|
throw new ServiceError('FOCUS_COMPACT_LAYOUT_REQUIRED', 'Compact focus planning did not produce a measurable layout candidate', { treeId: plan.id });
|
|
416
388
|
}
|
|
417
|
-
/** Refuses compact rewrites that
|
|
418
|
-
export function assertCompactLayoutQuality(
|
|
419
|
-
const regressions = [
|
|
420
|
-
...absoluteCompactRegressions(proposed),
|
|
421
|
-
...(current === undefined ? [] : relativeCompactRegressions(current, proposed)),
|
|
422
|
-
].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);
|
|
423
392
|
if (regressions.length === 0)
|
|
424
393
|
return;
|
|
425
|
-
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', {
|
|
426
395
|
treeId: proposed.treeId,
|
|
427
396
|
regressions,
|
|
428
|
-
|
|
429
|
-
...(current === undefined ? {} : { before: requiredMetrics(current) }),
|
|
397
|
+
...(_current === undefined ? {} : { before: requiredMetrics(_current) }),
|
|
430
398
|
proposed: requiredMetrics(proposed),
|
|
431
399
|
});
|
|
432
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,
|
|
@@ -1620,6 +1809,76 @@ function* connectorLengthDiagnostics(context, edges) {
|
|
|
1620
1809
|
});
|
|
1621
1810
|
}
|
|
1622
1811
|
}
|
|
1812
|
+
function* fakeComplexityDiagnostics(context, edges) {
|
|
1813
|
+
const incomingCount = new Map();
|
|
1814
|
+
const outgoingCount = new Map();
|
|
1815
|
+
for (const edge of edges) {
|
|
1816
|
+
incomingCount.set(edge.childId, (incomingCount.get(edge.childId) ?? 0) + 1);
|
|
1817
|
+
outgoingCount.set(edge.parentId, (outgoingCount.get(edge.parentId) ?? 0) + 1);
|
|
1818
|
+
}
|
|
1819
|
+
const linearEdges = edges.filter(({ parentId, childId }) => (outgoingCount.get(parentId) ?? 0) === 1 && (incomingCount.get(childId) ?? 0) === 1);
|
|
1820
|
+
const next = new Map(linearEdges.map((edge) => [edge.parentId, edge]));
|
|
1821
|
+
const linearChildren = new Set(linearEdges.map(({ childId }) => childId));
|
|
1822
|
+
for (const [edgeIndex, edge] of linearEdges.entries()) {
|
|
1823
|
+
yield* cancellationCheckpoint(context.signal, edgeIndex, 32);
|
|
1824
|
+
const spans = connectorSpans(edge);
|
|
1825
|
+
if (spans.horizontal === 0 && spans.vertical === 1)
|
|
1826
|
+
continue;
|
|
1827
|
+
const focus = context.focuses.get(edge.childId);
|
|
1828
|
+
context.diagnostics.push({
|
|
1829
|
+
code: 'FOCUS_LAYOUT_LINEAR_DETOUR',
|
|
1830
|
+
severity: 'warning',
|
|
1831
|
+
category: 'layout',
|
|
1832
|
+
message: `Mechanically linear connector ${edge.parentId} -> ${edge.childId} uses an unnecessary visual detour`,
|
|
1833
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1834
|
+
details: {
|
|
1835
|
+
parentId: edge.parentId,
|
|
1836
|
+
childId: edge.childId,
|
|
1837
|
+
horizontalSpan: spans.horizontal,
|
|
1838
|
+
verticalSpan: spans.vertical,
|
|
1839
|
+
expectedHorizontalSpan: 0,
|
|
1840
|
+
expectedVerticalSpan: 1,
|
|
1841
|
+
},
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
const starts = linearEdges
|
|
1845
|
+
.filter(({ parentId }) => !linearChildren.has(parentId))
|
|
1846
|
+
.sort((left, right) => compareCodeUnits(left.parentId, right.parentId));
|
|
1847
|
+
for (const start of starts) {
|
|
1848
|
+
const chain = [];
|
|
1849
|
+
const visited = new Set();
|
|
1850
|
+
let current = start;
|
|
1851
|
+
while (current !== undefined && !visited.has(current.parentId)) {
|
|
1852
|
+
visited.add(current.parentId);
|
|
1853
|
+
chain.push(current);
|
|
1854
|
+
current = next.get(current.childId);
|
|
1855
|
+
}
|
|
1856
|
+
const horizontalDirections = chain
|
|
1857
|
+
.map(({ parent, child }) => Math.sign(child.x - parent.x))
|
|
1858
|
+
.filter((direction) => direction !== 0);
|
|
1859
|
+
if (horizontalDirections.length < 2)
|
|
1860
|
+
continue;
|
|
1861
|
+
const sameDirection = horizontalDirections.every((direction) => direction === horizontalDirections[0]);
|
|
1862
|
+
const alternating = horizontalDirections.every((direction, index) => index === 0 || direction === -horizontalDirections[index - 1]);
|
|
1863
|
+
const code = sameDirection
|
|
1864
|
+
? 'FOCUS_LAYOUT_STAIRCASE_CHAIN'
|
|
1865
|
+
: alternating
|
|
1866
|
+
? 'FOCUS_LAYOUT_ZIGZAG_CHAIN'
|
|
1867
|
+
: undefined;
|
|
1868
|
+
if (code === undefined)
|
|
1869
|
+
continue;
|
|
1870
|
+
const focusIds = [chain[0].parentId, ...chain.map(({ childId }) => childId)];
|
|
1871
|
+
const focus = context.focuses.get(focusIds.at(-1) ?? '');
|
|
1872
|
+
context.diagnostics.push({
|
|
1873
|
+
code,
|
|
1874
|
+
severity: 'warning',
|
|
1875
|
+
category: 'layout',
|
|
1876
|
+
message: `${sameDirection ? 'Staircase' : 'Zigzag'} geometry makes the linear chain ${focusIds.join(' -> ')} look artificially complex`,
|
|
1877
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1878
|
+
details: { focusIds, horizontalDirections },
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1623
1882
|
function siblingCohortAnchor(context, cohort) {
|
|
1624
1883
|
const focus = context.focuses.get(cohort[0] ?? '');
|
|
1625
1884
|
const coordinates = cohort
|
|
@@ -1885,6 +2144,7 @@ function* layoutFocusTreeSteps(plan, options = {}) {
|
|
|
1885
2144
|
siblingCohorts: siblingLayout.cohorts,
|
|
1886
2145
|
nodeSpacing,
|
|
1887
2146
|
minimumMutualExclusionSpacing: nodeSpacing,
|
|
2147
|
+
aggressiveAestheticRepair: options.aggressiveAestheticRepair ?? false,
|
|
1888
2148
|
decisions: [],
|
|
1889
2149
|
diagnostics,
|
|
1890
2150
|
work: options.workBudget ?? new FocusLayoutWorkBudget(),
|
|
@@ -1917,6 +2177,7 @@ function* layoutFocusTreeSteps(plan, options = {}) {
|
|
|
1917
2177
|
const nodeIntersectionCount = yield* connectorNodeIntersectionDiagnostics(context, edges);
|
|
1918
2178
|
yield* connectorCrossingDiagnostics(context, crossingSummary.samples);
|
|
1919
2179
|
yield* connectorLengthDiagnostics(context, edges);
|
|
2180
|
+
yield* fakeComplexityDiagnostics(context, edges);
|
|
1920
2181
|
const symmetry = yield* siblingSymmetryDiagnostics(context);
|
|
1921
2182
|
const nodes = [...context.placed.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1922
2183
|
const decisions = [...context.decisions].sort((left, right) => compareCodeUnits(left.focusId, right.focusId));
|
|
@@ -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, and
|
|
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
|
}
|