hoi4-agent-tools 2.8.5 → 2.8.7
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 +11 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +113 -44
- package/dist/hoi4_agent_tools/gui/assets.js +0 -5
- package/dist/hoi4_agent_tools/gui/layout.js +4 -1
- package/dist/hoi4_agent_tools/gui/renderer.js +12 -1
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +402 -69
- package/dist/hoi4_agent_tools/gui/scenario.js +1 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +61 -9
- package/dist/hoi4_agent_tools/gui/studio.js +45 -4
- package/dist/hoi4_agent_tools/gui/validators.js +6 -1
- package/dist/hoi4_agent_tools/probability/service.js +3 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +4 -4
- 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
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes.
|
|
6
6
|
|
|
7
|
+
## 2.8.7 - 2026-08-28
|
|
8
|
+
|
|
9
|
+
- Isolate stale, foreign, oversized, malformed, and address-mismatched artifact manifests so one bad generated record cannot block unrelated probability, event, GUI, or other MCP evidence routes.
|
|
10
|
+
- Repair corrupted deterministic manifests from recomputed expected bytes, validate cached probability resources before reuse, and keep direct foreign-artifact access unavailable without exposing cross-workspace content.
|
|
11
|
+
|
|
12
|
+
## 2.8.6 - 2026-08-28
|
|
13
|
+
|
|
14
|
+
- Generate source-aware scripted-GUI scenarios from complete parent/child surfaces, real scripted-localisation candidates, simple threshold conditions, coherent tab states, scoped values, meter ranges, and dynamic-list rows while keeping explicit scenario values authoritative.
|
|
15
|
+
- Preserve native HOI4 bitmap-font antialiasing and shadow pixels when applying face and localisation colours, including global and per-font `textcolors` palettes.
|
|
16
|
+
- Prevent stale workspace artifacts and irrelevant language font atlases from blocking GUI calls, resolve alternate vanilla raster extensions and repeated path separators, and keep placeholder scenarios opt-in so their intentionally unresolved state does not pollute normal validation.
|
|
17
|
+
|
|
7
18
|
## 2.8.5 - 2026-08-28
|
|
8
19
|
|
|
9
20
|
- Render bitmap-font face and localisation colours through alpha masks, including black source atlases, and load exact per-font HOI4 `textcolors` palettes.
|
|
@@ -131,15 +131,50 @@ export function boundedSourceHashEvidence(sourceHashes) {
|
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
133
|
function assertManifestIntegrity(manifest, expected, workspace) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
manifest
|
|
134
|
+
assertManifestAddressIntegrity(manifest, expected);
|
|
135
|
+
if (!manifestBelongsToWorkspace(manifest, workspace)) {
|
|
136
|
+
throw new ServiceError('ARTIFACT_NOT_FOUND', 'Artifact provenance manifest is unavailable');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function manifestBelongsToWorkspace(manifest, workspace) {
|
|
140
|
+
return (manifest.workspaceIdentity === workspace.workspaceIdentity &&
|
|
141
|
+
manifest.ownerIdentity === workspace.ownerIdentity);
|
|
142
|
+
}
|
|
143
|
+
function assertManifestAddressIntegrity(manifest, expected) {
|
|
144
|
+
if (manifest.sha256 !== expected.sha256 ||
|
|
137
145
|
manifest.provenanceHash !== expected.provenanceHash ||
|
|
138
146
|
manifest.name !== expected.name ||
|
|
139
147
|
provenanceHashFor(manifest) !== expected.provenanceHash) {
|
|
140
148
|
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest does not match its immutable address');
|
|
141
149
|
}
|
|
142
150
|
}
|
|
151
|
+
function recoverableManifestError(error) {
|
|
152
|
+
return (error instanceof ServiceError &&
|
|
153
|
+
[
|
|
154
|
+
'ARTIFACT_NOT_FOUND',
|
|
155
|
+
'ARTIFACT_MANIFEST_INVALID',
|
|
156
|
+
'ARTIFACT_MANIFEST_LIMIT',
|
|
157
|
+
'ARTIFACT_MANIFEST_INTEGRITY_FAILED',
|
|
158
|
+
].includes(error.code));
|
|
159
|
+
}
|
|
160
|
+
async function enumerableArtifactManifest(workspace, directory, entryName, signal) {
|
|
161
|
+
try {
|
|
162
|
+
const manifest = await readArtifactManifest(path.join(directory, entryName), signal);
|
|
163
|
+
if (entryName !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`)
|
|
164
|
+
return undefined;
|
|
165
|
+
assertManifestAddressIntegrity(manifest, {
|
|
166
|
+
sha256: path.basename(directory),
|
|
167
|
+
provenanceHash: manifest.provenanceHash,
|
|
168
|
+
name: manifest.name,
|
|
169
|
+
});
|
|
170
|
+
return manifestBelongsToWorkspace(manifest, workspace) ? manifest : undefined;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (recoverableManifestError(error))
|
|
174
|
+
return undefined;
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
143
178
|
/** Return the only artifact fields that may cross the MCP trust boundary. */
|
|
144
179
|
export function publicArtifactLink(artifact) {
|
|
145
180
|
return {
|
|
@@ -496,6 +531,23 @@ async function existingManifestMatches(filePath, expectedBytes, signal) {
|
|
|
496
531
|
try {
|
|
497
532
|
const manifest = await readArtifactManifest(filePath, signal);
|
|
498
533
|
if (`${canonicalJson(manifest)}\n` !== expectedBytes) {
|
|
534
|
+
const expected = artifactManifestSchema.parse(JSON.parse(expectedBytes));
|
|
535
|
+
try {
|
|
536
|
+
assertManifestAddressIntegrity(manifest, {
|
|
537
|
+
sha256: expected.sha256,
|
|
538
|
+
provenanceHash: expected.provenanceHash,
|
|
539
|
+
name: expected.name,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
if (!recoverableManifestError(error))
|
|
544
|
+
throw error;
|
|
545
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
546
|
+
if (unlinkError.code !== 'ENOENT')
|
|
547
|
+
throw unlinkError;
|
|
548
|
+
});
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
499
551
|
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Immutable artifact provenance manifest contains different bytes');
|
|
500
552
|
}
|
|
501
553
|
return true;
|
|
@@ -503,11 +555,17 @@ async function existingManifestMatches(filePath, expectedBytes, signal) {
|
|
|
503
555
|
catch (error) {
|
|
504
556
|
if (error instanceof ServiceError && error.code === 'ARTIFACT_NOT_FOUND')
|
|
505
557
|
return false;
|
|
506
|
-
if (!(error instanceof ServiceError) ||
|
|
558
|
+
if (!(error instanceof ServiceError) ||
|
|
559
|
+
!['ARTIFACT_MANIFEST_INVALID', 'ARTIFACT_MANIFEST_LIMIT'].includes(error.code))
|
|
507
560
|
throw error;
|
|
508
561
|
const state = await interruptedManifestState(filePath, expectedBytes, signal);
|
|
509
|
-
if (!state.interrupted)
|
|
510
|
-
|
|
562
|
+
if (!state.interrupted) {
|
|
563
|
+
await unlink(filePath).catch((unlinkError) => {
|
|
564
|
+
if (unlinkError.code !== 'ENOENT')
|
|
565
|
+
throw unlinkError;
|
|
566
|
+
});
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
511
569
|
try {
|
|
512
570
|
await unlink(filePath);
|
|
513
571
|
}
|
|
@@ -684,19 +742,31 @@ export class ArtifactStore {
|
|
|
684
742
|
catch (error) {
|
|
685
743
|
if (error instanceof ServiceError && error.code === 'ARTIFACT_NOT_FOUND')
|
|
686
744
|
continue;
|
|
687
|
-
if (!(error
|
|
745
|
+
if (!recoverableManifestError(error))
|
|
688
746
|
throw error;
|
|
689
|
-
let interrupted;
|
|
690
747
|
try {
|
|
691
|
-
|
|
748
|
+
await unlink(candidate);
|
|
692
749
|
}
|
|
693
|
-
catch (
|
|
694
|
-
if (
|
|
695
|
-
|
|
696
|
-
continue;
|
|
697
|
-
throw interruptedError;
|
|
750
|
+
catch (unlinkError) {
|
|
751
|
+
if (unlinkError.code !== 'ENOENT')
|
|
752
|
+
throw unlinkError;
|
|
698
753
|
}
|
|
699
|
-
|
|
754
|
+
usage.entries = Math.max(0, usage.entries - 1);
|
|
755
|
+
usage.bytes = Math.max(0, usage.bytes - metadata.size);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
if (child.name !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`) {
|
|
760
|
+
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest filename does not match its contents');
|
|
761
|
+
}
|
|
762
|
+
assertManifestAddressIntegrity(manifest, {
|
|
763
|
+
sha256: path.basename(directory),
|
|
764
|
+
provenanceHash: manifest.provenanceHash,
|
|
765
|
+
name: manifest.name,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
if (!recoverableManifestError(error))
|
|
700
770
|
throw error;
|
|
701
771
|
try {
|
|
702
772
|
await unlink(candidate);
|
|
@@ -706,17 +776,9 @@ export class ArtifactStore {
|
|
|
706
776
|
throw unlinkError;
|
|
707
777
|
}
|
|
708
778
|
usage.entries = Math.max(0, usage.entries - 1);
|
|
709
|
-
usage.bytes = Math.max(0, usage.bytes -
|
|
779
|
+
usage.bytes = Math.max(0, usage.bytes - metadata.size);
|
|
710
780
|
continue;
|
|
711
781
|
}
|
|
712
|
-
if (child.name !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`) {
|
|
713
|
-
throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest filename does not match its contents');
|
|
714
|
-
}
|
|
715
|
-
assertManifestIntegrity(manifest, {
|
|
716
|
-
sha256: path.basename(directory),
|
|
717
|
-
provenanceHash: manifest.provenanceHash,
|
|
718
|
-
name: manifest.name,
|
|
719
|
-
}, workspace);
|
|
720
782
|
const targetPath = await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, path.join(directory, manifest.name)));
|
|
721
783
|
let targetBytes = 0;
|
|
722
784
|
try {
|
|
@@ -728,6 +790,8 @@ export class ArtifactStore {
|
|
|
728
790
|
}
|
|
729
791
|
retained.push({
|
|
730
792
|
manifest,
|
|
793
|
+
staleIdentity: manifest.workspaceIdentity !== workspace.workspaceIdentity ||
|
|
794
|
+
manifest.ownerIdentity !== workspace.ownerIdentity,
|
|
731
795
|
manifestPath: candidate,
|
|
732
796
|
manifestBytes: metadata.size,
|
|
733
797
|
modifiedAt: metadata.mtimeMs,
|
|
@@ -789,7 +853,8 @@ export class ArtifactStore {
|
|
|
789
853
|
}
|
|
790
854
|
};
|
|
791
855
|
await reclaimDebris(await containedGeneratedPath(workspace.artifactRoot));
|
|
792
|
-
retained.sort((left, right) =>
|
|
856
|
+
retained.sort((left, right) => Number(right.staleIdentity) - Number(left.staleIdentity) ||
|
|
857
|
+
left.modifiedAt - right.modifiedAt ||
|
|
793
858
|
compareCodeUnits(left.manifestPath, right.manifestPath));
|
|
794
859
|
const current = { ...usage };
|
|
795
860
|
for (const candidate of retained) {
|
|
@@ -822,7 +887,7 @@ export class ArtifactStore {
|
|
|
822
887
|
if (error.code !== 'ENOENT')
|
|
823
888
|
throw error;
|
|
824
889
|
}
|
|
825
|
-
const verificationPrefix = `${
|
|
890
|
+
const verificationPrefix = `${candidate.manifest.workspaceIdentity}\0${candidate.manifest.ownerIdentity}\0${candidate.targetPath}\0`;
|
|
826
891
|
for (const key of this.#verifiedContent.keys()) {
|
|
827
892
|
if (key.startsWith(verificationPrefix))
|
|
828
893
|
this.#verifiedContent.delete(key);
|
|
@@ -859,15 +924,10 @@ export class ArtifactStore {
|
|
|
859
924
|
await walk(await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, candidate)));
|
|
860
925
|
}
|
|
861
926
|
else if (entry.isFile() && entry.name.endsWith('.manifest.json')) {
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
assertManifestIntegrity(manifest, {
|
|
867
|
-
sha256: path.basename(directory),
|
|
868
|
-
provenanceHash: manifest.provenanceHash,
|
|
869
|
-
name: manifest.name,
|
|
870
|
-
}, workspace);
|
|
927
|
+
await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, candidate));
|
|
928
|
+
const manifest = await enumerableArtifactManifest(workspace, directory, entry.name, signal);
|
|
929
|
+
if (manifest === undefined)
|
|
930
|
+
continue;
|
|
871
931
|
results.push({
|
|
872
932
|
uri: this.uri(workspace.id, manifest.sha256, manifest.provenanceHash, manifest.name),
|
|
873
933
|
name: manifest.name,
|
|
@@ -900,15 +960,10 @@ export class ArtifactStore {
|
|
|
900
960
|
let hasMore = false;
|
|
901
961
|
const visitManifest = async (directory, entryName) => {
|
|
902
962
|
signal?.throwIfAborted();
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
assertManifestIntegrity(manifest, {
|
|
908
|
-
sha256: path.basename(directory),
|
|
909
|
-
provenanceHash: manifest.provenanceHash,
|
|
910
|
-
name: manifest.name,
|
|
911
|
-
}, workspace);
|
|
963
|
+
await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, path.join(directory, entryName)));
|
|
964
|
+
const manifest = await enumerableArtifactManifest(workspace, directory, entryName, signal);
|
|
965
|
+
if (manifest === undefined)
|
|
966
|
+
return;
|
|
912
967
|
const uri = this.uri(workspace.id, manifest.sha256, manifest.provenanceHash, manifest.name);
|
|
913
968
|
revision.update(`${Buffer.byteLength(uri, 'utf8')}:${uri}`);
|
|
914
969
|
total += 1;
|
|
@@ -981,6 +1036,20 @@ export class ArtifactStore {
|
|
|
981
1036
|
path: await containedGeneratedPath(workspace.artifactRoot, parsed.sha256.slice(0, 2), parsed.sha256, parsed.name),
|
|
982
1037
|
};
|
|
983
1038
|
}
|
|
1039
|
+
async available(workspace, artifacts, signal) {
|
|
1040
|
+
for (const artifact of artifacts) {
|
|
1041
|
+
signal?.throwIfAborted();
|
|
1042
|
+
try {
|
|
1043
|
+
await this.describe(workspace, artifact.uri, signal);
|
|
1044
|
+
}
|
|
1045
|
+
catch (error) {
|
|
1046
|
+
if (recoverableManifestError(error))
|
|
1047
|
+
return false;
|
|
1048
|
+
throw error;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return true;
|
|
1052
|
+
}
|
|
984
1053
|
async put(workspace, name, mimeType, content, provenance, description, signal) {
|
|
985
1054
|
return this.withAtomicWrites(workspace, [
|
|
986
1055
|
{
|
|
@@ -591,11 +591,6 @@ export class GuiAssetCatalog {
|
|
|
591
591
|
.ensureAlpha()
|
|
592
592
|
.raw()
|
|
593
593
|
.toBuffer({ resolveWithObject: true });
|
|
594
|
-
for (let index = 0; index < extracted.data.length; index += 4) {
|
|
595
|
-
extracted.data[index] = 255;
|
|
596
|
-
extracted.data[index + 1] = 255;
|
|
597
|
-
extracted.data[index + 2] = 255;
|
|
598
|
-
}
|
|
599
594
|
const png = await sharp(extracted.data, {
|
|
600
595
|
raw: { width: extracted.info.width, height: extracted.info.height, channels: 4 },
|
|
601
596
|
limitInputPixels: RENDER_MAX_DECODED_PIXELS,
|
|
@@ -750,7 +750,10 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
750
750
|
displayText = `\u00a7R${rawText}_MISSING\u00a7!`;
|
|
751
751
|
const fontName = scalarString(property(definition.attributes, 'font', 'buttonFont'));
|
|
752
752
|
const fontDefinition = fontName === undefined ? undefined : catalog.fontDefinition(fontName);
|
|
753
|
-
const visibleText = visibleHoiText(displayText,
|
|
753
|
+
const visibleText = visibleHoiText(displayText, {
|
|
754
|
+
...context.graph.textColours,
|
|
755
|
+
...fontDefinition?.textColours,
|
|
756
|
+
});
|
|
754
757
|
displayText = visibleText.text;
|
|
755
758
|
context.work.admitText(displayText, `GUI text for ${definition.name}`);
|
|
756
759
|
const resolvedFontMetrics = catalog.resolvedFontMetrics(fontName);
|
|
@@ -63,7 +63,18 @@ function sceneGlyphDefinitions(scene) {
|
|
|
63
63
|
].join('');
|
|
64
64
|
}
|
|
65
65
|
function bitmapTintMarkup(id, colour, glyphs, rect) {
|
|
66
|
-
|
|
66
|
+
const match = /^#([0-9a-f]{6})([0-9a-f]{2})?$/iu.exec(colour);
|
|
67
|
+
const rgb = match?.[1] ?? 'ffffff';
|
|
68
|
+
const alpha = match?.[2] ?? 'ff';
|
|
69
|
+
const channels = [
|
|
70
|
+
Number.parseInt(rgb.slice(0, 2), 16) / 255,
|
|
71
|
+
Number.parseInt(rgb.slice(2, 4), 16) / 255,
|
|
72
|
+
Number.parseInt(rgb.slice(4, 6), 16) / 255,
|
|
73
|
+
Number.parseInt(alpha, 16) / 255,
|
|
74
|
+
].map(finite);
|
|
75
|
+
const [red, green, blue, opacity] = channels;
|
|
76
|
+
const matrix = `${red} 0 0 0 0 0 ${green} 0 0 0 0 0 ${blue} 0 0 0 0 0 ${opacity} 0`;
|
|
77
|
+
return `<defs><filter id="${id}" filterUnits="userSpaceOnUse" ${rectAttributes(rect)} color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${matrix}"/></filter></defs><g filter="url(#${id})">${glyphs}</g>`;
|
|
67
78
|
}
|
|
68
79
|
function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale) {
|
|
69
80
|
if (glyphLine.source === 'fontkit-path')
|