hoi4-agent-tools 4.0.5 → 4.1.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 +12 -0
- package/README.md +2 -0
- package/dist/hoi4_agent_tools/core/bounded-source.js +210 -0
- package/dist/hoi4_agent_tools/core/diagnostic-rollup.js +91 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +12 -0
- package/dist/hoi4_agent_tools/core/engine.js +2 -1
- package/dist/hoi4_agent_tools/core/job-executor.js +9 -0
- package/dist/hoi4_agent_tools/core/managed-outputs.js +72 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +28 -2
- package/dist/hoi4_agent_tools/core/scanner.js +17 -0
- package/dist/hoi4_agent_tools/core/server-state.js +9 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +5 -1
- package/dist/hoi4_agent_tools/core/transaction-canonical.js +43 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js +7 -2
- package/dist/hoi4_agent_tools/core/transactions.js +355 -111
- package/dist/hoi4_agent_tools/map/derived.js +21 -6
- package/dist/hoi4_agent_tools/map/job-operations.js +9 -3
- package/dist/hoi4_agent_tools/map/operations.js +72 -21
- package/dist/hoi4_agent_tools/map/reference-labels.js +238 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +247 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +1097 -0
- package/dist/hoi4_agent_tools/map/render.js +9 -1
- package/dist/hoi4_agent_tools/map/service.js +222 -18
- package/dist/hoi4_agent_tools/map/tool-operations.js +71 -2
- package/dist/hoi4_agent_tools/map/world-context.js +148 -0
- package/dist/hoi4_agent_tools/map/world-drainage.js +273 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +55 -0
- package/dist/hoi4_agent_tools/map/world-preview-style.js +157 -0
- package/dist/hoi4_agent_tools/map/world-preview.js +230 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +524 -0
- package/dist/hoi4_agent_tools/map/world.js +374 -253
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -2
- package/dist/hoi4_agent_tools/schemas/map-requests.js +4 -0
- package/dist/hoi4_agent_tools/schemas/map.js +103 -10
- package/dist/hoi4_agent_tools/schemas/transaction.js +20 -3
- package/dist/hoi4_agent_tools/version.js +2 -2
- package/docs/images/reference-maps/political.png +0 -0
- package/docs/images/reference-maps/sketch.png +0 -0
- package/docs/map.md +45 -0
- package/docs/third-party-notices.md +6 -0
- package/npm-shrinkwrap.json +124 -2
- package/package.json +4 -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 +373 -70
- 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 +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.1.0 - 2026-10-15
|
|
6
|
+
|
|
7
|
+
- Add reference-image `create_world` requests with automatic dimensions, countries, biomes, coastline interpretation, drainage, offline English label recognition, optional aligned layers and reviewed semantic hints.
|
|
8
|
+
- Add `preview: true` to map rewrites. Previews validate and render without creating or applying a rewrite transaction; failed generation is never illustrated using the unchanged base-game map.
|
|
9
|
+
- Guard local reference inputs with canonical workspace checks, bounded decoding, fixed-memory original-source hashing and stale-source verification. OCR uses packaged local model data and bounded worker lifetimes.
|
|
10
|
+
- Generate smoother relief, connected terrain-dependent provinces and downhill rivers; use installed palette conventions, distinct country filenames and actual urban footprints.
|
|
11
|
+
- Cache map colors per province instead of hashing them for every rendered pixel. Retain deterministic pixels and artifact contents.
|
|
12
|
+
- Preserve reviewed city, state, sea and region names and report uncertain interpretation. Map generation remains an offline foundation, with live acceptance evidence reported separately.
|
|
13
|
+
- Add starter bookmarks, country histories, infantry templates, units, stockpiles and production; isolate old-world AI and startup hooks while retaining shared continent identifiers.
|
|
14
|
+
- Track generated outputs and safely remove obsolete files on regeneration; reject edited outputs and malformed inventories. Bound complete-world inline diffs so preview links remain available.
|
|
15
|
+
- Clean detached water speckles without turning lake islands into ocean pockets, preserve native province extent limits, and emit building positions without a spurious final record.
|
|
16
|
+
|
|
5
17
|
## 4.0.5 - 2026-10-14
|
|
6
18
|
|
|
7
19
|
- Keep child anchors independent of parent `origo`, so centered native windows place their controls inside the frame.
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Make focus trees, events and decisions. Edit maps and generate a new world. Prev
|
|
|
17
17
|
## What it does
|
|
18
18
|
|
|
19
19
|
Your agent can build and edit focus trees, events, decisions and maps, and generate a new world.
|
|
20
|
+
|
|
21
|
+
For a new map, give it a cleaned local reference image and preview the interpreted geography before writing. The [map workflow](docs/map.md#generate-a-world-from-one-image) creates provinces, states, terrain, cities, countries, supply and a provisional starting scenario. Reviewed names and masks refine ambiguous artwork; offline validation is separate from [native campaign acceptance](https://github.com/klimPaskov/hoi4-agent-tools/blob/v4.1.0/docs/evaluations/worldgen_acceptance_2026-10-15.md).
|
|
20
22
|
It previews focus trees, technology folders, windows and maps, finds the right rule in the game's documentation, fixes broken script and tunes AI choices.
|
|
21
23
|
You describe what you want; the agent picks the right tools.
|
|
22
24
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { ServiceError } from './result.js';
|
|
2
|
+
import { prepareSource, verifySource } from './scanner.js';
|
|
3
|
+
import { isWithin } from './workspace.js';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { lstat, mkdtemp, open, realpath, rmdir, unlink } from 'node:fs/promises';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import sharp from 'sharp';
|
|
9
|
+
import { TemporaryDirectoryLifetime, sweepTemporaryDirectories } from './temporary-directories.js';
|
|
10
|
+
export const REFERENCE_SOURCE_MAX_BYTES = 1_073_741_824;
|
|
11
|
+
export const REFERENCE_WORKING_MAX_SIDE = 3_072;
|
|
12
|
+
export const REFERENCE_WORKING_MAX_BYTES = 33_554_432;
|
|
13
|
+
function supportedRasterHeader(bytes) {
|
|
14
|
+
return ((bytes.length >= 8 &&
|
|
15
|
+
[137, 80, 78, 71, 13, 10, 26, 10].every((value, index) => bytes[index] === value)) ||
|
|
16
|
+
(bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) ||
|
|
17
|
+
(bytes.length >= 12 &&
|
|
18
|
+
[82, 73, 70, 70].every((value, index) => bytes[index] === value) &&
|
|
19
|
+
[87, 69, 66, 80].every((value, index) => bytes[index + 8] === value)));
|
|
20
|
+
}
|
|
21
|
+
function assertSnapshotIsolation(resolver, candidate, checkDescendants) {
|
|
22
|
+
for (const workspace of resolver.list())
|
|
23
|
+
for (const root of [
|
|
24
|
+
...workspace.roots,
|
|
25
|
+
{ path: workspace.artifactRoot },
|
|
26
|
+
{ path: workspace.cacheRoot },
|
|
27
|
+
])
|
|
28
|
+
if (isWithin(root.path, candidate) || (checkDescendants && isWithin(candidate, root.path)))
|
|
29
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshots must be outside workspace capability roots');
|
|
30
|
+
}
|
|
31
|
+
async function assertSnapshotDirectory(resolver, directory, parent) {
|
|
32
|
+
const canonical = path.normalize(await realpath(directory));
|
|
33
|
+
const metadata = await lstat(directory);
|
|
34
|
+
if (canonical !== path.normalize(directory) ||
|
|
35
|
+
!isWithin(parent, canonical) ||
|
|
36
|
+
canonical === parent ||
|
|
37
|
+
metadata.isSymbolicLink() ||
|
|
38
|
+
!metadata.isDirectory())
|
|
39
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshot directory is not a private canonical directory');
|
|
40
|
+
if (process.platform !== 'win32' &&
|
|
41
|
+
((metadata.mode & 0o077) !== 0 ||
|
|
42
|
+
(process.getuid !== undefined && metadata.uid !== process.getuid())))
|
|
43
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshot directory permissions are not private');
|
|
44
|
+
assertSnapshotIsolation(resolver, canonical, true);
|
|
45
|
+
}
|
|
46
|
+
async function createPrivateSnapshot(resolver) {
|
|
47
|
+
const parent = path.normalize(await realpath(resolver.serverState()?.root ?? tmpdir()));
|
|
48
|
+
assertSnapshotIsolation(resolver, parent, false);
|
|
49
|
+
const parentMetadata = await lstat(parent);
|
|
50
|
+
if (!parentMetadata.isDirectory() ||
|
|
51
|
+
parentMetadata.isSymbolicLink() ||
|
|
52
|
+
(process.platform !== 'win32' &&
|
|
53
|
+
(parentMetadata.mode & 0o022) !== 0 &&
|
|
54
|
+
(parentMetadata.mode & 0o1000) === 0))
|
|
55
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_UNSAFE', 'Reference snapshot parent is not protected against directory replacement');
|
|
56
|
+
// Hidden directories are excluded by mod-root discovery even with a temporary-root fallback.
|
|
57
|
+
await sweepTemporaryDirectories(parent, {
|
|
58
|
+
referenceSnapshotsOnly: true,
|
|
59
|
+
staleMs: 0,
|
|
60
|
+
maxBytes: REFERENCE_SOURCE_MAX_BYTES,
|
|
61
|
+
protectedRoots: resolver
|
|
62
|
+
.list()
|
|
63
|
+
.flatMap((workspace) => [
|
|
64
|
+
...workspace.roots.map(({ path }) => path),
|
|
65
|
+
workspace.artifactRoot,
|
|
66
|
+
workspace.cacheRoot,
|
|
67
|
+
]),
|
|
68
|
+
});
|
|
69
|
+
const directory = await mkdtemp(path.join(parent, '.reference-snapshot-'));
|
|
70
|
+
const lifetime = new TemporaryDirectoryLifetime(REFERENCE_SOURCE_MAX_BYTES);
|
|
71
|
+
try {
|
|
72
|
+
await assertSnapshotDirectory(resolver, directory, parent);
|
|
73
|
+
await lifetime.own(directory);
|
|
74
|
+
const filePath = path.join(directory, 'source.raster');
|
|
75
|
+
const handle = await open(filePath, 'wx', 0o600);
|
|
76
|
+
try {
|
|
77
|
+
await assertSnapshotDirectory(resolver, directory, parent);
|
|
78
|
+
return { directory, filePath, handle, closed: false, lifetime };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
await handle.close();
|
|
82
|
+
await unlink(filePath);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
await lifetime.dispose();
|
|
88
|
+
if (await lstat(directory).catch(() => undefined))
|
|
89
|
+
await rmdir(directory);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async function removePrivateSnapshot(snapshot) {
|
|
94
|
+
try {
|
|
95
|
+
let closeFailed = false;
|
|
96
|
+
try {
|
|
97
|
+
if (!snapshot.closed)
|
|
98
|
+
await snapshot.handle.close();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
closeFailed = true;
|
|
102
|
+
}
|
|
103
|
+
await snapshot.lifetime.dispose();
|
|
104
|
+
if (closeFailed || (await lstat(snapshot.directory).catch(() => undefined)))
|
|
105
|
+
throw new Error('Private snapshot ownership changed before cleanup');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_CLEANUP_FAILED', 'Private reference snapshot cleanup failed');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Decode a private snapshot copied and hashed from the same verified FD, using fixed memory. */
|
|
112
|
+
export async function readReferenceImageSource(resolver, workspaceId, relativePath, principal, signal) {
|
|
113
|
+
if (!/\.(?:png|jpe?g|webp)$/iu.test(relativePath) || relativePath.includes(':'))
|
|
114
|
+
throw new ServiceError('REFERENCE_IMAGE_PATH_INVALID', 'Reference must name a local raster image');
|
|
115
|
+
const resolved = await resolver.resolvePath(workspaceId, relativePath, 'read', ['mod'], principal);
|
|
116
|
+
const source = await prepareSource(resolved.root, relativePath, signal);
|
|
117
|
+
let snapshot;
|
|
118
|
+
try {
|
|
119
|
+
if (!source.metadata.isFile())
|
|
120
|
+
throw new ServiceError('SOURCE_NOT_REGULAR_FILE', 'Reference source must be a regular file');
|
|
121
|
+
if (source.metadata.size < 1 || source.metadata.size > REFERENCE_SOURCE_MAX_BYTES)
|
|
122
|
+
throw new ServiceError('SOURCE_BYTE_LIMIT', 'Reference source exceeds its byte limit', {
|
|
123
|
+
limit: REFERENCE_SOURCE_MAX_BYTES,
|
|
124
|
+
});
|
|
125
|
+
signal?.throwIfAborted();
|
|
126
|
+
snapshot = await createPrivateSnapshot(resolver);
|
|
127
|
+
const hash = createHash('sha256');
|
|
128
|
+
const chunk = Buffer.alloc(Math.min(1_048_576, source.metadata.size));
|
|
129
|
+
let offset = 0;
|
|
130
|
+
while (offset < source.metadata.size) {
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
const read = await source.handle.read(chunk, 0, Math.min(chunk.length, source.metadata.size - offset), offset);
|
|
133
|
+
if (read.bytesRead === 0)
|
|
134
|
+
throw new ServiceError('SCAN_SOURCE_CHANGED', 'Reference source changed while copying its snapshot');
|
|
135
|
+
if (offset === 0 && !supportedRasterHeader(chunk.subarray(0, read.bytesRead)))
|
|
136
|
+
throw new ServiceError('REFERENCE_IMAGE_DECODE_FAILED', 'Reference source is not a supported raster image');
|
|
137
|
+
hash.update(chunk.subarray(0, read.bytesRead));
|
|
138
|
+
let written = 0;
|
|
139
|
+
while (written < read.bytesRead) {
|
|
140
|
+
const result = await snapshot.handle.write(chunk, written, read.bytesRead - written, offset + written);
|
|
141
|
+
if (result.bytesWritten === 0)
|
|
142
|
+
throw new ServiceError('REFERENCE_SNAPSHOT_WRITE_FAILED', 'Reference snapshot could not be completed');
|
|
143
|
+
written += result.bytesWritten;
|
|
144
|
+
}
|
|
145
|
+
offset += read.bytesRead;
|
|
146
|
+
}
|
|
147
|
+
const originalHash = hash.digest('hex');
|
|
148
|
+
await snapshot.handle.close();
|
|
149
|
+
snapshot.closed = true;
|
|
150
|
+
await verifySource(resolved.root, source);
|
|
151
|
+
signal?.throwIfAborted();
|
|
152
|
+
sharp.cache({ memory: 32, files: 0, items: 32 });
|
|
153
|
+
const input = sharp(snapshot.filePath, {
|
|
154
|
+
sequentialRead: true,
|
|
155
|
+
limitInputPixels: 300_000_000,
|
|
156
|
+
failOn: 'warning',
|
|
157
|
+
});
|
|
158
|
+
const metadata = await input.metadata();
|
|
159
|
+
if (!metadata.width ||
|
|
160
|
+
!metadata.height ||
|
|
161
|
+
(metadata.pages ?? 1) > 1 ||
|
|
162
|
+
!['png', 'jpeg', 'webp'].includes(metadata.format))
|
|
163
|
+
throw new ServiceError('REFERENCE_IMAGE_UNSUPPORTED', 'Reference must be a single-frame supported raster image');
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
const decoded = await input
|
|
166
|
+
.resize({
|
|
167
|
+
width: REFERENCE_WORKING_MAX_SIDE,
|
|
168
|
+
height: REFERENCE_WORKING_MAX_SIDE,
|
|
169
|
+
fit: 'inside',
|
|
170
|
+
withoutEnlargement: true,
|
|
171
|
+
})
|
|
172
|
+
.png()
|
|
173
|
+
.toBuffer({ resolveWithObject: true });
|
|
174
|
+
await verifySource(resolved.root, source);
|
|
175
|
+
signal?.throwIfAborted();
|
|
176
|
+
if (decoded.data.length > REFERENCE_WORKING_MAX_BYTES)
|
|
177
|
+
throw new ServiceError('REFERENCE_WORKING_BYTE_LIMIT', 'Reference working image exceeds its byte limit');
|
|
178
|
+
return {
|
|
179
|
+
source: {
|
|
180
|
+
absolutePath: source.canonicalPath,
|
|
181
|
+
displayPath: `mod:${relativePath.replaceAll('\\', '/')}`,
|
|
182
|
+
relativePath: relativePath.replaceAll('\\', '/'),
|
|
183
|
+
rootKind: resolved.root.kind,
|
|
184
|
+
loadOrder: resolved.root.loadOrder,
|
|
185
|
+
size: source.metadata.size,
|
|
186
|
+
modifiedMs: source.metadata.mtimeMs,
|
|
187
|
+
sha256: originalHash,
|
|
188
|
+
},
|
|
189
|
+
bytes: decoded.data,
|
|
190
|
+
originalWidth: metadata.width,
|
|
191
|
+
originalHeight: metadata.height,
|
|
192
|
+
workingWidth: decoded.info.width,
|
|
193
|
+
workingHeight: decoded.info.height,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof ServiceError || signal?.aborted)
|
|
198
|
+
throw error;
|
|
199
|
+
throw new ServiceError('REFERENCE_IMAGE_DECODE_FAILED', 'Reference image could not be safely decoded');
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
try {
|
|
203
|
+
if (snapshot !== undefined)
|
|
204
|
+
await removePrivateSnapshot(snapshot);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await source.handle.close();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { canonicalJson, compareCodeUnits } from './canonical.js';
|
|
2
|
+
import { sortDiagnostics } from './diagnostics.js';
|
|
3
|
+
const SAMPLE_LIMIT = 8;
|
|
4
|
+
/** Count source occurrences represented by an original diagnostic or a soft rollup. */
|
|
5
|
+
export function diagnosticOccurrenceCount(diagnostic) {
|
|
6
|
+
const value = diagnostic.details?.occurrenceCount;
|
|
7
|
+
return diagnostic.details?.rollup === 'repeated-soft-diagnostics.v1' &&
|
|
8
|
+
typeof value === 'number' &&
|
|
9
|
+
Number.isSafeInteger(value) &&
|
|
10
|
+
value > 0
|
|
11
|
+
? value
|
|
12
|
+
: 1;
|
|
13
|
+
}
|
|
14
|
+
/** Include omission evidence emitted by bounded validators without counting their marker twice. */
|
|
15
|
+
export function diagnosticSeverityCounts(diagnostics) {
|
|
16
|
+
const counts = { info: 0, warning: 0, error: 0, blocker: 0 };
|
|
17
|
+
for (const diagnostic of diagnostics) {
|
|
18
|
+
if (diagnostic.code !== 'MAP_DIAGNOSTICS_TRUNCATED') {
|
|
19
|
+
counts[diagnostic.severity] += diagnosticOccurrenceCount(diagnostic);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const omissions = diagnostic.details?.omittedBySeverity;
|
|
23
|
+
if (typeof omissions !== 'object' || omissions === null)
|
|
24
|
+
continue;
|
|
25
|
+
for (const severity of ['info', 'warning', 'error', 'blocker']) {
|
|
26
|
+
const value = omissions[severity];
|
|
27
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
|
|
28
|
+
counts[severity] += value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return counts;
|
|
32
|
+
}
|
|
33
|
+
/** Keep every hard diagnostic; summarize repeated informational and warning codes only. */
|
|
34
|
+
export function rollupRepeatedSoftDiagnostics(diagnostics, evidence) {
|
|
35
|
+
const hard = [];
|
|
36
|
+
const groups = new Map();
|
|
37
|
+
const ordered = diagnostics
|
|
38
|
+
.map((diagnostic) => ({ diagnostic, key: canonicalJson(diagnostic) }))
|
|
39
|
+
.sort((left, right) => compareCodeUnits(left.key, right.key))
|
|
40
|
+
.map(({ diagnostic }) => diagnostic);
|
|
41
|
+
for (const diagnostic of ordered) {
|
|
42
|
+
if (diagnostic.severity === 'error' ||
|
|
43
|
+
diagnostic.severity === 'blocker' ||
|
|
44
|
+
diagnostic.code === 'MAP_DIAGNOSTICS_TRUNCATED') {
|
|
45
|
+
hard.push(diagnostic);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const key = `${diagnostic.severity}:${diagnostic.code}`;
|
|
49
|
+
const group = groups.get(key) ?? [];
|
|
50
|
+
group.push(diagnostic);
|
|
51
|
+
groups.set(key, group);
|
|
52
|
+
}
|
|
53
|
+
const soft = [...groups.values()].map((group) => {
|
|
54
|
+
if (group.length === 1)
|
|
55
|
+
return group[0];
|
|
56
|
+
const first = group[0];
|
|
57
|
+
const occurrences = group.reduce((total, diagnostic) => total + diagnosticOccurrenceCount(diagnostic), 0);
|
|
58
|
+
const locations = new Map();
|
|
59
|
+
for (const diagnostic of group)
|
|
60
|
+
for (const location of [diagnostic.location, ...(diagnostic.related ?? [])])
|
|
61
|
+
if (location !== undefined)
|
|
62
|
+
locations.set(canonicalJson(location), location);
|
|
63
|
+
const samples = [...locations]
|
|
64
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
65
|
+
.slice(0, SAMPLE_LIMIT)
|
|
66
|
+
.map(([, location]) => location);
|
|
67
|
+
const operationIds = [
|
|
68
|
+
...new Set(group.flatMap(({ operationId }) => (operationId === undefined ? [] : [operationId]))),
|
|
69
|
+
].sort(compareCodeUnits);
|
|
70
|
+
return {
|
|
71
|
+
code: first.code,
|
|
72
|
+
severity: first.severity,
|
|
73
|
+
category: first.category,
|
|
74
|
+
message: `${occurrences} occurrences of ${first.code}; ${first.message}`.slice(0, 4096),
|
|
75
|
+
...(samples[0] === undefined ? {} : { location: samples[0] }),
|
|
76
|
+
...(samples.length < 2 ? {} : { related: samples.slice(1) }),
|
|
77
|
+
...(operationIds.length === 1 ? { operationId: operationIds[0] } : {}),
|
|
78
|
+
details: {
|
|
79
|
+
rollup: 'repeated-soft-diagnostics.v1',
|
|
80
|
+
occurrenceCount: occurrences,
|
|
81
|
+
retainedSamples: Math.min(SAMPLE_LIMIT, group.length),
|
|
82
|
+
samples: group.slice(0, SAMPLE_LIMIT),
|
|
83
|
+
operationCount: operationIds.length,
|
|
84
|
+
operationIds: operationIds.slice(0, SAMPLE_LIMIT),
|
|
85
|
+
locationCount: locations.size,
|
|
86
|
+
...(evidence === undefined ? {} : { completeDiagnosticEvidence: evidence }),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
return sortDiagnostics([...hard, ...soft]);
|
|
91
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { boundedSourceHashEvidence, publicArtifactLink } from './artifacts.js';
|
|
2
2
|
import { canonicalJson } from './canonical.js';
|
|
3
3
|
import { exactChangedFilePatterns, focusDomainScanPatterns, mapChangeScanPatterns, } from './domain-scan-patterns.js';
|
|
4
|
+
import { rollupRepeatedSoftDiagnostics } from './diagnostic-rollup.js';
|
|
4
5
|
import { parseClausewitz } from './source/index.js';
|
|
5
6
|
import { TRANSACTION_MAX_DIAGNOSTICS } from './transaction-limits.js';
|
|
6
7
|
import { importContinuousFocusPalettes, importFocusTrees, layoutFocusTreeAsync, lintContinuousFocusPalette, lintFocusTree, } from '../focus/index.js';
|
|
@@ -158,6 +159,17 @@ export async function postValidateTransaction(engine, transaction, principal, si
|
|
|
158
159
|
.slice(0, retainedLimit - retainedHard.length);
|
|
159
160
|
const omittedHard = hard.length - retainedHard.length;
|
|
160
161
|
const artifactLink = publicArtifactLink(artifact);
|
|
162
|
+
if (transaction.operationKind === 'agent-nudger-map-changes') {
|
|
163
|
+
const rolled = rollupRepeatedSoftDiagnostics(diagnostics, artifactLink);
|
|
164
|
+
if (rolled.length <= availableDiagnostics) {
|
|
165
|
+
checks.push({
|
|
166
|
+
id: 'post-write-diagnostics-artifact',
|
|
167
|
+
passed: true,
|
|
168
|
+
message: `Complete bounded post-write diagnostics are stored at ${artifactLink.uri}`,
|
|
169
|
+
});
|
|
170
|
+
return { diagnostics: rolled, checks, artifacts: [artifactLink] };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
161
173
|
checks.push({
|
|
162
174
|
id: 'post-write-diagnostics-artifact',
|
|
163
175
|
passed: omittedHard === 0,
|
|
@@ -10,6 +10,7 @@ import { RequestScheduler } from './request-scheduler.js';
|
|
|
10
10
|
import { SharedRequestCapacity } from './shared-request-capacity.js';
|
|
11
11
|
import { JobStore } from './job-store.js';
|
|
12
12
|
import { ServiceError } from './result.js';
|
|
13
|
+
import { TRANSACTION_MAX_JOURNAL_BYTES } from './transaction-limits.js';
|
|
13
14
|
/**
|
|
14
15
|
* Completed scans kept per workspace. One tool call often scans several pattern sets (a GUI
|
|
15
16
|
* preview scans layouts, linked definitions and localisation), so a single slot made every
|
|
@@ -17,7 +18,7 @@ import { ServiceError } from './result.js';
|
|
|
17
18
|
*/
|
|
18
19
|
const SCAN_CACHE_ENTRIES_PER_WORKSPACE = 4;
|
|
19
20
|
const RECOVERY_TTL_SECONDS = 3_600;
|
|
20
|
-
const RECOVERY_MAX_BYTES =
|
|
21
|
+
const RECOVERY_MAX_BYTES = TRANSACTION_MAX_JOURNAL_BYTES;
|
|
21
22
|
const RECOVERY_MAX_RECORDS = 128;
|
|
22
23
|
function abortReason(signal) {
|
|
23
24
|
return signal.reason instanceof Error
|
|
@@ -34,6 +34,8 @@ export class JobOperations {
|
|
|
34
34
|
mutation: true,
|
|
35
35
|
prepare: async (input, context) => {
|
|
36
36
|
const prepared = await prepare(schema.parse(input), context);
|
|
37
|
+
if ('readOnlyPreview' in prepared)
|
|
38
|
+
return { readOnlyPreview: jsonObject.parse(prepared.readOnlyPreview) };
|
|
37
39
|
if (!('transaction' in prepared))
|
|
38
40
|
return { transaction: prepared };
|
|
39
41
|
return {
|
|
@@ -161,6 +163,13 @@ export class JobExecutor {
|
|
|
161
163
|
if (claimed.record.transaction !== undefined)
|
|
162
164
|
return writes.reconcile(workspaceId, id, owner.token, principal, undefined, true, (execution, reconciled, recipe) => operation.complete(initial.request.arguments, execution, reconciled, recipe, context));
|
|
163
165
|
const prepared = await operation.prepare(initial.request.arguments, context);
|
|
166
|
+
if ('readOnlyPreview' in prepared) {
|
|
167
|
+
controller.signal.throwIfAborted();
|
|
168
|
+
return this.jobs.store.updateOwned(scope, id, owner.token, {
|
|
169
|
+
status: 'completed',
|
|
170
|
+
result: prepared.readOnlyPreview,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
164
173
|
return writes.execute(workspaceId, id, owner.token, prepared.transaction, principal, controller.signal, prepared.recipe, (execution, reconciled, recipe) => operation.complete(initial.request.arguments, execution, reconciled, recipe, context));
|
|
165
174
|
}
|
|
166
175
|
const result = jsonObject.parse(await withAnalysisCheckpoints(jobAnalysisCheckpoints(context), () => operation.run(initial.request.arguments, context)));
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { canonicalJson, compareCodeUnits, sha256Bytes } from './canonical.js';
|
|
3
|
+
import { ServiceError } from './result.js';
|
|
4
|
+
const manifestSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
version: z.literal(1),
|
|
7
|
+
files: z
|
|
8
|
+
.array(z
|
|
9
|
+
.object({
|
|
10
|
+
relativePath: z.string().min(1).max(512),
|
|
11
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
12
|
+
})
|
|
13
|
+
.strict())
|
|
14
|
+
.max(5000),
|
|
15
|
+
})
|
|
16
|
+
.strict();
|
|
17
|
+
/** Pure planning only: the shared transaction engine performs every replacement and deletion. */
|
|
18
|
+
export function reconcileManagedOutputs(input) {
|
|
19
|
+
const sources = new Map(input.sources
|
|
20
|
+
.filter((f) => f.rootKind === 'mod')
|
|
21
|
+
.map((f) => [f.relativePath.replaceAll('\\', '/').toLowerCase(), f]));
|
|
22
|
+
const manifest = sources.get(input.manifestPath.toLowerCase());
|
|
23
|
+
const previous = manifest === undefined
|
|
24
|
+
? []
|
|
25
|
+
: (() => {
|
|
26
|
+
if (manifest.bytes.length > 2_000_000)
|
|
27
|
+
throw new ServiceError('MANAGED_OUTPUT_MANIFEST_INVALID', 'Generated-output inventory exceeds its byte limit');
|
|
28
|
+
try {
|
|
29
|
+
return manifestSchema.parse(JSON.parse(manifest.bytes.toString('utf8'))).files;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
throw new ServiceError('MANAGED_OUTPUT_MANIFEST_INVALID', 'Generated-output inventory is invalid');
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const file of previous) {
|
|
37
|
+
const key = file.relativePath.toLowerCase();
|
|
38
|
+
if (file.relativePath.includes('\\') ||
|
|
39
|
+
file.relativePath.split('/').some((s) => !s || s === '.' || s === '..') ||
|
|
40
|
+
/^[a-z]:/iu.test(file.relativePath) ||
|
|
41
|
+
!input.permitsPath(file.relativePath) ||
|
|
42
|
+
seen.has(key))
|
|
43
|
+
throw new ServiceError('MANAGED_OUTPUT_MANIFEST_INVALID', 'Generated-output inventory contains an unsupported or duplicate path');
|
|
44
|
+
seen.add(key);
|
|
45
|
+
const current = sources.get(key);
|
|
46
|
+
if (current !== undefined && sha256Bytes(current.bytes) !== file.sha256)
|
|
47
|
+
throw new ServiceError('MANAGED_OUTPUT_EDITED', 'A generated file has been edited; regeneration would discard those changes', { relativePath: file.relativePath });
|
|
48
|
+
}
|
|
49
|
+
const outputs = input.proposed.filter((f) => f.content !== null && input.permitsPath(f.relativePath));
|
|
50
|
+
const next = new Set(outputs.map((f) => f.relativePath.toLowerCase()));
|
|
51
|
+
const deletions = previous
|
|
52
|
+
.filter((f) => !next.has(f.relativePath.toLowerCase()) && sources.has(f.relativePath.toLowerCase()))
|
|
53
|
+
.map((f) => ({
|
|
54
|
+
relativePath: f.relativePath,
|
|
55
|
+
content: null,
|
|
56
|
+
operationIds: [input.operationId],
|
|
57
|
+
}));
|
|
58
|
+
return [
|
|
59
|
+
...deletions,
|
|
60
|
+
{
|
|
61
|
+
relativePath: input.manifestPath,
|
|
62
|
+
content: Buffer.from(canonicalJson({
|
|
63
|
+
version: 1,
|
|
64
|
+
files: outputs
|
|
65
|
+
.map((f) => ({ relativePath: f.relativePath, sha256: sha256Bytes(f.content) }))
|
|
66
|
+
.sort((a, b) => compareCodeUnits(a.relativePath, b.relativePath)),
|
|
67
|
+
}) + '\n'),
|
|
68
|
+
operationIds: [input.operationId],
|
|
69
|
+
mediaType: 'application/json',
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
@@ -500,10 +500,36 @@ export function errorResult(error, workspaceId = '', context = {}) {
|
|
|
500
500
|
? `REWRITE_${serviceError.code.slice('TRANSACTION_'.length)}`
|
|
501
501
|
: serviceError.code;
|
|
502
502
|
const publicMessage = internalWriteError
|
|
503
|
-
?
|
|
503
|
+
? serviceError.code === 'TRANSACTION_STRUCTURE_LIMIT'
|
|
504
|
+
? 'Rewrite exceeds the supported file, operation, validation, or evidence limits'
|
|
505
|
+
: serviceError.code === 'TRANSACTION_JOURNAL_LIMIT'
|
|
506
|
+
? 'The requested rewrite exceeds the bounded rollback storage allowance'
|
|
507
|
+
: 'Rewrite could not be completed'
|
|
504
508
|
: serviceError.message;
|
|
505
509
|
const publicDetails = internalWriteError
|
|
506
|
-
?
|
|
510
|
+
? ['TRANSACTION_STRUCTURE_LIMIT', 'TRANSACTION_JOURNAL_LIMIT'].includes(serviceError.code)
|
|
511
|
+
? Object.fromEntries(Object.entries(serviceError.details).filter(([key, value]) => [
|
|
512
|
+
'files',
|
|
513
|
+
'filesLimit',
|
|
514
|
+
'operations',
|
|
515
|
+
'operationsLimit',
|
|
516
|
+
'readDependencies',
|
|
517
|
+
'readDependenciesLimit',
|
|
518
|
+
'artifacts',
|
|
519
|
+
'artifactsLimit',
|
|
520
|
+
'diagnostics',
|
|
521
|
+
'diagnosticsLimit',
|
|
522
|
+
'checks',
|
|
523
|
+
'checksLimit',
|
|
524
|
+
'requestedWorkBytes',
|
|
525
|
+
'plannedJournalBytes',
|
|
526
|
+
'retainedBytes',
|
|
527
|
+
'maximumBytes',
|
|
528
|
+
].includes(key) &&
|
|
529
|
+
typeof value === 'number' &&
|
|
530
|
+
Number.isSafeInteger(value) &&
|
|
531
|
+
value >= 0))
|
|
532
|
+
: {}
|
|
507
533
|
: Object.fromEntries(Object.entries(serviceError.details).filter(([key]) => ![
|
|
508
534
|
'cause',
|
|
509
535
|
'stack',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { open, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { iterateFiles } from './file-glob.js';
|
|
4
5
|
import { compareCodeUnits, sha256Bytes } from './canonical.js';
|
|
@@ -7,6 +8,22 @@ import { ServiceError } from './result.js';
|
|
|
7
8
|
import { isWithin } from './workspace.js';
|
|
8
9
|
const SCAN_IO_CONCURRENCY = 8;
|
|
9
10
|
const SCAN_READ_BATCH_BYTES = 16_777_216;
|
|
11
|
+
/** Hash an open source using fixed memory; callers verify its canonical identity afterwards. */
|
|
12
|
+
export async function hashSourceBytes(source, signal) {
|
|
13
|
+
const hash = createHash('sha256');
|
|
14
|
+
const chunk = Buffer.alloc(Math.max(1, Math.min(1_048_576, source.metadata.size)));
|
|
15
|
+
let offset = 0;
|
|
16
|
+
while (offset < source.metadata.size) {
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
const read = await source.handle.read(chunk, 0, Math.min(chunk.length, source.metadata.size - offset), offset);
|
|
19
|
+
if (!read.bytesRead)
|
|
20
|
+
throw new ServiceError('SCAN_SOURCE_CHANGED', 'Source changed while being hashed');
|
|
21
|
+
hash.update(chunk.subarray(0, read.bytesRead));
|
|
22
|
+
offset += read.bytesRead;
|
|
23
|
+
}
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
return hash.digest('hex');
|
|
26
|
+
}
|
|
10
27
|
export async function verifySource(root, source) {
|
|
11
28
|
const resolved = await realpath(source.absolutePath);
|
|
12
29
|
if (!isWithin(root.path, resolved))
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { z } from 'zod/v4';
|
|
5
5
|
import { compareCodeUnits, canonicalJson, secureId } from './canonical.js';
|
|
6
6
|
import { ServiceError } from './result.js';
|
|
7
|
+
import { TRANSACTION_MAX_MANIFEST_BYTES } from './transaction-limits.js';
|
|
7
8
|
const journalKeyFile = 'journal-hmac.key';
|
|
8
9
|
const journalKeyBytes = 32;
|
|
9
10
|
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
@@ -157,7 +158,14 @@ export class ServerState {
|
|
|
157
158
|
return new ServerState(canonicalRoot, Buffer.from(key));
|
|
158
159
|
}
|
|
159
160
|
authenticateJournal(payload) {
|
|
160
|
-
return
|
|
161
|
+
return this.authenticateCanonicalJournal(canonicalJson(payload));
|
|
162
|
+
}
|
|
163
|
+
/** Internal canonical-byte path; the signature is identical to authenticateJournal. */
|
|
164
|
+
authenticateCanonicalJournal(payload) {
|
|
165
|
+
if (Buffer.byteLength(payload, 'utf8') > TRANSACTION_MAX_MANIFEST_BYTES) {
|
|
166
|
+
throw stateError('TRANSACTION_MANIFEST_LIMIT', 'Transaction manifest exceeds its fixed byte limit');
|
|
167
|
+
}
|
|
168
|
+
return createHmac('sha256', this.journalKey).update(payload).digest('hex');
|
|
161
169
|
}
|
|
162
170
|
verifyJournal(payload, authenticationTag) {
|
|
163
171
|
if (!/^[a-f0-9]{64}$/u.test(authenticationTag))
|
|
@@ -10,6 +10,7 @@ export const TEMPORARY_MAX_BYTES = 256 * 1024 * 1024;
|
|
|
10
10
|
export const TEMPORARY_STALE_MS = 3 * 60 * 60 * 1000;
|
|
11
11
|
// Dated event evaluation directories contain retained benchmark evidence, not scratch files.
|
|
12
12
|
const scratchName = /^hoi4-(?:reference-source|event-eval)-[A-Za-z0-9]{6}$/u;
|
|
13
|
+
const referenceSnapshotName = /^\.reference-snapshot-[A-Za-z0-9]{6}$/u;
|
|
13
14
|
const ownerSchema = z
|
|
14
15
|
.object({
|
|
15
16
|
version: z.literal(1),
|
|
@@ -100,7 +101,8 @@ export async function sweepTemporaryDirectories(parent, options = {}) {
|
|
|
100
101
|
}
|
|
101
102
|
}))).flat();
|
|
102
103
|
const candidates = [];
|
|
103
|
-
|
|
104
|
+
const names = options.referenceSnapshotsOnly ? referenceSnapshotName : scratchName;
|
|
105
|
+
for (const name of (await readdir(root)).filter((name) => names.test(name)).sort()) {
|
|
104
106
|
const directory = path.join(root, name);
|
|
105
107
|
if (protectedRoots.some((protectedRoot) => overlaps(directory, protectedRoot)))
|
|
106
108
|
continue;
|
|
@@ -112,6 +114,8 @@ export async function sweepTemporaryDirectories(parent, options = {}) {
|
|
|
112
114
|
(await realpath(directory)) !== directory)
|
|
113
115
|
continue;
|
|
114
116
|
const record = await owner(directory);
|
|
117
|
+
if (options.referenceSnapshotsOnly && record == null)
|
|
118
|
+
continue;
|
|
115
119
|
// An interrupted sweep must not leave a permanent lock. Malformed or live locks
|
|
116
120
|
// stay protected; only a confirmed dead local owner can relinquish its lock.
|
|
117
121
|
const previousSweep = await owner(directory, SWEEP_LOCK);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { canonicalJson, compareCodeUnits } from './canonical.js';
|
|
2
|
+
import { ServiceError } from './result.js';
|
|
3
|
+
import { TRANSACTION_MAX_MANIFEST_BYTES } from './transaction-limits.js';
|
|
4
|
+
/** A request-local snapshot of canonical immutable fields, without shared writable references. */
|
|
5
|
+
export class TransactionCanonicalSnapshot {
|
|
6
|
+
immutable = new Map();
|
|
7
|
+
mutable;
|
|
8
|
+
constructor(source, mutableKeys) {
|
|
9
|
+
this.mutable = new Set(mutableKeys);
|
|
10
|
+
let bytes = 2;
|
|
11
|
+
for (const [key, value] of Object.entries(source)) {
|
|
12
|
+
if (this.mutable.has(key) || value === undefined)
|
|
13
|
+
continue;
|
|
14
|
+
const encoded = canonicalJson(value);
|
|
15
|
+
bytes += Buffer.byteLength(JSON.stringify(key)) + Buffer.byteLength(encoded) + 2;
|
|
16
|
+
this.assertBound(bytes);
|
|
17
|
+
this.immutable.set(key, encoded);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
serialize(source, excludedKeys = []) {
|
|
21
|
+
const mutable = source;
|
|
22
|
+
const excluded = new Set(excludedKeys);
|
|
23
|
+
const keys = [...new Set([...this.immutable.keys(), ...this.mutable])]
|
|
24
|
+
.filter((key) => !excluded.has(key) && (this.immutable.has(key) || mutable[key] !== undefined))
|
|
25
|
+
.sort(compareCodeUnits);
|
|
26
|
+
// JSON objects emit integer keys before other keys, including after canonical insertion.
|
|
27
|
+
const orderedKeys = Object.keys(Object.fromEntries(keys.map((key) => [key, null])));
|
|
28
|
+
let bytes = 2;
|
|
29
|
+
const entries = orderedKeys.map((key) => {
|
|
30
|
+
const value = this.immutable.get(key) ?? canonicalJson(mutable[key]);
|
|
31
|
+
const entry = `${JSON.stringify(key)}:${value}`;
|
|
32
|
+
bytes += Buffer.byteLength(entry) + 1;
|
|
33
|
+
this.assertBound(bytes);
|
|
34
|
+
return entry;
|
|
35
|
+
});
|
|
36
|
+
return `{${entries.join(',')}}`;
|
|
37
|
+
}
|
|
38
|
+
assertBound(bytes) {
|
|
39
|
+
if (bytes > TRANSACTION_MAX_MANIFEST_BYTES) {
|
|
40
|
+
throw new ServiceError('TRANSACTION_MANIFEST_LIMIT', 'Transaction manifest exceeds its fixed byte limit');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
// A generated world can contain thousands of state/region files. Review collections
|
|
2
|
+
// remain bounded independently of the unchanged 16 MiB authenticated manifest cap.
|
|
3
|
+
export const TRANSACTION_MAX_FILES = 5_000;
|
|
2
4
|
export const TRANSACTION_MAX_OPERATIONS = 1_000;
|
|
3
5
|
export const TRANSACTION_MAX_VALIDATION_CHECKS = 100;
|
|
4
6
|
export const TRANSACTION_MAX_DIAGNOSTICS = 100;
|
|
5
|
-
export const TRANSACTION_MAX_ARTIFACTS =
|
|
7
|
+
export const TRANSACTION_MAX_ARTIFACTS = 6_000;
|
|
6
8
|
export const TRANSACTION_MAX_APPLIED_FILES = TRANSACTION_MAX_FILES;
|
|
7
9
|
export const TRANSACTION_MAX_READ_DEPENDENCIES = 10_000;
|
|
8
10
|
export const TRANSACTION_MAX_MANIFEST_BYTES = 16_777_216;
|
|
11
|
+
export const TRANSACTION_MAX_PENDING_FILES = 16;
|
|
12
|
+
// A maximum-size world needs before/after blobs plus staging and rollback copies.
|
|
13
|
+
export const TRANSACTION_MAX_JOURNAL_BYTES = 1_073_741_824;
|