hoi4-agent-tools 2.3.9 → 2.3.11
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 +11 -0
- package/dist/hoi4_agent_tools/core/workspace.js +24 -0
- package/dist/hoi4_agent_tools/focus/compact.js +44 -45
- package/dist/hoi4_agent_tools/focus/layout.js +71 -0
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +17 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +4 -4
- 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,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.3.11 - 2026-08-05
|
|
4
|
+
|
|
5
|
+
- Made compact focus rewrites straighten offset linear chains and reject artificial staircase or zigzag geometry.
|
|
6
|
+
- Tightened compact layout acceptance so long connectors, connector paths through focuses, crossings, asymmetric sibling groups, and off-anchor branches cannot survive a successful cleanup.
|
|
7
|
+
- Kept the stricter rewrite deterministic and verified it through the public 1,024-focus MCP workflow.
|
|
8
|
+
|
|
9
|
+
## 2.3.10 - 2026-08-04
|
|
10
|
+
|
|
11
|
+
- Routed focus operations to the unique discovered mod containing a requested source when a stale client root points at another mod.
|
|
12
|
+
- Added regression coverage for a stale client context requesting Ireland's focus source without a workspace selector.
|
|
13
|
+
|
|
3
14
|
## 2.3.9 - 2026-08-04
|
|
4
15
|
|
|
5
16
|
- Bound omitted-workspace tool calls to the connected MCP client's active filesystem root instead of the server process's original working directory.
|
|
@@ -261,6 +261,30 @@ export class WorkspaceResolver {
|
|
|
261
261
|
}
|
|
262
262
|
throw new ServiceError('WORKSPACE_CONTEXT_AMBIGUOUS', 'The active MCP client roots identify more than one HOI4 mod workspace', { workspaceIds: matches.map(({ id }) => id) });
|
|
263
263
|
}
|
|
264
|
+
/** Find one mod containing an existing source path when a stale client root selected another mod. */
|
|
265
|
+
async resolveUniqueModSourceWorkspaceId(relativePath, principal, signal) {
|
|
266
|
+
const matches = [];
|
|
267
|
+
for (const workspace of this.list(principal)) {
|
|
268
|
+
signal?.throwIfAborted();
|
|
269
|
+
if (workspace.registration.kind !== 'mod')
|
|
270
|
+
continue;
|
|
271
|
+
try {
|
|
272
|
+
const resolved = await this.resolvePath(workspace.id, relativePath, 'read', ['mod'], principal);
|
|
273
|
+
signal?.throwIfAborted();
|
|
274
|
+
if ((await lstat(resolved.path)).isFile())
|
|
275
|
+
matches.push(workspace.id);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.name === 'AbortError')
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (matches.length === 0)
|
|
283
|
+
return undefined;
|
|
284
|
+
if (matches.length === 1)
|
|
285
|
+
return matches[0];
|
|
286
|
+
throw new ServiceError('WORKSPACE_SOURCE_AMBIGUOUS', `The requested source exists in more than one mod workspace: ${relativePath}`, { relativePath, workspaceIds: matches });
|
|
287
|
+
}
|
|
264
288
|
resolveWorkspaceId(workspaceId, principal) {
|
|
265
289
|
if (workspaceId === CURRENT_WORKSPACE_ID)
|
|
266
290
|
return this.resolveCurrentWorkspaceId(principal);
|
|
@@ -10,14 +10,18 @@ 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',
|
|
15
|
+
'FOCUS_LAYOUT_LINEAR_DETOUR',
|
|
14
16
|
'FOCUS_LAYOUT_LANE_BOUNDS_UNSATISFIED',
|
|
15
17
|
'FOCUS_LAYOUT_MUTUAL_EXCLUSION_SPACING_UNSATISFIED',
|
|
16
18
|
'FOCUS_LAYOUT_PARENT_ORDER_UNSATISFIED',
|
|
17
19
|
'FOCUS_LAYOUT_SAME_ROW_SPACING_UNSATISFIED',
|
|
20
|
+
'FOCUS_LAYOUT_STAIRCASE_CHAIN',
|
|
18
21
|
'FOCUS_LAYOUT_VISIBLE_OVERLAP',
|
|
22
|
+
'FOCUS_LAYOUT_ZIGZAG_CHAIN',
|
|
19
23
|
]);
|
|
20
|
-
function compactLimits(focusCount
|
|
24
|
+
function compactLimits(focusCount) {
|
|
21
25
|
const graphScale = Math.sqrt(Math.max(1, focusCount));
|
|
22
26
|
const maximumHorizontalSpan = Math.max(14, Math.ceil(graphScale * 3));
|
|
23
27
|
const maximumVerticalSpan = Math.max(4, Math.ceil(Math.log2(Math.max(2, focusCount))));
|
|
@@ -26,10 +30,6 @@ function compactLimits(focusCount, connectorCount) {
|
|
|
26
30
|
maximumHorizontalSpan,
|
|
27
31
|
maximumVerticalSpan,
|
|
28
32
|
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
33
|
};
|
|
34
34
|
}
|
|
35
35
|
function requiredMetrics(layout) {
|
|
@@ -39,26 +39,18 @@ function requiredMetrics(layout) {
|
|
|
39
39
|
}
|
|
40
40
|
function absoluteCompactRegressions(layout) {
|
|
41
41
|
const metrics = requiredMetrics(layout);
|
|
42
|
-
const limits = compactLimits(layout.nodes.length
|
|
42
|
+
const limits = compactLimits(layout.nodes.length);
|
|
43
43
|
return [
|
|
44
44
|
...(layout.diagnostics.some(({ code }) => COMPACT_HARD_DIAGNOSTICS.has(code))
|
|
45
45
|
? ['hardLayoutDiagnostics']
|
|
46
46
|
: []),
|
|
47
47
|
...(metrics.connectors.crossingCount > 0 ? ['connectorCrossingCount'] : []),
|
|
48
|
-
...(metrics.connectors.nodeIntersectionCount >
|
|
49
|
-
? ['connectorNodeIntersectionBudget']
|
|
50
|
-
: []),
|
|
48
|
+
...(metrics.connectors.nodeIntersectionCount > 0 ? ['connectorNodeIntersections'] : []),
|
|
51
49
|
...(metrics.spacing.tooCloseSameRowPairCount > 0 ? ['sameRowSpacing'] : []),
|
|
52
|
-
...(metrics.symmetry.
|
|
53
|
-
...(metrics.symmetry.totalSiblingDeviation >
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
...(metrics.symmetry.maximumSiblingAnchorDeviation > limits.maximumSiblingDeviation
|
|
57
|
-
? ['maximumSiblingDeviationBudget']
|
|
58
|
-
: []),
|
|
59
|
-
...(metrics.symmetry.totalSiblingAnchorDeviation > limits.maximumTotalSiblingDeviation
|
|
60
|
-
? ['totalSiblingDeviationBudget']
|
|
61
|
-
: []),
|
|
50
|
+
...(metrics.symmetry.asymmetricSiblingCohortCount > 0 ? ['siblingMirrorDeviation'] : []),
|
|
51
|
+
...(metrics.symmetry.totalSiblingDeviation > 0 ? ['totalSiblingMirrorDeviation'] : []),
|
|
52
|
+
...(metrics.symmetry.offAnchorSiblingCohortCount > 0 ? ['siblingAnchorDeviation'] : []),
|
|
53
|
+
...(metrics.symmetry.totalSiblingAnchorDeviation > 0 ? ['totalSiblingAnchorDeviation'] : []),
|
|
62
54
|
...(metrics.symmetry.boundingCenterOffsetTwice > 1 ? ['boundingCenter'] : []),
|
|
63
55
|
...(metrics.bounds.columnCount > limits.maximumColumns ? ['columnBudget'] : []),
|
|
64
56
|
...(metrics.connectors.maximumHorizontalSpan > limits.maximumHorizontalSpan
|
|
@@ -70,9 +62,7 @@ function absoluteCompactRegressions(layout) {
|
|
|
70
62
|
...(metrics.connectors.maximumManhattanSpan > limits.maximumManhattanSpan
|
|
71
63
|
? ['maximumManhattanConnectorBudget']
|
|
72
64
|
: []),
|
|
73
|
-
...(metrics.connectors.longConnectorCount >
|
|
74
|
-
? ['longConnectorBudget']
|
|
75
|
-
: []),
|
|
65
|
+
...(metrics.connectors.longConnectorCount > 0 ? ['longConnectors'] : []),
|
|
76
66
|
];
|
|
77
67
|
}
|
|
78
68
|
function relativeCompactRegressions(current, proposed) {
|
|
@@ -161,11 +151,8 @@ function compactStructuralAnchorX(plan, focus, parents, preferredX, laneBases) {
|
|
|
161
151
|
function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
162
152
|
const cohorts = new Map();
|
|
163
153
|
const laneBases = compactLaneBases(plan);
|
|
164
|
-
const childCounts = new Map();
|
|
165
154
|
for (const focus of plan.focuses) {
|
|
166
155
|
const parents = prerequisiteIds(plan, focus.id);
|
|
167
|
-
for (const parentId of parents)
|
|
168
|
-
childCounts.set(parentId, (childCounts.get(parentId) ?? 0) + 1);
|
|
169
156
|
const key = JSON.stringify([
|
|
170
157
|
parents.length === 0 ? focusLaneId(plan, focus) : null,
|
|
171
158
|
parents,
|
|
@@ -184,11 +171,6 @@ function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
|
184
171
|
const focus = plan.focuses.find(({ id }) => id === cohort[0]);
|
|
185
172
|
if (focus === undefined)
|
|
186
173
|
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
174
|
const mirroredAt = (centerTwice) => {
|
|
193
175
|
const mirrored = [...coordinates];
|
|
194
176
|
for (let index = 0; index < pairCount; index += 1) {
|
|
@@ -209,24 +191,40 @@ function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
|
209
191
|
}
|
|
210
192
|
return mirrored;
|
|
211
193
|
};
|
|
212
|
-
const selfCenterTwice = (coordinates[0] ?? 0) + (coordinates.at(-1) ?? 0);
|
|
213
194
|
const anchorCenterTwice = 2 *
|
|
214
195
|
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
|
-
}
|
|
196
|
+
const mirrored = mirroredAt(anchorCenterTwice);
|
|
226
197
|
for (const [index, focusId] of cohort.entries())
|
|
227
198
|
preferredX.set(focusId, mirrored[index] ?? 0);
|
|
228
199
|
}
|
|
229
200
|
}
|
|
201
|
+
function straightenLinearChains(plan, preferredX, preferredY) {
|
|
202
|
+
const parentIds = new Map(plan.focuses.map((focus) => [focus.id, prerequisiteIds(plan, focus.id)]));
|
|
203
|
+
const childIds = new Map();
|
|
204
|
+
for (const [focusId, parents] of parentIds) {
|
|
205
|
+
for (const parentId of parents) {
|
|
206
|
+
const children = childIds.get(parentId) ?? [];
|
|
207
|
+
children.push(focusId);
|
|
208
|
+
childIds.set(parentId, children);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const ordered = [...plan.focuses].sort((left, right) => (preferredY.get(left.id) ?? 0) - (preferredY.get(right.id) ?? 0) ||
|
|
212
|
+
compareCodeUnits(left.id, right.id));
|
|
213
|
+
for (const focus of ordered) {
|
|
214
|
+
const parents = parentIds.get(focus.id) ?? [];
|
|
215
|
+
if (parents.length !== 1)
|
|
216
|
+
continue;
|
|
217
|
+
const parentId = parents[0];
|
|
218
|
+
if (parentId === undefined || (childIds.get(parentId)?.length ?? 0) !== 1)
|
|
219
|
+
continue;
|
|
220
|
+
const parentX = preferredX.get(parentId);
|
|
221
|
+
const parentY = preferredY.get(parentId);
|
|
222
|
+
if (parentX === undefined || parentY === undefined)
|
|
223
|
+
continue;
|
|
224
|
+
preferredX.set(focus.id, parentX);
|
|
225
|
+
preferredY.set(focus.id, parentY + 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
230
228
|
function compactCandidate(plan, layout, scale, compressRows) {
|
|
231
229
|
const minimumX = Math.min(...layout.nodes.map(({ x }) => x));
|
|
232
230
|
const maximumX = Math.max(...layout.nodes.map(({ x }) => x));
|
|
@@ -259,6 +257,7 @@ function compactCandidate(plan, layout, scale, compressRows) {
|
|
|
259
257
|
preferredX.set(node.id, (placed[index] ?? 0) + drift);
|
|
260
258
|
}
|
|
261
259
|
mirrorSiblingCohorts(plan, preferredX, preferredY);
|
|
260
|
+
straightenLinearChains(plan, preferredX, preferredY);
|
|
262
261
|
const compacted = structuredClone(plan);
|
|
263
262
|
compacted.laneGroups = compacted.laneGroups.map(({ id, label, order }) => ({
|
|
264
263
|
id,
|
|
@@ -328,7 +327,7 @@ export function compactFocusTreePlan(plan) {
|
|
|
328
327
|
let selected;
|
|
329
328
|
let fallback;
|
|
330
329
|
for (const scale of scales) {
|
|
331
|
-
for (const compressRows of
|
|
330
|
+
for (const compressRows of [false, true]) {
|
|
332
331
|
const candidate = compactCandidate(plan, currentLayout, scale, compressRows);
|
|
333
332
|
let layout;
|
|
334
333
|
try {
|
|
@@ -378,7 +377,7 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
|
|
|
378
377
|
let fallback;
|
|
379
378
|
let exhausted;
|
|
380
379
|
candidateSearch: for (const scale of compactScales(plan.focuses.length)) {
|
|
381
|
-
for (const compressRows of
|
|
380
|
+
for (const compressRows of [false, true]) {
|
|
382
381
|
options.signal?.throwIfAborted();
|
|
383
382
|
const candidate = compactCandidate(plan, currentLayout, scale, compressRows);
|
|
384
383
|
let layout;
|
|
@@ -425,7 +424,7 @@ export function assertCompactLayoutQuality(current, proposed) {
|
|
|
425
424
|
throw new ServiceError('FOCUS_COMPACT_QUALITY_BLOCKED', 'Compact focus reflow failed the absolute or relative layout-quality gate', {
|
|
426
425
|
treeId: proposed.treeId,
|
|
427
426
|
regressions,
|
|
428
|
-
limits: compactLimits(proposed.nodes.length
|
|
427
|
+
limits: compactLimits(proposed.nodes.length),
|
|
429
428
|
...(current === undefined ? {} : { before: requiredMetrics(current) }),
|
|
430
429
|
proposed: requiredMetrics(proposed),
|
|
431
430
|
});
|
|
@@ -1620,6 +1620,76 @@ function* connectorLengthDiagnostics(context, edges) {
|
|
|
1620
1620
|
});
|
|
1621
1621
|
}
|
|
1622
1622
|
}
|
|
1623
|
+
function* fakeComplexityDiagnostics(context, edges) {
|
|
1624
|
+
const incomingCount = new Map();
|
|
1625
|
+
const outgoingCount = new Map();
|
|
1626
|
+
for (const edge of edges) {
|
|
1627
|
+
incomingCount.set(edge.childId, (incomingCount.get(edge.childId) ?? 0) + 1);
|
|
1628
|
+
outgoingCount.set(edge.parentId, (outgoingCount.get(edge.parentId) ?? 0) + 1);
|
|
1629
|
+
}
|
|
1630
|
+
const linearEdges = edges.filter(({ parentId, childId }) => (outgoingCount.get(parentId) ?? 0) === 1 && (incomingCount.get(childId) ?? 0) === 1);
|
|
1631
|
+
const next = new Map(linearEdges.map((edge) => [edge.parentId, edge]));
|
|
1632
|
+
const linearChildren = new Set(linearEdges.map(({ childId }) => childId));
|
|
1633
|
+
for (const [edgeIndex, edge] of linearEdges.entries()) {
|
|
1634
|
+
yield* cancellationCheckpoint(context.signal, edgeIndex, 32);
|
|
1635
|
+
const spans = connectorSpans(edge);
|
|
1636
|
+
if (spans.horizontal === 0 && spans.vertical === 1)
|
|
1637
|
+
continue;
|
|
1638
|
+
const focus = context.focuses.get(edge.childId);
|
|
1639
|
+
context.diagnostics.push({
|
|
1640
|
+
code: 'FOCUS_LAYOUT_LINEAR_DETOUR',
|
|
1641
|
+
severity: 'warning',
|
|
1642
|
+
category: 'layout',
|
|
1643
|
+
message: `Mechanically linear connector ${edge.parentId} -> ${edge.childId} uses an unnecessary visual detour`,
|
|
1644
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1645
|
+
details: {
|
|
1646
|
+
parentId: edge.parentId,
|
|
1647
|
+
childId: edge.childId,
|
|
1648
|
+
horizontalSpan: spans.horizontal,
|
|
1649
|
+
verticalSpan: spans.vertical,
|
|
1650
|
+
expectedHorizontalSpan: 0,
|
|
1651
|
+
expectedVerticalSpan: 1,
|
|
1652
|
+
},
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
const starts = linearEdges
|
|
1656
|
+
.filter(({ parentId }) => !linearChildren.has(parentId))
|
|
1657
|
+
.sort((left, right) => compareCodeUnits(left.parentId, right.parentId));
|
|
1658
|
+
for (const start of starts) {
|
|
1659
|
+
const chain = [];
|
|
1660
|
+
const visited = new Set();
|
|
1661
|
+
let current = start;
|
|
1662
|
+
while (current !== undefined && !visited.has(current.parentId)) {
|
|
1663
|
+
visited.add(current.parentId);
|
|
1664
|
+
chain.push(current);
|
|
1665
|
+
current = next.get(current.childId);
|
|
1666
|
+
}
|
|
1667
|
+
const horizontalDirections = chain
|
|
1668
|
+
.map(({ parent, child }) => Math.sign(child.x - parent.x))
|
|
1669
|
+
.filter((direction) => direction !== 0);
|
|
1670
|
+
if (horizontalDirections.length < 2)
|
|
1671
|
+
continue;
|
|
1672
|
+
const sameDirection = horizontalDirections.every((direction) => direction === horizontalDirections[0]);
|
|
1673
|
+
const alternating = horizontalDirections.every((direction, index) => index === 0 || direction === -horizontalDirections[index - 1]);
|
|
1674
|
+
const code = sameDirection
|
|
1675
|
+
? 'FOCUS_LAYOUT_STAIRCASE_CHAIN'
|
|
1676
|
+
: alternating
|
|
1677
|
+
? 'FOCUS_LAYOUT_ZIGZAG_CHAIN'
|
|
1678
|
+
: undefined;
|
|
1679
|
+
if (code === undefined)
|
|
1680
|
+
continue;
|
|
1681
|
+
const focusIds = [chain[0].parentId, ...chain.map(({ childId }) => childId)];
|
|
1682
|
+
const focus = context.focuses.get(focusIds.at(-1) ?? '');
|
|
1683
|
+
context.diagnostics.push({
|
|
1684
|
+
code,
|
|
1685
|
+
severity: 'warning',
|
|
1686
|
+
category: 'layout',
|
|
1687
|
+
message: `${sameDirection ? 'Staircase' : 'Zigzag'} geometry makes the linear chain ${focusIds.join(' -> ')} look artificially complex`,
|
|
1688
|
+
...(focus?.sourceLocation === undefined ? {} : { location: focus.sourceLocation }),
|
|
1689
|
+
details: { focusIds, horizontalDirections },
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1623
1693
|
function siblingCohortAnchor(context, cohort) {
|
|
1624
1694
|
const focus = context.focuses.get(cohort[0] ?? '');
|
|
1625
1695
|
const coordinates = cohort
|
|
@@ -1917,6 +1987,7 @@ function* layoutFocusTreeSteps(plan, options = {}) {
|
|
|
1917
1987
|
const nodeIntersectionCount = yield* connectorNodeIntersectionDiagnostics(context, edges);
|
|
1918
1988
|
yield* connectorCrossingDiagnostics(context, crossingSummary.samples);
|
|
1919
1989
|
yield* connectorLengthDiagnostics(context, edges);
|
|
1990
|
+
yield* fakeComplexityDiagnostics(context, edges);
|
|
1920
1991
|
const symmetry = yield* siblingSymmetryDiagnostics(context);
|
|
1921
1992
|
const nodes = [...context.placed.values()].sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
1922
1993
|
const decisions = [...context.decisions].sort((left, right) => compareCodeUnits(left.focusId, right.focusId));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boundedSourceHashEvidence, publicArtifactLink } from '../../core/artifacts.js';
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
2
3
|
import { canonicalJson } from '../../core/canonical.js';
|
|
3
4
|
import { exactChangedFilePatterns, focusDomainScanPatterns, } from '../../core/domain-scan-patterns.js';
|
|
4
5
|
import { ServiceError } from '../../core/result.js';
|
|
@@ -14,6 +15,22 @@ export async function resolveServerWorkspaceId(engine, context, workspaceId, sig
|
|
|
14
15
|
}
|
|
15
16
|
return engine.resolver.resolveWorkspaceId(workspaceId, context.principal);
|
|
16
17
|
}
|
|
18
|
+
export async function resolveServerWorkspaceForSource(engine, context, workspaceId, relativePath, signal) {
|
|
19
|
+
const resolvedWorkspaceId = await resolveServerWorkspaceId(engine, context, workspaceId, signal);
|
|
20
|
+
if (workspaceId !== 'current' || relativePath === undefined)
|
|
21
|
+
return resolvedWorkspaceId;
|
|
22
|
+
try {
|
|
23
|
+
const resolved = await engine.resolver.resolvePath(resolvedWorkspaceId, relativePath, 'read', ['mod'], context.principal);
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
if ((await lstat(resolved.path)).isFile())
|
|
26
|
+
return resolvedWorkspaceId;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.name === 'AbortError')
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
return ((await engine.resolver.resolveUniqueModSourceWorkspaceId(relativePath, context.principal, signal)) ?? resolvedWorkspaceId);
|
|
33
|
+
}
|
|
17
34
|
export function requireServerScope(context, scope) {
|
|
18
35
|
if (context.scopes !== undefined && !context.scopes.includes(scope)) {
|
|
19
36
|
throw new ServiceError('AUTH_SCOPE_REQUIRED', `This operation requires the ${scope} scope`, {
|
|
@@ -13,7 +13,7 @@ import { PACKAGE_VERSION } from '../../version.js';
|
|
|
13
13
|
import { nonNegativeIntegerSchema, renderHashesSchema, sha256Schema, } from '../server/output-schemas.js';
|
|
14
14
|
import { progressReporter } from '../server/progress.js';
|
|
15
15
|
import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
|
|
16
|
-
import { requireServerScope,
|
|
16
|
+
import { requireServerScope, resolveServerWorkspaceForSource, } from '../server/base-tools.js';
|
|
17
17
|
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
18
18
|
import { autonomousFailureContext, autonomousResultArtifacts, executePlannedTransaction, } from '../server/transaction-execution.js';
|
|
19
19
|
const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
@@ -347,7 +347,7 @@ export async function computeFocusVisualRevisions(engine, context, workspaceId,
|
|
|
347
347
|
}
|
|
348
348
|
async function executeFocusVisualTool(engine, workbench, context, input, extra, rasterize) {
|
|
349
349
|
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
350
|
-
const workspaceId = await
|
|
350
|
+
const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
|
|
351
351
|
const outputName = rasterize ? 'raster' : 'render';
|
|
352
352
|
try {
|
|
353
353
|
const progress = progressReporter(extra);
|
|
@@ -607,7 +607,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
607
607
|
annotations: artifactProducing,
|
|
608
608
|
}, async (input, extra) => {
|
|
609
609
|
const { workspaceId: requestedWorkspaceId, relativePath } = input;
|
|
610
|
-
const workspaceId = await
|
|
610
|
+
const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
|
|
611
611
|
try {
|
|
612
612
|
const progress = progressReporter(extra);
|
|
613
613
|
await progress.report(0, 3, 'Building shared workspace index');
|
|
@@ -845,7 +845,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
845
845
|
},
|
|
846
846
|
}, async (input, extra) => {
|
|
847
847
|
const { workspaceId: requestedWorkspaceId, relativePath, createIfMissing, horizontalSpacing, verticalSpacing, padding, reviewScale, } = input;
|
|
848
|
-
const workspaceId = await
|
|
848
|
+
const workspaceId = await resolveServerWorkspaceForSource(engine, context, requestedWorkspaceId, relativePath, extra.signal);
|
|
849
849
|
try {
|
|
850
850
|
requireServerScope(context, 'hoi4:write');
|
|
851
851
|
const progress = progressReporter(extra);
|
|
@@ -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.11';
|
|
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 straightens mechanically linear chains and refuses a result with overlapping or cramped focuses, rendered crossings, connectors through unrelated focuses, long connectors, asymmetric or off-anchor sibling groups, offset linear detours, staircase or zigzag chains, an off-center canvas, or size metrics outside absolute and source-relative limits.
|
|
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.11",
|
|
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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11/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.11",
|
|
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.11",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|