hoi4-agent-tools 3.1.0 → 3.3.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 +16 -0
- package/README.md +10 -2
- package/dist/hoi4_agent_tools/core/analysis-evidence.js +9 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +48 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +21 -1
- package/dist/hoi4_agent_tools/core/impact-comparison.js +49 -0
- package/dist/hoi4_agent_tools/core/impact-graph.js +301 -0
- package/dist/hoi4_agent_tools/core/impact-semantic.js +256 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +5 -0
- package/dist/hoi4_agent_tools/core/job-worker.js +2 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +12 -0
- package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
- package/dist/hoi4_agent_tools/core/source-overlay.js +125 -0
- package/dist/hoi4_agent_tools/decision/ai.js +104 -0
- package/dist/hoi4_agent_tools/decision/comparison.js +106 -0
- package/dist/hoi4_agent_tools/decision/cost.js +177 -0
- package/dist/hoi4_agent_tools/decision/evaluation.js +256 -0
- package/dist/hoi4_agent_tools/decision/lifecycle.js +114 -0
- package/dist/hoi4_agent_tools/decision/service.js +172 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +182 -0
- package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
- package/dist/hoi4_agent_tools/impact/scenario-suites.js +74 -0
- package/dist/hoi4_agent_tools/impact/service.js +127 -0
- package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +5 -0
- package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +2 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +155 -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 +3 -1
- package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
- package/dist/hoi4_agent_tools/schemas/analysis.js +228 -0
- package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +2 -0
- package/docs/analysis.md +100 -0
- package/docs/jobs.md +2 -1
- package/docs/mechanics.md +70 -0
- package/package.json +6 -2
- 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 +290 -0
- 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 +203 -0
- 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 +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { workspaceIdSchema, workspaceRelativePathSchema } from './common.js';
|
|
3
|
+
const symbolKindSchema = z.enum([
|
|
4
|
+
'focus_tree',
|
|
5
|
+
'focus',
|
|
6
|
+
'continuous_focus_palette',
|
|
7
|
+
'continuous_focus',
|
|
8
|
+
'decision',
|
|
9
|
+
'decision_category',
|
|
10
|
+
'event',
|
|
11
|
+
'idea',
|
|
12
|
+
'leader',
|
|
13
|
+
'formable',
|
|
14
|
+
'scripted_effect',
|
|
15
|
+
'scripted_trigger',
|
|
16
|
+
'script_constant',
|
|
17
|
+
'mtth_variable',
|
|
18
|
+
'ai_strategy',
|
|
19
|
+
'ai_strategy_plan',
|
|
20
|
+
'sprite',
|
|
21
|
+
'texture',
|
|
22
|
+
'gui_element',
|
|
23
|
+
'scripted_gui',
|
|
24
|
+
'technology',
|
|
25
|
+
'technology_folder',
|
|
26
|
+
'technology_category',
|
|
27
|
+
'technology_tag',
|
|
28
|
+
'equipment',
|
|
29
|
+
'equipment_module',
|
|
30
|
+
'sub_unit',
|
|
31
|
+
'unit_category',
|
|
32
|
+
'building',
|
|
33
|
+
'ability',
|
|
34
|
+
'combat_tactic',
|
|
35
|
+
'doctrine_folder',
|
|
36
|
+
'grand_doctrine',
|
|
37
|
+
'doctrine_track',
|
|
38
|
+
'subdoctrine',
|
|
39
|
+
'localisation',
|
|
40
|
+
'variable',
|
|
41
|
+
'flag',
|
|
42
|
+
'event_target',
|
|
43
|
+
'state',
|
|
44
|
+
'province',
|
|
45
|
+
'province_color',
|
|
46
|
+
'strategic_region',
|
|
47
|
+
'adjacency',
|
|
48
|
+
'supply_node',
|
|
49
|
+
'railway',
|
|
50
|
+
]);
|
|
51
|
+
export const impactSymbolSelectorSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
kind: symbolKindSchema,
|
|
54
|
+
id: z.string().min(1).max(512),
|
|
55
|
+
})
|
|
56
|
+
.strict();
|
|
57
|
+
const sourceDisplayPathSchema = z
|
|
58
|
+
.string()
|
|
59
|
+
.min(1)
|
|
60
|
+
.max(4096)
|
|
61
|
+
.refine((value) => {
|
|
62
|
+
const separator = value.indexOf(':');
|
|
63
|
+
if (separator < 1)
|
|
64
|
+
return false;
|
|
65
|
+
const root = value.slice(0, separator);
|
|
66
|
+
if (!/^(?:mod|game|fixture|dependency-[0-9]+)$/u.test(root))
|
|
67
|
+
return false;
|
|
68
|
+
return workspaceRelativePathSchema.safeParse(value.slice(separator + 1)).success;
|
|
69
|
+
}, 'Use a scanned source display path under an authorized workspace root');
|
|
70
|
+
export const proposedAnalysisSourceSchema = z
|
|
71
|
+
.object({
|
|
72
|
+
relativePath: workspaceRelativePathSchema,
|
|
73
|
+
content: z
|
|
74
|
+
.string()
|
|
75
|
+
.max(8 * 1024 * 1024)
|
|
76
|
+
.nullable(),
|
|
77
|
+
})
|
|
78
|
+
.strict();
|
|
79
|
+
const scenarioScalarSchema = z.union([z.string().max(4096), z.number(), z.boolean(), z.null()]);
|
|
80
|
+
const scenarioValueSchema = z.union([
|
|
81
|
+
scenarioScalarSchema,
|
|
82
|
+
z.array(scenarioScalarSchema).max(10_000),
|
|
83
|
+
]);
|
|
84
|
+
const scenarioStateSchema = z
|
|
85
|
+
.record(z.string().min(1).max(1024), scenarioValueSchema)
|
|
86
|
+
.refine((state) => Object.keys(state).length <= 10_000, 'Scenario state has too many keys');
|
|
87
|
+
const scopeBindingSchema = z
|
|
88
|
+
.object({
|
|
89
|
+
id: z.string().min(1).max(512),
|
|
90
|
+
type: z
|
|
91
|
+
.enum([
|
|
92
|
+
'country',
|
|
93
|
+
'state',
|
|
94
|
+
'character',
|
|
95
|
+
'unit_leader',
|
|
96
|
+
'operative',
|
|
97
|
+
'strategic_region',
|
|
98
|
+
'province',
|
|
99
|
+
'unknown',
|
|
100
|
+
])
|
|
101
|
+
.optional(),
|
|
102
|
+
actor: z.string().max(256).optional(),
|
|
103
|
+
state: scenarioStateSchema,
|
|
104
|
+
flags: z.array(z.string().min(1).max(512)).max(10_000).optional(),
|
|
105
|
+
eventTargets: z.record(z.string().max(512), z.string().max(512)).optional(),
|
|
106
|
+
weight: z.union([z.number(), z.string().max(1024)]).optional(),
|
|
107
|
+
})
|
|
108
|
+
.strict();
|
|
109
|
+
export const declaredConditionScenarioSchema = z
|
|
110
|
+
.object({
|
|
111
|
+
id: z.string().min(1).max(512),
|
|
112
|
+
actor: z.string().max(256).optional(),
|
|
113
|
+
date: z.string().max(64).optional(),
|
|
114
|
+
state: scenarioStateSchema,
|
|
115
|
+
flags: z.array(z.string().min(1).max(512)).max(10_000).optional(),
|
|
116
|
+
eventTargets: z.record(z.string().max(512), z.string().max(512)).optional(),
|
|
117
|
+
scopes: z
|
|
118
|
+
.record(z.string().min(1).max(128), scopeBindingSchema)
|
|
119
|
+
.refine((scopes) => Object.keys(scopes).length <= 64, 'Too many declared scopes')
|
|
120
|
+
.optional(),
|
|
121
|
+
candidateOverrides: z.record(z.string().max(512), z.boolean()).optional(),
|
|
122
|
+
closedFlags: z.boolean().optional(),
|
|
123
|
+
})
|
|
124
|
+
.strict();
|
|
125
|
+
export const impactInspectRequestSchema = z
|
|
126
|
+
.object({
|
|
127
|
+
workspaceId: workspaceIdSchema,
|
|
128
|
+
symbols: z.array(impactSymbolSelectorSchema).max(64).default([]),
|
|
129
|
+
changedFiles: z.array(sourceDisplayPathSchema).max(64).default([]),
|
|
130
|
+
scenarioSuites: z
|
|
131
|
+
.array(workspaceRelativePathSchema.refine((value) => value.toLowerCase().endsWith('.json'), 'Scenario suite path must name a JSON file'))
|
|
132
|
+
.max(32)
|
|
133
|
+
.default([]),
|
|
134
|
+
proposedSources: z.array(proposedAnalysisSourceSchema).min(1).max(64).optional(),
|
|
135
|
+
maxNodes: z.number().int().min(1).max(50_000).optional(),
|
|
136
|
+
maxEdges: z.number().int().min(1).max(100_000).optional(),
|
|
137
|
+
maxDepth: z.number().int().min(1).max(64).optional(),
|
|
138
|
+
expectedRevision: z
|
|
139
|
+
.string()
|
|
140
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
141
|
+
.optional(),
|
|
142
|
+
refresh: z.boolean().default(false),
|
|
143
|
+
})
|
|
144
|
+
.strict()
|
|
145
|
+
.superRefine((input, context) => {
|
|
146
|
+
if (input.symbols.length === 0 &&
|
|
147
|
+
input.changedFiles.length === 0 &&
|
|
148
|
+
input.proposedSources === undefined)
|
|
149
|
+
context.addIssue({
|
|
150
|
+
code: 'custom',
|
|
151
|
+
message: 'Impact inspection requires a symbol, changed file, or proposed source',
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
export const decisionInspectRequestSchema = z
|
|
155
|
+
.object({
|
|
156
|
+
workspaceId: workspaceIdSchema,
|
|
157
|
+
mode: z.enum(['inventory', 'inspect', 'compare']).default('inventory'),
|
|
158
|
+
id: z.string().min(1).max(512).optional(),
|
|
159
|
+
scenarios: z.array(declaredConditionScenarioSchema).max(64).default([]),
|
|
160
|
+
proposedSources: z.array(proposedAnalysisSourceSchema).min(1).max(64).optional(),
|
|
161
|
+
expectedRevision: z
|
|
162
|
+
.string()
|
|
163
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
164
|
+
.optional(),
|
|
165
|
+
refresh: z.boolean().default(false),
|
|
166
|
+
})
|
|
167
|
+
.strict()
|
|
168
|
+
.superRefine((input, context) => {
|
|
169
|
+
const ids = new Set();
|
|
170
|
+
for (const [index, scenario] of input.scenarios.entries()) {
|
|
171
|
+
if (ids.has(scenario.id))
|
|
172
|
+
context.addIssue({
|
|
173
|
+
code: 'custom',
|
|
174
|
+
path: ['scenarios', index, 'id'],
|
|
175
|
+
message: 'Scenario IDs must be unique',
|
|
176
|
+
});
|
|
177
|
+
ids.add(scenario.id);
|
|
178
|
+
}
|
|
179
|
+
if (input.mode === 'inventory') {
|
|
180
|
+
if (input.proposedSources !== undefined)
|
|
181
|
+
context.addIssue({
|
|
182
|
+
code: 'custom',
|
|
183
|
+
path: ['proposedSources'],
|
|
184
|
+
message: 'Inventory mode does not accept proposed sources',
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (input.id === undefined)
|
|
189
|
+
context.addIssue({
|
|
190
|
+
code: 'custom',
|
|
191
|
+
path: ['id'],
|
|
192
|
+
message: 'Decision inspection requires an identifier',
|
|
193
|
+
});
|
|
194
|
+
if (input.scenarios.length === 0)
|
|
195
|
+
context.addIssue({
|
|
196
|
+
code: 'custom',
|
|
197
|
+
path: ['scenarios'],
|
|
198
|
+
message: 'Decision inspection requires declared scenarios',
|
|
199
|
+
});
|
|
200
|
+
if (input.mode === 'compare' && input.proposedSources === undefined)
|
|
201
|
+
context.addIssue({
|
|
202
|
+
code: 'custom',
|
|
203
|
+
path: ['proposedSources'],
|
|
204
|
+
message: 'Decision comparison requires proposed sources',
|
|
205
|
+
});
|
|
206
|
+
if (input.mode === 'inspect' && input.proposedSources !== undefined)
|
|
207
|
+
context.addIssue({
|
|
208
|
+
code: 'custom',
|
|
209
|
+
path: ['proposedSources'],
|
|
210
|
+
message: 'Use compare mode for proposed sources',
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
/** A reference projection of future scenario suites; Stage 3 reads but never executes it. */
|
|
214
|
+
export const impactScenarioSuiteReferenceSchema = z
|
|
215
|
+
.object({
|
|
216
|
+
schemaVersion: z.literal('1.0'),
|
|
217
|
+
id: z.string().min(1).max(512),
|
|
218
|
+
cases: z
|
|
219
|
+
.array(z
|
|
220
|
+
.object({
|
|
221
|
+
id: z.string().min(1).max(512),
|
|
222
|
+
sourceSelectors: z.array(impactSymbolSelectorSchema).max(64).default([]),
|
|
223
|
+
sourceFiles: z.array(sourceDisplayPathSchema).max(64).default([]),
|
|
224
|
+
})
|
|
225
|
+
.loose())
|
|
226
|
+
.max(1_000),
|
|
227
|
+
})
|
|
228
|
+
.loose();
|