hoi4-agent-tools 3.0.9 → 3.1.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 +23 -1
- package/README.md +4 -0
- package/dist/bin/stdio.js +23 -12
- package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
- package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
- package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
- package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
- package/dist/hoi4_agent_tools/core/engine.js +48 -5
- package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
- package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
- package/dist/hoi4_agent_tools/core/index.js +47 -4
- package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
- package/dist/hoi4_agent_tools/core/job-service.js +112 -0
- package/dist/hoi4_agent_tools/core/job-store.js +641 -0
- package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +200 -0
- package/dist/hoi4_agent_tools/core/job-worker.js +62 -0
- package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +382 -0
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
- package/dist/hoi4_agent_tools/core/result.js +1 -0
- package/dist/hoi4_agent_tools/core/scanner.js +15 -75
- package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +43 -11
- package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
- package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
- package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
- package/dist/hoi4_agent_tools/core/transactions.js +80 -27
- package/dist/hoi4_agent_tools/event/graph.js +214 -290
- package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
- package/dist/hoi4_agent_tools/event/operations.js +179 -0
- package/dist/hoi4_agent_tools/event/service.js +67 -15
- package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
- package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
- package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
- package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
- package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
- package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
- package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
- package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
- package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +16 -6
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
- package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +43 -0
- package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +14 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
- package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
- package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
- package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
- package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
- package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
- package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
- package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
- package/dist/hoi4_agent_tools/probability/operations.js +183 -0
- package/dist/hoi4_agent_tools/probability/service.js +24 -3
- package/dist/hoi4_agent_tools/schemas/common.js +2 -0
- package/dist/hoi4_agent_tools/schemas/event.js +72 -1
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
- package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
- package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
- package/dist/hoi4_agent_tools/technology/graph.js +83 -78
- package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
- package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
- package/dist/hoi4_agent_tools/technology/operations.js +125 -0
- package/dist/hoi4_agent_tools/technology/service.js +89 -15
- package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +2 -0
- package/docs/development.md +2 -0
- package/docs/events.md +7 -2
- package/docs/helper-expansion.md +78 -0
- package/docs/http.md +2 -0
- package/docs/jobs.md +85 -0
- package/docs/technology.md +6 -2
- package/package.json +11 -5
- 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/helper-expansion-request.schema.json +44 -0
- package/schemas/helper-expansion-summary.schema.json +70 -0
- 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
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
import { ServiceError } from '../core/result.js';
|
|
3
|
+
import { parseClausewitz } from '../core/source/index.js';
|
|
4
|
+
import { enrichFocusPlanFromSidecar, focusPlanningSidecarPath, importContinuousFocusPalettes, importFocusTrees, linkContinuousFocusPalettes, parseFocusPlanningSidecar, } from './index.js';
|
|
5
|
+
function normalizedRoot(value) {
|
|
6
|
+
return value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/+$/u, '').toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
export function underConfiguredRoot(relativePath, roots) {
|
|
9
|
+
const candidate = relativePath.replaceAll('\\', '/').toLowerCase();
|
|
10
|
+
return roots.some((root) => {
|
|
11
|
+
const normalized = normalizedRoot(root);
|
|
12
|
+
return candidate === normalized || candidate.startsWith(`${normalized}/`);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export function activeFocusFiles(snapshot, focusRoots) {
|
|
16
|
+
return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
|
|
17
|
+
underConfiguredRoot(relativePath, focusRoots) &&
|
|
18
|
+
relativePath.toLowerCase().endsWith('.txt'));
|
|
19
|
+
}
|
|
20
|
+
export function activeContinuousFocusFiles(snapshot) {
|
|
21
|
+
return snapshot.files.filter(({ relativePath, shadowedBy }) => shadowedBy === undefined &&
|
|
22
|
+
relativePath.replaceAll('\\', '/').toLowerCase().startsWith('common/continuous_focus/') &&
|
|
23
|
+
relativePath.toLowerCase().endsWith('.txt'));
|
|
24
|
+
}
|
|
25
|
+
export function continuousPalettes(snapshot) {
|
|
26
|
+
const palettes = [];
|
|
27
|
+
const diagnostics = [];
|
|
28
|
+
for (const file of activeContinuousFocusFiles(snapshot)) {
|
|
29
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
30
|
+
const imported = importContinuousFocusPalettes(document);
|
|
31
|
+
palettes.push(...imported.continuousFocusPalettes);
|
|
32
|
+
diagnostics.push(...imported.diagnostics);
|
|
33
|
+
}
|
|
34
|
+
return { palettes, diagnostics };
|
|
35
|
+
}
|
|
36
|
+
export function activeSidecar(snapshot, focusFile) {
|
|
37
|
+
const relativePath = focusPlanningSidecarPath(focusFile.relativePath).toLowerCase();
|
|
38
|
+
const file = snapshot.files.find((candidate) => candidate.shadowedBy === undefined && candidate.relativePath.toLowerCase() === relativePath);
|
|
39
|
+
return file === undefined ? undefined : { file, sidecar: parseFocusPlanningSidecar(file.bytes) };
|
|
40
|
+
}
|
|
41
|
+
export function importFocusFile(snapshot, file, palettes) {
|
|
42
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
43
|
+
const imported = importFocusTrees(document, { references: referenceCatalog(snapshot) });
|
|
44
|
+
const sidecar = activeSidecar(snapshot, file);
|
|
45
|
+
imported.plans = imported.plans.map((plan) => {
|
|
46
|
+
const enriched = sidecar === undefined
|
|
47
|
+
? { plan, diagnostics: [] }
|
|
48
|
+
: enrichFocusPlanFromSidecar(plan, sidecar.sidecar);
|
|
49
|
+
imported.diagnostics.push(...enriched.diagnostics);
|
|
50
|
+
return linkContinuousFocusPalettes(enriched.plan, palettes);
|
|
51
|
+
});
|
|
52
|
+
return imported;
|
|
53
|
+
}
|
|
54
|
+
export function selectedFocusFile(snapshot, focusRoots, relativePath) {
|
|
55
|
+
const file = activeFocusFiles(snapshot, focusRoots).find((candidate) => candidate.relativePath === relativePath);
|
|
56
|
+
if (file === undefined)
|
|
57
|
+
throw new ServiceError('FOCUS_SOURCE_NOT_FOUND', `Focus source was not found: ${relativePath}`);
|
|
58
|
+
return file;
|
|
59
|
+
}
|
|
60
|
+
export function selectedContinuousFocusFile(snapshot, relativePath) {
|
|
61
|
+
const file = activeContinuousFocusFiles(snapshot).find((candidate) => candidate.relativePath === relativePath);
|
|
62
|
+
if (file === undefined)
|
|
63
|
+
throw new ServiceError('CONTINUOUS_FOCUS_SOURCE_NOT_FOUND', `Continuous focus source was not found: ${relativePath}`);
|
|
64
|
+
return file;
|
|
65
|
+
}
|
|
66
|
+
export function selectPlan(result, treeId) {
|
|
67
|
+
if (result.plans.length === 0)
|
|
68
|
+
throw new ServiceError('FOCUS_TREE_NOT_FOUND', 'The source file contains no focus tree');
|
|
69
|
+
if (treeId === undefined) {
|
|
70
|
+
if (result.plans.length !== 1)
|
|
71
|
+
throw new ServiceError('FOCUS_TREE_ID_REQUIRED', 'The source file contains multiple focus trees; provide treeId', { treeIds: result.plans.map(({ id }) => id) });
|
|
72
|
+
return result.plans[0];
|
|
73
|
+
}
|
|
74
|
+
const plan = result.plans.find(({ id }) => id === treeId);
|
|
75
|
+
if (plan === undefined)
|
|
76
|
+
throw new ServiceError('FOCUS_TREE_NOT_FOUND', `Focus tree was not found: ${treeId}`);
|
|
77
|
+
return plan;
|
|
78
|
+
}
|
|
79
|
+
export function selectContinuousPlan(result, paletteId) {
|
|
80
|
+
if (result.continuousFocusPalettes.length === 0)
|
|
81
|
+
throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', 'The source file contains no continuous focus palette');
|
|
82
|
+
if (paletteId === undefined) {
|
|
83
|
+
if (result.continuousFocusPalettes.length !== 1)
|
|
84
|
+
throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_ID_REQUIRED', 'The source file contains multiple continuous focus palettes; provide paletteId', { paletteIds: result.continuousFocusPalettes.map(({ id }) => id) });
|
|
85
|
+
return result.continuousFocusPalettes[0];
|
|
86
|
+
}
|
|
87
|
+
const plan = result.continuousFocusPalettes.find(({ id }) => id === paletteId);
|
|
88
|
+
if (plan === undefined)
|
|
89
|
+
throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', `Continuous focus palette was not found: ${paletteId}`);
|
|
90
|
+
return plan;
|
|
91
|
+
}
|
|
92
|
+
export function referenceCatalog(snapshot) {
|
|
93
|
+
const identifiers = (kind) => [
|
|
94
|
+
...new Set(snapshot.index
|
|
95
|
+
.findAll(kind)
|
|
96
|
+
.filter(({ overridden }) => !overridden)
|
|
97
|
+
.map(({ id }) => id)),
|
|
98
|
+
].sort((left, right) => compareCodeUnits(left, right));
|
|
99
|
+
return {
|
|
100
|
+
decision: identifiers('decision'),
|
|
101
|
+
decision_category: identifiers('decision_category'),
|
|
102
|
+
event: identifiers('event'),
|
|
103
|
+
idea: identifiers('idea'),
|
|
104
|
+
leader: identifiers('leader'),
|
|
105
|
+
formable: identifiers('formable'),
|
|
106
|
+
helper: identifiers('scripted_effect'),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function validationFromDiagnostics(diagnostics) {
|
|
110
|
+
const blocking = diagnostics.filter(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
111
|
+
return {
|
|
112
|
+
passed: blocking.length === 0,
|
|
113
|
+
checks: [
|
|
114
|
+
{
|
|
115
|
+
id: 'focus-diagnostics',
|
|
116
|
+
passed: blocking.length === 0,
|
|
117
|
+
message: blocking.length === 0
|
|
118
|
+
? 'Focus source, references, and layout have no blocking diagnostics'
|
|
119
|
+
: `${blocking.length} blocking focus diagnostics`,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
|
|
3
|
+
import { errorResult, toolResult } from '../core/operation-result.js';
|
|
4
|
+
import { ServiceError } from '../core/result.js';
|
|
5
|
+
import { guiInspectRequestSchema, guiRenderRequestSchema, guiRewriteRequestSchema, } from '../schemas/gui-requests.js';
|
|
6
|
+
import { ScriptedGuiStudio } from './studio.js';
|
|
7
|
+
import { inspectGui, completeGuiRewrite, normalizeGuiInspectRequest, normalizeGuiRenderRequest, normalizeGuiRewriteRequest, prepareGuiRewrite, renderGui, } from './tool-operations.js';
|
|
8
|
+
const wireResult = z.record(z.string(), z.json());
|
|
9
|
+
/** Registers read-only GUI operations without invoking transport handlers. */
|
|
10
|
+
export function registerGuiJobs(operations, engine) {
|
|
11
|
+
const studio = new ScriptedGuiStudio(engine);
|
|
12
|
+
const lifetime = new IdleCacheLifetime(() => studio.clearCaches());
|
|
13
|
+
function register(name, schema, normalize, execute) {
|
|
14
|
+
operations.registerRead(name, schema, async (input, context) => {
|
|
15
|
+
const release = lifetime.begin();
|
|
16
|
+
try {
|
|
17
|
+
if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
|
|
18
|
+
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
19
|
+
const result = await execute(studio, normalize(input), {
|
|
20
|
+
workspaceId: context.workspaceId,
|
|
21
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
22
|
+
signal: context.signal,
|
|
23
|
+
progress: (completed, total, message) => context.progress({ completed, total, message }),
|
|
24
|
+
});
|
|
25
|
+
const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
|
|
26
|
+
const revision = result.data !== null &&
|
|
27
|
+
typeof result.data === 'object' &&
|
|
28
|
+
'sourceRevision' in result.data &&
|
|
29
|
+
typeof result.data.sourceRevision === 'string'
|
|
30
|
+
? result.data.sourceRevision
|
|
31
|
+
: result.data !== null &&
|
|
32
|
+
typeof result.data === 'object' &&
|
|
33
|
+
'sharedRevision' in result.data &&
|
|
34
|
+
typeof result.data.sharedRevision === 'string'
|
|
35
|
+
? result.data.sharedRevision
|
|
36
|
+
: undefined;
|
|
37
|
+
if (revision !== undefined)
|
|
38
|
+
await context.stageResult(wire, { sourceRevision: revision });
|
|
39
|
+
return wire;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (context.signal.aborted)
|
|
43
|
+
throw error;
|
|
44
|
+
return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
release();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
register('hoi4.gui_inspect', guiInspectRequestSchema, normalizeGuiInspectRequest, (studio, input, context) => inspectGui(engine, studio, input, context));
|
|
52
|
+
register('hoi4.gui_render', guiRenderRequestSchema, normalizeGuiRenderRequest, renderGui);
|
|
53
|
+
operations.registerWrite('hoi4.gui_rewrite', guiRewriteRequestSchema, async (input, context) => {
|
|
54
|
+
if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
|
|
55
|
+
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
56
|
+
const release = lifetime.begin();
|
|
57
|
+
try {
|
|
58
|
+
return await prepareGuiRewrite(studio, normalizeGuiRewriteRequest(input), {
|
|
59
|
+
workspaceId: context.workspaceId,
|
|
60
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
61
|
+
signal: context.signal,
|
|
62
|
+
progress: (completed, total, message) => context.progress({ completed, total, message }),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
release();
|
|
67
|
+
}
|
|
68
|
+
}, async (_input, execution, _reconciled, recipe, context) => {
|
|
69
|
+
const result = completeGuiRewrite(context.workspaceId, execution, recipe);
|
|
70
|
+
await context.progress({
|
|
71
|
+
completed: 3,
|
|
72
|
+
total: 3,
|
|
73
|
+
message: execution.outcome === 'applied'
|
|
74
|
+
? 'GUI rewrite complete'
|
|
75
|
+
: execution.outcome === 'unchanged'
|
|
76
|
+
? 'GUI content already satisfied the rewrite'
|
|
77
|
+
: 'GUI rewrite blocked',
|
|
78
|
+
});
|
|
79
|
+
return wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { boundedSourceHashEvidence, publicArtifactLink } from '../core/artifacts.js';
|
|
3
|
+
import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
4
|
+
import { autonomousResultArtifacts, } from '../core/transaction-execution.js';
|
|
5
|
+
import { emptyServiceResult } from '../core/result.js';
|
|
6
|
+
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
7
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
8
|
+
import { compileGuiHelpers } from './helpers.js';
|
|
9
|
+
import { encodeGuiInspectionArtifact, projectGuiGraphForArtifact } from './inspection-artifact.js';
|
|
10
|
+
const guiRewriteRecipeSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
mode: z.enum(['source', 'helpers', 'patches']),
|
|
13
|
+
filesScanned: z.array(z.string().min(1).max(4096)),
|
|
14
|
+
compilation: z
|
|
15
|
+
.object({
|
|
16
|
+
nodeCount: z.number().int().nonnegative(),
|
|
17
|
+
templateInstanceCount: z.number().int().nonnegative(),
|
|
18
|
+
rawEscapeCount: z.number().int().nonnegative(),
|
|
19
|
+
})
|
|
20
|
+
.strict()
|
|
21
|
+
.optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
function validationSummary(validation) {
|
|
25
|
+
return {
|
|
26
|
+
passed: validation.checks.every(({ passed }) => passed) &&
|
|
27
|
+
!validation.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'),
|
|
28
|
+
checks: validation.checks,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function normalizeGuiInspectRequest(input) {
|
|
32
|
+
return input;
|
|
33
|
+
}
|
|
34
|
+
export function normalizeGuiRenderRequest(input) {
|
|
35
|
+
return input;
|
|
36
|
+
}
|
|
37
|
+
export function normalizeGuiRewriteRequest(input) {
|
|
38
|
+
return input;
|
|
39
|
+
}
|
|
40
|
+
export async function prepareGuiRewrite(studio, input, context) {
|
|
41
|
+
await context.progress(0, 3, 'Validating the requested GUI result');
|
|
42
|
+
const compilation = input.mode === 'helpers' ? compileGuiHelpers(input.helper) : undefined;
|
|
43
|
+
const transaction = await studio.planSource({
|
|
44
|
+
workspaceId: context.workspaceId,
|
|
45
|
+
relativePath: input.relativePath,
|
|
46
|
+
...(input.mode === 'patches'
|
|
47
|
+
? { expectedSourceHash: input.expectedSourceHash, patches: input.patches }
|
|
48
|
+
: {
|
|
49
|
+
source: input.mode === 'source' ? input.source : compilation.source,
|
|
50
|
+
...(input.additionalFiles === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { additionalFiles: input.additionalFiles }),
|
|
53
|
+
}),
|
|
54
|
+
windowName: input.windowName,
|
|
55
|
+
scenario: input.scenario,
|
|
56
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
57
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
58
|
+
});
|
|
59
|
+
await context.progress(2, 3, 'GUI rewrite prepared for journaled application');
|
|
60
|
+
return {
|
|
61
|
+
transaction,
|
|
62
|
+
recipe: guiRewriteRecipeSchema.parse({
|
|
63
|
+
mode: input.mode,
|
|
64
|
+
filesScanned: [
|
|
65
|
+
input.relativePath,
|
|
66
|
+
...(input.additionalFiles ?? []).map(({ relativePath }) => relativePath),
|
|
67
|
+
].sort(compareCodeUnits),
|
|
68
|
+
...(compilation === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: {
|
|
71
|
+
compilation: {
|
|
72
|
+
nodeCount: compilation.nodeCount,
|
|
73
|
+
templateInstanceCount: compilation.templateInstanceCount,
|
|
74
|
+
rawEscapeCount: compilation.rawEscapeCount,
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function completeGuiRewrite(workspaceId, execution, recipe) {
|
|
81
|
+
const parsed = guiRewriteRecipeSchema.parse(recipe);
|
|
82
|
+
const transaction = execution.transaction;
|
|
83
|
+
const result = emptyServiceResult(workspaceId, {
|
|
84
|
+
mode: parsed.mode,
|
|
85
|
+
execution: execution.outcome,
|
|
86
|
+
fileCount: transaction.files.length,
|
|
87
|
+
artifactCount: transaction.artifacts.length,
|
|
88
|
+
...(parsed.compilation ?? {}),
|
|
89
|
+
});
|
|
90
|
+
result.status = transaction.validation.passed ? 'ok' : 'blocked';
|
|
91
|
+
result.code =
|
|
92
|
+
execution.outcome === 'applied'
|
|
93
|
+
? 'GUI_CHANGES_APPLIED'
|
|
94
|
+
: execution.outcome === 'unchanged'
|
|
95
|
+
? 'GUI_CHANGES_UNCHANGED'
|
|
96
|
+
: 'GUI_CHANGES_BLOCKED';
|
|
97
|
+
setInlineFilesScanned(result, parsed.filesScanned);
|
|
98
|
+
result.proposedFiles = transaction.files.slice(0, 100).map(({ relativePath }) => relativePath);
|
|
99
|
+
result.changedFiles = transaction.appliedFiles.slice(0, 100);
|
|
100
|
+
result.diagnostics = transaction.diagnostics.slice(0, 100);
|
|
101
|
+
result.artifacts = autonomousResultArtifacts(execution);
|
|
102
|
+
result.validation = transaction.validation;
|
|
103
|
+
result.blockers = transaction.diagnostics
|
|
104
|
+
.filter(({ severity }) => severity === 'error' || severity === 'blocker')
|
|
105
|
+
.map(({ code, message, details }) => ({
|
|
106
|
+
code,
|
|
107
|
+
message,
|
|
108
|
+
...(details === undefined ? {} : { details }),
|
|
109
|
+
}));
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
export async function inspectGui(engine, studio, input, context) {
|
|
113
|
+
await context.progress(0, 3, 'Building GUI source graph');
|
|
114
|
+
const inspected = input.windowName === undefined || input.scenario === undefined
|
|
115
|
+
? {
|
|
116
|
+
graph: (await studio.scan(context.workspaceId, context.principal, context.signal)).graph,
|
|
117
|
+
linted: undefined,
|
|
118
|
+
}
|
|
119
|
+
: await (async () => {
|
|
120
|
+
const linted = await studio.lint({
|
|
121
|
+
workspaceId: context.workspaceId,
|
|
122
|
+
windowName: input.windowName,
|
|
123
|
+
scenario: input.scenario,
|
|
124
|
+
...(input.relatedScenarios === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: { relatedScenarios: input.relatedScenarios }),
|
|
127
|
+
generatedScenarios: input.generatedScenarios ?? {},
|
|
128
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
129
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
130
|
+
});
|
|
131
|
+
return { graph: linted.graph, linted };
|
|
132
|
+
})();
|
|
133
|
+
const { graph, linted } = inspected;
|
|
134
|
+
const sharedRevision = hashCanonical(graph.sourceHashes);
|
|
135
|
+
const workspace = engine.resolver.get(context.workspaceId, context.principal);
|
|
136
|
+
const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
|
|
137
|
+
const artifactGraph = projectGuiGraphForArtifact(graph, linted?.scene.elements.map(({ sourceId }) => sourceId));
|
|
138
|
+
const inspectionName = `gui-inspect.${sharedRevision.slice(0, 16)}.json`;
|
|
139
|
+
const inspectionJson = `${canonicalJson({
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
sharedRevision,
|
|
142
|
+
offline: true,
|
|
143
|
+
graph: artifactGraph.graph,
|
|
144
|
+
...(artifactGraph.projection === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { graphProjection: artifactGraph.projection }),
|
|
147
|
+
...(linted === undefined
|
|
148
|
+
? {}
|
|
149
|
+
: {
|
|
150
|
+
scenario: linted.scene.scenario,
|
|
151
|
+
fidelity: linted.scene.fidelity,
|
|
152
|
+
validation: linted.validation,
|
|
153
|
+
}),
|
|
154
|
+
})}\n`;
|
|
155
|
+
const encodedInspection = await encodeGuiInspectionArtifact(inspectionName, inspectionJson);
|
|
156
|
+
const artifact = await engine.artifacts.putChunked(workspace, encodedInspection.name, encodedInspection.mimeType, encodedInspection.content, {
|
|
157
|
+
kind: 'gui-inspect',
|
|
158
|
+
toolVersion: PACKAGE_VERSION,
|
|
159
|
+
schemaVersion: 'gui-inspect.v1',
|
|
160
|
+
sourceHashes: sourceEvidence.sourceHashes,
|
|
161
|
+
metadata: {
|
|
162
|
+
sharedRevision,
|
|
163
|
+
offline: true,
|
|
164
|
+
complete: graph.complete,
|
|
165
|
+
skippedSourceCount: graph.skippedSourceCount,
|
|
166
|
+
sourceHashInventory: sourceEvidence.inventory,
|
|
167
|
+
compressed: encodedInspection.compressed,
|
|
168
|
+
uncompressedBytes: encodedInspection.uncompressedBytes,
|
|
169
|
+
graphProjection: artifactGraph.projection?.mode ?? 'full',
|
|
170
|
+
},
|
|
171
|
+
}, encodedInspection.compressed
|
|
172
|
+
? 'Gzip-compressed scripted GUI graph, diagnostics, and optional scenario fidelity inspection'
|
|
173
|
+
: 'Scripted GUI graph, diagnostics, and optional scenario fidelity inspection', context.signal);
|
|
174
|
+
const diagnostics = linted === undefined ? graph.diagnostics : linted.validation.diagnostics;
|
|
175
|
+
const result = emptyServiceResult(context.workspaceId, {
|
|
176
|
+
sharedRevision,
|
|
177
|
+
complete: graph.complete,
|
|
178
|
+
skippedSourceCount: graph.skippedSourceCount,
|
|
179
|
+
skippedSources: graph.skippedSources,
|
|
180
|
+
nodes: graph.nodes.length,
|
|
181
|
+
edges: graph.edges.length,
|
|
182
|
+
elements: graph.elements.length,
|
|
183
|
+
sprites: graph.sprites.length,
|
|
184
|
+
fonts: graph.fonts.length,
|
|
185
|
+
scriptedGuis: graph.scriptedGuis.length,
|
|
186
|
+
...(linted === undefined
|
|
187
|
+
? {}
|
|
188
|
+
: {
|
|
189
|
+
windowName: input.windowName,
|
|
190
|
+
scenarioId: linted.scene.scenario.id,
|
|
191
|
+
inspectedElementCount: linted.scene.elements.length,
|
|
192
|
+
fidelityCounts: Object.fromEntries(Object.entries(linted.scene.fidelity).map(([key, values]) => [key, values.length])),
|
|
193
|
+
}),
|
|
194
|
+
});
|
|
195
|
+
result.code = graph.complete ? 'GUI_INSPECTED' : 'GUI_INSPECTED_PARTIAL';
|
|
196
|
+
setInlineFilesScanned(result, graph.filesScanned);
|
|
197
|
+
result.diagnostics = diagnostics.slice(0, 100);
|
|
198
|
+
result.artifacts = [publicArtifactLink(artifact)];
|
|
199
|
+
result.validation =
|
|
200
|
+
linted === undefined
|
|
201
|
+
? {
|
|
202
|
+
passed: !diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'),
|
|
203
|
+
checks: [
|
|
204
|
+
{
|
|
205
|
+
id: 'gui-inspection',
|
|
206
|
+
passed: true,
|
|
207
|
+
message: graph.complete
|
|
208
|
+
? `${graph.nodes.length} GUI graph nodes connected from a complete inventory`
|
|
209
|
+
: `${graph.nodes.length} GUI graph nodes connected; ${graph.skippedSourceCount} over-limit source(s) were skipped`,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
}
|
|
213
|
+
: validationSummary({ diagnostics, checks: linted.validation.checks });
|
|
214
|
+
await context.progress(3, 3, 'GUI inspection complete');
|
|
215
|
+
return result;
|
|
216
|
+
}
|
|
217
|
+
export async function renderGui(studio, input, context) {
|
|
218
|
+
await context.progress(0, 4, 'Building GUI source graph');
|
|
219
|
+
const rendered = await studio.renderAndStore({
|
|
220
|
+
workspaceId: context.workspaceId,
|
|
221
|
+
windowName: input.windowName,
|
|
222
|
+
scenario: input.scenario,
|
|
223
|
+
...(input.states === undefined ? {} : { states: input.states }),
|
|
224
|
+
...(input.resolutions === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: {
|
|
227
|
+
resolutions: input.resolutions.map(({ width, height, uiScale }) => ({
|
|
228
|
+
width,
|
|
229
|
+
height,
|
|
230
|
+
...(uiScale === undefined ? {} : { uiScale }),
|
|
231
|
+
})),
|
|
232
|
+
}),
|
|
233
|
+
...(input.relatedScenarios === undefined ? {} : { relatedScenarios: input.relatedScenarios }),
|
|
234
|
+
generatedScenarios: input.generatedScenarios ?? {},
|
|
235
|
+
...(input.comparisonScenario === undefined
|
|
236
|
+
? {}
|
|
237
|
+
: { comparisonScenario: input.comparisonScenario }),
|
|
238
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
239
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
240
|
+
});
|
|
241
|
+
await context.progress(3, 4, 'Preparing GUI result');
|
|
242
|
+
const diagnostics = rendered.validation.diagnostics;
|
|
243
|
+
const result = emptyServiceResult(context.workspaceId, {
|
|
244
|
+
windowName: input.windowName,
|
|
245
|
+
scenarioId: rendered.render.scene.scenario.id,
|
|
246
|
+
sourceRevision: rendered.render.scene.sourceRevision,
|
|
247
|
+
variantCount: rendered.render.images.length,
|
|
248
|
+
variants: rendered.render.images.slice(0, 64).map(({ variant, width, height }) => ({
|
|
249
|
+
variant,
|
|
250
|
+
width,
|
|
251
|
+
height,
|
|
252
|
+
})),
|
|
253
|
+
stateCount: rendered.stateScenes.length,
|
|
254
|
+
scenarioCount: rendered.scenarioScenes.length,
|
|
255
|
+
resolutionCount: rendered.resolutionScenes.length,
|
|
256
|
+
comparison: {
|
|
257
|
+
changedPixels: rendered.comparison.changedPixels,
|
|
258
|
+
changedRatio: rendered.comparison.changedRatio,
|
|
259
|
+
},
|
|
260
|
+
fidelityCounts: Object.fromEntries(Object.entries(rendered.render.fidelity).map(([key, values]) => [key, values.length])),
|
|
261
|
+
offlineRepresentation: true,
|
|
262
|
+
});
|
|
263
|
+
result.code = 'GUI_RENDERED';
|
|
264
|
+
setInlineFilesScanned(result, rendered.filesScanned);
|
|
265
|
+
result.diagnostics = diagnostics.slice(0, 100);
|
|
266
|
+
result.artifacts = rendered.artifacts.map(publicArtifactLink);
|
|
267
|
+
result.validation = validationSummary({ diagnostics, checks: rendered.validation.checks });
|
|
268
|
+
await context.progress(4, 4, 'GUI render complete');
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { IdleCacheLifetime } from '../core/idle-cache-lifetime.js';
|
|
3
|
+
import { errorResult, toolResult } from '../core/operation-result.js';
|
|
4
|
+
import { ServiceError } from '../core/result.js';
|
|
5
|
+
import { mapInspectRequestSchema, mapRenderRequestSchema, mapRewriteRequestSchema, } from '../schemas/map-requests.js';
|
|
6
|
+
import { AgentNudger } from './service.js';
|
|
7
|
+
import { inspectMap, completeMapRewrite, normalizeMapInspectRequest, normalizeMapRenderRequest, normalizeMapRewriteRequest, prepareMapRewrite, renderMapView, } from './tool-operations.js';
|
|
8
|
+
const wireResult = z.record(z.string(), z.json());
|
|
9
|
+
/** Registers read-only map operations without invoking transport handlers. */
|
|
10
|
+
export function registerMapJobs(operations, engine) {
|
|
11
|
+
const nudger = new AgentNudger(engine);
|
|
12
|
+
const lifetime = new IdleCacheLifetime(() => engine.releaseScanCaches());
|
|
13
|
+
function register(name, schema, normalize, execute) {
|
|
14
|
+
operations.registerRead(name, schema, async (input, context) => {
|
|
15
|
+
const release = lifetime.begin();
|
|
16
|
+
try {
|
|
17
|
+
if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
|
|
18
|
+
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
19
|
+
const result = await execute(nudger, normalize(input), {
|
|
20
|
+
workspaceId: context.workspaceId,
|
|
21
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
22
|
+
signal: context.signal,
|
|
23
|
+
progress: (completed, total, message) => context.progress({ completed, total, message }),
|
|
24
|
+
});
|
|
25
|
+
const wire = wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
|
|
26
|
+
const revision = result.data !== null &&
|
|
27
|
+
typeof result.data === 'object' &&
|
|
28
|
+
'revision' in result.data &&
|
|
29
|
+
typeof result.data.revision === 'string'
|
|
30
|
+
? result.data.revision
|
|
31
|
+
: undefined;
|
|
32
|
+
if (revision !== undefined)
|
|
33
|
+
await context.stageResult(wire, { sourceRevision: revision });
|
|
34
|
+
return wire;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (context.signal.aborted)
|
|
38
|
+
throw error;
|
|
39
|
+
return wireResult.parse(JSON.parse(JSON.stringify(errorResult(error, context.workspaceId))));
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
release();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
register('hoi4.map_inspect', mapInspectRequestSchema, normalizeMapInspectRequest, (nudger, input, context) => inspectMap(engine, nudger, input, context));
|
|
47
|
+
register('hoi4.map_render', mapRenderRequestSchema, normalizeMapRenderRequest, renderMapView);
|
|
48
|
+
operations.registerWrite('hoi4.map_rewrite', mapRewriteRequestSchema, async (input, context) => {
|
|
49
|
+
if (input.workspaceId !== 'current' && input.workspaceId !== context.workspaceId)
|
|
50
|
+
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
51
|
+
const release = lifetime.begin();
|
|
52
|
+
try {
|
|
53
|
+
return await prepareMapRewrite(nudger, normalizeMapRewriteRequest(input), {
|
|
54
|
+
workspaceId: context.workspaceId,
|
|
55
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
56
|
+
signal: context.signal,
|
|
57
|
+
progress: (completed, total, message) => context.progress({ completed, total, message }),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
release();
|
|
62
|
+
}
|
|
63
|
+
}, async (_input, execution, _reconciled, recipe, context) => {
|
|
64
|
+
const result = completeMapRewrite(context.workspaceId, execution, recipe);
|
|
65
|
+
await context.progress({
|
|
66
|
+
completed: 5,
|
|
67
|
+
total: 5,
|
|
68
|
+
message: execution.outcome === 'applied'
|
|
69
|
+
? 'Map rewrite complete'
|
|
70
|
+
: execution.outcome === 'unchanged'
|
|
71
|
+
? 'Map content already satisfied the operations'
|
|
72
|
+
: 'Map rewrite blocked',
|
|
73
|
+
});
|
|
74
|
+
return wireResult.parse(JSON.parse(JSON.stringify(toolResult(result))));
|
|
75
|
+
});
|
|
76
|
+
}
|