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,145 @@
|
|
|
1
|
+
import { analysisSourceEvidence } from '../core/analysis-evidence.js';
|
|
2
|
+
import { publicArtifactLink } from '../core/artifacts.js';
|
|
3
|
+
import { canonicalJson, hashCanonical } from '../core/canonical.js';
|
|
4
|
+
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
5
|
+
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
6
|
+
import { emptyServiceResult, ServiceError } from '../core/result.js';
|
|
7
|
+
import { copyScenario } from '../core/scenario-model.js';
|
|
8
|
+
import { inspectDecisionCost } from '../decision/cost.js';
|
|
9
|
+
import { decisionSourceInventory } from '../decision/source-inventory.js';
|
|
10
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
11
|
+
import { evaluateMechanicAssertions } from './assertions.js';
|
|
12
|
+
import { MechanicInterpreter } from './interpreter.js';
|
|
13
|
+
import { scriptedEffectSources, selectMechanicSource } from './source.js';
|
|
14
|
+
export function runMechanicCase(snapshot, test) {
|
|
15
|
+
// Zod's optional output permits explicit undefined; JSON cloning drops those
|
|
16
|
+
// keys before the stricter internal scenario interface is used.
|
|
17
|
+
const scenario = JSON.parse(JSON.stringify(test.scenario));
|
|
18
|
+
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
19
|
+
const interpreter = new MechanicInterpreter(scenario, definitions, scriptedEffectSources(snapshot));
|
|
20
|
+
const before = copyScenario(scenario);
|
|
21
|
+
const checkpoints = [copyScenario(scenario)];
|
|
22
|
+
const decisions = decisionSourceInventory(snapshot).decisions;
|
|
23
|
+
for (const step of test.steps) {
|
|
24
|
+
if (step.kind === 'advance_days')
|
|
25
|
+
interpreter.advanceDays(step.days);
|
|
26
|
+
else {
|
|
27
|
+
const source = selectMechanicSource(snapshot, step.source);
|
|
28
|
+
for (let iteration = 0; iteration < step.repeat; iteration += 1) {
|
|
29
|
+
if (step.source.kind === 'decision') {
|
|
30
|
+
const decision = decisions.find(({ id }) => id === step.source.id);
|
|
31
|
+
if (decision === undefined)
|
|
32
|
+
throw new ServiceError('MECHANIC_SOURCE_MISSING', 'Selected decision is absent', step.source);
|
|
33
|
+
const cost = inspectDecisionCost(snapshot, decision, interpreter.result().state.scenario);
|
|
34
|
+
if (cost.kind === 'invalid' ||
|
|
35
|
+
cost.affordable === 'unresolved' ||
|
|
36
|
+
cost.unresolved.length > 0) {
|
|
37
|
+
interpreter.unresolvedDecision(decision.path, 'Decision cost or eligibility is unresolved');
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (cost.affordable === 'false') {
|
|
41
|
+
interpreter.skipDecision(decision.path, 'Decision is unaffordable');
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (cost.kind === 'engine_political_power') {
|
|
45
|
+
if (cost.amount === null) {
|
|
46
|
+
interpreter.unresolvedDecision(decision.path, 'Engine cost amount is unresolved');
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
interpreter.payDecisionCost(cost.amount, decision.path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
interpreter.execute(source);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const current = interpreter.result().state;
|
|
56
|
+
checkpoints.push({
|
|
57
|
+
scenario: structuredClone(current.scenario),
|
|
58
|
+
unknown: new Set(current.unknown),
|
|
59
|
+
day: current.day,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const execution = interpreter.result();
|
|
63
|
+
const assertions = evaluateMechanicAssertions(test.assertions, before, execution.state, checkpoints, execution.trace);
|
|
64
|
+
return {
|
|
65
|
+
id: test.id,
|
|
66
|
+
sourceRevision: snapshot.revision,
|
|
67
|
+
status: assertions.some(({ status }) => status === 'failed')
|
|
68
|
+
? 'failed'
|
|
69
|
+
: assertions.some(({ status }) => status === 'unresolved') || !execution.complete
|
|
70
|
+
? 'unresolved'
|
|
71
|
+
: 'passed',
|
|
72
|
+
assertions,
|
|
73
|
+
trace: execution.trace,
|
|
74
|
+
unresolved: execution.unresolved,
|
|
75
|
+
finalState: execution.state.scenario,
|
|
76
|
+
unknown: [...execution.state.unknown].sort(),
|
|
77
|
+
day: execution.state.day,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Source-backed, read-only declared mechanic scenario tests. */
|
|
81
|
+
export class MechanicAnalyzer {
|
|
82
|
+
engine;
|
|
83
|
+
constructor(engine) {
|
|
84
|
+
this.engine = engine;
|
|
85
|
+
}
|
|
86
|
+
async test(input) {
|
|
87
|
+
const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
|
|
88
|
+
if (input.refresh)
|
|
89
|
+
this.engine.invalidate(input.workspaceId);
|
|
90
|
+
const snapshot = await this.engine.scan(input.workspaceId, {}, input.principal, input.signal);
|
|
91
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== snapshot.revision)
|
|
92
|
+
throw new ServiceError('MECHANIC_SOURCE_STALE', 'Mechanic source revision changed', {
|
|
93
|
+
expectedRevision: input.expectedRevision,
|
|
94
|
+
observedRevision: snapshot.revision,
|
|
95
|
+
});
|
|
96
|
+
const report = {
|
|
97
|
+
schemaVersion: 'mechanic-test.v1',
|
|
98
|
+
...runMechanicCase(snapshot, input.test),
|
|
99
|
+
};
|
|
100
|
+
const reportHash = hashCanonical(report);
|
|
101
|
+
const evidence = analysisSourceEvidence(snapshot);
|
|
102
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `mechanic-${reportHash.slice(0, 24)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
103
|
+
kind: 'mechanic-test',
|
|
104
|
+
toolVersion: PACKAGE_VERSION,
|
|
105
|
+
schemaVersion: 'mechanic-test.v1',
|
|
106
|
+
sourceHashes: evidence.sourceHashes,
|
|
107
|
+
metadata: {
|
|
108
|
+
beforeRevision: snapshot.revision,
|
|
109
|
+
sourceInventory: evidence.inventory,
|
|
110
|
+
},
|
|
111
|
+
}, 'Revision-pinned mechanic trace and assertion evidence', input.signal);
|
|
112
|
+
const result = emptyServiceResult(input.workspaceId, {
|
|
113
|
+
sourceRevision: snapshot.revision,
|
|
114
|
+
reportHash,
|
|
115
|
+
status: report.status,
|
|
116
|
+
assertions: report.assertions.length,
|
|
117
|
+
passed: report.assertions.filter(({ status }) => status === 'passed').length,
|
|
118
|
+
failed: report.assertions.filter(({ status }) => status === 'failed').length,
|
|
119
|
+
unresolved: report.assertions.filter(({ status }) => status === 'unresolved').length +
|
|
120
|
+
report.unresolved.length,
|
|
121
|
+
steps: input.test.steps.length,
|
|
122
|
+
});
|
|
123
|
+
result.code =
|
|
124
|
+
report.status === 'passed'
|
|
125
|
+
? 'MECHANIC_TEST_PASSED'
|
|
126
|
+
: report.status === 'failed'
|
|
127
|
+
? 'MECHANIC_TEST_FAILED'
|
|
128
|
+
: 'MECHANIC_TEST_UNRESOLVED';
|
|
129
|
+
result.artifacts = [publicArtifactLink(artifact)];
|
|
130
|
+
setInlineFilesScanned(result, snapshot.files.map(({ displayPath }) => displayPath));
|
|
131
|
+
result.validation = {
|
|
132
|
+
passed: report.status === 'passed',
|
|
133
|
+
checks: [
|
|
134
|
+
{
|
|
135
|
+
id: 'mechanic-assertions',
|
|
136
|
+
passed: report.status === 'passed',
|
|
137
|
+
message: report.status === 'passed'
|
|
138
|
+
? 'All declared mechanic assertions passed'
|
|
139
|
+
: 'Inspect the linked trace for failed or unresolved assertions',
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { assignments, firstScalar, parseClausewitz } from '../core/source/index.js';
|
|
2
|
+
import { ServiceError } from '../core/result.js';
|
|
3
|
+
import { decisionSourceInventory } from '../decision/source-inventory.js';
|
|
4
|
+
const eventKinds = new Set([
|
|
5
|
+
'country_event',
|
|
6
|
+
'state_event',
|
|
7
|
+
'news_event',
|
|
8
|
+
'unit_leader_event',
|
|
9
|
+
'operative_leader_event',
|
|
10
|
+
]);
|
|
11
|
+
const optionMetadata = new Set([
|
|
12
|
+
'name',
|
|
13
|
+
'ai_chance',
|
|
14
|
+
'trigger',
|
|
15
|
+
'highlighted',
|
|
16
|
+
'original_tag_only',
|
|
17
|
+
'show_but_dont_execute',
|
|
18
|
+
]);
|
|
19
|
+
function activeDocument(snapshot, path) {
|
|
20
|
+
const file = snapshot.files.find((candidate) => candidate.displayPath === path && candidate.shadowedBy === undefined);
|
|
21
|
+
return file === undefined ? undefined : parseClausewitz(file.bytes, file.displayPath);
|
|
22
|
+
}
|
|
23
|
+
function source(id, path, block) {
|
|
24
|
+
if (block === undefined)
|
|
25
|
+
throw new ServiceError('MECHANIC_SOURCE_MISSING', 'Selected source has no effect block', {
|
|
26
|
+
id,
|
|
27
|
+
path,
|
|
28
|
+
});
|
|
29
|
+
return { id, path, block };
|
|
30
|
+
}
|
|
31
|
+
export function selectMechanicSource(snapshot, selector) {
|
|
32
|
+
if (selector.kind === 'decision') {
|
|
33
|
+
const decision = decisionSourceInventory(snapshot).decisions.find(({ id }) => id === selector.id);
|
|
34
|
+
return source(selector.id, decision?.path ?? '', decision?.fields.complete_effect[0]?.value.type === 'block'
|
|
35
|
+
? decision.fields.complete_effect[0].value
|
|
36
|
+
: undefined);
|
|
37
|
+
}
|
|
38
|
+
const indexedKind = selector.kind === 'focus_reward'
|
|
39
|
+
? 'focus'
|
|
40
|
+
: selector.kind === 'event_option'
|
|
41
|
+
? 'event'
|
|
42
|
+
: 'scripted_effect';
|
|
43
|
+
const symbol = snapshot.index.find(indexedKind, selector.id);
|
|
44
|
+
const document = symbol === undefined ? undefined : activeDocument(snapshot, symbol.path);
|
|
45
|
+
if (document === undefined)
|
|
46
|
+
throw new ServiceError('MECHANIC_SOURCE_MISSING', 'Selected source definition is absent', selector);
|
|
47
|
+
if (selector.kind === 'scripted_effect') {
|
|
48
|
+
const block = assignments(document.root, selector.id)[0]?.value;
|
|
49
|
+
return source(selector.id, symbol.path, block?.type === 'block' ? block : undefined);
|
|
50
|
+
}
|
|
51
|
+
if (selector.kind === 'event_option') {
|
|
52
|
+
if (selector.option === undefined)
|
|
53
|
+
throw new ServiceError('MECHANIC_OPTION_REQUIRED', 'Event option selector requires option');
|
|
54
|
+
const event = assignments(document.root).find((candidate) => eventKinds.has(candidate.key.value) &&
|
|
55
|
+
candidate.value.type === 'block' &&
|
|
56
|
+
firstScalar(candidate.value, 'id')?.value === selector.id);
|
|
57
|
+
const option = event?.value.type === 'block'
|
|
58
|
+
? assignments(event.value, 'option').find((candidate) => candidate.value.type === 'block' &&
|
|
59
|
+
firstScalar(candidate.value, 'name')?.value === selector.option)
|
|
60
|
+
: undefined;
|
|
61
|
+
if (option?.value.type !== 'block')
|
|
62
|
+
throw new ServiceError('MECHANIC_OPTION_MISSING', 'Selected event option is absent', selector);
|
|
63
|
+
return source(selector.id + ':' + selector.option, symbol.path, {
|
|
64
|
+
...option.value,
|
|
65
|
+
entries: option.value.entries.filter((entry) => entry.type !== 'assignment' || !optionMetadata.has(entry.key.value)),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const focusTree = assignments(document.root, 'focus_tree');
|
|
69
|
+
const focus = focusTree
|
|
70
|
+
.flatMap((tree) => (tree.value.type === 'block' ? assignments(tree.value, 'focus') : []))
|
|
71
|
+
.find((candidate) => candidate.value.type === 'block' &&
|
|
72
|
+
firstScalar(candidate.value, 'id')?.value === selector.id);
|
|
73
|
+
const reward = focus?.value.type === 'block'
|
|
74
|
+
? assignments(focus.value, 'completion_reward')[0]?.value
|
|
75
|
+
: undefined;
|
|
76
|
+
return source(selector.id, symbol.path, reward?.type === 'block' ? reward : undefined);
|
|
77
|
+
}
|
|
78
|
+
export function scriptedEffectSources(snapshot) {
|
|
79
|
+
const result = new Map();
|
|
80
|
+
for (const symbol of snapshot.index.symbols) {
|
|
81
|
+
if (symbol.kind !== 'scripted_effect' || symbol.overridden || symbol.sourceShadowed)
|
|
82
|
+
continue;
|
|
83
|
+
const document = activeDocument(snapshot, symbol.path);
|
|
84
|
+
const block = document === undefined ? undefined : assignments(document.root, symbol.id)[0]?.value;
|
|
85
|
+
if (block?.type === 'block')
|
|
86
|
+
result.set(symbol.id, { id: symbol.id, path: symbol.path, block });
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { analysisSourceEvidence } from '../core/analysis-evidence.js';
|
|
3
|
+
import { publicArtifactLink } from '../core/artifacts.js';
|
|
4
|
+
import { canonicalJson, hashCanonical, sha256Bytes } from '../core/canonical.js';
|
|
5
|
+
import { scanImpactSemanticReferences } from '../core/impact-semantic.js';
|
|
6
|
+
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
7
|
+
import { emptyServiceResult, ServiceError } from '../core/result.js';
|
|
8
|
+
import { parseClausewitz } from '../core/source/index.js';
|
|
9
|
+
import { scenarioSuiteSchema } from '../schemas/scenarios.js';
|
|
10
|
+
import { PACKAGE_VERSION } from '../version.js';
|
|
11
|
+
const MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
12
|
+
function registrationPresent(bytes, path, token) {
|
|
13
|
+
if (!/\.(?:txt|gfx|gui|asset)$/iu.test(path))
|
|
14
|
+
return undefined;
|
|
15
|
+
const document = parseClausewitz(bytes, path);
|
|
16
|
+
if (document.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'))
|
|
17
|
+
return undefined;
|
|
18
|
+
const visit = (entry) => {
|
|
19
|
+
if (entry.type === 'scalar')
|
|
20
|
+
return entry.value === token;
|
|
21
|
+
if (entry.type === 'block')
|
|
22
|
+
return entry.entries.some(visit);
|
|
23
|
+
return entry.key.value === token || visit(entry.value);
|
|
24
|
+
};
|
|
25
|
+
return visit(document.root);
|
|
26
|
+
}
|
|
27
|
+
function indexedChecks(snapshot, manifest) {
|
|
28
|
+
const results = [];
|
|
29
|
+
const semantic = manifest.calls.length > 0 ? scanImpactSemanticReferences(snapshot) : undefined;
|
|
30
|
+
const references = semantic === undefined
|
|
31
|
+
? snapshot.index.references
|
|
32
|
+
: [...snapshot.index.references, ...semantic.references];
|
|
33
|
+
for (const definition of manifest.definitions) {
|
|
34
|
+
const active = snapshot.index.find(definition.kind, definition.id);
|
|
35
|
+
const all = snapshot.index.findAll(definition.kind, definition.id);
|
|
36
|
+
const file = snapshot.files.find(({ displayPath }) => displayPath === active?.path);
|
|
37
|
+
results.push({
|
|
38
|
+
category: 'definition',
|
|
39
|
+
id: `${definition.kind}:${definition.id}`,
|
|
40
|
+
status: active === undefined
|
|
41
|
+
? all.length > 0
|
|
42
|
+
? 'shadowed'
|
|
43
|
+
: snapshot.complete
|
|
44
|
+
? 'absent'
|
|
45
|
+
: 'unresolved'
|
|
46
|
+
: 'present',
|
|
47
|
+
...(active?.path === undefined ? {} : { path: active.path }),
|
|
48
|
+
...(file?.sha256 === undefined ? {} : { sha256: file.sha256 }),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
for (const call of manifest.calls) {
|
|
52
|
+
const from = snapshot.index.find(call.from.kind, call.from.id);
|
|
53
|
+
const to = snapshot.index.find(call.to.kind, call.to.id);
|
|
54
|
+
const reference = references.find((candidate) => candidate.from === call.from.id &&
|
|
55
|
+
candidate.toKind === call.to.kind &&
|
|
56
|
+
candidate.to === call.to.id &&
|
|
57
|
+
candidate.path === from?.path);
|
|
58
|
+
results.push({
|
|
59
|
+
category: 'call',
|
|
60
|
+
id: `${call.from.kind}:${call.from.id}->${call.to.kind}:${call.to.id}`,
|
|
61
|
+
status: from === undefined || to === undefined
|
|
62
|
+
? snapshot.complete
|
|
63
|
+
? 'absent'
|
|
64
|
+
: 'unresolved'
|
|
65
|
+
: reference === undefined
|
|
66
|
+
? snapshot.complete && (semantic?.complete ?? true)
|
|
67
|
+
? 'absent'
|
|
68
|
+
: 'unresolved'
|
|
69
|
+
: 'present',
|
|
70
|
+
...(reference?.path === undefined ? {} : { path: reference.path }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const id of manifest.localisation) {
|
|
74
|
+
const symbol = snapshot.index.find('localisation', id) ??
|
|
75
|
+
snapshot.index.find('localisation', `l_english:${id}`);
|
|
76
|
+
results.push({
|
|
77
|
+
category: 'localisation',
|
|
78
|
+
id,
|
|
79
|
+
status: symbol === undefined ? (snapshot.complete ? 'absent' : 'unresolved') : 'present',
|
|
80
|
+
...(symbol?.path === undefined ? {} : { path: symbol.path }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
/** Declarative package inventory; no manifest field is interpreted as code. */
|
|
86
|
+
export class PackageAnalyzer {
|
|
87
|
+
engine;
|
|
88
|
+
constructor(engine) {
|
|
89
|
+
this.engine = engine;
|
|
90
|
+
}
|
|
91
|
+
async check(input) {
|
|
92
|
+
const workspace = this.engine.resolver.get(input.workspaceId, input.principal);
|
|
93
|
+
if (input.refresh)
|
|
94
|
+
this.engine.invalidate(input.workspaceId);
|
|
95
|
+
const snapshot = await this.engine.scan(input.workspaceId, {}, input.principal, input.signal);
|
|
96
|
+
if (input.expectedRevision !== undefined && input.expectedRevision !== snapshot.revision)
|
|
97
|
+
throw new ServiceError('PACKAGE_SOURCE_STALE', 'Package source revision changed', {
|
|
98
|
+
expectedRevision: input.expectedRevision,
|
|
99
|
+
observedRevision: snapshot.revision,
|
|
100
|
+
});
|
|
101
|
+
const results = indexedChecks(snapshot, input.manifest);
|
|
102
|
+
const read = async (relativePath) => {
|
|
103
|
+
const resolved = await this.engine.resolver.resolvePath(input.workspaceId, relativePath, 'read', ['mod', 'game', 'dependency', 'fixture'], input.principal);
|
|
104
|
+
const metadata = await stat(resolved.path);
|
|
105
|
+
if (!metadata.isFile() || metadata.size > MAX_FILE_BYTES)
|
|
106
|
+
throw new ServiceError('PACKAGE_FILE_LIMIT', 'Package file exceeds the bounded reader');
|
|
107
|
+
return { bytes: await readFile(resolved.path), path: resolved.path };
|
|
108
|
+
};
|
|
109
|
+
for (const registration of input.manifest.registrations) {
|
|
110
|
+
input.signal?.throwIfAborted();
|
|
111
|
+
try {
|
|
112
|
+
const file = await read(registration.path);
|
|
113
|
+
const present = registrationPresent(file.bytes, registration.path, registration.token);
|
|
114
|
+
results.push({
|
|
115
|
+
category: 'registration',
|
|
116
|
+
id: `${registration.path}:${registration.token}`,
|
|
117
|
+
status: present === undefined ? 'unresolved' : present ? 'present' : 'absent',
|
|
118
|
+
path: registration.path,
|
|
119
|
+
sha256: sha256Bytes(file.bytes),
|
|
120
|
+
...(present === undefined ? { reason: 'PACKAGE_REGISTRATION_UNSUPPORTED_SOURCE' } : {}),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error.name === 'AbortError')
|
|
125
|
+
throw error;
|
|
126
|
+
results.push({
|
|
127
|
+
category: 'registration',
|
|
128
|
+
id: `${registration.path}:${registration.token}`,
|
|
129
|
+
status: error instanceof ServiceError && error.code === 'PATH_NOT_FOUND_IN_ROOTS'
|
|
130
|
+
? 'absent'
|
|
131
|
+
: 'unresolved',
|
|
132
|
+
reason: error instanceof ServiceError ? error.code : 'PACKAGE_FILE_READ',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const asset of input.manifest.assets) {
|
|
137
|
+
input.signal?.throwIfAborted();
|
|
138
|
+
try {
|
|
139
|
+
const file = await read(asset);
|
|
140
|
+
results.push({
|
|
141
|
+
category: 'asset',
|
|
142
|
+
id: asset,
|
|
143
|
+
status: 'present',
|
|
144
|
+
path: asset,
|
|
145
|
+
sha256: sha256Bytes(file.bytes),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.name === 'AbortError')
|
|
150
|
+
throw error;
|
|
151
|
+
results.push({
|
|
152
|
+
category: 'asset',
|
|
153
|
+
id: asset,
|
|
154
|
+
status: error instanceof ServiceError && error.code === 'PATH_NOT_FOUND_IN_ROOTS'
|
|
155
|
+
? 'absent'
|
|
156
|
+
: 'unresolved',
|
|
157
|
+
reason: error instanceof ServiceError ? error.code : 'PACKAGE_FILE_READ',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const required of input.manifest.requiredCases) {
|
|
162
|
+
input.signal?.throwIfAborted();
|
|
163
|
+
try {
|
|
164
|
+
const file = await read(required.suite);
|
|
165
|
+
const suite = scenarioSuiteSchema.parse(JSON.parse(file.bytes.toString('utf8')));
|
|
166
|
+
results.push({
|
|
167
|
+
category: 'required_case',
|
|
168
|
+
id: `${required.suite}:${required.caseId}`,
|
|
169
|
+
status: suite.cases.some(({ id }) => id === required.caseId) ? 'present' : 'absent',
|
|
170
|
+
path: required.suite,
|
|
171
|
+
sha256: sha256Bytes(file.bytes),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (error.name === 'AbortError')
|
|
176
|
+
throw error;
|
|
177
|
+
results.push({
|
|
178
|
+
category: 'required_case',
|
|
179
|
+
id: `${required.suite}:${required.caseId}`,
|
|
180
|
+
status: error instanceof ServiceError && error.code === 'PATH_NOT_FOUND_IN_ROOTS'
|
|
181
|
+
? 'absent'
|
|
182
|
+
: 'unresolved',
|
|
183
|
+
reason: error instanceof ServiceError ? error.code : 'PACKAGE_SUITE_INVALID',
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const report = {
|
|
188
|
+
schemaVersion: 'package-check.v1',
|
|
189
|
+
packageId: input.manifest.id,
|
|
190
|
+
sourceRevision: snapshot.revision,
|
|
191
|
+
manifestHash: hashCanonical(input.manifest),
|
|
192
|
+
items: results,
|
|
193
|
+
complete: snapshot.complete && results.every(({ status }) => status === 'present'),
|
|
194
|
+
};
|
|
195
|
+
const reportHash = hashCanonical(report);
|
|
196
|
+
const evidence = analysisSourceEvidence(snapshot);
|
|
197
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `package-${reportHash.slice(0, 24)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
198
|
+
kind: 'package-check',
|
|
199
|
+
toolVersion: PACKAGE_VERSION,
|
|
200
|
+
schemaVersion: 'package-check.v1',
|
|
201
|
+
sourceHashes: evidence.sourceHashes,
|
|
202
|
+
metadata: { beforeRevision: snapshot.revision, sourceInventory: evidence.inventory },
|
|
203
|
+
}, 'Revision-pinned package connection inventory', input.signal);
|
|
204
|
+
const result = emptyServiceResult(input.workspaceId, {
|
|
205
|
+
sourceRevision: snapshot.revision,
|
|
206
|
+
reportHash,
|
|
207
|
+
complete: report.complete,
|
|
208
|
+
items: results.length,
|
|
209
|
+
present: results.filter(({ status }) => status === 'present').length,
|
|
210
|
+
absent: results.filter(({ status }) => status === 'absent').length,
|
|
211
|
+
shadowed: results.filter(({ status }) => status === 'shadowed').length,
|
|
212
|
+
unresolved: results.filter(({ status }) => status === 'unresolved').length,
|
|
213
|
+
});
|
|
214
|
+
result.code = report.complete ? 'PACKAGE_CHECK_PASSED' : 'PACKAGE_CHECK_INCOMPLETE';
|
|
215
|
+
result.artifacts = [publicArtifactLink(artifact)];
|
|
216
|
+
setInlineFilesScanned(result, snapshot.files.map(({ displayPath }) => displayPath));
|
|
217
|
+
result.validation = {
|
|
218
|
+
passed: report.complete,
|
|
219
|
+
checks: [
|
|
220
|
+
{
|
|
221
|
+
id: 'package-connections',
|
|
222
|
+
passed: report.complete,
|
|
223
|
+
message: report.complete
|
|
224
|
+
? 'Every declared package connection is present'
|
|
225
|
+
: 'A declared package connection is absent, shadowed, or unresolved',
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
};
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
import { scenarioFromProbability } from '../core/scenario-model.js';
|
|
2
3
|
import { assignments } from '../core/source/index.js';
|
|
3
4
|
import { Rational, sumRationals, uniformRaceProbabilities } from './rational.js';
|
|
4
5
|
import { withScenarioPathValue } from './scenario-state.js';
|
|
@@ -84,8 +85,9 @@ function externalFactor(surface, candidate, scenario) {
|
|
|
84
85
|
return { factor, trace, unresolved };
|
|
85
86
|
}
|
|
86
87
|
export function evaluateExactCandidates(surface, scenario, definitions) {
|
|
88
|
+
const declaredConditions = scenarioFromProbability(scenario);
|
|
87
89
|
const candidates = surface.candidates.map((candidate) => {
|
|
88
|
-
const eligibility = evaluateTriggerBlock(candidate.eligibilityBlock,
|
|
90
|
+
const eligibility = evaluateTriggerBlock(candidate.eligibilityBlock, declaredConditions, candidate, definitions);
|
|
89
91
|
const value = evaluateCandidateValue(candidate, scenario, definitions);
|
|
90
92
|
const external = externalFactor(surface, candidate, scenario);
|
|
91
93
|
const exactValue = eligibility.state === 'false'
|
|
@@ -112,6 +114,9 @@ export function evaluateExactCandidates(surface, scenario, definitions) {
|
|
|
112
114
|
...external.trace,
|
|
113
115
|
],
|
|
114
116
|
unresolved: [...eligibility.unresolved, ...value.unresolved, ...external.unresolved],
|
|
117
|
+
eligibilityUnresolved: eligibility.unresolved,
|
|
118
|
+
valueUnresolved: value.unresolved,
|
|
119
|
+
externalUnresolved: external.unresolved,
|
|
115
120
|
provenance: [
|
|
116
121
|
...candidate.provenance,
|
|
117
122
|
...(eligibility.helperProvenance ?? []),
|
|
@@ -21,6 +21,9 @@ export function probabilityAnalysisData(result) {
|
|
|
21
21
|
cacheKey: result.metadata.cacheKey,
|
|
22
22
|
scenarios: result.scenarios.length,
|
|
23
23
|
candidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.length, 0),
|
|
24
|
+
eligibleCandidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.filter(({ eligibility }) => eligibility === 'true').length, 0),
|
|
25
|
+
excludedCandidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.filter(({ eligibility }) => eligibility === 'false').length, 0),
|
|
26
|
+
unresolvedCandidates: result.scenarios.reduce((sum, scenario) => sum + scenario.candidates.filter(({ eligibility }) => eligibility === 'unresolved').length, 0),
|
|
24
27
|
...(result.scenarios.some(({ scopePools }) => (scopePools?.length ?? 0) > 0)
|
|
25
28
|
? {
|
|
26
29
|
scopePools: result.scenarios.reduce((sum, scenario) => sum + (scenario.scopePools?.length ?? 0), 0),
|
|
@@ -87,6 +90,8 @@ export function normalizeProbabilityRenderRequest(input) {
|
|
|
87
90
|
}
|
|
88
91
|
export async function inspectProbabilities(analyzer, input, context) {
|
|
89
92
|
const inspected = await analyzer.inspect(runtime(context, input.refresh), input.adapter, input.source, input.candidatePool, input.customPoolManifest);
|
|
93
|
+
const sourceRequiredInputPaths = (inspected.surface?.requiredInputs ?? []).filter((path) => path !== 'focus.external_factors_complete' && path !== 'technology.external_factors_complete');
|
|
94
|
+
const adapterRequiredInputPaths = (inspected.surface?.requiredInputs ?? []).filter((path) => path === 'focus.external_factors_complete' || path === 'technology.external_factors_complete');
|
|
90
95
|
const result = emptyServiceResult(context.workspaceId, {
|
|
91
96
|
adapters: inspected.adapters.length,
|
|
92
97
|
...(inspected.surface === undefined
|
|
@@ -120,6 +125,9 @@ export async function inspectProbabilities(analyzer, input, context) {
|
|
|
120
125
|
})) ?? [],
|
|
121
126
|
candidateExamples: inspected.discovery?.exampleCandidateIds ?? [],
|
|
122
127
|
requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
|
|
128
|
+
requiredInputPaths: sourceRequiredInputPaths.slice(0, 32),
|
|
129
|
+
requiredInputPathsTruncated: sourceRequiredInputPaths.length > 32,
|
|
130
|
+
adapterRequiredInputPaths,
|
|
123
131
|
unresolved: inspected.surface?.unsupported.length ?? 0,
|
|
124
132
|
});
|
|
125
133
|
result.code =
|
|
@@ -9,7 +9,7 @@ import { PACKAGE_VERSION } from '../version.js';
|
|
|
9
9
|
import { probabilityAnalysisResultSchema } from '../schemas/probability.js';
|
|
10
10
|
import { probabilityAdapter, probabilityAdapters } from './adapters.js';
|
|
11
11
|
import { compareProbabilityResults } from './compare.js';
|
|
12
|
-
import { evaluateExactCandidates, evaluateSurfaceScenarios } from './evaluation.js';
|
|
12
|
+
import { evaluateExactCandidates, evaluateSurfaceScenarios, } from './evaluation.js';
|
|
13
13
|
import { VERIFIED_GAME_CHECKSUM, VERIFIED_GAME_RAW_VERSION, VERIFIED_GAME_VERSION, } from './model.js';
|
|
14
14
|
import { renderProbabilityResult } from './render.js';
|
|
15
15
|
import { analyzeSequence, evaluateCustomPoolPoint } from './sequence.js';
|
|
@@ -17,6 +17,28 @@ import { simulateSurface } from './simulation.js';
|
|
|
17
17
|
import { Rational } from './rational.js';
|
|
18
18
|
import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
|
|
19
19
|
import { sweepSurface } from './sweep.js';
|
|
20
|
+
function missingScenarioInputs(unresolved) {
|
|
21
|
+
return [
|
|
22
|
+
...new Set(unresolved.flatMap(({ code, path, details }) => {
|
|
23
|
+
if (typeof details?.scenarioInput === 'string')
|
|
24
|
+
return [details.scenarioInput];
|
|
25
|
+
if (code === 'VALUE_UNRESOLVED' && path !== undefined && !/^(?:@|constant:)/u.test(path))
|
|
26
|
+
return [path];
|
|
27
|
+
if (path !== undefined &&
|
|
28
|
+
(code === 'FOCUS_EXTERNAL_FACTORS_UNDECLARED' ||
|
|
29
|
+
code === 'TECHNOLOGY_EXTERNAL_FACTORS_UNDECLARED'))
|
|
30
|
+
return [path];
|
|
31
|
+
return [];
|
|
32
|
+
})),
|
|
33
|
+
].sort(compareCodeUnits);
|
|
34
|
+
}
|
|
35
|
+
function blockingInspectionUnresolved(candidate) {
|
|
36
|
+
if (candidate.eligibility === 'false')
|
|
37
|
+
return [];
|
|
38
|
+
if (candidate.eligibility === 'unresolved')
|
|
39
|
+
return [...candidate.eligibilityUnresolved, ...candidate.externalUnresolved];
|
|
40
|
+
return [...candidate.valueUnresolved, ...candidate.externalUnresolved];
|
|
41
|
+
}
|
|
20
42
|
const states = new WeakMap();
|
|
21
43
|
const ANALYSIS_CACHE_ENTRIES = 16;
|
|
22
44
|
function resolveSourceBackedAdapter(snapshot, source, candidatePool, requested) {
|
|
@@ -1063,9 +1085,7 @@ export class ProbabilityAnalyzer {
|
|
|
1063
1085
|
}
|
|
1064
1086
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
1065
1087
|
const inspectedCandidates = evaluateExactCandidates(surface, { id: 'inspection', state: {} }, definitions);
|
|
1066
|
-
const requiredInputs =
|
|
1067
|
-
...new Set(inspectedCandidates.flatMap(({ unresolved }) => unresolved.flatMap(({ path }) => (path === undefined ? [] : [path])))),
|
|
1068
|
-
].sort();
|
|
1088
|
+
const requiredInputs = missingScenarioInputs(inspectedCandidates.flatMap(blockingInspectionUnresolved));
|
|
1069
1089
|
const report = {
|
|
1070
1090
|
schemaVersion: 'probability-inspection.v2',
|
|
1071
1091
|
workspaceId: context.workspaceId,
|
|
@@ -1079,9 +1099,8 @@ export class ProbabilityAnalyzer {
|
|
|
1079
1099
|
candidateCount: surface.candidates.length,
|
|
1080
1100
|
candidates: surface.candidates.map((candidate, index) => {
|
|
1081
1101
|
const evaluated = inspectedCandidates[index];
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
].sort();
|
|
1102
|
+
const blockingUnresolved = blockingInspectionUnresolved(evaluated);
|
|
1103
|
+
const candidateRequiredInputs = missingScenarioInputs(blockingUnresolved);
|
|
1085
1104
|
return {
|
|
1086
1105
|
id: candidate.id,
|
|
1087
1106
|
sourceKind: candidate.sourceKind,
|
|
@@ -1092,11 +1111,13 @@ export class ProbabilityAnalyzer {
|
|
|
1092
1111
|
referencedProvenance: evaluated.provenance.filter(({ path, symbol }) => !candidate.provenance.some((source) => source.path === path && source.symbol === symbol)),
|
|
1093
1112
|
analysisSupport: candidateRequiredInputs.length > 0
|
|
1094
1113
|
? 'requires_scenario'
|
|
1095
|
-
:
|
|
1096
|
-
? '
|
|
1097
|
-
: surface.adapter.
|
|
1098
|
-
? '
|
|
1099
|
-
:
|
|
1114
|
+
: blockingUnresolved.length > 0
|
|
1115
|
+
? 'unresolved_source'
|
|
1116
|
+
: surface.adapter.selectionRule === 'score_only'
|
|
1117
|
+
? 'score_only'
|
|
1118
|
+
: surface.adapter.completePoolRequired && !surface.poolComplete
|
|
1119
|
+
? 'requires_complete_pool'
|
|
1120
|
+
: 'exact',
|
|
1100
1121
|
};
|
|
1101
1122
|
}),
|
|
1102
1123
|
requiredInputs,
|
|
@@ -421,6 +421,7 @@ export function inventoryWeightedSurfaces(snapshot, source, candidatePool = [],
|
|
|
421
421
|
? candidates.length
|
|
422
422
|
: candidates.filter(({ id }) => wanted.has(id)).length,
|
|
423
423
|
exampleCandidateIds: [...new Set(candidates.map(({ id }) => id))].slice(0, 10),
|
|
424
|
+
candidateCatalog: candidates.map(({ id, provenance }) => ({ id, provenance })),
|
|
424
425
|
sourcePaths: [
|
|
425
426
|
...new Set(candidates.flatMap(({ provenance }) => provenance.map(({ path }) => path))),
|
|
426
427
|
].sort(compareCodeUnits),
|