hoi4-agent-tools 3.2.0 → 3.4.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 +19 -0
- package/README.md +4 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -9
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
- package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
- package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +16 -0
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +38 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
- package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
- package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
- package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
- package/dist/hoi4_agent_tools/package-check/service.js +231 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/render.js +286 -31
- package/dist/hoi4_agent_tools/technology/service.js +55 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/events.md +3 -1
- package/docs/focus.md +10 -0
- package/docs/gui.md +49 -3
- package/docs/map.md +8 -0
- package/docs/mechanics.md +70 -0
- package/docs/probability.md +3 -1
- package/docs/technology.md +7 -2
- package/package.json +2 -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/decision-inspect-input.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 +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +650 -0
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +164 -0
- package/schemas/package-manifest.schema.json +142 -0
- 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/schemas/scenario-suite.schema.json +1004 -0
- package/schemas/scenario-test-input.schema.json +1057 -0
- package/schemas/shared-scenario.schema.json +237 -0
- package/server.json +2 -2
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
import { analysisSourceEvidence } from '../core/analysis-evidence.js';
|
|
4
|
+
import { publicArtifactLink } from '../core/artifacts.js';
|
|
5
|
+
import { canonicalJson, hashCanonical, sha256Bytes } from '../core/canonical.js';
|
|
6
|
+
import { registerAnalysisJobs } from '../core/analysis-job-operations.js';
|
|
7
|
+
import { JobOperations } from '../core/job-executor.js';
|
|
8
|
+
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
9
|
+
import { emptyServiceResult, ServiceError } from '../core/result.js';
|
|
10
|
+
import { registerEventJobs } from '../event/job-operations.js';
|
|
11
|
+
import { registerFocusJobs } from '../focus/job-operations.js';
|
|
12
|
+
import { registerGuiJobs } from '../gui/job-operations.js';
|
|
13
|
+
import { registerMapJobs } from '../map/job-operations.js';
|
|
14
|
+
import { runMechanicCase } from '../mechanic/service.js';
|
|
15
|
+
import { PackageAnalyzer } from '../package-check/service.js';
|
|
16
|
+
import { registerProbabilityJobs } from '../probability/job-operations.js';
|
|
17
|
+
import { scenarioSuiteSchema, } from '../schemas/scenarios.js';
|
|
18
|
+
import { registerTechnologyJobs } from '../technology/job-operations.js';
|
|
19
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
20
|
+
const MAX_SUITE_BYTES = 8 * 1024 * 1024;
|
|
21
|
+
const continuationSchema = z
|
|
22
|
+
.object({
|
|
23
|
+
version: z.literal(1),
|
|
24
|
+
workspaceIdentity: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
25
|
+
principal: z.string(),
|
|
26
|
+
suiteHash: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
27
|
+
sourceRevision: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
28
|
+
nextIndex: z.number().int().nonnegative(),
|
|
29
|
+
completedHash: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
30
|
+
failed: z.number().int().nonnegative(),
|
|
31
|
+
unresolved: z.number().int().nonnegative(),
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
function issueToken(engine, payload) {
|
|
35
|
+
const state = engine.resolver.serverState();
|
|
36
|
+
if (state === undefined)
|
|
37
|
+
throw new ServiceError('SCENARIO_CONTINUATION_UNAVAILABLE', 'Persistent server state is required for suite continuation');
|
|
38
|
+
const encoded = Buffer.from(canonicalJson(payload)).toString('base64url');
|
|
39
|
+
return `${encoded}.${state.authenticateJournal(payload)}`;
|
|
40
|
+
}
|
|
41
|
+
function readToken(engine, token) {
|
|
42
|
+
const state = engine.resolver.serverState();
|
|
43
|
+
if (state === undefined)
|
|
44
|
+
throw new ServiceError('SCENARIO_CONTINUATION_UNAVAILABLE', 'Persistent server state is required for suite continuation');
|
|
45
|
+
const [encoded, tag, extra] = token.split('.');
|
|
46
|
+
if (encoded === undefined || tag === undefined || extra !== undefined)
|
|
47
|
+
throw new ServiceError('SCENARIO_CONTINUATION_INVALID', 'Malformed continuation');
|
|
48
|
+
let payload;
|
|
49
|
+
try {
|
|
50
|
+
payload = continuationSchema.parse(JSON.parse(Buffer.from(encoded, 'base64url').toString('utf8')));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new ServiceError('SCENARIO_CONTINUATION_INVALID', 'Malformed continuation');
|
|
54
|
+
}
|
|
55
|
+
if (!state.verifyJournal(payload, tag))
|
|
56
|
+
throw new ServiceError('SCENARIO_CONTINUATION_INVALID', 'Continuation authentication failed');
|
|
57
|
+
return payload;
|
|
58
|
+
}
|
|
59
|
+
function atPath(object, path) {
|
|
60
|
+
let cursor = object;
|
|
61
|
+
for (const key of path) {
|
|
62
|
+
if (cursor === null || typeof cursor !== 'object' || !Object.hasOwn(cursor, key))
|
|
63
|
+
return undefined;
|
|
64
|
+
cursor = cursor[key];
|
|
65
|
+
}
|
|
66
|
+
return cursor;
|
|
67
|
+
}
|
|
68
|
+
function caseAssertions(assertions, output) {
|
|
69
|
+
return assertions.map((assertion) => {
|
|
70
|
+
const observed = atPath(output, assertion.path);
|
|
71
|
+
return {
|
|
72
|
+
id: assertion.id,
|
|
73
|
+
status: observed === undefined
|
|
74
|
+
? 'unresolved'
|
|
75
|
+
: observed === assertion.expected
|
|
76
|
+
? 'passed'
|
|
77
|
+
: 'failed',
|
|
78
|
+
...(observed === undefined ? {} : { observed }),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function typedOperations(engine) {
|
|
83
|
+
const operations = new JobOperations();
|
|
84
|
+
registerAnalysisJobs(operations, engine);
|
|
85
|
+
registerEventJobs(operations, engine);
|
|
86
|
+
registerTechnologyJobs(operations, engine);
|
|
87
|
+
registerProbabilityJobs(operations, engine);
|
|
88
|
+
registerFocusJobs(operations, engine);
|
|
89
|
+
registerGuiJobs(operations, engine);
|
|
90
|
+
registerMapJobs(operations, engine);
|
|
91
|
+
return operations;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Runs a versioned suite in bounded pages through fixed read-only core
|
|
95
|
+
* operations. The continuation is signed and pins all selection inputs.
|
|
96
|
+
*/
|
|
97
|
+
export class ScenarioAnalyzer {
|
|
98
|
+
engine;
|
|
99
|
+
constructor(engine) {
|
|
100
|
+
this.engine = engine;
|
|
101
|
+
}
|
|
102
|
+
async test(input) {
|
|
103
|
+
const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
|
|
104
|
+
if (input.refresh)
|
|
105
|
+
this.engine.invalidate(input.workspaceId);
|
|
106
|
+
const snapshot = await this.engine.scan(input.workspaceId, {}, input.principal, input.signal);
|
|
107
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== snapshot.revision)
|
|
108
|
+
throw new ServiceError('SCENARIO_SOURCE_STALE', 'Scenario source revision changed');
|
|
109
|
+
let suite;
|
|
110
|
+
let suiteFileHash;
|
|
111
|
+
if (input.suitePath !== undefined) {
|
|
112
|
+
const resolved = await this.engine.resolver.resolvePath(input.workspaceId, input.suitePath, 'read', ['mod'], input.principal);
|
|
113
|
+
const details = await stat(resolved.path);
|
|
114
|
+
if (!details.isFile() || details.size > MAX_SUITE_BYTES)
|
|
115
|
+
throw new ServiceError('SCENARIO_SUITE_LIMIT', 'Suite file exceeds the bounded reader');
|
|
116
|
+
const bytes = await readFile(resolved.path);
|
|
117
|
+
suiteFileHash = sha256Bytes(bytes);
|
|
118
|
+
suite = scenarioSuiteSchema.parse(JSON.parse(bytes.toString('utf8')));
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
suite = scenarioSuiteSchema.parse(input.suite);
|
|
122
|
+
const suiteHash = hashCanonical({ suite, suiteFileHash });
|
|
123
|
+
const resumed = input.continuation === undefined ? undefined : readToken(this.engine, input.continuation);
|
|
124
|
+
if (resumed !== undefined &&
|
|
125
|
+
(resumed.workspaceIdentity !== workspace.workspaceIdentity ||
|
|
126
|
+
resumed.principal !== (input.principal ?? '') ||
|
|
127
|
+
resumed.suiteHash !== suiteHash ||
|
|
128
|
+
resumed.sourceRevision !== snapshot.revision ||
|
|
129
|
+
resumed.nextIndex >= suite.cases.length ||
|
|
130
|
+
resumed.failed + resumed.unresolved > resumed.nextIndex))
|
|
131
|
+
throw new ServiceError('SCENARIO_CONTINUATION_STALE', 'Continuation does not match the authorized suite and source revision');
|
|
132
|
+
const start = resumed?.nextIndex ?? 0;
|
|
133
|
+
const end = Math.min(suite.cases.length, start + input.maxCases);
|
|
134
|
+
const operations = typedOperations(this.engine);
|
|
135
|
+
const packageAnalyzer = new PackageAnalyzer(this.engine);
|
|
136
|
+
const caseResults = [];
|
|
137
|
+
let completedHash = resumed?.completedHash ??
|
|
138
|
+
hashCanonical({
|
|
139
|
+
suiteHash,
|
|
140
|
+
sourceRevision: snapshot.revision,
|
|
141
|
+
workspaceIdentity: workspace.workspaceIdentity,
|
|
142
|
+
});
|
|
143
|
+
const evidence = analysisSourceEvidence(snapshot);
|
|
144
|
+
for (let index = start; index < end; index += 1) {
|
|
145
|
+
input.signal?.throwIfAborted();
|
|
146
|
+
const item = suite.cases[index];
|
|
147
|
+
const missing = item.sourceSelectors.filter(({ kind, id }) => snapshot.index.find(kind, id) === undefined);
|
|
148
|
+
let output;
|
|
149
|
+
let executionStatus;
|
|
150
|
+
let reason;
|
|
151
|
+
if (missing.length > 0) {
|
|
152
|
+
executionStatus = snapshot.complete ? 'failed' : 'unresolved';
|
|
153
|
+
reason = 'Declared source selector is absent from the active source index';
|
|
154
|
+
output = { missing };
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
try {
|
|
158
|
+
if (item.domain === 'mechanic') {
|
|
159
|
+
const mechanic = runMechanicCase(snapshot, item.test);
|
|
160
|
+
output = mechanic;
|
|
161
|
+
executionStatus =
|
|
162
|
+
mechanic.status === 'passed'
|
|
163
|
+
? 'completed'
|
|
164
|
+
: mechanic.status === 'failed'
|
|
165
|
+
? 'failed'
|
|
166
|
+
: 'unresolved';
|
|
167
|
+
}
|
|
168
|
+
else if (item.domain === 'package') {
|
|
169
|
+
const checked = await packageAnalyzer.check({
|
|
170
|
+
workspaceId: input.workspaceId,
|
|
171
|
+
manifest: item.manifest,
|
|
172
|
+
expectedRevision: snapshot.revision,
|
|
173
|
+
...(input.principal === undefined ? {} : { principal: input.principal }),
|
|
174
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
175
|
+
refresh: false,
|
|
176
|
+
});
|
|
177
|
+
output = checked;
|
|
178
|
+
executionStatus = checked.data.complete ? 'completed' : 'failed';
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const operation = operations.get(item.tool);
|
|
182
|
+
if (operation.mutation)
|
|
183
|
+
throw new ServiceError('SCENARIO_TOOL_MUTATION', 'Suite operation is not read-only');
|
|
184
|
+
const context = {
|
|
185
|
+
engine: this.engine,
|
|
186
|
+
workspaceId: input.workspaceId,
|
|
187
|
+
principal: input.principal,
|
|
188
|
+
signal: input.signal ?? new AbortController().signal,
|
|
189
|
+
checkpoint: undefined,
|
|
190
|
+
progress: () => Promise.resolve(),
|
|
191
|
+
saveCheckpoint: () => Promise.reject(new ServiceError('SCENARIO_CHECKPOINT_UNAVAILABLE', 'Nested jobs cannot publish a suite checkpoint')),
|
|
192
|
+
stageResult: () => Promise.resolve(),
|
|
193
|
+
};
|
|
194
|
+
const wire = await operation.run({
|
|
195
|
+
...item.arguments,
|
|
196
|
+
workspaceId: input.workspaceId,
|
|
197
|
+
}, context);
|
|
198
|
+
output = atPath(wire, ['structuredContent']) ?? wire;
|
|
199
|
+
const status = atPath(output, ['status']);
|
|
200
|
+
const passed = atPath(output, ['validation', 'passed']);
|
|
201
|
+
executionStatus =
|
|
202
|
+
status === 'error' || status === 'blocked' || passed === false
|
|
203
|
+
? 'failed'
|
|
204
|
+
: 'completed';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (input.signal?.aborted)
|
|
209
|
+
throw error;
|
|
210
|
+
executionStatus = 'unresolved';
|
|
211
|
+
reason =
|
|
212
|
+
error instanceof ServiceError
|
|
213
|
+
? error.code
|
|
214
|
+
: error instanceof Error
|
|
215
|
+
? error.message
|
|
216
|
+
: 'Case execution failed';
|
|
217
|
+
output = {};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const assertions = caseAssertions(item.assertions, output);
|
|
221
|
+
const status = assertions.some(({ status }) => status === 'failed')
|
|
222
|
+
? 'failed'
|
|
223
|
+
: assertions.some(({ status }) => status === 'unresolved') ||
|
|
224
|
+
executionStatus === 'unresolved'
|
|
225
|
+
? 'unresolved'
|
|
226
|
+
: executionStatus === 'failed'
|
|
227
|
+
? 'failed'
|
|
228
|
+
: 'completed';
|
|
229
|
+
const caseReport = {
|
|
230
|
+
schemaVersion: 'scenario-case.v1',
|
|
231
|
+
suiteId: suite.id,
|
|
232
|
+
caseId: item.id,
|
|
233
|
+
index,
|
|
234
|
+
domain: item.domain,
|
|
235
|
+
sourceRevision: snapshot.revision,
|
|
236
|
+
status,
|
|
237
|
+
assertions,
|
|
238
|
+
...(reason === undefined ? {} : { reason }),
|
|
239
|
+
output,
|
|
240
|
+
};
|
|
241
|
+
const caseHash = hashCanonical(caseReport);
|
|
242
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `scenario-case-${caseHash.slice(0, 24)}.json`, 'application/json', `${canonicalJson(caseReport)}\n`, {
|
|
243
|
+
kind: 'scenario-case',
|
|
244
|
+
toolVersion: PACKAGE_VERSION,
|
|
245
|
+
schemaVersion: 'scenario-case.v1',
|
|
246
|
+
sourceHashes: evidence.sourceHashes,
|
|
247
|
+
metadata: { beforeRevision: snapshot.revision, suiteHash, caseId: item.id },
|
|
248
|
+
}, 'Content-addressed suite case result', input.signal);
|
|
249
|
+
completedHash = hashCanonical({ prior: completedHash, caseId: item.id, caseHash });
|
|
250
|
+
caseResults.push({
|
|
251
|
+
id: item.id,
|
|
252
|
+
index,
|
|
253
|
+
status,
|
|
254
|
+
caseHash,
|
|
255
|
+
artifact: publicArtifactLink(artifact),
|
|
256
|
+
...(item.views.includes('summary') ? { assertions } : {}),
|
|
257
|
+
...(item.views.includes('artifacts')
|
|
258
|
+
? { outputArtifacts: atPath(output, ['artifacts']) }
|
|
259
|
+
: {}),
|
|
260
|
+
...(item.views.includes('diagnostics')
|
|
261
|
+
? { diagnostics: atPath(output, ['diagnostics']) }
|
|
262
|
+
: {}),
|
|
263
|
+
});
|
|
264
|
+
const observed = await this.engine.scan(input.workspaceId, {}, input.principal, input.signal);
|
|
265
|
+
if (observed.revision !== snapshot.revision)
|
|
266
|
+
throw new ServiceError('SCENARIO_SOURCE_STALE', 'Source changed during suite execution');
|
|
267
|
+
}
|
|
268
|
+
const failed = (resumed?.failed ?? 0) + caseResults.filter(({ status }) => status === 'failed').length;
|
|
269
|
+
const unresolved = (resumed?.unresolved ?? 0) +
|
|
270
|
+
caseResults.filter(({ status }) => status === 'unresolved').length;
|
|
271
|
+
const continuation = end < suite.cases.length
|
|
272
|
+
? issueToken(this.engine, {
|
|
273
|
+
version: 1,
|
|
274
|
+
workspaceIdentity: workspace.workspaceIdentity,
|
|
275
|
+
principal: input.principal ?? '',
|
|
276
|
+
suiteHash,
|
|
277
|
+
sourceRevision: snapshot.revision,
|
|
278
|
+
nextIndex: end,
|
|
279
|
+
completedHash,
|
|
280
|
+
failed,
|
|
281
|
+
unresolved,
|
|
282
|
+
})
|
|
283
|
+
: undefined;
|
|
284
|
+
const report = {
|
|
285
|
+
schemaVersion: 'scenario-suite-batch.v1',
|
|
286
|
+
suiteId: suite.id,
|
|
287
|
+
suiteHash,
|
|
288
|
+
sourceRevision: snapshot.revision,
|
|
289
|
+
start,
|
|
290
|
+
end,
|
|
291
|
+
total: suite.cases.length,
|
|
292
|
+
completedHash,
|
|
293
|
+
failed,
|
|
294
|
+
unresolved,
|
|
295
|
+
cases: caseResults,
|
|
296
|
+
...(continuation === undefined ? {} : { continuation }),
|
|
297
|
+
};
|
|
298
|
+
const reportHash = hashCanonical(report);
|
|
299
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `scenario-batch-${reportHash.slice(0, 24)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
300
|
+
kind: 'scenario-suite-batch',
|
|
301
|
+
toolVersion: PACKAGE_VERSION,
|
|
302
|
+
schemaVersion: 'scenario-suite-batch.v1',
|
|
303
|
+
sourceHashes: evidence.sourceHashes,
|
|
304
|
+
metadata: { beforeRevision: snapshot.revision, suiteHash, start, end },
|
|
305
|
+
}, 'Revision-pinned suite batch and continuation evidence', input.signal);
|
|
306
|
+
const result = emptyServiceResult(input.workspaceId, {
|
|
307
|
+
sourceRevision: snapshot.revision,
|
|
308
|
+
suiteHash,
|
|
309
|
+
reportHash,
|
|
310
|
+
start,
|
|
311
|
+
end,
|
|
312
|
+
total: suite.cases.length,
|
|
313
|
+
completed: end,
|
|
314
|
+
failed,
|
|
315
|
+
unresolved,
|
|
316
|
+
pending: suite.cases.length - end,
|
|
317
|
+
...(continuation === undefined ? {} : { continuation }),
|
|
318
|
+
});
|
|
319
|
+
result.code =
|
|
320
|
+
continuation !== undefined
|
|
321
|
+
? 'SCENARIO_SUITE_PENDING'
|
|
322
|
+
: failed > 0 || unresolved > 0
|
|
323
|
+
? 'SCENARIO_SUITE_INCOMPLETE'
|
|
324
|
+
: 'SCENARIO_SUITE_PASSED';
|
|
325
|
+
result.artifacts = [publicArtifactLink(artifact)];
|
|
326
|
+
setInlineFilesScanned(result, snapshot.files.map(({ displayPath }) => displayPath));
|
|
327
|
+
result.validation = {
|
|
328
|
+
passed: continuation === undefined && failed === 0 && unresolved === 0,
|
|
329
|
+
checks: [
|
|
330
|
+
{
|
|
331
|
+
id: 'suite-cases',
|
|
332
|
+
passed: continuation === undefined && failed === 0 && unresolved === 0,
|
|
333
|
+
message: continuation === undefined
|
|
334
|
+
? 'All requested cases were reported in this suite'
|
|
335
|
+
: 'Additional cases are pending under the authenticated continuation',
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
@@ -151,6 +151,12 @@ export function validateEventCompareRequest(value, context) {
|
|
|
151
151
|
path: ['after'],
|
|
152
152
|
message: 'after and proposedSources are mutually exclusive',
|
|
153
153
|
});
|
|
154
|
+
if (value.maxChainNodes !== undefined && value.selector === undefined)
|
|
155
|
+
context.addIssue({
|
|
156
|
+
code: 'custom',
|
|
157
|
+
path: ['maxChainNodes'],
|
|
158
|
+
message: 'maxChainNodes requires selector',
|
|
159
|
+
});
|
|
154
160
|
}
|
|
155
161
|
export const eventCompareRequestSchema = z
|
|
156
162
|
.object({
|
|
@@ -158,6 +164,8 @@ export const eventCompareRequestSchema = z
|
|
|
158
164
|
before: eventGraphReferenceSchema.optional(),
|
|
159
165
|
after: eventGraphReferenceSchema.optional(),
|
|
160
166
|
proposedSources: z.array(eventProposedSourceSchema).min(1).max(64).optional(),
|
|
167
|
+
selector: eventSelectorSchema.optional(),
|
|
168
|
+
maxChainNodes: z.number().int().min(1).max(5_000).optional(),
|
|
161
169
|
render: z.boolean().optional(),
|
|
162
170
|
maxRenderNodes: z.number().int().min(1).max(240).optional(),
|
|
163
171
|
refresh: z.boolean().optional(),
|
|
@@ -17,6 +17,10 @@ export const focusInspectRequestSchema = z
|
|
|
17
17
|
previous: compactFocusLayoutSchema.optional(),
|
|
18
18
|
laneSpacing: z.number().int().min(1).max(100).optional(),
|
|
19
19
|
nodeSpacing: z.number().int().min(1).max(100).optional(),
|
|
20
|
+
scenario: z
|
|
21
|
+
.object({ completedFocusIds: z.array(z.string().min(1).max(256)).max(10_000) })
|
|
22
|
+
.strict()
|
|
23
|
+
.optional(),
|
|
20
24
|
})
|
|
21
25
|
.strict()
|
|
22
26
|
.superRefine((value, context) => {
|
|
@@ -34,7 +38,7 @@ export const focusInspectRequestSchema = z
|
|
|
34
38
|
message: 'treeId is only valid in national mode',
|
|
35
39
|
});
|
|
36
40
|
if (mode === 'continuous') {
|
|
37
|
-
for (const field of ['previous', 'laneSpacing', 'nodeSpacing']) {
|
|
41
|
+
for (const field of ['previous', 'laneSpacing', 'nodeSpacing', 'scenario']) {
|
|
38
42
|
if (value[field] !== undefined)
|
|
39
43
|
context.addIssue({
|
|
40
44
|
code: 'custom',
|
|
@@ -49,6 +53,12 @@ export const focusInspectRequestSchema = z
|
|
|
49
53
|
path: ['previous'],
|
|
50
54
|
message: 'previous requires treeId so it applies to exactly one tree',
|
|
51
55
|
});
|
|
56
|
+
if (value.scenario !== undefined && value.treeId === undefined)
|
|
57
|
+
context.addIssue({
|
|
58
|
+
code: 'custom',
|
|
59
|
+
path: ['scenario'],
|
|
60
|
+
message: 'scenario requires treeId so it applies to exactly one tree',
|
|
61
|
+
});
|
|
52
62
|
});
|
|
53
63
|
export const focusRenderRequestSchema = z
|
|
54
64
|
.object({
|
|
@@ -64,6 +74,7 @@ export const focusRenderRequestSchema = z
|
|
|
64
74
|
.min(FOCUS_RENDER_MIN_OUTPUT_SCALE)
|
|
65
75
|
.max(FOCUS_RENDER_MAX_OUTPUT_SCALE)
|
|
66
76
|
.optional(),
|
|
77
|
+
cropFocusIds: z.array(z.string().min(1).max(256)).max(16).optional(),
|
|
67
78
|
columns: z.number().int().min(1).max(12).optional(),
|
|
68
79
|
padding: z.number().int().min(0).max(1000).optional(),
|
|
69
80
|
})
|
|
@@ -80,6 +91,7 @@ export const focusRenderRequestSchema = z
|
|
|
80
91
|
['horizontalSpacing', value.horizontalSpacing],
|
|
81
92
|
['verticalSpacing', value.verticalSpacing],
|
|
82
93
|
['reviewScale', value.reviewScale],
|
|
94
|
+
['cropFocusIds', value.cropFocusIds],
|
|
83
95
|
];
|
|
84
96
|
for (const [field, fieldValue] of invalid) {
|
|
85
97
|
if (fieldValue !== undefined)
|
|
@@ -103,6 +115,25 @@ export const focusRewriteRequestSchema = z
|
|
|
103
115
|
relativePath: workspaceRelativePathSchema,
|
|
104
116
|
treeId: z.string().min(1).max(256).optional(),
|
|
105
117
|
layoutMode: z.enum(['authored', 'compact']).default('authored'),
|
|
118
|
+
compactFocusIds: z.array(z.string().min(1).max(256)).min(1).max(10_000).optional(),
|
|
119
|
+
pinnedFocusIds: z.array(z.string().min(1).max(256)).max(10_000).optional(),
|
|
120
|
+
symmetryGroups: z
|
|
121
|
+
.array(z
|
|
122
|
+
.object({
|
|
123
|
+
centerFocusId: z.string().min(1).max(256),
|
|
124
|
+
pairs: z
|
|
125
|
+
.array(z
|
|
126
|
+
.object({
|
|
127
|
+
leftFocusId: z.string().min(1).max(256),
|
|
128
|
+
rightFocusId: z.string().min(1).max(256),
|
|
129
|
+
})
|
|
130
|
+
.strict())
|
|
131
|
+
.min(1)
|
|
132
|
+
.max(10_000),
|
|
133
|
+
})
|
|
134
|
+
.strict())
|
|
135
|
+
.max(64)
|
|
136
|
+
.optional(),
|
|
106
137
|
plan: compactFocusPlanSchema.optional(),
|
|
107
138
|
createIfMissing: z.boolean().default(false),
|
|
108
139
|
horizontalSpacing: z.number().int().min(80).max(1000).optional(),
|
|
@@ -117,6 +148,14 @@ export const focusRewriteRequestSchema = z
|
|
|
117
148
|
})
|
|
118
149
|
.strict()
|
|
119
150
|
.superRefine((value, context) => {
|
|
151
|
+
if (value.layoutMode !== 'compact')
|
|
152
|
+
for (const field of ['compactFocusIds', 'pinnedFocusIds', 'symmetryGroups'])
|
|
153
|
+
if (value[field] !== undefined)
|
|
154
|
+
context.addIssue({
|
|
155
|
+
code: 'custom',
|
|
156
|
+
path: [field],
|
|
157
|
+
message: `${field} requires compact layout mode`,
|
|
158
|
+
});
|
|
120
159
|
const mode = value.mode ?? 'national';
|
|
121
160
|
const schema = mode === 'continuous' ? continuousFocusPaletteSchema : focusTreePlanSchema;
|
|
122
161
|
if (value.plan !== undefined && !schema.safeParse(value.plan).success)
|
|
@@ -82,6 +82,14 @@ export const guiRenderRequestSchema = z
|
|
|
82
82
|
relatedScenarios: z.array(compactGuiScenarioSchema).max(32).optional(),
|
|
83
83
|
generatedScenarios: compactGeneratedScenarioOptionsSchema.optional(),
|
|
84
84
|
comparisonScenario: compactGuiScenarioSchema.optional(),
|
|
85
|
+
sourceBaseline: z
|
|
86
|
+
.object({
|
|
87
|
+
relativePath: workspaceRelativePathSchema,
|
|
88
|
+
source: z.string().max(SOURCE_MAX_BYTES),
|
|
89
|
+
})
|
|
90
|
+
.strict()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Previous mod-owned .gui source for a matched source comparison using the requested scenario.'),
|
|
85
93
|
})
|
|
86
94
|
.strict()
|
|
87
95
|
.superRefine(({ scenario, states, resolutions, relatedScenarios, generatedScenarios }, context) => {
|
|
@@ -63,6 +63,7 @@ export const mapInspectRequestSchema = z
|
|
|
63
63
|
]))
|
|
64
64
|
.max(100)
|
|
65
65
|
.default([]),
|
|
66
|
+
lookupOnly: z.boolean().default(false),
|
|
66
67
|
includeOverview: z.boolean().default(true),
|
|
67
68
|
allocationRequests: z.array(mapAllocationRequestSchema).max(100).default([]),
|
|
68
69
|
})
|
|
@@ -73,8 +74,28 @@ export const mapRenderRequestSchema = z
|
|
|
73
74
|
layer: mapLayerSchema.optional(),
|
|
74
75
|
overlays: z.array(mapOverlaySchema).max(12).optional(),
|
|
75
76
|
scale: z.number().int().min(1).max(16).optional(),
|
|
77
|
+
tile: z
|
|
78
|
+
.object({
|
|
79
|
+
x: z.number().int().min(0),
|
|
80
|
+
y: z.number().int().min(0),
|
|
81
|
+
width: z.number().int().min(1).max(2_048),
|
|
82
|
+
height: z.number().int().min(1).max(2_048),
|
|
83
|
+
})
|
|
84
|
+
.strict()
|
|
85
|
+
.optional(),
|
|
86
|
+
area: z
|
|
87
|
+
.object({
|
|
88
|
+
provinceIds: z.array(z.number().int().nonnegative()).max(256).optional(),
|
|
89
|
+
stateIds: z.array(z.number().int().nonnegative()).max(64).optional(),
|
|
90
|
+
regionIds: z.array(z.number().int().nonnegative()).max(64).optional(),
|
|
91
|
+
padding: z.number().int().min(0).max(256).optional(),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
.refine(({ provinceIds, stateIds, regionIds }) => (provinceIds?.length ?? 0) + (stateIds?.length ?? 0) + (regionIds?.length ?? 0) > 0, 'Map area requires at least one province, state, or region ID')
|
|
95
|
+
.optional(),
|
|
76
96
|
})
|
|
77
|
-
.strict()
|
|
97
|
+
.strict()
|
|
98
|
+
.refine(({ tile, area }) => tile === undefined || area === undefined, 'Choose tile or area, not both');
|
|
78
99
|
export const mapRewriteRequestSchema = z
|
|
79
100
|
.object({
|
|
80
101
|
workspaceId: workspaceIdSchema,
|