hoi4-agent-tools 1.0.0 → 1.2.0
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 +21 -0
- package/README.md +27 -16
- package/dist/hoi4_agent_tools/core/engine.js +12 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +208 -0
- package/dist/hoi4_agent_tools/event/artifact-validation.js +572 -0
- package/dist/hoi4_agent_tools/event/compare.js +413 -0
- package/dist/hoi4_agent_tools/event/graph.js +1586 -0
- package/dist/hoi4_agent_tools/event/index.js +10 -0
- package/dist/hoi4_agent_tools/event/layout.js +218 -0
- package/dist/hoi4_agent_tools/event/limits.js +41 -0
- package/dist/hoi4_agent_tools/event/model.js +2 -0
- package/dist/hoi4_agent_tools/event/queries.js +643 -0
- package/dist/hoi4_agent_tools/event/render.js +443 -0
- package/dist/hoi4_agent_tools/event/scc.js +72 -0
- package/dist/hoi4_agent_tools/event/service.js +1147 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1458 -0
- package/dist/hoi4_agent_tools/focus/compact.js +432 -0
- package/dist/hoi4_agent_tools/focus/compiler.js +1 -7
- package/dist/hoi4_agent_tools/focus/geometry.js +203 -16
- package/dist/hoi4_agent_tools/focus/index.js +1 -0
- package/dist/hoi4_agent_tools/focus/layout.js +1256 -92
- package/dist/hoi4_agent_tools/focus/limits.js +1 -1
- package/dist/hoi4_agent_tools/focus/lint.js +11 -3
- package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
- package/dist/hoi4_agent_tools/focus/planning.js +6 -1
- package/dist/hoi4_agent_tools/focus/render.js +14 -9
- package/dist/hoi4_agent_tools/mcp/resources/register.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/context-schemas.js +8 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +4 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +117 -52
- package/dist/hoi4_agent_tools/mcp/tools/event.js +459 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +93 -7
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +12 -9
- package/dist/hoi4_agent_tools/mcp/tools/map.js +6 -4
- package/dist/hoi4_agent_tools/mcp/tools/mods.js +1 -1
- package/dist/hoi4_agent_tools/schemas/event.js +95 -0
- package/dist/hoi4_agent_tools/schemas/focus.js +64 -8
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +10 -0
- package/docs/events.md +113 -0
- package/docs/focus.md +17 -7
- package/docs/setup.md +3 -3
- package/package.json +9 -5
- package/server.json +3 -3
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { publicArtifactLink } from '../../core/artifacts.js';
|
|
3
|
+
import { compareCodeUnits } from '../../core/canonical.js';
|
|
4
|
+
import { emptyServiceResult } from '../../core/result.js';
|
|
5
|
+
import { EventChainViewer, eventDiagnostics, } from '../../event/service.js';
|
|
6
|
+
import { eventDirectionSchema, eventGraphReferenceSchema, eventImpactSubjectSchema, eventInspectModeSchema, eventProposedSourceSchema, eventRenderViewSchema, eventSelectorSchema, eventStateSubjectSchema, } from '../../schemas/event.js';
|
|
7
|
+
import { workspaceIdSchema } from '../../schemas/common.js';
|
|
8
|
+
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
9
|
+
import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
|
|
10
|
+
import { progressReporter } from '../server/progress.js';
|
|
11
|
+
import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
|
|
12
|
+
const nestedSelectorSchema = compactValidatedInputSchema(eventSelectorSchema, 'Event selector; see docs/events.md.');
|
|
13
|
+
const nestedStateSubjectSchema = compactValidatedInputSchema(eventStateSubjectSchema, 'State kind and name.');
|
|
14
|
+
const nestedImpactSubjectSchema = compactValidatedInputSchema(eventImpactSubjectSchema, 'Impact kind and name.');
|
|
15
|
+
const nestedGraphReferenceSchema = compactValidatedInputSchema(eventGraphReferenceSchema, 'Cached revision or graph artifact URI.');
|
|
16
|
+
const nestedProposedSourceSchema = compactValidatedInputSchema(eventProposedSourceSchema, 'In-memory source overlay; never written.');
|
|
17
|
+
const eventInspectInputSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
workspaceId: workspaceIdSchema,
|
|
20
|
+
mode: eventInspectModeSchema,
|
|
21
|
+
selector: nestedSelectorSchema.optional(),
|
|
22
|
+
from: nestedSelectorSchema.optional(),
|
|
23
|
+
to: nestedSelectorSchema.optional(),
|
|
24
|
+
direction: eventDirectionSchema.optional(),
|
|
25
|
+
maxDepth: z.number().int().min(1).max(64).optional(),
|
|
26
|
+
maxNodes: z.number().int().min(1).max(5_000).optional(),
|
|
27
|
+
maxEdges: z.number().int().min(1).max(20_000).optional(),
|
|
28
|
+
expandHelpers: z.boolean().optional(),
|
|
29
|
+
stateSubject: nestedStateSubjectSchema.optional(),
|
|
30
|
+
impactSubject: nestedImpactSubjectSchema.optional(),
|
|
31
|
+
refresh: z.boolean().optional(),
|
|
32
|
+
})
|
|
33
|
+
.strict()
|
|
34
|
+
.superRefine((value, context) => {
|
|
35
|
+
if (value.mode === 'trace' && value.selector === undefined) {
|
|
36
|
+
context.addIssue({ code: 'custom', path: ['selector'], message: 'Trace requires selector' });
|
|
37
|
+
}
|
|
38
|
+
if (value.mode === 'explain_path' && (value.from === undefined || value.to === undefined)) {
|
|
39
|
+
context.addIssue({
|
|
40
|
+
code: 'custom',
|
|
41
|
+
path: value.from === undefined ? ['from'] : ['to'],
|
|
42
|
+
message: 'Path explanation requires from and to',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (value.mode === 'impact' && value.impactSubject === undefined) {
|
|
46
|
+
context.addIssue({
|
|
47
|
+
code: 'custom',
|
|
48
|
+
path: ['impactSubject'],
|
|
49
|
+
message: 'Impact analysis requires impactSubject',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const eventRenderInputSchema = z
|
|
54
|
+
.object({
|
|
55
|
+
workspaceId: workspaceIdSchema,
|
|
56
|
+
view: eventRenderViewSchema,
|
|
57
|
+
selector: nestedSelectorSchema.optional(),
|
|
58
|
+
direction: eventDirectionSchema.optional(),
|
|
59
|
+
maxDepth: z.number().int().min(1).max(64).optional(),
|
|
60
|
+
maxNodes: z.number().int().min(1).max(240).optional(),
|
|
61
|
+
expandHelpers: z.boolean().optional(),
|
|
62
|
+
includeHtml: z.boolean().optional(),
|
|
63
|
+
refresh: z.boolean().optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
const eventCompareInputSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
workspaceId: workspaceIdSchema,
|
|
69
|
+
before: nestedGraphReferenceSchema.optional(),
|
|
70
|
+
after: nestedGraphReferenceSchema.optional(),
|
|
71
|
+
proposedSources: z.array(nestedProposedSourceSchema).min(1).max(64).optional(),
|
|
72
|
+
render: z.boolean().optional(),
|
|
73
|
+
maxRenderNodes: z.number().int().min(1).max(240).optional(),
|
|
74
|
+
refresh: z.boolean().optional(),
|
|
75
|
+
})
|
|
76
|
+
.strict()
|
|
77
|
+
.superRefine((value, context) => {
|
|
78
|
+
if (value.after !== undefined && value.proposedSources !== undefined) {
|
|
79
|
+
context.addIssue({
|
|
80
|
+
code: 'custom',
|
|
81
|
+
path: ['after'],
|
|
82
|
+
message: 'after and proposedSources are mutually exclusive',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const eventGraphCountsSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
events: nonNegativeIntegerSchema,
|
|
89
|
+
options: nonNegativeIntegerSchema,
|
|
90
|
+
entries: nonNegativeIntegerSchema,
|
|
91
|
+
helpers: nonNegativeIntegerSchema,
|
|
92
|
+
unresolvedNodes: nonNegativeIntegerSchema,
|
|
93
|
+
terminals: nonNegativeIntegerSchema,
|
|
94
|
+
edges: nonNegativeIntegerSchema,
|
|
95
|
+
derivedEdges: nonNegativeIntegerSchema,
|
|
96
|
+
stateAccesses: nonNegativeIntegerSchema,
|
|
97
|
+
issues: nonNegativeIntegerSchema,
|
|
98
|
+
diagnostics: nonNegativeIntegerSchema,
|
|
99
|
+
blockingDiagnostics: nonNegativeIntegerSchema,
|
|
100
|
+
skippedSources: nonNegativeIntegerSchema,
|
|
101
|
+
artifacts: nonNegativeIntegerSchema,
|
|
102
|
+
})
|
|
103
|
+
.strict();
|
|
104
|
+
const inspectBoundarySchema = z
|
|
105
|
+
.object({
|
|
106
|
+
direction: eventDirectionSchema,
|
|
107
|
+
maxDepth: nonNegativeIntegerSchema,
|
|
108
|
+
maxNodes: nonNegativeIntegerSchema,
|
|
109
|
+
maxEdges: nonNegativeIntegerSchema,
|
|
110
|
+
expandHelpers: z.boolean(),
|
|
111
|
+
refresh: z.boolean(),
|
|
112
|
+
})
|
|
113
|
+
.strict();
|
|
114
|
+
const renderHashSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
json: sha256Schema,
|
|
117
|
+
svg: sha256Schema,
|
|
118
|
+
png: sha256Schema,
|
|
119
|
+
html: sha256Schema.optional(),
|
|
120
|
+
})
|
|
121
|
+
.strict();
|
|
122
|
+
const eventInspectOutputSchema = strictOperationResultSchema(z
|
|
123
|
+
.object({
|
|
124
|
+
mode: eventInspectModeSchema,
|
|
125
|
+
revision: sha256Schema,
|
|
126
|
+
graphHash: sha256Schema,
|
|
127
|
+
counts: eventGraphCountsSchema,
|
|
128
|
+
boundary: inspectBoundarySchema,
|
|
129
|
+
})
|
|
130
|
+
.strict());
|
|
131
|
+
const eventRenderOutputSchema = strictOperationResultSchema(z
|
|
132
|
+
.object({
|
|
133
|
+
view: eventRenderViewSchema,
|
|
134
|
+
revision: sha256Schema,
|
|
135
|
+
graphHash: sha256Schema,
|
|
136
|
+
layoutHash: sha256Schema,
|
|
137
|
+
hashes: renderHashSchema,
|
|
138
|
+
counts: eventGraphCountsSchema.extend({
|
|
139
|
+
selectedNodes: nonNegativeIntegerSchema,
|
|
140
|
+
omittedNodes: nonNegativeIntegerSchema,
|
|
141
|
+
branchRenders: nonNegativeIntegerSchema,
|
|
142
|
+
}),
|
|
143
|
+
boundary: z
|
|
144
|
+
.object({
|
|
145
|
+
direction: eventDirectionSchema,
|
|
146
|
+
maxDepth: nonNegativeIntegerSchema,
|
|
147
|
+
maxNodes: nonNegativeIntegerSchema,
|
|
148
|
+
expandHelpers: z.boolean(),
|
|
149
|
+
includeHtml: z.boolean(),
|
|
150
|
+
refresh: z.boolean(),
|
|
151
|
+
})
|
|
152
|
+
.strict(),
|
|
153
|
+
})
|
|
154
|
+
.strict());
|
|
155
|
+
const eventCompareOutputSchema = strictOperationResultSchema(z
|
|
156
|
+
.object({
|
|
157
|
+
beforeRevision: sha256Schema,
|
|
158
|
+
afterRevision: sha256Schema,
|
|
159
|
+
beforeGraphHash: sha256Schema,
|
|
160
|
+
afterGraphHash: sha256Schema,
|
|
161
|
+
renderHashes: renderHashSchema.optional(),
|
|
162
|
+
counts: z
|
|
163
|
+
.object({
|
|
164
|
+
changes: nonNegativeIntegerSchema,
|
|
165
|
+
addedNodes: nonNegativeIntegerSchema,
|
|
166
|
+
removedNodes: nonNegativeIntegerSchema,
|
|
167
|
+
changedNodes: nonNegativeIntegerSchema,
|
|
168
|
+
addedEdges: nonNegativeIntegerSchema,
|
|
169
|
+
removedEdges: nonNegativeIntegerSchema,
|
|
170
|
+
changedEdges: nonNegativeIntegerSchema,
|
|
171
|
+
addedStateAccesses: nonNegativeIntegerSchema,
|
|
172
|
+
removedStateAccesses: nonNegativeIntegerSchema,
|
|
173
|
+
changedStateAccesses: nonNegativeIntegerSchema,
|
|
174
|
+
addedStateLinks: nonNegativeIntegerSchema,
|
|
175
|
+
removedStateLinks: nonNegativeIntegerSchema,
|
|
176
|
+
changedStateLinks: nonNegativeIntegerSchema,
|
|
177
|
+
addedDiagnostics: nonNegativeIntegerSchema,
|
|
178
|
+
resolvedDiagnostics: nonNegativeIntegerSchema,
|
|
179
|
+
addedUnresolved: nonNegativeIntegerSchema,
|
|
180
|
+
resolvedUnresolved: nonNegativeIntegerSchema,
|
|
181
|
+
disconnectedRoots: nonNegativeIntegerSchema,
|
|
182
|
+
disconnectedBranches: nonNegativeIntegerSchema,
|
|
183
|
+
disconnectedTerminals: nonNegativeIntegerSchema,
|
|
184
|
+
beforeSkippedSources: nonNegativeIntegerSchema,
|
|
185
|
+
afterSkippedSources: nonNegativeIntegerSchema,
|
|
186
|
+
artifacts: nonNegativeIntegerSchema,
|
|
187
|
+
})
|
|
188
|
+
.strict(),
|
|
189
|
+
boundary: z
|
|
190
|
+
.object({
|
|
191
|
+
proposedSources: nonNegativeIntegerSchema,
|
|
192
|
+
render: z.boolean(),
|
|
193
|
+
maxRenderNodes: nonNegativeIntegerSchema,
|
|
194
|
+
refresh: z.boolean(),
|
|
195
|
+
})
|
|
196
|
+
.strict(),
|
|
197
|
+
})
|
|
198
|
+
.strict());
|
|
199
|
+
const readOnlyEventTool = {
|
|
200
|
+
readOnlyHint: true,
|
|
201
|
+
destructiveHint: false,
|
|
202
|
+
idempotentHint: true,
|
|
203
|
+
openWorldHint: false,
|
|
204
|
+
};
|
|
205
|
+
function graphCounts(graph, artifacts) {
|
|
206
|
+
const diagnostics = eventDiagnostics(graph);
|
|
207
|
+
return {
|
|
208
|
+
events: graph.statistics.eventCount,
|
|
209
|
+
options: graph.statistics.optionCount,
|
|
210
|
+
entries: graph.statistics.entryCount,
|
|
211
|
+
helpers: graph.statistics.helperCount,
|
|
212
|
+
unresolvedNodes: graph.statistics.unresolvedNodeCount,
|
|
213
|
+
terminals: graph.statistics.terminalCount,
|
|
214
|
+
edges: graph.statistics.edgeCount,
|
|
215
|
+
derivedEdges: graph.statistics.derivedEdgeCount,
|
|
216
|
+
stateAccesses: graph.statistics.stateAccessCount,
|
|
217
|
+
issues: graph.statistics.issueCount,
|
|
218
|
+
diagnostics: diagnostics.length,
|
|
219
|
+
blockingDiagnostics: diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker').length,
|
|
220
|
+
skippedSources: graph.skippedSourceCount,
|
|
221
|
+
artifacts,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function graphValidation(graph) {
|
|
225
|
+
const counts = graphCounts(graph, 0);
|
|
226
|
+
return {
|
|
227
|
+
passed: graph.complete && counts.blockingDiagnostics === 0,
|
|
228
|
+
checks: [
|
|
229
|
+
{
|
|
230
|
+
id: 'event-analysis',
|
|
231
|
+
passed: graph.complete && counts.blockingDiagnostics === 0,
|
|
232
|
+
message: graph.complete
|
|
233
|
+
? `${counts.blockingDiagnostics} blocking event-chain diagnostics; full evidence is linked`
|
|
234
|
+
: `${graph.skippedSourceCount} event-analysis source(s) were skipped; full evidence is linked`,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function inspectRequest(input, context, signal) {
|
|
240
|
+
return {
|
|
241
|
+
workspaceId: input.workspaceId,
|
|
242
|
+
mode: input.mode,
|
|
243
|
+
...(input.selector === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: { selector: input.selector }),
|
|
246
|
+
...(input.from === undefined
|
|
247
|
+
? {}
|
|
248
|
+
: { from: input.from }),
|
|
249
|
+
...(input.to === undefined ? {} : { to: input.to }),
|
|
250
|
+
...(input.direction === undefined ? {} : { direction: input.direction }),
|
|
251
|
+
...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
|
|
252
|
+
...(input.maxNodes === undefined ? {} : { maxNodes: input.maxNodes }),
|
|
253
|
+
...(input.maxEdges === undefined ? {} : { maxEdges: input.maxEdges }),
|
|
254
|
+
...(input.expandHelpers === undefined ? {} : { expandHelpers: input.expandHelpers }),
|
|
255
|
+
...(input.stateSubject === undefined ? {} : { stateSubject: input.stateSubject }),
|
|
256
|
+
...(input.impactSubject === undefined ? {} : { impactSubject: input.impactSubject }),
|
|
257
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
258
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
259
|
+
signal,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function renderRequest(input, context, signal) {
|
|
263
|
+
return {
|
|
264
|
+
workspaceId: input.workspaceId,
|
|
265
|
+
view: input.view,
|
|
266
|
+
...(input.selector === undefined
|
|
267
|
+
? {}
|
|
268
|
+
: { selector: input.selector }),
|
|
269
|
+
...(input.direction === undefined ? {} : { direction: input.direction }),
|
|
270
|
+
...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
|
|
271
|
+
...(input.maxNodes === undefined ? {} : { maxNodes: input.maxNodes }),
|
|
272
|
+
...(input.expandHelpers === undefined ? {} : { expandHelpers: input.expandHelpers }),
|
|
273
|
+
...(input.includeHtml === undefined ? {} : { includeHtml: input.includeHtml }),
|
|
274
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
275
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
276
|
+
signal,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function compareRequest(input, context, signal) {
|
|
280
|
+
return {
|
|
281
|
+
workspaceId: input.workspaceId,
|
|
282
|
+
...(input.before === undefined
|
|
283
|
+
? {}
|
|
284
|
+
: { before: input.before }),
|
|
285
|
+
...(input.after === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: { after: input.after }),
|
|
288
|
+
...(input.proposedSources === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
proposedSources: input.proposedSources,
|
|
292
|
+
}),
|
|
293
|
+
...(input.render === undefined ? {} : { render: input.render }),
|
|
294
|
+
...(input.maxRenderNodes === undefined ? {} : { maxRenderNodes: input.maxRenderNodes }),
|
|
295
|
+
...(input.refresh === undefined ? {} : { refresh: input.refresh }),
|
|
296
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
297
|
+
signal,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
export function registerEventTools(server, engine, context) {
|
|
301
|
+
const viewer = new EventChainViewer(engine);
|
|
302
|
+
server.registerTool('hoi4.event_inspect', {
|
|
303
|
+
title: 'Inspect event chains',
|
|
304
|
+
description: 'Scan, find roots, trace, explain paths, inspect state flow, lint, or assess impact. Full source-linked reports are resources.',
|
|
305
|
+
inputSchema: eventInspectInputSchema,
|
|
306
|
+
outputSchema: eventInspectOutputSchema,
|
|
307
|
+
annotations: readOnlyEventTool,
|
|
308
|
+
}, async (input, extra) => {
|
|
309
|
+
const { workspaceId } = input;
|
|
310
|
+
try {
|
|
311
|
+
const progress = progressReporter(extra);
|
|
312
|
+
await progress.report(0, 3, 'Analyzing event chain');
|
|
313
|
+
const inspected = await viewer.inspect(inspectRequest(input, context, progress.signal));
|
|
314
|
+
await progress.report(2, 3, 'Linking complete event analysis');
|
|
315
|
+
const result = emptyServiceResult(workspaceId, {
|
|
316
|
+
mode: inspected.mode,
|
|
317
|
+
revision: inspected.graph.revision,
|
|
318
|
+
graphHash: inspected.graphHash,
|
|
319
|
+
counts: graphCounts(inspected.graph, inspected.artifacts.length),
|
|
320
|
+
boundary: {
|
|
321
|
+
direction: input.direction ?? 'both',
|
|
322
|
+
maxDepth: input.maxDepth ?? 8,
|
|
323
|
+
maxNodes: input.maxNodes ?? 500,
|
|
324
|
+
maxEdges: input.maxEdges ?? 2_000,
|
|
325
|
+
expandHelpers: input.expandHelpers ?? (input.mode === 'explain_path' ? true : false),
|
|
326
|
+
refresh: input.refresh ?? input.mode === 'scan',
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
result.code = inspected.graph.complete ? 'EVENT_INSPECTED' : 'EVENT_INSPECTED_PARTIAL';
|
|
330
|
+
setInlineFilesScanned(result, inspected.graph.filesScanned);
|
|
331
|
+
result.artifacts = inspected.artifacts.map(publicArtifactLink);
|
|
332
|
+
result.validation = graphValidation(inspected.graph);
|
|
333
|
+
await progress.report(3, 3, 'Event inspection complete');
|
|
334
|
+
return toolResult(result);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return errorResult(error, workspaceId);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
server.registerTool('hoi4.event_render', {
|
|
341
|
+
title: 'Render event chains',
|
|
342
|
+
description: 'Render deterministic JSON, SVG, PNG, and optional HTML for an event-chain view. Complete artifacts retain source links.',
|
|
343
|
+
inputSchema: eventRenderInputSchema,
|
|
344
|
+
outputSchema: eventRenderOutputSchema,
|
|
345
|
+
annotations: readOnlyEventTool,
|
|
346
|
+
}, async (input, extra) => {
|
|
347
|
+
const { workspaceId } = input;
|
|
348
|
+
try {
|
|
349
|
+
const progress = progressReporter(extra);
|
|
350
|
+
await progress.report(0, 3, 'Rendering event-chain view');
|
|
351
|
+
const rendered = await viewer.renderAndStore(renderRequest(input, context, progress.signal));
|
|
352
|
+
await progress.report(2, 3, 'Linking complete event renders');
|
|
353
|
+
const result = emptyServiceResult(workspaceId, {
|
|
354
|
+
view: rendered.render.view,
|
|
355
|
+
revision: rendered.graph.revision,
|
|
356
|
+
graphHash: rendered.graphHash,
|
|
357
|
+
layoutHash: rendered.render.layout.layoutHash,
|
|
358
|
+
hashes: rendered.render.hashes,
|
|
359
|
+
counts: {
|
|
360
|
+
...graphCounts(rendered.graph, rendered.artifacts.length),
|
|
361
|
+
selectedNodes: rendered.render.selectedNodeIds.length,
|
|
362
|
+
omittedNodes: rendered.render.omittedNodeCount,
|
|
363
|
+
branchRenders: rendered.branches.length,
|
|
364
|
+
},
|
|
365
|
+
boundary: {
|
|
366
|
+
direction: input.direction ?? 'both',
|
|
367
|
+
maxDepth: input.maxDepth ?? 4,
|
|
368
|
+
maxNodes: input.maxNodes ?? 120,
|
|
369
|
+
expandHelpers: input.expandHelpers ?? false,
|
|
370
|
+
includeHtml: input.includeHtml ?? false,
|
|
371
|
+
refresh: input.refresh ?? false,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
result.code = rendered.graph.complete ? 'EVENT_RENDERED' : 'EVENT_RENDERED_PARTIAL';
|
|
375
|
+
setInlineFilesScanned(result, rendered.graph.filesScanned);
|
|
376
|
+
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
377
|
+
result.validation = graphValidation(rendered.graph);
|
|
378
|
+
await progress.report(3, 3, 'Event render complete');
|
|
379
|
+
return toolResult(result);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
return errorResult(error, workspaceId);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
server.registerTool('hoi4.event_compare', {
|
|
386
|
+
title: 'Compare event chains',
|
|
387
|
+
description: 'Compare cached, artifact-backed, current, or in-memory proposed event graphs without writing source. Full changes are resources.',
|
|
388
|
+
inputSchema: eventCompareInputSchema,
|
|
389
|
+
outputSchema: eventCompareOutputSchema,
|
|
390
|
+
annotations: readOnlyEventTool,
|
|
391
|
+
}, async (input, extra) => {
|
|
392
|
+
const { workspaceId } = input;
|
|
393
|
+
try {
|
|
394
|
+
const progress = progressReporter(extra);
|
|
395
|
+
await progress.report(0, 3, 'Comparing event-chain graphs');
|
|
396
|
+
const compared = await viewer.compareAndStore(compareRequest(input, context, progress.signal));
|
|
397
|
+
await progress.report(2, 3, 'Linking complete event comparison');
|
|
398
|
+
const { comparison } = compared;
|
|
399
|
+
const result = emptyServiceResult(workspaceId, {
|
|
400
|
+
beforeRevision: comparison.beforeRevision,
|
|
401
|
+
afterRevision: comparison.afterRevision,
|
|
402
|
+
beforeGraphHash: comparison.beforeGraphHash,
|
|
403
|
+
afterGraphHash: comparison.afterGraphHash,
|
|
404
|
+
...(compared.render === undefined ? {} : { renderHashes: compared.render.hashes }),
|
|
405
|
+
counts: {
|
|
406
|
+
changes: comparison.changes.length,
|
|
407
|
+
addedNodes: comparison.addedNodeIds.length,
|
|
408
|
+
removedNodes: comparison.removedNodeIds.length,
|
|
409
|
+
changedNodes: comparison.changedNodeIds.length,
|
|
410
|
+
addedEdges: comparison.addedEdgeIds.length,
|
|
411
|
+
removedEdges: comparison.removedEdgeIds.length,
|
|
412
|
+
changedEdges: comparison.changedEdgeIds.length,
|
|
413
|
+
addedStateAccesses: comparison.addedStateAccessIds.length,
|
|
414
|
+
removedStateAccesses: comparison.removedStateAccessIds.length,
|
|
415
|
+
changedStateAccesses: comparison.changedStateAccessIds.length,
|
|
416
|
+
addedStateLinks: comparison.addedStateLinkIds.length,
|
|
417
|
+
removedStateLinks: comparison.removedStateLinkIds.length,
|
|
418
|
+
changedStateLinks: comparison.changedStateLinkIds.length,
|
|
419
|
+
addedDiagnostics: comparison.addedIssueIds.length,
|
|
420
|
+
resolvedDiagnostics: comparison.resolvedIssueIds.length,
|
|
421
|
+
addedUnresolved: comparison.addedUnresolvedIds.length,
|
|
422
|
+
resolvedUnresolved: comparison.resolvedUnresolvedIds.length,
|
|
423
|
+
disconnectedRoots: comparison.newlyDisconnectedRootIds.length,
|
|
424
|
+
disconnectedBranches: comparison.newlyDisconnectedBranchIds.length,
|
|
425
|
+
disconnectedTerminals: comparison.newlyDisconnectedTerminalIds.length,
|
|
426
|
+
beforeSkippedSources: compared.before.skippedSourceCount,
|
|
427
|
+
afterSkippedSources: compared.after.skippedSourceCount,
|
|
428
|
+
artifacts: compared.artifacts.length,
|
|
429
|
+
},
|
|
430
|
+
boundary: {
|
|
431
|
+
proposedSources: input.proposedSources?.length ?? 0,
|
|
432
|
+
render: input.render ?? true,
|
|
433
|
+
maxRenderNodes: input.maxRenderNodes ?? 120,
|
|
434
|
+
refresh: compared.refresh,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
result.code =
|
|
438
|
+
compared.before.complete && compared.after.complete
|
|
439
|
+
? 'EVENT_COMPARED'
|
|
440
|
+
: 'EVENT_COMPARED_PARTIAL';
|
|
441
|
+
setInlineFilesScanned(result, [...new Set([...compared.before.filesScanned, ...compared.after.filesScanned])].sort(compareCodeUnits));
|
|
442
|
+
result.artifacts = compared.artifacts.map(publicArtifactLink);
|
|
443
|
+
const beforeValidation = graphValidation(compared.before);
|
|
444
|
+
const afterValidation = graphValidation(compared.after);
|
|
445
|
+
result.validation = {
|
|
446
|
+
passed: beforeValidation.passed && afterValidation.passed,
|
|
447
|
+
checks: [...beforeValidation.checks, ...afterValidation.checks].map((check, index) => ({
|
|
448
|
+
...check,
|
|
449
|
+
id: `${index === 0 ? 'before' : 'after'}-${check.id}`,
|
|
450
|
+
})),
|
|
451
|
+
};
|
|
452
|
+
await progress.report(3, 3, 'Event comparison complete');
|
|
453
|
+
return toolResult(result);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return errorResult(error, workspaceId);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
@@ -6,14 +6,15 @@ import { RenderBudget } from '../../core/render-budget.js';
|
|
|
6
6
|
import { emptyServiceResult, ServiceError } from '../../core/result.js';
|
|
7
7
|
import { parseClausewitz } from '../../core/source/index.js';
|
|
8
8
|
import { readDependenciesFromScannedFiles } from '../../core/transactions.js';
|
|
9
|
-
import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, renderContinuousFocusPalette, renderFocusTree, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
|
|
10
|
-
import { continuousFocusPaletteSchema, focusLayoutSchema, focusTreePlanSchema, } from '../../schemas/focus.js';
|
|
9
|
+
import { FocusWorkbench, FOCUS_RENDER_MAX_OUTPUT_SCALE, FOCUS_RENDER_MIN_OUTPUT_SCALE, assertCompactLayoutQuality, compactFocusTreePlanAsync, focusPlanHash, focusPresentationEvidence, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, renderContinuousFocusPalette, renderFocusTree, resolveFocusPresentation, enrichFocusPlanFromSidecar, } from '../../focus/index.js';
|
|
10
|
+
import { continuousFocusPaletteSchema, focusLayoutMetricsSchema, focusLayoutSchema, focusTreePlanSchema, } from '../../schemas/focus.js';
|
|
11
11
|
import { workspaceIdSchema, workspaceRelativePathSchema } from '../../schemas/common.js';
|
|
12
12
|
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
16
|
import { requireServerScope } from '../server/base-tools.js';
|
|
17
|
+
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
17
18
|
import { autonomousFailureContext, autonomousResultArtifacts, executePlannedTransaction, } from '../server/transaction-execution.js';
|
|
18
19
|
const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
19
20
|
.object({
|
|
@@ -33,6 +34,7 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
|
33
34
|
resolvedTitleCount: nonNegativeIntegerSchema,
|
|
34
35
|
layoutHash: sha256Schema,
|
|
35
36
|
layoutDecisionCount: nonNegativeIntegerSchema,
|
|
37
|
+
layoutMetrics: focusLayoutMetricsSchema,
|
|
36
38
|
diagnosticCount: nonNegativeIntegerSchema,
|
|
37
39
|
})
|
|
38
40
|
.strict())
|
|
@@ -114,6 +116,7 @@ const focusPlanOutputSchema = strictOperationResultSchema(z.discriminatedUnion('
|
|
|
114
116
|
})
|
|
115
117
|
.strict(),
|
|
116
118
|
]));
|
|
119
|
+
const compactFocusPlanSchema = compactValidatedInputSchema(z.union([focusTreePlanSchema, continuousFocusPaletteSchema]), `Complete focus plan: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/focus.md`);
|
|
117
120
|
const focusInspectInput = z
|
|
118
121
|
.object({
|
|
119
122
|
mode: z.enum(['national', 'continuous']).optional(),
|
|
@@ -208,7 +211,9 @@ const focusPlanInput = z
|
|
|
208
211
|
mode: z.enum(['national', 'continuous']).optional(),
|
|
209
212
|
workspaceId: workspaceIdSchema,
|
|
210
213
|
relativePath: workspaceRelativePathSchema,
|
|
211
|
-
|
|
214
|
+
treeId: z.string().min(1).max(256).optional(),
|
|
215
|
+
layoutMode: z.enum(['authored', 'compact']).default('authored'),
|
|
216
|
+
plan: compactFocusPlanSchema.optional(),
|
|
212
217
|
createIfMissing: z.boolean().default(false),
|
|
213
218
|
horizontalSpacing: z.number().int().min(80).max(1000).optional(),
|
|
214
219
|
verticalSpacing: z.number().int().min(60).max(1000).optional(),
|
|
@@ -223,13 +228,31 @@ const focusPlanInput = z
|
|
|
223
228
|
.superRefine((value, context) => {
|
|
224
229
|
const mode = value.mode ?? 'national';
|
|
225
230
|
const schema = mode === 'continuous' ? continuousFocusPaletteSchema : focusTreePlanSchema;
|
|
226
|
-
if (!schema.safeParse(value.plan).success)
|
|
231
|
+
if (value.plan !== undefined && !schema.safeParse(value.plan).success)
|
|
227
232
|
context.addIssue({
|
|
228
233
|
code: 'custom',
|
|
229
234
|
path: ['plan'],
|
|
230
235
|
message: `plan must be a ${mode} focus plan`,
|
|
231
236
|
});
|
|
232
237
|
if (mode === 'continuous') {
|
|
238
|
+
if (value.plan === undefined)
|
|
239
|
+
context.addIssue({
|
|
240
|
+
code: 'custom',
|
|
241
|
+
path: ['plan'],
|
|
242
|
+
message: 'plan is required in continuous mode',
|
|
243
|
+
});
|
|
244
|
+
if (value.treeId !== undefined)
|
|
245
|
+
context.addIssue({
|
|
246
|
+
code: 'custom',
|
|
247
|
+
path: ['treeId'],
|
|
248
|
+
message: 'treeId is only valid in national mode',
|
|
249
|
+
});
|
|
250
|
+
if (value.layoutMode !== 'authored')
|
|
251
|
+
context.addIssue({
|
|
252
|
+
code: 'custom',
|
|
253
|
+
path: ['layoutMode'],
|
|
254
|
+
message: 'compact layout mode is only valid in national mode',
|
|
255
|
+
});
|
|
233
256
|
for (const [field, fieldValue] of [
|
|
234
257
|
['horizontalSpacing', value.horizontalSpacing],
|
|
235
258
|
['verticalSpacing', value.verticalSpacing],
|
|
@@ -244,6 +267,35 @@ const focusPlanInput = z
|
|
|
244
267
|
});
|
|
245
268
|
}
|
|
246
269
|
}
|
|
270
|
+
else {
|
|
271
|
+
if (value.plan === undefined && value.layoutMode !== 'compact')
|
|
272
|
+
context.addIssue({
|
|
273
|
+
code: 'custom',
|
|
274
|
+
path: ['plan'],
|
|
275
|
+
message: 'plan is required unless layoutMode is compact',
|
|
276
|
+
});
|
|
277
|
+
if (value.plan === undefined && value.treeId === undefined)
|
|
278
|
+
context.addIssue({
|
|
279
|
+
code: 'custom',
|
|
280
|
+
path: ['treeId'],
|
|
281
|
+
message: 'treeId is required for plan-free compact reflow',
|
|
282
|
+
});
|
|
283
|
+
if (value.plan === undefined && value.createIfMissing)
|
|
284
|
+
context.addIssue({
|
|
285
|
+
code: 'custom',
|
|
286
|
+
path: ['createIfMissing'],
|
|
287
|
+
message: 'plan-free compact reflow requires an existing focus source',
|
|
288
|
+
});
|
|
289
|
+
if (value.plan !== undefined &&
|
|
290
|
+
value.treeId !== undefined &&
|
|
291
|
+
'id' in value.plan &&
|
|
292
|
+
value.plan.id !== value.treeId)
|
|
293
|
+
context.addIssue({
|
|
294
|
+
code: 'custom',
|
|
295
|
+
path: ['treeId'],
|
|
296
|
+
message: 'treeId must match the supplied national focus plan',
|
|
297
|
+
});
|
|
298
|
+
}
|
|
247
299
|
});
|
|
248
300
|
const artifactProducing = {
|
|
249
301
|
readOnlyHint: false,
|
|
@@ -584,6 +636,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
584
636
|
resolvedTitleCount: plan.focuses.filter(({ id }) => presentation.entries[id]?.titleSourceLocation !== undefined).length,
|
|
585
637
|
layoutHash: layout.layoutHash,
|
|
586
638
|
layoutDecisionCount: layout.decisions.length,
|
|
639
|
+
layoutMetrics: layout.metrics,
|
|
587
640
|
diagnosticCount,
|
|
588
641
|
})),
|
|
589
642
|
palettes: [],
|
|
@@ -727,7 +780,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
727
780
|
});
|
|
728
781
|
server.registerTool('hoi4.focus_rewrite', {
|
|
729
782
|
title: 'Create or clean up focus content',
|
|
730
|
-
description: 'Create or clean up a national tree or continuous palette
|
|
783
|
+
description: 'Create or clean up a national tree or continuous palette, validate it, and apply it in one call. Supply a complete plan; set layoutMode compact for automatic arrangement. Existing national trees can omit the plan and use treeId plus layoutMode compact. Set createIfMissing for a new file.',
|
|
731
784
|
inputSchema: focusPlanInput,
|
|
732
785
|
outputSchema: focusPlanOutputSchema,
|
|
733
786
|
annotations: {
|
|
@@ -901,7 +954,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
901
954
|
: 'Continuous focus rewrite blocked');
|
|
902
955
|
return toolResult(result);
|
|
903
956
|
}
|
|
904
|
-
const
|
|
957
|
+
const suppliedPlan = input.plan;
|
|
905
958
|
if (createIfMissing &&
|
|
906
959
|
(!relativePath.toLowerCase().endsWith('.txt') ||
|
|
907
960
|
!underConfiguredRoot(relativePath, workspace.registration.roots.focus)))
|
|
@@ -919,6 +972,30 @@ export function registerFocusTools(server, engine, context) {
|
|
|
919
972
|
const paletteImport = continuousPalettes(snapshot);
|
|
920
973
|
if (imported !== undefined)
|
|
921
974
|
imported.result.plans = imported.result.plans.map((current) => linkContinuousFocusPalettes(current, paletteImport.palettes));
|
|
975
|
+
let compactPlanning;
|
|
976
|
+
let compactSourcePlan;
|
|
977
|
+
let plan;
|
|
978
|
+
if (suppliedPlan !== undefined) {
|
|
979
|
+
if (input.layoutMode === 'compact') {
|
|
980
|
+
compactSourcePlan = suppliedPlan;
|
|
981
|
+
compactPlanning = await compactFocusTreePlanAsync(suppliedPlan, {
|
|
982
|
+
signal: progress.signal,
|
|
983
|
+
});
|
|
984
|
+
plan = compactPlanning.plan;
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
plan = suppliedPlan;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
if (imported === undefined)
|
|
992
|
+
throw new ServiceError('FOCUS_COMPACT_SOURCE_REQUIRED', 'Plan-free compact reflow requires an existing national focus source', { relativePath });
|
|
993
|
+
compactSourcePlan = selectPlan(imported.result, input.treeId);
|
|
994
|
+
compactPlanning = await compactFocusTreePlanAsync(compactSourcePlan, {
|
|
995
|
+
signal: progress.signal,
|
|
996
|
+
});
|
|
997
|
+
plan = compactPlanning.plan;
|
|
998
|
+
}
|
|
922
999
|
const currentPlan = imported?.result.plans.find(({ id }) => id === plan.id);
|
|
923
1000
|
if (currentPlan === undefined && !createIfMissing) {
|
|
924
1001
|
if (imported === undefined)
|
|
@@ -954,7 +1031,9 @@ export function registerFocusTools(server, engine, context) {
|
|
|
954
1031
|
});
|
|
955
1032
|
const currentLayout = currentPlan === undefined
|
|
956
1033
|
? undefined
|
|
957
|
-
:
|
|
1034
|
+
: currentPlan === compactSourcePlan && compactPlanning !== undefined
|
|
1035
|
+
? compactPlanning.currentLayout
|
|
1036
|
+
: await workbench.layoutAsync(currentPlan, { signal: progress.signal });
|
|
958
1037
|
const currentDiagnostics = currentPlan === undefined || currentLayout === undefined
|
|
959
1038
|
? []
|
|
960
1039
|
: workbench.lint(currentPlan, {
|
|
@@ -980,6 +1059,11 @@ export function registerFocusTools(server, engine, context) {
|
|
|
980
1059
|
importedPlanHash: proposedPlanHash,
|
|
981
1060
|
},
|
|
982
1061
|
};
|
|
1062
|
+
const proposedLayout = compactPlanning?.proposedLayout ??
|
|
1063
|
+
(await workbench.layoutAsync(reviewPlan, { signal: progress.signal }));
|
|
1064
|
+
if (input.layoutMode === 'compact') {
|
|
1065
|
+
assertCompactLayoutQuality(currentLayout, proposedLayout);
|
|
1066
|
+
}
|
|
983
1067
|
const proposed = await workbench.renderAndStore(workspaceId, reviewPlan, {
|
|
984
1068
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
985
1069
|
index: snapshot.index,
|
|
@@ -994,6 +1078,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
994
1078
|
},
|
|
995
1079
|
budget: renderBudget,
|
|
996
1080
|
signal: progress.signal,
|
|
1081
|
+
layout: proposedLayout,
|
|
997
1082
|
});
|
|
998
1083
|
const beforePng = currentRender?.png ??
|
|
999
1084
|
(await createTransparentPng(proposed.bundle.width, proposed.bundle.height, 'new national focus baseline', renderBudget));
|
|
@@ -1043,6 +1128,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
1043
1128
|
plan,
|
|
1044
1129
|
createIfMissing,
|
|
1045
1130
|
authority: 'plan',
|
|
1131
|
+
layout: proposed.layout,
|
|
1046
1132
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
1047
1133
|
index: snapshot.index,
|
|
1048
1134
|
references: catalog,
|