hoi4-agent-tools 2.4.0 → 2.5.1
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 +14 -0
- package/README.md +25 -25
- package/dist/hoi4_agent_tools/core/artifacts.js +167 -30
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +8 -1
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +52 -6
- package/dist/hoi4_agent_tools/probability/service.js +81 -7
- package/dist/hoi4_agent_tools/probability/source-analysis.js +83 -32
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/probability.md +3 -1
- 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/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/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- 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/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.1 - 2026-08-13
|
|
4
|
+
|
|
5
|
+
- Published artifact contents and provenance manifests atomically across independent MCP processes, preventing event, probability, GUI, and other concurrent inspections from observing partially written manifests.
|
|
6
|
+
- Recovered clearly interrupted zero-filled or truncated provenance manifests during matching writes and storage pruning while continuing to reject arbitrary malformed or tampered manifests.
|
|
7
|
+
- Tolerated temporary-file disappearance during concurrent storage scans without weakening artifact integrity validation.
|
|
8
|
+
- Added cross-process publication, interrupted-write recovery, and fail-closed integrity regressions, then verified event inspection, probability inspection, and artifact-resource workflows against the repaired store.
|
|
9
|
+
|
|
10
|
+
## 2.5.0 - 2026-08-09
|
|
11
|
+
|
|
12
|
+
- Added exact continuous-focus coordinates and linked palette IDs to compact national focus inspection results.
|
|
13
|
+
- Added source-first probability inspection that discovers compatible adapters, candidate counts, and selector examples without requiring the caller to guess a surface type.
|
|
14
|
+
- Changed empty or mismatched probability inspection into an actionable discovery result with a reason and suggested adapter while keeping evaluation, sweeps, and simulation strict.
|
|
15
|
+
- Prevented focus-tree blocks from being misclassified as decision or mission weighted surfaces.
|
|
16
|
+
|
|
3
17
|
## 2.4.0 - 2026-08-09
|
|
4
18
|
|
|
5
19
|
- Added named value-driven scripted-GUI scenario matrices with deterministic SVG, PNG, JSON, and per-element visibility, movement, size, text, and frame-change evidence.
|
package/README.md
CHANGED
|
@@ -31,31 +31,31 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
|
|
|
31
31
|
|
|
32
32
|
## Tools
|
|
33
33
|
|
|
34
|
-
| Tool | Purpose
|
|
35
|
-
| --------------------------- |
|
|
36
|
-
| `hoi4.focus_inspect` | Read focus trees and
|
|
37
|
-
| `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts.
|
|
38
|
-
| `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons.
|
|
39
|
-
| `hoi4.focus_rewrite` | Create or update a focus tree.
|
|
40
|
-
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic.
|
|
41
|
-
| `hoi4.gui_render` | Render GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics.
|
|
42
|
-
| `hoi4.gui_rewrite` | Create or update a GUI source package.
|
|
43
|
-
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data.
|
|
44
|
-
| `hoi4.map_render` | Render map layers and overlays.
|
|
45
|
-
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes.
|
|
46
|
-
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow.
|
|
47
|
-
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges.
|
|
48
|
-
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions.
|
|
49
|
-
| `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems.
|
|
50
|
-
| `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage.
|
|
51
|
-
| `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays.
|
|
52
|
-
| `hoi4.probability_inspect` | Locate weighted logic, candidates, required inputs
|
|
53
|
-
| `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs.
|
|
54
|
-
| `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges.
|
|
55
|
-
| `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data.
|
|
56
|
-
| `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states.
|
|
57
|
-
| `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source.
|
|
58
|
-
| `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons.
|
|
34
|
+
| Tool | Purpose |
|
|
35
|
+
| --------------------------- | ---------------------------------------------------------------------------------------- |
|
|
36
|
+
| `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
|
|
37
|
+
| `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
|
|
38
|
+
| `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
|
|
39
|
+
| `hoi4.focus_rewrite` | Create or update a focus tree. |
|
|
40
|
+
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
|
|
41
|
+
| `hoi4.gui_render` | Render GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics. |
|
|
42
|
+
| `hoi4.gui_rewrite` | Create or update a GUI source package. |
|
|
43
|
+
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
|
|
44
|
+
| `hoi4.map_render` | Render map layers and overlays. |
|
|
45
|
+
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes. |
|
|
46
|
+
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
47
|
+
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
48
|
+
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
49
|
+
| `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
|
|
50
|
+
| `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
|
|
51
|
+
| `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
|
|
52
|
+
| `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
|
|
53
|
+
| `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
|
|
54
|
+
| `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
|
|
55
|
+
| `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
|
|
56
|
+
| `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
|
|
57
|
+
| `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
|
|
58
|
+
| `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
|
|
59
59
|
|
|
60
60
|
Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
|
|
61
61
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { lstat, mkdir, open, readFile, readdir, stat, unlink
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { link, lstat, mkdir, open, readFile, readdir, stat, unlink } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { z } from 'zod/v4';
|
|
5
5
|
import { canonicalJson, compareCodeUnits, sha256Bytes } from './canonical.js';
|
|
@@ -159,6 +159,8 @@ const artifactManifestMaxBytes = 1_048_576;
|
|
|
159
159
|
const artifactLogicalBatchMaxBytes = 536_870_912;
|
|
160
160
|
const artifactHashYieldBytes = 16_777_216;
|
|
161
161
|
const artifactPreflightYieldEntries = 256;
|
|
162
|
+
const interruptedManifestMinimumAgeMs = 1_000;
|
|
163
|
+
const artifactTemporaryPrefix = '.artifact-tmp-';
|
|
162
164
|
const ownedArtifactContent = Symbol('ownedArtifactContent');
|
|
163
165
|
const precomputedArtifactSha256 = Symbol('precomputedArtifactSha256');
|
|
164
166
|
export const chunkedArtifactIndexMimeType = 'application/json';
|
|
@@ -320,11 +322,22 @@ async function artifactUsage(root, signal) {
|
|
|
320
322
|
if (child.isSymbolicLink()) {
|
|
321
323
|
throw new ServiceError('ARTIFACT_STORAGE_UNSAFE', 'Artifact storage contains a symbolic link or junction');
|
|
322
324
|
}
|
|
325
|
+
if (child.isFile() && child.name.startsWith(artifactTemporaryPrefix))
|
|
326
|
+
continue;
|
|
323
327
|
const candidate = await containedGeneratedPath(root, path.relative(root, path.join(directory, child.name)));
|
|
324
328
|
if (child.isDirectory())
|
|
325
329
|
await walk(candidate);
|
|
326
330
|
else if (child.isFile()) {
|
|
327
|
-
|
|
331
|
+
let size;
|
|
332
|
+
try {
|
|
333
|
+
size = (await stat(candidate)).size;
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
if (error.code === 'ENOENT')
|
|
337
|
+
continue;
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
bytes += size;
|
|
328
341
|
if (child.name.endsWith('.manifest.json'))
|
|
329
342
|
entries += 1;
|
|
330
343
|
if (!Number.isSafeInteger(bytes)) {
|
|
@@ -375,6 +388,101 @@ async function readArtifactManifest(filePath, signal) {
|
|
|
375
388
|
throw new ServiceError('ARTIFACT_MANIFEST_INVALID', 'Artifact provenance manifest is invalid');
|
|
376
389
|
}
|
|
377
390
|
}
|
|
391
|
+
async function readBoundedManifestBytes(filePath, signal) {
|
|
392
|
+
signal?.throwIfAborted();
|
|
393
|
+
let metadata;
|
|
394
|
+
try {
|
|
395
|
+
metadata = await lstat(filePath);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
throw new ServiceError('ARTIFACT_NOT_FOUND', 'Artifact provenance manifest is unavailable');
|
|
399
|
+
}
|
|
400
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
401
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INVALID', 'Artifact provenance manifest is invalid');
|
|
402
|
+
}
|
|
403
|
+
if (metadata.size > artifactManifestMaxBytes) {
|
|
404
|
+
throw new ServiceError('ARTIFACT_MANIFEST_LIMIT', 'Artifact provenance manifest exceeds its fixed byte limit');
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const bytes = signal === undefined ? await readFile(filePath) : await readFile(filePath, { signal });
|
|
408
|
+
return { bytes, metadata };
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (error.name === 'AbortError')
|
|
412
|
+
throw error;
|
|
413
|
+
throw new ServiceError('ARTIFACT_NOT_FOUND', 'Artifact provenance manifest is unavailable');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function zeroFilled(bytes) {
|
|
417
|
+
return bytes.length === 0 || bytes.every((value) => value === 0);
|
|
418
|
+
}
|
|
419
|
+
async function interruptedManifestState(filePath, expectedBytes, signal) {
|
|
420
|
+
const { bytes, metadata } = await readBoundedManifestBytes(filePath, signal);
|
|
421
|
+
const expected = expectedBytes === undefined ? undefined : Buffer.from(expectedBytes, 'utf8');
|
|
422
|
+
const expectedPrefix = expected !== undefined &&
|
|
423
|
+
bytes.length < expected.length &&
|
|
424
|
+
expected.subarray(0, bytes.length).equals(bytes);
|
|
425
|
+
const staleZeroFill = zeroFilled(bytes) && Date.now() - Number(metadata.mtimeMs) >= interruptedManifestMinimumAgeMs;
|
|
426
|
+
return { interrupted: expectedPrefix || staleZeroFill, size: Number(metadata.size) };
|
|
427
|
+
}
|
|
428
|
+
async function existingManifestMatches(filePath, expectedBytes, signal) {
|
|
429
|
+
if (!(await fileExists(filePath)))
|
|
430
|
+
return false;
|
|
431
|
+
try {
|
|
432
|
+
const manifest = await readArtifactManifest(filePath, signal);
|
|
433
|
+
if (`${canonicalJson(manifest)}\n` !== expectedBytes) {
|
|
434
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Immutable artifact provenance manifest contains different bytes');
|
|
435
|
+
}
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (error instanceof ServiceError && error.code === 'ARTIFACT_NOT_FOUND')
|
|
440
|
+
return false;
|
|
441
|
+
if (!(error instanceof ServiceError) || error.code !== 'ARTIFACT_MANIFEST_INVALID')
|
|
442
|
+
throw error;
|
|
443
|
+
const state = await interruptedManifestState(filePath, expectedBytes, signal);
|
|
444
|
+
if (!state.interrupted)
|
|
445
|
+
throw error;
|
|
446
|
+
try {
|
|
447
|
+
await unlink(filePath);
|
|
448
|
+
}
|
|
449
|
+
catch (unlinkError) {
|
|
450
|
+
if (unlinkError.code !== 'ENOENT')
|
|
451
|
+
throw unlinkError;
|
|
452
|
+
}
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function publishExclusiveFile(target, bytes, signal) {
|
|
457
|
+
signal?.throwIfAborted();
|
|
458
|
+
const temporary = path.join(path.dirname(target), `${artifactTemporaryPrefix}${process.pid}-${randomUUID().replaceAll('-', '')}`);
|
|
459
|
+
let handle;
|
|
460
|
+
try {
|
|
461
|
+
handle = await open(temporary, 'wx');
|
|
462
|
+
await handle.writeFile(bytes, signal === undefined ? undefined : { signal });
|
|
463
|
+
await handle.sync();
|
|
464
|
+
await handle.close();
|
|
465
|
+
handle = undefined;
|
|
466
|
+
signal?.throwIfAborted();
|
|
467
|
+
try {
|
|
468
|
+
await link(temporary, target);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
if (error.code === 'EEXIST')
|
|
473
|
+
return false;
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
finally {
|
|
478
|
+
if (handle !== undefined)
|
|
479
|
+
await handle.close().catch(() => undefined);
|
|
480
|
+
await unlink(temporary).catch((error) => {
|
|
481
|
+
if (error.code !== 'ENOENT')
|
|
482
|
+
throw error;
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
378
486
|
async function readVerifiedArtifactRange(filePath, expectedSha256, range, verifiedIdentity, signal) {
|
|
379
487
|
signal?.throwIfAborted();
|
|
380
488
|
if (range !== undefined &&
|
|
@@ -495,8 +603,47 @@ export class ArtifactStore {
|
|
|
495
603
|
}
|
|
496
604
|
if (!child.isFile() || !child.name.endsWith('.manifest.json'))
|
|
497
605
|
continue;
|
|
498
|
-
|
|
499
|
-
|
|
606
|
+
let metadata;
|
|
607
|
+
try {
|
|
608
|
+
metadata = await lstat(candidate);
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
if (error.code === 'ENOENT')
|
|
612
|
+
continue;
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
let manifest;
|
|
616
|
+
try {
|
|
617
|
+
manifest = await readArtifactManifest(candidate, signal);
|
|
618
|
+
}
|
|
619
|
+
catch (error) {
|
|
620
|
+
if (error instanceof ServiceError && error.code === 'ARTIFACT_NOT_FOUND')
|
|
621
|
+
continue;
|
|
622
|
+
if (!(error instanceof ServiceError) || error.code !== 'ARTIFACT_MANIFEST_INVALID')
|
|
623
|
+
throw error;
|
|
624
|
+
let interrupted;
|
|
625
|
+
try {
|
|
626
|
+
interrupted = await interruptedManifestState(candidate, undefined, signal);
|
|
627
|
+
}
|
|
628
|
+
catch (interruptedError) {
|
|
629
|
+
if (interruptedError instanceof ServiceError &&
|
|
630
|
+
interruptedError.code === 'ARTIFACT_NOT_FOUND')
|
|
631
|
+
continue;
|
|
632
|
+
throw interruptedError;
|
|
633
|
+
}
|
|
634
|
+
if (!interrupted.interrupted)
|
|
635
|
+
throw error;
|
|
636
|
+
try {
|
|
637
|
+
await unlink(candidate);
|
|
638
|
+
}
|
|
639
|
+
catch (unlinkError) {
|
|
640
|
+
if (unlinkError.code !== 'ENOENT')
|
|
641
|
+
throw unlinkError;
|
|
642
|
+
}
|
|
643
|
+
usage.entries = Math.max(0, usage.entries - 1);
|
|
644
|
+
usage.bytes = Math.max(0, usage.bytes - interrupted.size);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
500
647
|
if (child.name !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`) {
|
|
501
648
|
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest filename does not match its contents');
|
|
502
649
|
}
|
|
@@ -993,13 +1140,8 @@ export class ArtifactStore {
|
|
|
993
1140
|
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact batch maps different provenance to the same immutable manifest');
|
|
994
1141
|
}
|
|
995
1142
|
if (duplicateManifest === undefined) {
|
|
996
|
-
const exists = await
|
|
1143
|
+
const exists = await existingManifestMatches(artifact.manifestPath, artifact.manifestBytes, signal);
|
|
997
1144
|
signal?.throwIfAborted();
|
|
998
|
-
if (exists &&
|
|
999
|
-
`${canonicalJson(await readArtifactManifest(artifact.manifestPath, signal))}\n` !==
|
|
1000
|
-
artifact.manifestBytes) {
|
|
1001
|
-
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Immutable artifact provenance manifest contains different bytes');
|
|
1002
|
-
}
|
|
1003
1145
|
uniqueManifests.set(artifact.manifestPath, { ...artifact, exists });
|
|
1004
1146
|
}
|
|
1005
1147
|
}
|
|
@@ -1020,18 +1162,19 @@ export class ArtifactStore {
|
|
|
1020
1162
|
continue;
|
|
1021
1163
|
await mkdir(artifact.directory, { recursive: true });
|
|
1022
1164
|
signal?.throwIfAborted();
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
flag: 'wx',
|
|
1026
|
-
...(signal === undefined ? {} : { signal }),
|
|
1027
|
-
});
|
|
1165
|
+
const created = await publishExclusiveFile(artifact.target, artifact.bytes, signal);
|
|
1166
|
+
if (created) {
|
|
1028
1167
|
createdTargets.push(artifact.target);
|
|
1029
1168
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1169
|
+
else {
|
|
1170
|
+
try {
|
|
1171
|
+
await readVerifiedArtifactRange(artifact.target, artifact.artifact.sha256, { offset: 0, length: 1 }, undefined, signal);
|
|
1172
|
+
}
|
|
1173
|
+
catch (error) {
|
|
1174
|
+
if (error.name === 'AbortError')
|
|
1175
|
+
throw error;
|
|
1176
|
+
throw new ServiceError('ARTIFACT_HASH_COLLISION', 'Artifact path contains different bytes');
|
|
1033
1177
|
}
|
|
1034
|
-
throw error;
|
|
1035
1178
|
}
|
|
1036
1179
|
}
|
|
1037
1180
|
for (const artifact of uniqueManifests.values()) {
|
|
@@ -1040,18 +1183,12 @@ export class ArtifactStore {
|
|
|
1040
1183
|
continue;
|
|
1041
1184
|
await mkdir(artifact.directory, { recursive: true });
|
|
1042
1185
|
signal?.throwIfAborted();
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
flag: 'wx',
|
|
1046
|
-
...(signal === undefined ? {} : { signal }),
|
|
1047
|
-
});
|
|
1186
|
+
const created = await publishExclusiveFile(artifact.manifestPath, artifact.manifestBytes, signal);
|
|
1187
|
+
if (created) {
|
|
1048
1188
|
createdManifests.push(artifact.manifestPath);
|
|
1049
1189
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
createdManifests.push(artifact.manifestPath);
|
|
1053
|
-
}
|
|
1054
|
-
throw error;
|
|
1190
|
+
else if (!(await existingManifestMatches(artifact.manifestPath, artifact.manifestBytes, signal))) {
|
|
1191
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INVALID', 'Concurrent artifact provenance manifest publication did not complete');
|
|
1055
1192
|
}
|
|
1056
1193
|
}
|
|
1057
1194
|
signal?.throwIfAborted();
|
|
@@ -31,6 +31,11 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
|
31
31
|
branchCount: nonNegativeIntegerSchema,
|
|
32
32
|
continuousPaletteCount: nonNegativeIntegerSchema,
|
|
33
33
|
continuousFocusCount: nonNegativeIntegerSchema,
|
|
34
|
+
continuousFocusPosition: z
|
|
35
|
+
.object({ x: z.number().int(), y: z.number().int() })
|
|
36
|
+
.strict()
|
|
37
|
+
.nullable(),
|
|
38
|
+
continuousFocusPaletteIds: z.array(z.string().max(256)).max(100),
|
|
34
39
|
resolvedTitleCount: nonNegativeIntegerSchema,
|
|
35
40
|
layoutHash: sha256Schema,
|
|
36
41
|
layoutDecisionCount: nonNegativeIntegerSchema,
|
|
@@ -601,7 +606,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
601
606
|
const workbench = new FocusWorkbench(engine.resolver, engine.transactions, engine.artifacts);
|
|
602
607
|
server.registerTool('hoi4.focus_inspect', {
|
|
603
608
|
title: 'Inspect focus trees',
|
|
604
|
-
description: 'Inspect national trees or continuous palettes for creation and cleanup, including complete plans, references, diagnostics, and stable layout decisions.',
|
|
609
|
+
description: 'Inspect national trees or continuous palettes for creation and cleanup, including continuous-focus placement, complete plans, references, diagnostics, and stable layout decisions.',
|
|
605
610
|
inputSchema: focusInspectInput,
|
|
606
611
|
outputSchema: focusInspectOutputSchema,
|
|
607
612
|
annotations: artifactProducing,
|
|
@@ -791,6 +796,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
791
796
|
branchCount: plan.branchGroups.length,
|
|
792
797
|
continuousPaletteCount: plan.continuousFocusPaletteIds.length,
|
|
793
798
|
continuousFocusCount: plan.continuousFocusIds.length,
|
|
799
|
+
continuousFocusPosition: plan.continuousFocusPosition ?? null,
|
|
800
|
+
continuousFocusPaletteIds: plan.continuousFocusPaletteIds.slice(0, 100),
|
|
794
801
|
resolvedTitleCount: plan.focuses.filter(({ id }) => presentation.entries[id]?.titleSourceLocation !== undefined).length,
|
|
795
802
|
layoutHash: layout.layoutHash,
|
|
796
803
|
layoutDecisionCount: layout.decisions.length,
|
|
@@ -39,10 +39,10 @@ const inspectInput = z
|
|
|
39
39
|
})
|
|
40
40
|
.strict()
|
|
41
41
|
.superRefine((value, context) => {
|
|
42
|
-
if (
|
|
42
|
+
if (value.adapter !== undefined && value.source === undefined)
|
|
43
43
|
context.addIssue({
|
|
44
44
|
code: 'custom',
|
|
45
|
-
message: '
|
|
45
|
+
message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
const evaluateInput = z.object(commonShape).strict();
|
|
@@ -135,9 +135,32 @@ const inspectDataSchema = z
|
|
|
135
135
|
.object({
|
|
136
136
|
adapters: nonNegativeIntegerSchema,
|
|
137
137
|
adapterId: z.string().max(256).optional(),
|
|
138
|
+
requestedAdapter: z.string().max(256).optional(),
|
|
139
|
+
suggestedAdapter: z.string().max(256).optional(),
|
|
140
|
+
discoveryReason: z
|
|
141
|
+
.enum([
|
|
142
|
+
'source_inventory',
|
|
143
|
+
'requested_adapter_empty',
|
|
144
|
+
'identifier_not_found',
|
|
145
|
+
'candidate_pool_not_found',
|
|
146
|
+
'no_weighted_surfaces',
|
|
147
|
+
])
|
|
148
|
+
.optional(),
|
|
138
149
|
sourceRevision: sha256Schema.optional(),
|
|
139
150
|
sourceHash: sha256Schema.optional(),
|
|
140
151
|
candidates: nonNegativeIntegerSchema,
|
|
152
|
+
availableCandidates: nonNegativeIntegerSchema,
|
|
153
|
+
availableAdapters: z
|
|
154
|
+
.array(z
|
|
155
|
+
.object({
|
|
156
|
+
adapterId: z.string().max(256),
|
|
157
|
+
candidates: nonNegativeIntegerSchema,
|
|
158
|
+
identifierMatches: nonNegativeIntegerSchema,
|
|
159
|
+
candidatePoolMatches: nonNegativeIntegerSchema,
|
|
160
|
+
})
|
|
161
|
+
.strict())
|
|
162
|
+
.max(10),
|
|
163
|
+
candidateExamples: z.array(z.string().max(512)).max(10),
|
|
141
164
|
poolComplete: z.boolean().optional(),
|
|
142
165
|
requiredInputs: nonNegativeIntegerSchema,
|
|
143
166
|
unresolved: nonNegativeIntegerSchema,
|
|
@@ -211,7 +234,7 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
211
234
|
const analyzer = new ProbabilityAnalyzer(engine);
|
|
212
235
|
server.registerTool('hoi4.probability_inspect', {
|
|
213
236
|
title: 'Inspect AI and MTTH weighted logic',
|
|
214
|
-
description: 'List versioned adapters or discover weighted
|
|
237
|
+
description: 'List versioned adapters, or provide a source alone to discover compatible weighted adapters, candidate pools, capabilities, provenance, and unsupported constructs before narrowing the request.',
|
|
215
238
|
inputSchema: inspectInput,
|
|
216
239
|
outputSchema: inspectOutput,
|
|
217
240
|
annotations: readOnly,
|
|
@@ -231,14 +254,37 @@ export function registerProbabilityTools(server, engine, context) {
|
|
|
231
254
|
sourceHash: inspected.surface.sourceHash,
|
|
232
255
|
poolComplete: inspected.surface.poolComplete,
|
|
233
256
|
}),
|
|
257
|
+
...(inspected.discovery === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: {
|
|
260
|
+
...(inspected.discovery.requestedAdapter === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { requestedAdapter: inspected.discovery.requestedAdapter }),
|
|
263
|
+
...(inspected.discovery.suggestedAdapter === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { suggestedAdapter: inspected.discovery.suggestedAdapter }),
|
|
266
|
+
discoveryReason: inspected.discovery.reason,
|
|
267
|
+
sourceRevision: inspected.discovery.sourceRevision,
|
|
268
|
+
sourceHash: inspected.discovery.sourceHash,
|
|
269
|
+
}),
|
|
234
270
|
candidates: inspected.surface?.candidateCount ?? 0,
|
|
271
|
+
availableCandidates: inspected.discovery?.availableAdapters.reduce((sum, { candidateCount }) => sum + candidateCount, 0) ?? 0,
|
|
272
|
+
availableAdapters: inspected.discovery?.availableAdapters.map((available) => ({
|
|
273
|
+
adapterId: available.adapterId,
|
|
274
|
+
candidates: available.candidateCount,
|
|
275
|
+
identifierMatches: available.identifierMatchCount,
|
|
276
|
+
candidatePoolMatches: available.candidatePoolMatchCount,
|
|
277
|
+
})) ?? [],
|
|
278
|
+
candidateExamples: inspected.discovery?.exampleCandidateIds ?? [],
|
|
235
279
|
requiredInputs: inspected.surface?.requiredInputs.length ?? 0,
|
|
236
280
|
unresolved: inspected.surface?.unsupported.length ?? 0,
|
|
237
281
|
});
|
|
238
282
|
result.code =
|
|
239
|
-
inspected.surface
|
|
240
|
-
? '
|
|
241
|
-
:
|
|
283
|
+
inspected.surface !== undefined
|
|
284
|
+
? 'PROBABILITY_SOURCE_INSPECTED'
|
|
285
|
+
: inspected.discovery !== undefined
|
|
286
|
+
? 'PROBABILITY_SOURCE_DISCOVERED'
|
|
287
|
+
: 'PROBABILITY_ADAPTERS_LISTED';
|
|
242
288
|
result.artifacts = inspected.artifacts;
|
|
243
289
|
setInlineFilesScanned(result, inspected.filesScanned);
|
|
244
290
|
await progress.report(2, 2, 'Weighted source inspection complete');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { canonicalJson, hashCanonical } from '../core/canonical.js';
|
|
3
|
+
import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
4
4
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
5
5
|
import { probabilityDomainScanPatterns } from '../core/domain-scan-patterns.js';
|
|
6
6
|
import { publicArtifactLink } from '../core/artifacts.js';
|
|
@@ -13,7 +13,7 @@ import { VERIFIED_GAME_CHECKSUM, VERIFIED_GAME_RAW_VERSION, VERIFIED_GAME_VERSIO
|
|
|
13
13
|
import { renderProbabilityResult } from './render.js';
|
|
14
14
|
import { analyzeSequence } from './sequence.js';
|
|
15
15
|
import { simulateSurface } from './simulation.js';
|
|
16
|
-
import { discoverWeightedSurface } from './source-analysis.js';
|
|
16
|
+
import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
|
|
17
17
|
import { sweepSurface } from './sweep.js';
|
|
18
18
|
const states = new WeakMap();
|
|
19
19
|
function analyzerState(engine) {
|
|
@@ -775,18 +775,92 @@ export class ProbabilityAnalyzer {
|
|
|
775
775
|
return complete;
|
|
776
776
|
}
|
|
777
777
|
async inspect(context, adapter, source, candidatePool = []) {
|
|
778
|
-
if (
|
|
778
|
+
if (source === undefined)
|
|
779
779
|
return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
|
|
780
|
-
const gameVersionVerification = await this.verifyGameVersion(context, adapter);
|
|
781
780
|
const snapshot = await this.scan(context, [source.path]);
|
|
782
|
-
const
|
|
781
|
+
const storeDiscovery = async (inventory, reason) => {
|
|
782
|
+
const availableAdapters = inventory.adapters.filter(({ candidateCount }) => candidateCount > 0);
|
|
783
|
+
const matchingIdentifier = availableAdapters.filter(({ identifierMatchCount }) => source.identifier !== undefined && identifierMatchCount > 0);
|
|
784
|
+
const matchingPool = availableAdapters.filter(({ candidatePoolMatchCount }) => candidatePool.length > 0 && candidatePoolMatchCount > 0);
|
|
785
|
+
const requested = availableAdapters.find(({ adapterId }) => adapterId === adapter);
|
|
786
|
+
const ranked = [
|
|
787
|
+
...(matchingIdentifier.length > 0
|
|
788
|
+
? matchingIdentifier
|
|
789
|
+
: matchingPool.length > 0
|
|
790
|
+
? matchingPool
|
|
791
|
+
: requested === undefined
|
|
792
|
+
? availableAdapters
|
|
793
|
+
: [requested]),
|
|
794
|
+
].sort((left, right) => right.identifierMatchCount - left.identifierMatchCount ||
|
|
795
|
+
right.candidatePoolMatchCount - left.candidatePoolMatchCount ||
|
|
796
|
+
right.candidateCount - left.candidateCount ||
|
|
797
|
+
compareCodeUnits(left.adapterId, right.adapterId));
|
|
798
|
+
const suggested = ranked[0];
|
|
799
|
+
const discovery = {
|
|
800
|
+
...(adapter === undefined ? {} : { requestedAdapter: adapter }),
|
|
801
|
+
reason,
|
|
802
|
+
sourceRevision: inventory.sourceRevision,
|
|
803
|
+
sourceHash: inventory.sourceHash,
|
|
804
|
+
availableAdapters,
|
|
805
|
+
...(suggested === undefined ? {} : { suggestedAdapter: suggested.adapterId }),
|
|
806
|
+
exampleCandidateIds: suggested?.exampleCandidateIds ?? [],
|
|
807
|
+
};
|
|
808
|
+
const report = {
|
|
809
|
+
schemaVersion: 'probability-inspection.v2',
|
|
810
|
+
workspaceId: context.workspaceId,
|
|
811
|
+
discovery,
|
|
812
|
+
};
|
|
813
|
+
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
814
|
+
const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${inventory.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
815
|
+
kind: 'probability-inspection',
|
|
816
|
+
toolVersion: PACKAGE_VERSION,
|
|
817
|
+
schemaVersion: 'probability-inspection.v2',
|
|
818
|
+
sourceHashes: { aggregate: inventory.sourceHash },
|
|
819
|
+
}, 'Weighted-source adapter inventory and candidate examples', context.signal);
|
|
820
|
+
return {
|
|
821
|
+
adapters: probabilityAdapters(),
|
|
822
|
+
discovery,
|
|
823
|
+
artifacts: [publicArtifactLink(artifact)],
|
|
824
|
+
filesScanned: inventory.filesScanned,
|
|
825
|
+
};
|
|
826
|
+
};
|
|
827
|
+
if (adapter === undefined) {
|
|
828
|
+
const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
|
|
829
|
+
return storeDiscovery(inventory, inventory.adapters.some(({ candidateCount }) => candidateCount > 0)
|
|
830
|
+
? 'source_inventory'
|
|
831
|
+
: 'no_weighted_surfaces');
|
|
832
|
+
}
|
|
833
|
+
const gameVersionVerification = await this.verifyGameVersion(context, adapter);
|
|
834
|
+
let surface;
|
|
835
|
+
try {
|
|
836
|
+
surface = discoverWeightedSurface(snapshot, adapter, source, candidatePool);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
if (!(error instanceof ServiceError) ||
|
|
840
|
+
(error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
|
|
841
|
+
error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
|
|
842
|
+
throw error;
|
|
843
|
+
const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
|
|
844
|
+
const requested = inventory.adapters.find(({ adapterId }) => adapterId === adapter);
|
|
845
|
+
const reason = error.code === 'PROBABILITY_IDENTIFIER_NOT_FOUND'
|
|
846
|
+
? 'identifier_not_found'
|
|
847
|
+
: requested !== undefined &&
|
|
848
|
+
requested.candidateCount > 0 &&
|
|
849
|
+
candidatePool.length > 0 &&
|
|
850
|
+
requested.candidatePoolMatchCount === 0
|
|
851
|
+
? 'candidate_pool_not_found'
|
|
852
|
+
: inventory.adapters.every(({ candidateCount }) => candidateCount === 0)
|
|
853
|
+
? 'no_weighted_surfaces'
|
|
854
|
+
: 'requested_adapter_empty';
|
|
855
|
+
return storeDiscovery(inventory, reason);
|
|
856
|
+
}
|
|
783
857
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
784
858
|
const inspectedCandidates = evaluateExactCandidates(surface, { id: 'inspection', state: {} }, definitions);
|
|
785
859
|
const requiredInputs = [
|
|
786
860
|
...new Set(inspectedCandidates.flatMap(({ unresolved }) => unresolved.flatMap(({ path }) => (path === undefined ? [] : [path])))),
|
|
787
861
|
].sort();
|
|
788
862
|
const report = {
|
|
789
|
-
schemaVersion: 'probability-inspection.
|
|
863
|
+
schemaVersion: 'probability-inspection.v2',
|
|
790
864
|
workspaceId: context.workspaceId,
|
|
791
865
|
surface: {
|
|
792
866
|
id: surface.id,
|
|
@@ -826,7 +900,7 @@ export class ProbabilityAnalyzer {
|
|
|
826
900
|
const artifact = await this.engine.artifacts.putChunked(workspace, `probability-inspect-${surface.sourceHash.slice(0, 12)}.json`, 'application/json', `${canonicalJson(report)}\n`, {
|
|
827
901
|
kind: 'probability-inspection',
|
|
828
902
|
toolVersion: PACKAGE_VERSION,
|
|
829
|
-
schemaVersion: 'probability-inspection.
|
|
903
|
+
schemaVersion: 'probability-inspection.v2',
|
|
830
904
|
sourceHashes: { aggregate: surface.sourceHash },
|
|
831
905
|
}, 'Discovered weighted surfaces and adapter capabilities', context.signal);
|
|
832
906
|
return {
|
|
@@ -2,6 +2,18 @@ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
|
2
2
|
import { ServiceError } from '../core/result.js';
|
|
3
3
|
import { assignments, astPathFor, childBlocks, firstScalar, nodeLocation, parseClausewitz, } from '../core/source/index.js';
|
|
4
4
|
import { probabilityAdapter } from './adapters.js';
|
|
5
|
+
const sourceBackedAdapterIds = [
|
|
6
|
+
'event_mean_time_to_happen',
|
|
7
|
+
'event_option_ai_chance',
|
|
8
|
+
'decision_ai_will_do',
|
|
9
|
+
'mission_ai_will_do',
|
|
10
|
+
'national_focus_ai_will_do',
|
|
11
|
+
'technology_ai_will_do',
|
|
12
|
+
'doctrine_ai_will_do',
|
|
13
|
+
'direct_random',
|
|
14
|
+
'random_list',
|
|
15
|
+
'ai_strategy_factor',
|
|
16
|
+
];
|
|
5
17
|
const eventKeys = new Set([
|
|
6
18
|
'country_event',
|
|
7
19
|
'news_event',
|
|
@@ -116,6 +128,9 @@ function focusCandidates(context) {
|
|
|
116
128
|
return output;
|
|
117
129
|
}
|
|
118
130
|
function decisionCandidates(context, adapterId) {
|
|
131
|
+
const sourcePath = normalized(context.file.relativePath);
|
|
132
|
+
if (context.file.absolutePath !== '<inline>' && !sourcePath.startsWith('common/decisions/'))
|
|
133
|
+
return [];
|
|
119
134
|
const output = [];
|
|
120
135
|
const ignored = new Set([
|
|
121
136
|
'allowed',
|
|
@@ -131,6 +146,10 @@ function decisionCandidates(context, adapterId) {
|
|
|
131
146
|
for (const { assignment, ancestors } of walkAssignments(context.document.root)) {
|
|
132
147
|
if (assignment.value.type !== 'block' ||
|
|
133
148
|
ancestors.length !== 1 ||
|
|
149
|
+
ancestors[0].key === 'focus_tree' ||
|
|
150
|
+
ancestors[0].key === 'continuous_focus_palette' ||
|
|
151
|
+
ancestors[0].key === 'technologies' ||
|
|
152
|
+
eventKeys.has(ancestors[0].key) ||
|
|
134
153
|
ignored.has(assignment.key.value))
|
|
135
154
|
continue;
|
|
136
155
|
const mission = firstScalar(assignment.value, 'is_mission')?.value === 'yes' ||
|
|
@@ -353,42 +372,74 @@ function samePool(candidate, selected) {
|
|
|
353
372
|
return candidate.metadata.randomListId === selected.metadata.randomListId;
|
|
354
373
|
return candidate.id === selected.id;
|
|
355
374
|
}
|
|
375
|
+
function candidatesAtSourceLine(candidates, adapterId, line) {
|
|
376
|
+
if (line === undefined)
|
|
377
|
+
return candidates;
|
|
378
|
+
let selected = candidates.filter(({ metadata, provenance: [first] }) => {
|
|
379
|
+
const location = first?.location;
|
|
380
|
+
const poolStartLine = metadata.poolStartLine;
|
|
381
|
+
const poolEndLine = metadata.poolEndLine;
|
|
382
|
+
return ((location !== undefined && line >= location.start.line && line <= location.end.line) ||
|
|
383
|
+
(typeof poolStartLine === 'number' &&
|
|
384
|
+
typeof poolEndLine === 'number' &&
|
|
385
|
+
line >= poolStartLine &&
|
|
386
|
+
line <= poolEndLine));
|
|
387
|
+
});
|
|
388
|
+
if (adapterId !== 'event_option_ai_chance' && adapterId !== 'random_list')
|
|
389
|
+
return selected;
|
|
390
|
+
const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
|
|
391
|
+
const pools = new Map();
|
|
392
|
+
for (const candidate of selected) {
|
|
393
|
+
const start = candidate.metadata.poolStartLine;
|
|
394
|
+
const end = candidate.metadata.poolEndLine;
|
|
395
|
+
const span = typeof start === 'number' && typeof end === 'number' ? end - start : Number.MAX_SAFE_INTEGER;
|
|
396
|
+
const key = candidate.metadata[poolProperty];
|
|
397
|
+
const existing = pools.get(key);
|
|
398
|
+
if (existing === undefined || span < existing.span)
|
|
399
|
+
pools.set(key, { span });
|
|
400
|
+
}
|
|
401
|
+
const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
|
|
402
|
+
if (selectedPool !== undefined)
|
|
403
|
+
selected = selected.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
|
|
404
|
+
return selected;
|
|
405
|
+
}
|
|
406
|
+
export function inventoryWeightedSurfaces(snapshot, source, candidatePool = [], adapterIds = sourceBackedAdapterIds) {
|
|
407
|
+
const contexts = sourceContexts(snapshot, source);
|
|
408
|
+
const wanted = new Set(candidatePool);
|
|
409
|
+
const adapters = adapterIds
|
|
410
|
+
.filter((adapterId) => adapterId !== 'custom_weighted_pool')
|
|
411
|
+
.map((adapterId) => {
|
|
412
|
+
const candidates = candidatesAtSourceLine(contexts.flatMap((context) => candidatesFor(context, adapterId)), adapterId, source.line).sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
413
|
+
return {
|
|
414
|
+
adapterId,
|
|
415
|
+
candidateCount: candidates.length,
|
|
416
|
+
identifierMatchCount: source.identifier === undefined
|
|
417
|
+
? candidates.length
|
|
418
|
+
: candidates.filter((candidate) => identifierMatches(candidate, source.identifier))
|
|
419
|
+
.length,
|
|
420
|
+
candidatePoolMatchCount: candidatePool.length === 0
|
|
421
|
+
? candidates.length
|
|
422
|
+
: candidates.filter(({ id }) => wanted.has(id)).length,
|
|
423
|
+
exampleCandidateIds: [...new Set(candidates.map(({ id }) => id))].slice(0, 10),
|
|
424
|
+
sourcePaths: [
|
|
425
|
+
...new Set(candidates.flatMap(({ provenance }) => provenance.map(({ path }) => path))),
|
|
426
|
+
].sort(compareCodeUnits),
|
|
427
|
+
};
|
|
428
|
+
})
|
|
429
|
+
.sort((left, right) => compareCodeUnits(left.adapterId, right.adapterId));
|
|
430
|
+
const hashes = Object.fromEntries(contexts.map(({ file }) => [file.displayPath, file.sha256]));
|
|
431
|
+
return {
|
|
432
|
+
sourceRevision: snapshot.revision,
|
|
433
|
+
sourceHash: hashCanonical(hashes),
|
|
434
|
+
filesScanned: contexts.map(({ file }) => file.displayPath),
|
|
435
|
+
adapters,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
356
438
|
export function discoverWeightedSurface(snapshot, adapterId, source, candidatePool = []) {
|
|
357
439
|
const contexts = sourceContexts(snapshot, source);
|
|
358
440
|
let candidates = contexts.flatMap((context) => candidatesFor(context, adapterId));
|
|
359
441
|
candidates.sort((left, right) => compareCodeUnits(left.id, right.id));
|
|
360
|
-
|
|
361
|
-
candidates = candidates.filter(({ metadata, provenance: [first] }) => {
|
|
362
|
-
const location = first?.location;
|
|
363
|
-
const poolStartLine = metadata.poolStartLine;
|
|
364
|
-
const poolEndLine = metadata.poolEndLine;
|
|
365
|
-
return ((location !== undefined &&
|
|
366
|
-
source.line >= location.start.line &&
|
|
367
|
-
source.line <= location.end.line) ||
|
|
368
|
-
(typeof poolStartLine === 'number' &&
|
|
369
|
-
typeof poolEndLine === 'number' &&
|
|
370
|
-
source.line >= poolStartLine &&
|
|
371
|
-
source.line <= poolEndLine));
|
|
372
|
-
});
|
|
373
|
-
if (adapterId === 'event_option_ai_chance' || adapterId === 'random_list') {
|
|
374
|
-
const poolProperty = adapterId === 'event_option_ai_chance' ? 'eventId' : 'randomListId';
|
|
375
|
-
const pools = new Map();
|
|
376
|
-
for (const candidate of candidates) {
|
|
377
|
-
const start = candidate.metadata.poolStartLine;
|
|
378
|
-
const end = candidate.metadata.poolEndLine;
|
|
379
|
-
const span = typeof start === 'number' && typeof end === 'number'
|
|
380
|
-
? end - start
|
|
381
|
-
: Number.MAX_SAFE_INTEGER;
|
|
382
|
-
const key = candidate.metadata[poolProperty];
|
|
383
|
-
const existing = pools.get(key);
|
|
384
|
-
if (existing === undefined || span < existing.span)
|
|
385
|
-
pools.set(key, { span });
|
|
386
|
-
}
|
|
387
|
-
const selectedPool = [...pools].sort(([leftKey, left], [rightKey, right]) => left.span - right.span || compareCodeUnits(String(leftKey), String(rightKey)))[0]?.[0];
|
|
388
|
-
if (selectedPool !== undefined)
|
|
389
|
-
candidates = candidates.filter((candidate) => candidate.metadata[poolProperty] === selectedPool);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
442
|
+
candidates = candidatesAtSourceLine(candidates, adapterId, source.line);
|
|
392
443
|
const localCategoricalPool = adapterId === 'event_option_ai_chance' || adapterId === 'random_list';
|
|
393
444
|
let completeLocalPoolIds;
|
|
394
445
|
if (source.identifier !== undefined) {
|
|
@@ -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 = '2.
|
|
3
|
+
export const PACKAGE_VERSION = '2.5.1';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/focus.md
CHANGED
|
@@ -44,7 +44,7 @@ If malformed or unsupported script makes a requested change ambiguous, the rewri
|
|
|
44
44
|
|
|
45
45
|
## What inspection checks
|
|
46
46
|
|
|
47
|
-
Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. Renders are offline review artifacts, not game screenshots.
|
|
47
|
+
Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. National-tree inspection also returns the exact `continuous_focus_position` coordinates and linked palette IDs in its compact result, so an agent can identify and move the continuous-focus area without opening the full artifact. Renders are offline review artifacts, not game screenshots.
|
|
48
48
|
|
|
49
49
|
`hoi4.focus_render` is the normal structural view and writes complete HTML, SVG, JSON, and source-map artifacts without decoding every icon or creating a PNG. `hoi4.focus_raster` adds decoded icons and the high-fidelity PNG. National trees with 200 or more focuses raster at half scale by default; pass `reviewScale` from `0.25` through `1` when a different PNG size is needed.
|
|
50
50
|
|
package/docs/probability.md
CHANGED
|
@@ -6,7 +6,7 @@ Use them for event timing and option weights, decision and mission scores, focus
|
|
|
6
6
|
|
|
7
7
|
| Tool | Use |
|
|
8
8
|
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
-
| `hoi4.probability_inspect` | Discover weighted blocks, adapters, candidates, provenance, capabilities, and unsupported constructs.
|
|
9
|
+
| `hoi4.probability_inspect` | Discover weighted blocks, compatible adapters, candidates, provenance, capabilities, and unsupported constructs. |
|
|
10
10
|
| `hoi4.probability_evaluate` | Evaluate eligibility, modifier traces, raw values, proven probabilities, and MTTH horizon chances across scenarios. |
|
|
11
11
|
| `hoi4.probability_sweep` | Sweep declared ranges and locate sensitivity changes, breakpoints, and rank reversals. |
|
|
12
12
|
| `hoi4.probability_simulate` | Sample declared distributions with a deterministic seed and confidence intervals. |
|
|
@@ -16,6 +16,8 @@ Use them for event timing and option weights, decision and mission scores, focus
|
|
|
16
16
|
|
|
17
17
|
All seven tools are read-only. Proposed source is parsed in memory and never written. When an installed game root is configured, evaluation fails closed unless `launcher-settings.json` identifies the supported HOI4 build and checksum. Results otherwise state that they target the adapter version without claiming local-game verification.
|
|
18
18
|
|
|
19
|
+
Pass a source without an adapter when the surface type is not yet known. Inspection returns every compatible adapter, candidate counts, and example candidate IDs. If a requested adapter, identifier, or candidate pool does not match the source, inspection returns the same discovery result with an explanation and suggested adapter instead of `PROBABILITY_SURFACE_EMPTY`; evaluation, sweeps, and simulation remain strict and still reject an empty requested surface.
|
|
20
|
+
|
|
19
21
|
## Scenarios
|
|
20
22
|
|
|
21
23
|
A scenario contains only state the caller is willing to declare. Missing values stay unresolved. Alternatives and ranges produce exact branches or bounds; probability distributions require `hoi4.probability_simulate`.
|
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": "2.
|
|
4
|
+
"version": "2.5.1",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/map-operation.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"oneOf": [
|
|
5
5
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/operation-result.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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-analysis-result.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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-compare-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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-evaluate-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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-render-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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-scenario-set.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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-sequence-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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-simulate-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/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.1/schemas/probability-sweep-input.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.klimPaskov/hoi4-agent-tools",
|
|
4
4
|
"title": "HOI4 Agent Tools",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.5.1",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/klimPaskov/hoi4-agent-tools",
|
|
9
9
|
"source": "github"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "hoi4-agent-tools",
|
|
16
|
-
"version": "2.
|
|
16
|
+
"version": "2.5.1",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|