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.
Files changed (99) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -0
  3. package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
  4. package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -9
  5. package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
  6. package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
  7. package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
  8. package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
  9. package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
  10. package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
  11. package/dist/hoi4_agent_tools/event/operations.js +16 -0
  12. package/dist/hoi4_agent_tools/event/service.js +21 -2
  13. package/dist/hoi4_agent_tools/focus/compact.js +241 -27
  14. package/dist/hoi4_agent_tools/focus/layout.js +119 -8
  15. package/dist/hoi4_agent_tools/focus/render.js +62 -0
  16. package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
  17. package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
  18. package/dist/hoi4_agent_tools/gui/assets.js +54 -4
  19. package/dist/hoi4_agent_tools/gui/layout.js +935 -104
  20. package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
  21. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
  22. package/dist/hoi4_agent_tools/gui/shader.js +515 -0
  23. package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
  24. package/dist/hoi4_agent_tools/gui/studio.js +245 -19
  25. package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
  26. package/dist/hoi4_agent_tools/gui/validators.js +38 -12
  27. package/dist/hoi4_agent_tools/map/operations.js +70 -1
  28. package/dist/hoi4_agent_tools/map/render.js +70 -1
  29. package/dist/hoi4_agent_tools/map/service.js +30 -6
  30. package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
  31. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  32. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
  33. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
  34. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
  35. package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
  36. package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
  37. package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
  38. package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
  39. package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
  40. package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
  41. package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
  42. package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
  43. package/dist/hoi4_agent_tools/package-check/service.js +231 -0
  44. package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
  45. package/dist/hoi4_agent_tools/probability/operations.js +8 -0
  46. package/dist/hoi4_agent_tools/probability/service.js +33 -12
  47. package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
  48. package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
  49. package/dist/hoi4_agent_tools/schemas/event.js +8 -0
  50. package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
  51. package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
  52. package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
  53. package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
  54. package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
  55. package/dist/hoi4_agent_tools/technology/graph.js +25 -10
  56. package/dist/hoi4_agent_tools/technology/render.js +286 -31
  57. package/dist/hoi4_agent_tools/technology/service.js +55 -8
  58. package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
  59. package/dist/hoi4_agent_tools/version.js +1 -1
  60. package/docs/README.md +1 -0
  61. package/docs/events.md +3 -1
  62. package/docs/focus.md +10 -0
  63. package/docs/gui.md +49 -3
  64. package/docs/map.md +8 -0
  65. package/docs/mechanics.md +70 -0
  66. package/docs/probability.md +3 -1
  67. package/docs/technology.md +7 -2
  68. package/package.json +2 -1
  69. package/schemas/configuration.schema.json +1 -1
  70. package/schemas/continuous-focus-palette.schema.json +1 -1
  71. package/schemas/custom-weighted-pool.schema.json +1 -1
  72. package/schemas/decision-inspect-input.schema.json +1 -1
  73. package/schemas/event-feature-manifest.schema.json +1 -1
  74. package/schemas/focus-plan.schema.json +1 -1
  75. package/schemas/focus-planning-sidecar.schema.json +1 -1
  76. package/schemas/gui-animation-source.schema.json +1 -1
  77. package/schemas/gui-helper.schema.json +1 -1
  78. package/schemas/gui-scenario.schema.json +1 -1
  79. package/schemas/helper-expansion-request.schema.json +1 -1
  80. package/schemas/helper-expansion-summary.schema.json +1 -1
  81. package/schemas/impact-inspect-input.schema.json +1 -1
  82. package/schemas/map-operation.schema.json +1 -1
  83. package/schemas/mechanic-test-input.schema.json +650 -0
  84. package/schemas/operation-result.schema.json +1 -1
  85. package/schemas/package-check-input.schema.json +164 -0
  86. package/schemas/package-manifest.schema.json +142 -0
  87. package/schemas/probability-analysis-result.schema.json +1 -1
  88. package/schemas/probability-compare-input.schema.json +1 -1
  89. package/schemas/probability-evaluate-input.schema.json +1 -1
  90. package/schemas/probability-inspect-input.schema.json +1 -1
  91. package/schemas/probability-render-input.schema.json +1 -1
  92. package/schemas/probability-scenario-set.schema.json +1 -1
  93. package/schemas/probability-sequence-input.schema.json +1 -1
  94. package/schemas/probability-simulate-input.schema.json +1 -1
  95. package/schemas/probability-sweep-input.schema.json +1 -1
  96. package/schemas/scenario-suite.schema.json +1004 -0
  97. package/schemas/scenario-test-input.schema.json +1057 -0
  98. package/schemas/shared-scenario.schema.json +237 -0
  99. 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, scenario, candidate, definitions);
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 candidateRequiredInputs = [
1083
- ...new Set(evaluated.unresolved.flatMap(({ path }) => (path === undefined ? [] : [path]))),
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
- : surface.adapter.selectionRule === 'score_only'
1096
- ? 'score_only'
1097
- : surface.adapter.completePoolRequired && !surface.poolComplete
1098
- ? 'requires_complete_pool'
1099
- : 'exact',
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),