hoi4-agent-tools 4.1.0 → 4.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 +25 -0
- package/dist/hoi4_agent_tools/core/canonical.js +42 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +12 -8
- package/dist/hoi4_agent_tools/core/dependency-walk.js +21 -2
- package/dist/hoi4_agent_tools/core/domain-validation.js +6 -1
- package/dist/hoi4_agent_tools/core/engine.js +3 -0
- package/dist/hoi4_agent_tools/core/game-documentation.js +24 -0
- package/dist/hoi4_agent_tools/core/impact-graph.js +6 -2
- package/dist/hoi4_agent_tools/core/impact-semantic.js +12 -2
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +12 -2
- package/dist/hoi4_agent_tools/event/algorithms.js +3 -2
- package/dist/hoi4_agent_tools/event/graph.js +60 -36
- package/dist/hoi4_agent_tools/event/limits.js +4 -0
- package/dist/hoi4_agent_tools/event/operations.js +11 -0
- package/dist/hoi4_agent_tools/event/queries.js +13 -34
- package/dist/hoi4_agent_tools/event/render.js +4 -2
- package/dist/hoi4_agent_tools/event/service.js +101 -33
- package/dist/hoi4_agent_tools/event/source-analysis.js +101 -7
- package/dist/hoi4_agent_tools/event/state-keys.js +63 -0
- package/dist/hoi4_agent_tools/event/summary.js +130 -0
- package/dist/hoi4_agent_tools/focus/importer.js +8 -6
- package/dist/hoi4_agent_tools/focus/native-effects.js +3 -1
- package/dist/hoi4_agent_tools/focus/tool-support.js +10 -1
- package/dist/hoi4_agent_tools/impact/service.js +2 -1
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +66 -0
- package/dist/hoi4_agent_tools/probability/service.js +18 -18
- package/dist/hoi4_agent_tools/reference/source-lookup.js +4 -2
- package/dist/hoi4_agent_tools/schemas/probability.js +7 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/analysis.md +4 -0
- package/docs/events.md +12 -0
- package/docs/probability.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -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/error-log-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 +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +7 -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/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
/** Findings returned inline, before an agent opens the linked report. */
|
|
3
|
+
export const EVENT_INLINE_FINDINGS = 10;
|
|
4
|
+
const EVENT_INLINE_CODES = 10;
|
|
5
|
+
const EVENT_INLINE_EVENT_IDS = 20;
|
|
6
|
+
const EVENT_INLINE_MESSAGE_LENGTH = 300;
|
|
7
|
+
const CLASSIFICATION_ORDER = {
|
|
8
|
+
confirmed_error: 0,
|
|
9
|
+
probable_defect: 1,
|
|
10
|
+
design_warning: 2,
|
|
11
|
+
unresolved_analysis: 3,
|
|
12
|
+
};
|
|
13
|
+
const CONFIDENCE_ORDER = {
|
|
14
|
+
confirmed: 0,
|
|
15
|
+
high: 1,
|
|
16
|
+
medium: 2,
|
|
17
|
+
low: 3,
|
|
18
|
+
unresolved: 4,
|
|
19
|
+
};
|
|
20
|
+
function countBy(values, key) {
|
|
21
|
+
const counts = new Map();
|
|
22
|
+
for (const value of values)
|
|
23
|
+
counts.set(key(value), (counts.get(key(value)) ?? 0) + 1);
|
|
24
|
+
return Object.fromEntries([...counts].sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
25
|
+
}
|
|
26
|
+
function layer(issue) {
|
|
27
|
+
const path = issue.location?.path;
|
|
28
|
+
return path === undefined ? 'unknown' : (/^([a-z]+):/u.exec(path)?.[1] ?? 'unknown');
|
|
29
|
+
}
|
|
30
|
+
/** The mod's own files first, then most severe: classification, confidence, source order. */
|
|
31
|
+
function issueOrder(left, right) {
|
|
32
|
+
const leftMod = layer(left) === 'mod' ? 0 : 1;
|
|
33
|
+
const rightMod = layer(right) === 'mod' ? 0 : 1;
|
|
34
|
+
return (leftMod - rightMod ||
|
|
35
|
+
CLASSIFICATION_ORDER[left.classification] - CLASSIFICATION_ORDER[right.classification] ||
|
|
36
|
+
CONFIDENCE_ORDER[left.confidence] - CONFIDENCE_ORDER[right.confidence] ||
|
|
37
|
+
compareCodeUnits(left.location?.path ?? '', right.location?.path ?? '') ||
|
|
38
|
+
(left.location?.start.line ?? 0) - (right.location?.start.line ?? 0) ||
|
|
39
|
+
compareCodeUnits(left.code, right.code) ||
|
|
40
|
+
compareCodeUnits(left.message, right.message));
|
|
41
|
+
}
|
|
42
|
+
/** Counts and the most severe findings of a selection, small enough to return inline. */
|
|
43
|
+
export function summarizeEventIssues(issues) {
|
|
44
|
+
const byCode = Object.entries(countBy(issues, ({ code }) => code))
|
|
45
|
+
.sort(([leftCode, left], [rightCode, right]) => right - left || compareCodeUnits(leftCode, rightCode))
|
|
46
|
+
.slice(0, EVENT_INLINE_CODES);
|
|
47
|
+
return {
|
|
48
|
+
issues: issues.length,
|
|
49
|
+
byClassification: countBy(issues, ({ classification }) => classification),
|
|
50
|
+
// `mod` findings are the workspace's own; `game` and `dependency` ones are context.
|
|
51
|
+
byLayer: countBy(issues, layer),
|
|
52
|
+
topCodes: Object.fromEntries(byCode),
|
|
53
|
+
findings: [...issues]
|
|
54
|
+
.sort(issueOrder)
|
|
55
|
+
.slice(0, EVENT_INLINE_FINDINGS)
|
|
56
|
+
.map((issue) => ({
|
|
57
|
+
code: issue.code,
|
|
58
|
+
classification: issue.classification,
|
|
59
|
+
confidence: issue.confidence,
|
|
60
|
+
message: issue.message.length > EVENT_INLINE_MESSAGE_LENGTH
|
|
61
|
+
? `${issue.message.slice(0, EVENT_INLINE_MESSAGE_LENGTH - 1)}…`
|
|
62
|
+
: issue.message,
|
|
63
|
+
...(issue.location === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { path: issue.location.path, line: issue.location.start.line }),
|
|
66
|
+
...(typeof issue.details.name === 'string' ? { subject: issue.details.name } : {}),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Size of a traced chain and the events it reaches. */
|
|
71
|
+
export function summarizeEventTrace(trace) {
|
|
72
|
+
const events = trace.nodes
|
|
73
|
+
.flatMap(({ kind, eventId }) => (kind === 'event' && eventId !== undefined ? [eventId] : []))
|
|
74
|
+
.sort(compareCodeUnits);
|
|
75
|
+
return {
|
|
76
|
+
nodes: trace.nodes.length,
|
|
77
|
+
edges: trace.edges.length,
|
|
78
|
+
events: events.length,
|
|
79
|
+
eventIds: events.slice(0, EVENT_INLINE_EVENT_IDS),
|
|
80
|
+
boundaryNodes: trace.boundaryNodeIds.length,
|
|
81
|
+
truncated: trace.truncated,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** How many chains start where, in a form an agent can read without the report. */
|
|
85
|
+
export function summarizeEventRoots(roots) {
|
|
86
|
+
const nodeLayer = ({ sourcePath }) => sourcePath === undefined ? 'unknown' : (/^([a-z]+):/u.exec(sourcePath)?.[1] ?? 'unknown');
|
|
87
|
+
// The mod's own events first: vanilla callerless events are context, not the mod's defects.
|
|
88
|
+
const ids = (nodes) => [...nodes]
|
|
89
|
+
.sort((left, right) => (nodeLayer(left) === 'mod' ? 0 : 1) - (nodeLayer(right) === 'mod' ? 0 : 1) ||
|
|
90
|
+
compareCodeUnits(left.eventId ?? '', right.eventId ?? ''))
|
|
91
|
+
.flatMap(({ eventId }) => (eventId === undefined ? [] : [eventId]))
|
|
92
|
+
.slice(0, EVENT_INLINE_EVENT_IDS);
|
|
93
|
+
return {
|
|
94
|
+
entryPoints: roots.entryPoints.length,
|
|
95
|
+
entryKinds: countBy(roots.entryPoints, ({ metadata }) => typeof metadata.entryKind === 'string' ? metadata.entryKind : 'unknown'),
|
|
96
|
+
automaticEvents: roots.automaticEvents.length,
|
|
97
|
+
callerlessTriggeredEvents: roots.callerlessTriggeredEvents.length,
|
|
98
|
+
callerlessTriggeredByLayer: countBy(roots.callerlessTriggeredEvents, nodeLayer),
|
|
99
|
+
callerlessTriggeredEventIds: ids(roots.callerlessTriggeredEvents),
|
|
100
|
+
cycles: roots.stronglyConnectedComponents.filter(({ cyclic }) => cyclic).length,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Whether a route exists and, if so, the events it passes through in order. */
|
|
104
|
+
export function summarizeEventPath(path) {
|
|
105
|
+
return {
|
|
106
|
+
found: path.found,
|
|
107
|
+
steps: path.steps.length,
|
|
108
|
+
eventIds: path.steps
|
|
109
|
+
.flatMap(({ node }) => node.kind === 'event' && node.eventId !== undefined ? [node.eventId] : [])
|
|
110
|
+
.slice(0, EVENT_INLINE_EVENT_IDS),
|
|
111
|
+
...(path.failureReason === undefined ? {} : { failureReason: path.failureReason }),
|
|
112
|
+
...(path.failureMessage === undefined ? {} : { failureMessage: path.failureMessage }),
|
|
113
|
+
unresolvedAssumptions: path.unresolvedAssumptions.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** How far a change to the subject reaches, by count. */
|
|
117
|
+
export function summarizeEventImpact(impact) {
|
|
118
|
+
return {
|
|
119
|
+
direct: impact.directNodeIds.length,
|
|
120
|
+
upstream: impact.upstreamNodeIds.length,
|
|
121
|
+
downstream: impact.downstreamNodeIds.length,
|
|
122
|
+
stateAccesses: impact.stateAccessIds.length,
|
|
123
|
+
affectedRoots: impact.affectedRootIds.length,
|
|
124
|
+
affectedTerminals: impact.affectedTerminalIds.length,
|
|
125
|
+
wouldDisconnect: impact.wouldDisconnectNodeIds.length,
|
|
126
|
+
wouldDisconnectTerminals: impact.wouldDisconnectTerminalIds.length,
|
|
127
|
+
unresolved: impact.unresolvedNodeIds.length,
|
|
128
|
+
sampleDownstream: impact.downstreamNodeIds.slice(0, EVENT_INLINE_EVENT_IDS),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -135,9 +135,9 @@ function isFormableDecision(target, options) {
|
|
|
135
135
|
return false;
|
|
136
136
|
return /(?:^form_|formable|form_country)/iu.test(target);
|
|
137
137
|
}
|
|
138
|
-
function isEffectContainer(key) {
|
|
138
|
+
function isEffectContainer(key, nativeEffects) {
|
|
139
139
|
return (effectContainerKeys.has(key) ||
|
|
140
|
-
(nativeFocusEffectKeys.has(key) &&
|
|
140
|
+
((nativeFocusEffectKeys.has(key) || nativeEffects?.has(key) === true) &&
|
|
141
141
|
/^(?:(?:global_)?every|ordered|random)_[a-z0-9_]+$/u.test(key)));
|
|
142
142
|
}
|
|
143
143
|
function isScopeKey(key, knownLeaders) {
|
|
@@ -342,6 +342,7 @@ function referenceLinks(document, block, options) {
|
|
|
342
342
|
const links = [];
|
|
343
343
|
const knownHelpers = catalogValues(options, 'helper');
|
|
344
344
|
const knownLeaders = catalogValues(options, 'leader');
|
|
345
|
+
const isNative = (key) => nativeFocusEffectKeys.has(key) || options.nativeEffects?.has(key) === true;
|
|
345
346
|
function recordReference(assignment, specification) {
|
|
346
347
|
for (const target of referenceTargets(assignment, specification)) {
|
|
347
348
|
const kind = specification.kind === 'decision' && isFormableDecision(target, options)
|
|
@@ -364,7 +365,8 @@ function referenceLinks(document, block, options) {
|
|
|
364
365
|
recordReference(assignment, reference);
|
|
365
366
|
if (assignment.value.type === 'block') {
|
|
366
367
|
for (const nested of assignments(assignment.value)) {
|
|
367
|
-
if (nested.value.type === 'block' &&
|
|
368
|
+
if (nested.value.type === 'block' &&
|
|
369
|
+
isEffectContainer(nested.key.value.toLowerCase(), options.nativeEffects)) {
|
|
368
370
|
effectAssignment(nested, new Set());
|
|
369
371
|
}
|
|
370
372
|
}
|
|
@@ -373,7 +375,7 @@ function referenceLinks(document, block, options) {
|
|
|
373
375
|
}
|
|
374
376
|
if (knownHelpers.has(key) &&
|
|
375
377
|
!effectContainerKeys.has(normalizedKey) &&
|
|
376
|
-
!
|
|
378
|
+
!isNative(normalizedKey)) {
|
|
377
379
|
links.push({
|
|
378
380
|
kind: 'helper',
|
|
379
381
|
target: key,
|
|
@@ -381,7 +383,7 @@ function referenceLinks(document, block, options) {
|
|
|
381
383
|
});
|
|
382
384
|
return;
|
|
383
385
|
}
|
|
384
|
-
if (isEffectContainer(normalizedKey)) {
|
|
386
|
+
if (isEffectContainer(normalizedKey, options.nativeEffects)) {
|
|
385
387
|
if (assignment.value.type !== 'block')
|
|
386
388
|
return;
|
|
387
389
|
if (normalizedKey === 'random_list') {
|
|
@@ -397,7 +399,7 @@ function referenceLinks(document, block, options) {
|
|
|
397
399
|
}
|
|
398
400
|
return;
|
|
399
401
|
}
|
|
400
|
-
if (
|
|
402
|
+
if (isNative(normalizedKey))
|
|
401
403
|
return;
|
|
402
404
|
if (assignment.value.type === 'block' && isScopeKey(key, knownLeaders)) {
|
|
403
405
|
effectWalk(assignment.value, new Set());
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Native effect identifiers used to distinguish direct scripted-effect calls
|
|
3
3
|
* from engine effects while importing focus completion rewards.
|
|
4
4
|
*
|
|
5
|
-
* Source: the official HOI4 1.19.2 effects documentation reviewed 2026-07-10.
|
|
5
|
+
* Source: the official HOI4 1.19.2 effects documentation reviewed 2026-07-10. This is the
|
|
6
|
+
* fallback list; analysis also accepts every effect the installed game documents in
|
|
7
|
+
* documentation/effects_documentation.md, so effects added by later patches are recognized.
|
|
6
8
|
* Keep this list synchronized during compatibility reviews.
|
|
7
9
|
*/
|
|
8
10
|
export const nativeEffectKeys = new Set([
|
|
@@ -40,7 +40,7 @@ export function activeSidecar(snapshot, focusFile) {
|
|
|
40
40
|
}
|
|
41
41
|
export function importFocusFile(snapshot, file, palettes) {
|
|
42
42
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
43
|
-
const imported = importFocusTrees(document,
|
|
43
|
+
const imported = importFocusTrees(document, focusImportOptions(snapshot));
|
|
44
44
|
const sidecar = activeSidecar(snapshot, file);
|
|
45
45
|
imported.plans = imported.plans.map((plan) => {
|
|
46
46
|
const enriched = sidecar === undefined
|
|
@@ -89,6 +89,15 @@ export function selectContinuousPlan(result, paletteId) {
|
|
|
89
89
|
throw new ServiceError('CONTINUOUS_FOCUS_PALETTE_NOT_FOUND', `Continuous focus palette was not found: ${paletteId}`);
|
|
90
90
|
return plan;
|
|
91
91
|
}
|
|
92
|
+
/** Import options from a scan: its reference catalog and the installed game's documented effects. */
|
|
93
|
+
export function focusImportOptions(snapshot) {
|
|
94
|
+
return {
|
|
95
|
+
references: referenceCatalog(snapshot),
|
|
96
|
+
...(snapshot.documentedEffects === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { nativeEffects: snapshot.documentedEffects.names }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
92
101
|
export function referenceCatalog(snapshot) {
|
|
93
102
|
const identifiers = (kind) => [
|
|
94
103
|
...new Set(snapshot.index
|
|
@@ -94,6 +94,8 @@ export class ImpactAnalyzer {
|
|
|
94
94
|
directConsumers: graph.directConsumers.length,
|
|
95
95
|
transitiveConsumers: graph.transitiveConsumers.length,
|
|
96
96
|
affectedFiles: graph.affectedFiles.length,
|
|
97
|
+
// Affected files are evidence, not writes: this read-only analysis never changes a file.
|
|
98
|
+
affectedFilePaths: graph.affectedFiles.slice(0, 64),
|
|
97
99
|
cycles: graph.cycles.length,
|
|
98
100
|
scenarioSuites: scenarioSuites.scannedSuites,
|
|
99
101
|
affectedCases: scenarioSuites.matches.length,
|
|
@@ -108,7 +110,6 @@ export class ImpactAnalyzer {
|
|
|
108
110
|
result.code = complete ? 'IMPACT_ANALYZED' : 'IMPACT_ANALYZED_PARTIAL';
|
|
109
111
|
result.artifacts = [publicArtifactLink(artifact)];
|
|
110
112
|
result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
|
|
111
|
-
result.changedFiles = graph.affectedFiles.slice(0, 64);
|
|
112
113
|
setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
|
|
113
114
|
result.validation = {
|
|
114
115
|
passed: complete,
|
|
@@ -16,6 +16,7 @@ const impactOutputSchema = strictOperationResultSchema(z
|
|
|
16
16
|
directConsumers: nonNegativeIntegerSchema,
|
|
17
17
|
transitiveConsumers: nonNegativeIntegerSchema,
|
|
18
18
|
affectedFiles: nonNegativeIntegerSchema,
|
|
19
|
+
affectedFilePaths: z.array(z.string().max(1024)).max(64).optional(),
|
|
19
20
|
cycles: nonNegativeIntegerSchema,
|
|
20
21
|
scenarioSuites: nonNegativeIntegerSchema,
|
|
21
22
|
affectedCases: nonNegativeIntegerSchema,
|
|
@@ -70,6 +70,70 @@ const renderHashSchema = z
|
|
|
70
70
|
html: sha256Schema.optional(),
|
|
71
71
|
})
|
|
72
72
|
.strict();
|
|
73
|
+
const countMapSchema = z.record(z.string().max(256), nonNegativeIntegerSchema);
|
|
74
|
+
const inlineIdsSchema = z.array(z.string().max(256)).max(20);
|
|
75
|
+
/** Bounded inline answer of an inspection; see docs/events.md. */
|
|
76
|
+
const eventInspectSummarySchema = z
|
|
77
|
+
.object({
|
|
78
|
+
issues: nonNegativeIntegerSchema.optional(),
|
|
79
|
+
byClassification: countMapSchema.optional(),
|
|
80
|
+
byLayer: countMapSchema.optional(),
|
|
81
|
+
topCodes: countMapSchema.optional(),
|
|
82
|
+
findings: z
|
|
83
|
+
.array(z
|
|
84
|
+
.object({
|
|
85
|
+
code: z.string().max(256),
|
|
86
|
+
classification: z.enum([
|
|
87
|
+
'confirmed_error',
|
|
88
|
+
'probable_defect',
|
|
89
|
+
'design_warning',
|
|
90
|
+
'unresolved_analysis',
|
|
91
|
+
]),
|
|
92
|
+
confidence: z.enum(['confirmed', 'high', 'medium', 'low', 'unresolved']),
|
|
93
|
+
message: z.string().max(300),
|
|
94
|
+
path: z.string().max(1024).optional(),
|
|
95
|
+
line: nonNegativeIntegerSchema.optional(),
|
|
96
|
+
subject: z.string().max(1024).optional(),
|
|
97
|
+
})
|
|
98
|
+
.strict())
|
|
99
|
+
.max(10)
|
|
100
|
+
.optional(),
|
|
101
|
+
trace: z
|
|
102
|
+
.object({
|
|
103
|
+
nodes: nonNegativeIntegerSchema,
|
|
104
|
+
edges: nonNegativeIntegerSchema,
|
|
105
|
+
events: nonNegativeIntegerSchema,
|
|
106
|
+
eventIds: inlineIdsSchema,
|
|
107
|
+
boundaryNodes: nonNegativeIntegerSchema,
|
|
108
|
+
truncated: z.boolean(),
|
|
109
|
+
})
|
|
110
|
+
.strict()
|
|
111
|
+
.optional(),
|
|
112
|
+
entryPoints: nonNegativeIntegerSchema.optional(),
|
|
113
|
+
entryKinds: countMapSchema.optional(),
|
|
114
|
+
automaticEvents: nonNegativeIntegerSchema.optional(),
|
|
115
|
+
callerlessTriggeredEvents: nonNegativeIntegerSchema.optional(),
|
|
116
|
+
callerlessTriggeredByLayer: countMapSchema.optional(),
|
|
117
|
+
callerlessTriggeredEventIds: inlineIdsSchema.optional(),
|
|
118
|
+
cycles: nonNegativeIntegerSchema.optional(),
|
|
119
|
+
found: z.boolean().optional(),
|
|
120
|
+
steps: nonNegativeIntegerSchema.optional(),
|
|
121
|
+
eventIds: inlineIdsSchema.optional(),
|
|
122
|
+
failureReason: z.string().max(64).optional(),
|
|
123
|
+
failureMessage: z.string().max(1024).optional(),
|
|
124
|
+
unresolvedAssumptions: nonNegativeIntegerSchema.optional(),
|
|
125
|
+
direct: nonNegativeIntegerSchema.optional(),
|
|
126
|
+
upstream: nonNegativeIntegerSchema.optional(),
|
|
127
|
+
downstream: nonNegativeIntegerSchema.optional(),
|
|
128
|
+
stateAccesses: nonNegativeIntegerSchema.optional(),
|
|
129
|
+
affectedRoots: nonNegativeIntegerSchema.optional(),
|
|
130
|
+
affectedTerminals: nonNegativeIntegerSchema.optional(),
|
|
131
|
+
wouldDisconnect: nonNegativeIntegerSchema.optional(),
|
|
132
|
+
wouldDisconnectTerminals: nonNegativeIntegerSchema.optional(),
|
|
133
|
+
unresolved: nonNegativeIntegerSchema.optional(),
|
|
134
|
+
sampleDownstream: z.array(z.string().max(1024)).max(20).optional(),
|
|
135
|
+
})
|
|
136
|
+
.strict();
|
|
73
137
|
const eventInspectDataSchema = z
|
|
74
138
|
.object({
|
|
75
139
|
mode: eventInspectModeSchema,
|
|
@@ -78,6 +142,7 @@ const eventInspectDataSchema = z
|
|
|
78
142
|
graphHash: sha256Schema,
|
|
79
143
|
counts: eventGraphCountsSchema,
|
|
80
144
|
helperExpansion: helperExpansionSummarySchema.optional(),
|
|
145
|
+
summary: eventInspectSummarySchema.optional(),
|
|
81
146
|
boundary: inspectBoundarySchema,
|
|
82
147
|
})
|
|
83
148
|
.strict();
|
|
@@ -93,6 +158,7 @@ const eventRenderOutputSchema = strictOperationResultSchema(z
|
|
|
93
158
|
selectedNodes: nonNegativeIntegerSchema,
|
|
94
159
|
omittedNodes: nonNegativeIntegerSchema,
|
|
95
160
|
branchRenders: nonNegativeIntegerSchema,
|
|
161
|
+
omittedBranchRenders: nonNegativeIntegerSchema,
|
|
96
162
|
}),
|
|
97
163
|
boundary: z
|
|
98
164
|
.object({
|
|
@@ -775,39 +775,39 @@ export class ProbabilityAnalyzer {
|
|
|
775
775
|
if (adapter === 'custom_weighted_pool' || workspace.gameRoot === undefined)
|
|
776
776
|
return { status: 'adapter_target_only', adapterTarget: VERIFIED_GAME_VERSION };
|
|
777
777
|
const sourcePath = path.join(workspace.gameRoot, 'launcher-settings.json');
|
|
778
|
+
const unknown = (reason) => ({
|
|
779
|
+
status: 'version_unknown',
|
|
780
|
+
adapterTarget: VERIFIED_GAME_VERSION,
|
|
781
|
+
sourcePath,
|
|
782
|
+
reason,
|
|
783
|
+
});
|
|
778
784
|
let parsed;
|
|
779
785
|
try {
|
|
780
786
|
parsed = JSON.parse(await readFile(sourcePath, 'utf8'));
|
|
781
787
|
}
|
|
782
788
|
catch (error) {
|
|
783
|
-
|
|
789
|
+
return unknown(error instanceof Error ? error.message : String(error));
|
|
784
790
|
}
|
|
785
791
|
if (typeof parsed !== 'object' || parsed === null)
|
|
786
|
-
|
|
792
|
+
return unknown('launcher-settings.json does not contain a version object');
|
|
787
793
|
const settings = parsed;
|
|
788
794
|
const observedVersion = typeof settings.version === 'string' ? settings.version : undefined;
|
|
789
795
|
const observedRawVersion = typeof settings.rawVersion === 'string' ? settings.rawVersion : undefined;
|
|
790
|
-
const observedChecksum = observedVersion?.match(/\(([a-z0-9]+)\)\s*$/iu)?.[1];
|
|
791
|
-
if (observedVersion === undefined ||
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (observedRawVersion !== VERIFIED_GAME_RAW_VERSION ||
|
|
796
|
-
observedChecksum.toLowerCase() !== VERIFIED_GAME_CHECKSUM)
|
|
797
|
-
throw new ServiceError('PROBABILITY_GAME_VERSION_UNSUPPORTED', `Probability adapters are verified for ${VERIFIED_GAME_VERSION}, not the configured game version`, {
|
|
798
|
-
sourcePath,
|
|
799
|
-
observedVersion,
|
|
800
|
-
observedRawVersion,
|
|
801
|
-
observedChecksum,
|
|
802
|
-
adapterTarget: VERIFIED_GAME_VERSION,
|
|
803
|
-
});
|
|
796
|
+
const observedChecksum = observedVersion?.match(/\(([a-z0-9]+)\)\s*$/iu)?.[1]?.toLowerCase();
|
|
797
|
+
if (observedVersion === undefined || observedRawVersion === undefined)
|
|
798
|
+
return unknown('launcher-settings.json is missing version or rawVersion');
|
|
799
|
+
// Any installed version is analyzed with the documented rules; the observed identity is
|
|
800
|
+
// recorded so a result can be traced to the game it describes.
|
|
804
801
|
return {
|
|
805
|
-
status:
|
|
802
|
+
status: observedRawVersion === VERIFIED_GAME_RAW_VERSION &&
|
|
803
|
+
observedChecksum === VERIFIED_GAME_CHECKSUM
|
|
804
|
+
? 'workspace_verified'
|
|
805
|
+
: 'workspace_observed',
|
|
806
806
|
adapterTarget: VERIFIED_GAME_VERSION,
|
|
807
807
|
sourcePath,
|
|
808
808
|
observedVersion,
|
|
809
809
|
observedRawVersion,
|
|
810
|
-
observedChecksum: observedChecksum
|
|
810
|
+
...(observedChecksum === undefined ? {} : { observedChecksum }),
|
|
811
811
|
};
|
|
812
812
|
}
|
|
813
813
|
metadata(context, snapshot, surface, scenarioSet, candidatePool, gameVersionVerification, extra = {}) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ServiceError } from '../core/result.js';
|
|
2
2
|
import { decodeSource } from '../core/source/encoding.js';
|
|
3
3
|
import { readBoundedJsonLines } from './lines.js';
|
|
4
|
-
import { scanImpactSemanticReferences } from '../core/impact-semantic.js';
|
|
4
|
+
import { IMPACT_SEMANTIC_MAX_REFERENCES, scanImpactSemanticReferences, } from '../core/impact-semantic.js';
|
|
5
5
|
import { parseClausewitz } from '../core/source/parser.js';
|
|
6
6
|
import { navigateSource } from './source-navigation.js';
|
|
7
7
|
import { locatePatterns, locateSource } from './source-locate.js';
|
|
@@ -126,7 +126,9 @@ export async function sourceLookup(engine, workspaceId, input, principal, signal
|
|
|
126
126
|
});
|
|
127
127
|
let semantic = input.includeReferences ? semanticCache.get(snapshot) : undefined;
|
|
128
128
|
if (input.includeReferences && semantic === undefined) {
|
|
129
|
-
semantic = scanImpactSemanticReferences(snapshot,
|
|
129
|
+
semantic = scanImpactSemanticReferences(snapshot, IMPACT_SEMANTIC_MAX_REFERENCES, {
|
|
130
|
+
includeOnActions: true,
|
|
131
|
+
});
|
|
130
132
|
semanticCache.set(snapshot, semantic);
|
|
131
133
|
}
|
|
132
134
|
const referenceIdentities = new Set();
|
|
@@ -770,7 +770,13 @@ const probabilityMetadataSchema = z
|
|
|
770
770
|
gameVersion: z.string(),
|
|
771
771
|
gameVersionVerification: z
|
|
772
772
|
.object({
|
|
773
|
-
status: z.enum([
|
|
773
|
+
status: z.enum([
|
|
774
|
+
'workspace_verified',
|
|
775
|
+
'workspace_observed',
|
|
776
|
+
'version_unknown',
|
|
777
|
+
'adapter_target_only',
|
|
778
|
+
]),
|
|
779
|
+
reason: z.string().max(1024).optional(),
|
|
774
780
|
adapterTarget: z.string(),
|
|
775
781
|
sourcePath: z.string().optional(),
|
|
776
782
|
observedVersion: z.string().optional(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export const PACKAGE_NAME = 'hoi4-agent-tools';
|
|
3
|
-
export const PACKAGE_VERSION = '4.
|
|
3
|
+
export const PACKAGE_VERSION = '4.3.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 2;
|
package/docs/analysis.md
CHANGED
|
@@ -58,6 +58,8 @@ Malformed, missing, or oversized suite files produce explicit partial coverage.
|
|
|
58
58
|
The graph includes active and overridden definitions, direct and transitive consumers, source locations, state-key reads and writes, and coverage limits.
|
|
59
59
|
It connects source-backed event, focus, decision, idea, technology, scripted-helper, localisation, GUI, sprite, and texture references where a typed static edge can be established.
|
|
60
60
|
Unresolved dynamic names, missing definitions, skipped sources, cycles, and traversal limits remain explicit in the report.
|
|
61
|
+
A dynamic reference counts against completeness only when it names a symbol kind the traversal reaches, so `activate_decision = event_target:choice` affects the impact of a decision but not of an idea that no decision consumes.
|
|
62
|
+
`data.affectedFilePaths` lists up to 64 affected files; the result's `changedFiles` stays empty because the analysis never writes.
|
|
61
63
|
An affected file is a source dependency lead, not a claim that the game executed that path.
|
|
62
64
|
|
|
63
65
|
## Decisions and missions
|
|
@@ -99,6 +101,8 @@ The inventory lists active decisions, category fragments, overridden source defi
|
|
|
99
101
|
Category and decision `allowed`, `visible`, and `available` results remain separate; mission visibility is reported as ignored where documented.
|
|
100
102
|
Target selection covers explicit lists and arrays plus supported state ownership, control, and continent filters.
|
|
101
103
|
When a target, flag, variable, or helper input is unknown, the corresponding gate remains unresolved.
|
|
104
|
+
This includes `has_variable` and `has_event_target`: an undeclared variable or event target is unknown, not absent, unless the scenario sets `closedFlags: true`.
|
|
105
|
+
AI weights use the probability adapters, which run on any installed game version and record the observed build in their evidence.
|
|
102
106
|
|
|
103
107
|
Costs distinguish automatic engine political-power payment from a custom affordability trigger and its separate `complete_effect` payment.
|
|
104
108
|
The report identifies direct deductions, possible double payment, missing direct payment, conditional or helper-dependent paths, repeatability, cooldowns, removal, activation, cancellation, and mission timeout paths.
|
package/docs/events.md
CHANGED
|
@@ -26,6 +26,18 @@ For a large helper closure, use [bounded helper expansion](helper-expansion.md)
|
|
|
26
26
|
|
|
27
27
|
The compact response summarizes the result. Small scan resources contain the complete graph; large scan resources contain exact totals, grouped inventories, representative diagnostics, and the revision needed for focused follow-up queries. Large focused scans keep direct event and on-action evidence, call sites, state accesses, and source locations while leaving helper expansion and workspace-wide lifecycle analysis explicit as deferred boundaries. This keeps an agent's prompt and artifact storage bounded without allowing a broad scan to exhaust the MCP process.
|
|
28
28
|
|
|
29
|
+
`data.summary` answers the request inline before the linked report is opened. `scan`, `lint` and `state_flow` return the number of findings in the selection, counts by classification, the ten most frequent codes and the ten most severe findings with their path, line and subject. `trace` adds the traced node, edge and event counts with up to twenty event IDs. `roots` returns entry-point counts by kind, automatic and callerless triggered events, and cycle counts. `data.counts` still describes the whole analyzed graph, including vanilla events.
|
|
30
|
+
|
|
31
|
+
`explain_path` adds whether a route was found, its step count, event IDs and failure reason; `impact` adds direct, upstream, downstream, affected-root and disconnection counts.
|
|
32
|
+
|
|
33
|
+
Workspace-wide modes expand scripted helpers up to a fixed work ceiling. On a very large mod the expansion stops there with an `EVENT_HELPER_PROJECTION_LIMIT` finding that names the completed and total roots; calls from the remaining roots stay structural, and a namespace, event or file selector or `helper_expansion` mode covers them. An unselected overview render returns the overview and as many branch renders as the request's raster budget allows, reporting the rest in `omittedBranchRenders`.
|
|
34
|
+
|
|
35
|
+
A `scan` retains the complete graph as a snapshot artifact only up to 128 MiB of canonical JSON. A larger graph, such as a mod with thousands of events and hundreds of thousands of state accesses, returns the scan report with the info diagnostic `EVENT_GRAPH_SNAPSHOT_OMITTED`; comparisons then use the revisions this server holds in memory.
|
|
36
|
+
|
|
37
|
+
An assignment such as `custom_effect = yes` is reported as a missing helper only where it can execute: in effect blocks of events, decisions, focuses and on-actions, inside scope and loop effects, and in conventionally named effect blocks (`on_*`, `*_effect`, `*_effects`) of other definition files. Arguments of native effects such as `add_building_construction = { instant_build = yes }`, condition blocks such as `visible`, `available`, `allowed`, `ai_will_do`, `NOT` or `any_*`, and data fields of ideas, raids, special projects and scripted GUIs are not calls.
|
|
38
|
+
|
|
39
|
+
A read is linked to a writer of the same name when the writer is a temporary variable, a write inside a scripted helper (whose scope is decided by its caller), a scope-prefixed name such as `GER.value`, or the array behind `entries^num`. `event_target:name` reads targets saved by `save_event_target_as` and `save_global_event_target_as`; `global_event_target:name` reads only global targets. A finding is kept per owner and subject, so different flags or targets read in one event remain separate findings.
|
|
40
|
+
|
|
29
41
|
Selectors use one of these forms:
|
|
30
42
|
|
|
31
43
|
```json
|
package/docs/probability.md
CHANGED
|
@@ -12,7 +12,7 @@ Use them for event timing and option weights, decision and mission scores, focus
|
|
|
12
12
|
|
|
13
13
|
## Game version
|
|
14
14
|
|
|
15
|
-
When `gameRoot` is configured, `launcher-settings.json`
|
|
15
|
+
The adapters apply the documented weighting rules of `ai_will_do`, `ai_chance`, mean time to happen, `random_list` and AI strategies, which are the same in every game version, so evaluation runs on whatever HOI4 version is installed. When `gameRoot` is configured, `metadata.gameVersionVerification` records the build read from `launcher-settings.json`: `workspace_verified` for the build the adapters were last reviewed against, `workspace_observed` for any other build, and `version_unknown` with a `reason` when the file cannot be read. Without a game root, results identify the adapter version (`adapter_target_only`). Proposed source is evaluated in memory.
|
|
16
16
|
|
|
17
17
|
## One call for current weights
|
|
18
18
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoi4-agent-tools",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "hoi4-agent-tools",
|
|
9
|
-
"version": "4.
|
|
9
|
+
"version": "4.3.0",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@fontsource-variable/roboto": "5.3.0",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoi4-agent-tools",
|
|
3
3
|
"mcpName": "io.github.klimPaskov/hoi4-agent-tools",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"description": "Build HOI4 mods: focus trees, events, decisions, worlds, GUI previews, docs and AI balance.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/configuration.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/continuous-focus-palette.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/custom-weighted-pool.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/decision-inspect-input.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/error-log-input.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/event-feature-manifest.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/focus-plan.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/focus-planning-sidecar.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/gui-animation-source.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/gui-helper.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/gui-scenario.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/helper-expansion-request.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.3.0/schemas/helper-expansion-summary.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|