hoi4-agent-tools 4.4.0 → 4.5.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 +8 -0
- package/dist/hoi4_agent_tools/core/managed-outputs.js +26 -6
- package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
- package/dist/hoi4_agent_tools/core/source/parser.js +16 -1
- package/dist/hoi4_agent_tools/focus/drift.js +34 -0
- package/dist/hoi4_agent_tools/focus/service.js +9 -3
- package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
- package/dist/hoi4_agent_tools/map/model.js +2 -1
- package/dist/hoi4_agent_tools/map/operations.js +173 -6
- package/dist/hoi4_agent_tools/map/reference-preparation.js +4 -1
- package/dist/hoi4_agent_tools/map/service.js +15 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +41 -1
- package/dist/hoi4_agent_tools/map/world-context.js +23 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +22 -10
- package/dist/hoi4_agent_tools/map/world-realms.js +152 -0
- package/dist/hoi4_agent_tools/map/world.js +100 -39
- package/dist/hoi4_agent_tools/schemas/map.js +71 -13
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +6 -0
- package/docs/map.md +34 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/geography-model.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 +725 -589
- 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/reference-sheets.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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.5.0 - 2026-10-25
|
|
4
|
+
|
|
5
|
+
- Refuse `hoi4.focus_rewrite` with `FOCUS_SOURCE_CHANGED_SINCE_PLAN` when the focus source changed in meaning after the plan was inspected, instead of reverting the external change to the plan's values (national trees and continuous palettes). Comment and formatting changes and a source that already equals the plan are still accepted. Reported by the HOI4 Factory S01 EXP-02 probe.
|
|
6
|
+
- Refuse every rewrite of a file that is not valid UTF-8 and contains bytes Windows-1252 cannot represent (0x81, 0x8D, 0x8F, 0x90, 0x9D) with `SOURCE_ENCODING_NOT_ROUND_TRIPPABLE`. Previously the encoder wrote 0x9D for each of them without a diagnostic. Reads and inspection continue and add a warning with the same code. Valid UTF-8 and valid Windows-1252 files are unchanged.
|
|
7
|
+
- Place countries the mod already owns (ADR 0069): `create_world.countries` accepts `{ "source": "workspace", "realms": [...] }` besides the array. Each realm is a declared tag with a capital point and optional seeds, claim and exclusion polygons and a weight; `unowned` polygons and `islands` (`nearest`, `one-owner`, `unowned`) decide land no country claims. The world then writes only the map, states, strategic regions and their names, and none of the countries' files. New refusals `MAP_WORLD_REALM_TAG_UNKNOWN`, `MAP_WORLD_REALM_TAG_DYNAMIC`, `MAP_WORLD_REALM_POINT_OUTSIDE`, `MAP_WORLD_REALM_POINT_WATER`, `MAP_WORLD_REALM_POINT_CONFLICT` and `MAP_WORLD_REALM_CLAIM_OVERLAP`; notes `MAP_WORLD_REALMS_PLACED` and `MAP_WORLD_STATE_UNOWNED`; result field `data.realms` and artifact `map-world-realms.json`. Public schema change: `countries` becomes a union of the existing array and the new object (minor version). Requests with the array are unchanged.
|
|
8
|
+
- Report what a world write does to the previous world's files: `MAP_WORLD_OUTPUTS_RECONCILED`, `data.outputs` (written, retired and released counts with up to 200 paths each) and the artifact `map-world-outputs.json`. With workspace countries an edited country file of the previous world is released to the mod (`MAP_WORLD_OUTPUT_RELEASED`) instead of refusing the write; every other edited output still refuses with `MANAGED_OUTPUT_EDITED`.
|
|
9
|
+
- Report the declared country tags after every world write (`MAP_WORLD_TAG_COUNT`) and warn above 439 (`MAP_WORLD_TAG_COUNT_HIGH`), the count at which one measured game build overflowed its flag sheet.
|
|
10
|
+
|
|
3
11
|
## 4.4.0 - 2026-10-24
|
|
4
12
|
|
|
5
13
|
- Collect test fixtures that a killed or crashed test run left in the temporary folder: the test setup now also sweeps `hoi4-*` fixtures that carry an owner record of a process that has ended (`sweepTemporaryDirectories` option `ownedFixturesOnly`). Unmarked folders and folders of a live process are never removed. Interrupted local runs had left 2,441 such folders (640 MB).
|
|
@@ -16,6 +16,10 @@ const manifestSchema = z
|
|
|
16
16
|
.strict();
|
|
17
17
|
/** Pure planning only: the shared transaction engine performs every replacement and deletion. */
|
|
18
18
|
export function reconcileManagedOutputs(input) {
|
|
19
|
+
return planManagedOutputs(input).changes;
|
|
20
|
+
}
|
|
21
|
+
/** Plans the managed-output reconciliation and reports what it writes, retires and releases. */
|
|
22
|
+
export function planManagedOutputs(input) {
|
|
19
23
|
const sources = new Map(input.sources
|
|
20
24
|
.filter((f) => f.rootKind === 'mod')
|
|
21
25
|
.map((f) => [f.relativePath.replaceAll('\\', '/').toLowerCase(), f]));
|
|
@@ -32,6 +36,9 @@ export function reconcileManagedOutputs(input) {
|
|
|
32
36
|
throw new ServiceError('MANAGED_OUTPUT_MANIFEST_INVALID', 'Generated-output inventory is invalid');
|
|
33
37
|
}
|
|
34
38
|
})();
|
|
39
|
+
const outputs = input.proposed.filter((f) => f.content !== null && input.permitsPath(f.relativePath));
|
|
40
|
+
const next = new Set(outputs.map((f) => f.relativePath.toLowerCase()));
|
|
41
|
+
const released = new Set();
|
|
35
42
|
const seen = new Set();
|
|
36
43
|
for (const file of previous) {
|
|
37
44
|
const key = file.relativePath.toLowerCase();
|
|
@@ -43,19 +50,26 @@ export function reconcileManagedOutputs(input) {
|
|
|
43
50
|
throw new ServiceError('MANAGED_OUTPUT_MANIFEST_INVALID', 'Generated-output inventory contains an unsupported or duplicate path');
|
|
44
51
|
seen.add(key);
|
|
45
52
|
const current = sources.get(key);
|
|
46
|
-
if (current
|
|
47
|
-
|
|
53
|
+
if (current === undefined || sha256Bytes(current.bytes) === file.sha256)
|
|
54
|
+
continue;
|
|
55
|
+
if (!next.has(key) && input.releasable?.(file.relativePath) === true) {
|
|
56
|
+
released.add(key);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new ServiceError('MANAGED_OUTPUT_EDITED', 'A generated file has been edited; regeneration would discard those changes', { relativePath: file.relativePath });
|
|
48
60
|
}
|
|
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
61
|
const deletions = previous
|
|
52
|
-
.filter((f) =>
|
|
62
|
+
.filter((f) => {
|
|
63
|
+
const key = f.relativePath.toLowerCase();
|
|
64
|
+
return !next.has(key) && !released.has(key) && sources.has(key);
|
|
65
|
+
})
|
|
53
66
|
.map((f) => ({
|
|
54
67
|
relativePath: f.relativePath,
|
|
55
68
|
content: null,
|
|
56
69
|
operationIds: [input.operationId],
|
|
57
70
|
}));
|
|
58
|
-
|
|
71
|
+
const sorted = (paths) => [...paths].sort(compareCodeUnits);
|
|
72
|
+
const changes = [
|
|
59
73
|
...deletions,
|
|
60
74
|
{
|
|
61
75
|
relativePath: input.manifestPath,
|
|
@@ -69,4 +83,10 @@ export function reconcileManagedOutputs(input) {
|
|
|
69
83
|
mediaType: 'application/json',
|
|
70
84
|
},
|
|
71
85
|
];
|
|
86
|
+
return {
|
|
87
|
+
changes,
|
|
88
|
+
written: sorted(outputs.map((f) => f.relativePath)),
|
|
89
|
+
retired: sorted(deletions.map((f) => f.relativePath)),
|
|
90
|
+
released: sorted(previous.filter((f) => released.has(f.relativePath.toLowerCase())).map((f) => f.relativePath)),
|
|
91
|
+
};
|
|
72
92
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import iconv from 'iconv-lite';
|
|
2
2
|
import { ServiceError } from '../result.js';
|
|
3
3
|
const utf8Bom = Buffer.from([0xef, 0xbb, 0xbf]);
|
|
4
|
+
const REPLACEMENT_CHARACTER = String.fromCharCode(0xfffd);
|
|
4
5
|
function detectNewline(text) {
|
|
5
6
|
const crlf = text.indexOf('\r\n');
|
|
6
7
|
const lf = text.indexOf('\n');
|
|
@@ -22,10 +23,55 @@ function isUtf8(bytes) {
|
|
|
22
23
|
return false;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
let windows1252RoundTrip;
|
|
27
|
+
function windows1252Table() {
|
|
28
|
+
windows1252RoundTrip ??= Array.from({ length: 256 }, (_, value) => {
|
|
29
|
+
const original = Buffer.from([value]);
|
|
30
|
+
const decoded = iconv.decode(original, 'windows-1252');
|
|
31
|
+
return (decoded !== REPLACEMENT_CHARACTER && iconv.encode(decoded, 'windows-1252').equals(original));
|
|
32
|
+
});
|
|
33
|
+
return windows1252RoundTrip;
|
|
34
|
+
}
|
|
35
|
+
function findWindows1252Loss(bytes) {
|
|
36
|
+
const table = windows1252Table();
|
|
37
|
+
let count = 0;
|
|
38
|
+
let firstOffset = -1;
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (let offset = 0; offset < bytes.length; offset += 1) {
|
|
41
|
+
const value = bytes[offset];
|
|
42
|
+
if (table[value])
|
|
43
|
+
continue;
|
|
44
|
+
if (firstOffset < 0)
|
|
45
|
+
firstOffset = offset;
|
|
46
|
+
count += 1;
|
|
47
|
+
seen.add(value);
|
|
48
|
+
}
|
|
49
|
+
if (count === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
return { count, firstOffset, byteValues: [...seen].sort((left, right) => left - right) };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Refuses a mutation of a decoded source that cannot be re-encoded to its original bytes.
|
|
55
|
+
* Reads of such a source continue to work; only writes are blocked.
|
|
56
|
+
*/
|
|
57
|
+
export function assertSourceEncodingPreserved(decoded, sourcePath) {
|
|
58
|
+
const loss = decoded.encodingLoss;
|
|
59
|
+
if (loss === undefined)
|
|
60
|
+
return;
|
|
61
|
+
const values = loss.byteValues.map((value) => `0x${value.toString(16).toUpperCase()}`);
|
|
62
|
+
throw new ServiceError('SOURCE_ENCODING_NOT_ROUND_TRIPPABLE', `Source is not valid UTF-8 and contains ${loss.count} byte(s) (${values.join(', ')}) that Windows-1252 cannot represent; editing it would silently rewrite them. Convert the file to UTF-8 or replace those bytes, then retry`, {
|
|
63
|
+
path: sourcePath,
|
|
64
|
+
encoding: decoded.encoding,
|
|
65
|
+
byteCount: loss.count,
|
|
66
|
+
byteValues: values,
|
|
67
|
+
firstByteOffset: loss.firstOffset,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
25
70
|
export function decodeSource(input) {
|
|
26
71
|
const bytes = Buffer.from(input);
|
|
27
72
|
let encoding;
|
|
28
73
|
let text;
|
|
74
|
+
let encodingLoss;
|
|
29
75
|
if (bytes.subarray(0, 3).equals(utf8Bom)) {
|
|
30
76
|
encoding = 'utf8-bom';
|
|
31
77
|
text = bytes.subarray(3).toString('utf8');
|
|
@@ -37,16 +83,24 @@ export function decodeSource(input) {
|
|
|
37
83
|
else {
|
|
38
84
|
encoding = 'windows-1252';
|
|
39
85
|
text = iconv.decode(bytes, 'windows-1252');
|
|
86
|
+
encodingLoss = findWindows1252Loss(bytes);
|
|
40
87
|
}
|
|
41
|
-
return {
|
|
88
|
+
return {
|
|
89
|
+
encoding,
|
|
90
|
+
text,
|
|
91
|
+
bytes,
|
|
92
|
+
newline: detectNewline(text),
|
|
93
|
+
...(encodingLoss === undefined ? {} : { encodingLoss }),
|
|
94
|
+
};
|
|
42
95
|
}
|
|
43
96
|
export function encodeSource(text, encoding) {
|
|
44
97
|
if (encoding === 'utf8')
|
|
45
98
|
return Buffer.from(text, 'utf8');
|
|
46
99
|
if (encoding === 'utf8-bom')
|
|
47
100
|
return Buffer.concat([utf8Bom, Buffer.from(text, 'utf8')]);
|
|
101
|
+
// U+FFFD is what undefined Windows-1252 bytes decode to; iconv-lite would write it back as 0x9D.
|
|
48
102
|
const encoded = iconv.encode(text, 'windows-1252');
|
|
49
|
-
if (iconv.decode(encoded, 'windows-1252') !== text) {
|
|
103
|
+
if (text.includes(REPLACEMENT_CHARACTER) || iconv.decode(encoded, 'windows-1252') !== text) {
|
|
50
104
|
throw new ServiceError('SOURCE_ENCODING_UNREPRESENTABLE', 'The edit introduces characters that cannot be represented in the source encoding', { encoding });
|
|
51
105
|
}
|
|
52
106
|
return encoded;
|
|
@@ -2,7 +2,7 @@ import { hasBlockingDiagnostics } from '../diagnostics.js';
|
|
|
2
2
|
import { ServiceError } from '../result.js';
|
|
3
3
|
import { sha256Bytes } from '../canonical.js';
|
|
4
4
|
import { sourceDocumentCacheKey, sourceDocuments } from './cache.js';
|
|
5
|
-
import { decodeSource, encodeSource } from './encoding.js';
|
|
5
|
+
import { assertSourceEncodingPreserved, decodeSource, encodeSource, } from './encoding.js';
|
|
6
6
|
import { createSourceLineIndex, isTrivia, lexClausewitz, locationFor, } from './lexer.js';
|
|
7
7
|
import { SOURCE_LINE_LIMIT, SOURCE_MAX_BYTES, SOURCE_MAX_NESTING, SourceDiagnosticCollector, } from './limits.js';
|
|
8
8
|
function scalarValue(token) {
|
|
@@ -298,6 +298,20 @@ function parseClausewitzUncached(bytes, sourcePath) {
|
|
|
298
298
|
sourceRevision: sha256Bytes(decoded.bytes),
|
|
299
299
|
};
|
|
300
300
|
}
|
|
301
|
+
const encodingLoss = decoded.encodingLoss;
|
|
302
|
+
if (encodingLoss !== undefined) {
|
|
303
|
+
diagnostics.add(() => ({
|
|
304
|
+
code: 'SOURCE_ENCODING_NOT_ROUND_TRIPPABLE',
|
|
305
|
+
severity: 'warning',
|
|
306
|
+
category: 'syntax',
|
|
307
|
+
message: 'Source is not valid UTF-8 and contains bytes that Windows-1252 cannot represent; it can be read but any rewrite is refused',
|
|
308
|
+
location: locationFor(sourcePath, lineIndex, encodingLoss.firstOffset, encodingLoss.firstOffset + 1),
|
|
309
|
+
details: {
|
|
310
|
+
byteCount: encodingLoss.count,
|
|
311
|
+
byteValues: encodingLoss.byteValues.map((value) => `0x${value.toString(16).toUpperCase()}`),
|
|
312
|
+
},
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
301
315
|
const lexed = lexClausewitz(decoded.text, sourcePath, { lineIndex, diagnostics });
|
|
302
316
|
const parser = new Parser(lexed.tokens, decoded.text, sourcePath, lineIndex, diagnostics);
|
|
303
317
|
const root = parser.parseRoot();
|
|
@@ -323,6 +337,7 @@ export function assertRewriteSafe(document) {
|
|
|
323
337
|
}
|
|
324
338
|
}
|
|
325
339
|
export function encodeDocumentText(document, text) {
|
|
340
|
+
assertSourceEncodingPreserved(document, document.path);
|
|
326
341
|
return encodeSource(text, document.encoding);
|
|
327
342
|
}
|
|
328
343
|
export function assignments(block, key) {
|
|
@@ -72,6 +72,40 @@ export function assertFocusPlanAuthority(drift, authority) {
|
|
|
72
72
|
currentPlanHash: drift.currentPlanHash,
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* True when the source changed in meaning after the plan was imported, so regenerating from the
|
|
77
|
+
* plan would overwrite those changes. A formatting-only change (the source still imports to the
|
|
78
|
+
* plan's original baseline), a source that already equals the plan, and an unchanged source are not
|
|
79
|
+
* semantic drift. This is deliberately independent of whether the plan itself was edited: a plan
|
|
80
|
+
* edit plus an external formatting change reports `conflict`, which is still safe to apply.
|
|
81
|
+
*/
|
|
82
|
+
export function hasSemanticSourceDrift(drift) {
|
|
83
|
+
if (!drift.sourceChanged)
|
|
84
|
+
return false;
|
|
85
|
+
if (drift.status === 'tree_removed')
|
|
86
|
+
return true;
|
|
87
|
+
return (drift.currentPlanHash !== drift.importedPlanHash &&
|
|
88
|
+
drift.currentPlanHash !== drift.savedPlanHash);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Refuses to regenerate a tree from a plan when the source changed in meaning after the plan was
|
|
92
|
+
* imported. Applying the plan would overwrite those external changes with stale plan values.
|
|
93
|
+
* Re-import the source with the inspect tool, reapply the intended change to the fresh plan, and
|
|
94
|
+
* retry.
|
|
95
|
+
*/
|
|
96
|
+
export function assertNoSemanticSourceDrift(drift, sourcePath) {
|
|
97
|
+
if (!hasSemanticSourceDrift(drift))
|
|
98
|
+
return;
|
|
99
|
+
throw new ServiceError('FOCUS_SOURCE_CHANGED_SINCE_PLAN', 'The focus source changed in meaning after the plan was imported; rewriting from the plan would overwrite those changes. Import the current source with hoi4.focus_inspect, reapply the intended edit to the fresh plan, and retry', {
|
|
100
|
+
relativePath: sourcePath,
|
|
101
|
+
status: drift.status,
|
|
102
|
+
savedSourceHash: drift.savedSourceHash,
|
|
103
|
+
currentSourceHash: drift.currentSourceHash,
|
|
104
|
+
savedPlanHash: drift.savedPlanHash,
|
|
105
|
+
currentPlanHash: drift.currentPlanHash,
|
|
106
|
+
importedPlanHash: drift.importedPlanHash,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
75
109
|
export function detectContinuousFocusDrift(savedPlan, currentDocument) {
|
|
76
110
|
const currentSourceHash = sha256Bytes(currentDocument.bytes);
|
|
77
111
|
const savedPlanHash = focusPlanHash(savedPlan);
|
|
@@ -5,7 +5,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
5
5
|
import { parseClausewitz } from '../core/source/index.js';
|
|
6
6
|
import { TRANSACTION_MAX_DIAGNOSTICS } from '../core/transaction-limits.js';
|
|
7
7
|
import { PACKAGE_VERSION } from '../version.js';
|
|
8
|
-
import { assertFocusPlanAuthority, detectContinuousFocusDrift, detectFocusDrift, } from './drift.js';
|
|
8
|
+
import { assertFocusPlanAuthority, assertNoSemanticSourceDrift, detectContinuousFocusDrift, detectFocusDrift, } from './drift.js';
|
|
9
9
|
import { importContinuousFocusPalettes, importFocusTrees } from './importer.js';
|
|
10
10
|
import { layoutFocusTree, layoutFocusTreeAsync } from './layout.js';
|
|
11
11
|
import { lintContinuousFocusPalette, lintFocusTree } from './lint.js';
|
|
@@ -191,8 +191,11 @@ export class FocusWorkbench {
|
|
|
191
191
|
const drift = targetCreated
|
|
192
192
|
? missingTargetDrift(input.plan, document)
|
|
193
193
|
: detectFocusDrift(input.plan, document, input.references);
|
|
194
|
-
if (!targetCreated)
|
|
194
|
+
if (!targetCreated) {
|
|
195
|
+
if (input.onSemanticSourceDrift === 'refuse')
|
|
196
|
+
assertNoSemanticSourceDrift(drift, input.relativePath);
|
|
195
197
|
assertFocusPlanAuthority(drift, input.authority ?? 'plan');
|
|
198
|
+
}
|
|
196
199
|
const layout = input.layout ??
|
|
197
200
|
(await layoutFocusTreeAsync(input.plan, input.signal === undefined ? {} : { signal: input.signal }));
|
|
198
201
|
const after = updateFocusTreeSource(document, targetCreated ? undefined : drift.currentSourcePlan, input.plan, layout);
|
|
@@ -414,8 +417,11 @@ export class FocusWorkbench {
|
|
|
414
417
|
const drift = targetCreated
|
|
415
418
|
? missingTargetDrift(input.plan, document)
|
|
416
419
|
: detectContinuousFocusDrift(input.plan, document);
|
|
417
|
-
if (!targetCreated)
|
|
420
|
+
if (!targetCreated) {
|
|
421
|
+
if (input.onSemanticSourceDrift === 'refuse')
|
|
422
|
+
assertNoSemanticSourceDrift(drift, input.relativePath);
|
|
418
423
|
assertFocusPlanAuthority(drift, input.authority ?? 'plan');
|
|
424
|
+
}
|
|
419
425
|
const after = updateContinuousFocusPaletteSource(document, targetCreated ? undefined : drift.currentSourcePlan, input.plan);
|
|
420
426
|
const afterHash = sha256Bytes(after);
|
|
421
427
|
const operationId = deterministicId('continuous_focus_change', {
|
|
@@ -147,6 +147,7 @@ export async function prepareFocusRewrite(engine, workbench, input, context) {
|
|
|
147
147
|
plan,
|
|
148
148
|
createIfMissing,
|
|
149
149
|
authority: 'plan',
|
|
150
|
+
onSemanticSourceDrift: 'refuse',
|
|
150
151
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
151
152
|
artifacts: reviewArtifacts,
|
|
152
153
|
readDependencies: readDependenciesFromScannedFiles(snapshot.files),
|
|
@@ -301,6 +302,7 @@ export async function prepareFocusRewrite(engine, workbench, input, context) {
|
|
|
301
302
|
plan,
|
|
302
303
|
createIfMissing,
|
|
303
304
|
authority: 'plan',
|
|
305
|
+
onSemanticSourceDrift: 'refuse',
|
|
304
306
|
layout: proposed.layout,
|
|
305
307
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
306
308
|
index: snapshot.index,
|
|
@@ -4,7 +4,7 @@ import { SymbolIndex } from '../core/index.js';
|
|
|
4
4
|
import { assertRenderDimensions } from '../core/render-budget.js';
|
|
5
5
|
import { addMapDiagnostic, addMapDiagnostics } from './diagnostic-limit.js';
|
|
6
6
|
import { ServiceError } from '../core/result.js';
|
|
7
|
-
import { assignments, childBlocks, decodeSource, encodeSource, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
|
|
7
|
+
import { assignments, assertSourceEncodingPreserved, childBlocks, decodeSource, encodeSource, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
|
|
8
8
|
import { BmpImage, rgbKey } from './bmp.js';
|
|
9
9
|
const defaultMapIndexSourceRoots = {
|
|
10
10
|
map: ['map'],
|
|
@@ -119,6 +119,7 @@ export function parseTextDocument(file) {
|
|
|
119
119
|
return { ...decoded, file, lines };
|
|
120
120
|
}
|
|
121
121
|
export function encodeTextDocument(document, text) {
|
|
122
|
+
assertSourceEncodingPreserved(document, document.file.displayPath);
|
|
122
123
|
return encodeSource(text, document.encoding);
|
|
123
124
|
}
|
|
124
125
|
function lineLocation(document, line) {
|
|
@@ -10,8 +10,8 @@ import { planReferenceRemap, ReferenceRemapError } from './references.js';
|
|
|
10
10
|
import { generateWorld } from './world.js';
|
|
11
11
|
import { resolveEdge } from './world-edge.js';
|
|
12
12
|
import { planMapPositions, planSupplyRebuild } from './derived.js';
|
|
13
|
-
import { buildWorldContext } from './world-context.js';
|
|
14
|
-
import {
|
|
13
|
+
import { buildWorldContext, countryTagCatalog, WORLD_COUNTRY_TAG_FILE, } from './world-context.js';
|
|
14
|
+
import { planWorldOutputs } from './world-outputs.js';
|
|
15
15
|
import { resolvePartitionMode } from './partition.js';
|
|
16
16
|
import { unsupportedGeographyParts } from './relief/index.js';
|
|
17
17
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
@@ -41,6 +41,8 @@ async function completeCooperatively(steps, signal) {
|
|
|
41
41
|
yieldDeadline = performance.now() + 8;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
/** Paths listed per kind in diagnostics and tool results; the artifact lists them all. */
|
|
45
|
+
export const WORLD_OUTPUT_PATH_SAMPLE = 200;
|
|
44
46
|
function normalizeRelative(value) {
|
|
45
47
|
return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
|
|
46
48
|
}
|
|
@@ -774,6 +776,7 @@ function proposedChanges(changes) {
|
|
|
774
776
|
/** Diagnostics an operation raises while applying, drained into the plan after it succeeds. */
|
|
775
777
|
const diagnosticsByPlan = new WeakMap();
|
|
776
778
|
const replacedRootsByPlan = new WeakMap();
|
|
779
|
+
const worldOutputsByPlan = new WeakMap();
|
|
777
780
|
function currentIndex(base, changes) {
|
|
778
781
|
return indexWithProposedChanges(base, proposedChanges(changes), replacedRootsByPlan.get(changes) ?? []);
|
|
779
782
|
}
|
|
@@ -3314,6 +3317,20 @@ function applyCreateWorld(base, changes, operation, context, signal) {
|
|
|
3314
3317
|
throw new ServiceError('MAP_LOOK_CUSTOM_PALETTE_REQUIRED', 'look "custom" needs colormap.palette with at least one class colour');
|
|
3315
3318
|
if (colormapProfile === 'legacy' && look !== undefined)
|
|
3316
3319
|
throw new ServiceError('MAP_LOOK_REQUIRES_CALIBRATED_COLORMAP', 'look selects a calibrated palette; remove colormap.profile "legacy" or look');
|
|
3320
|
+
const workspaceCountries = operation.countries !== undefined && !Array.isArray(operation.countries)
|
|
3321
|
+
? operation.countries
|
|
3322
|
+
: undefined;
|
|
3323
|
+
const tagCatalog = countryTagCatalog(base);
|
|
3324
|
+
if (workspaceCountries !== undefined) {
|
|
3325
|
+
const declared = new Set(tagCatalog.static);
|
|
3326
|
+
const dynamicTags = new Set(tagCatalog.dynamic);
|
|
3327
|
+
for (const realm of workspaceCountries.realms) {
|
|
3328
|
+
if (dynamicTags.has(realm.tag))
|
|
3329
|
+
throw new ServiceError('MAP_WORLD_REALM_TAG_DYNAMIC', `${realm.tag} is a dynamic tag; a world can only place countries with static tags`, { tag: realm.tag });
|
|
3330
|
+
if (!declared.has(realm.tag))
|
|
3331
|
+
throw new ServiceError('MAP_WORLD_REALM_TAG_UNKNOWN', `No active common/country_tags file other than the generated world's declares ${realm.tag}`, { tag: realm.tag });
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3317
3334
|
const partitionMode = resolvePartitionMode(operation.partition, hasGeography);
|
|
3318
3335
|
if (partitionMode === 'legacy' && operation.partitionTuning !== undefined)
|
|
3319
3336
|
throw new ServiceError('MAP_PARTITION_TUNING_REQUIRES_FEATURE_AWARE', 'partitionTuning applies to the feature-aware partition; set partition to "feature-aware" or remove it');
|
|
@@ -3332,13 +3349,29 @@ function applyCreateWorld(base, changes, operation, context, signal) {
|
|
|
3332
3349
|
provincesPerState: operation.provincesPerState ?? (reference === undefined ? 5 : 12),
|
|
3333
3350
|
landProvincesPerRegion: operation.landProvincesPerRegion ?? (reference === undefined ? 25 : 60),
|
|
3334
3351
|
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? (reference === undefined ? 10 : 30),
|
|
3335
|
-
countries:
|
|
3352
|
+
countries: workspaceCountries?.realms.map((realm) => ({
|
|
3353
|
+
tag: realm.tag,
|
|
3354
|
+
name: realm.tag,
|
|
3355
|
+
...(realm.weight === undefined ? {} : { share: realm.weight }),
|
|
3356
|
+
})) ??
|
|
3357
|
+
operation.countries ??
|
|
3336
3358
|
reference?.countries ?? [
|
|
3337
3359
|
{ tag: 'X01', name: 'Aster' },
|
|
3338
3360
|
{ tag: 'X02', name: 'Brum' },
|
|
3339
3361
|
{ tag: 'X03', name: 'Corvel' },
|
|
3340
3362
|
{ tag: 'X04', name: 'Doran' },
|
|
3341
3363
|
],
|
|
3364
|
+
...(workspaceCountries === undefined
|
|
3365
|
+
? {}
|
|
3366
|
+
: {
|
|
3367
|
+
realms: {
|
|
3368
|
+
list: workspaceCountries.realms,
|
|
3369
|
+
...(workspaceCountries.unowned === undefined
|
|
3370
|
+
? {}
|
|
3371
|
+
: { unowned: workspaceCountries.unowned }),
|
|
3372
|
+
islands: workspaceCountries.islands ?? 'nearest',
|
|
3373
|
+
},
|
|
3374
|
+
}),
|
|
3342
3375
|
...(reference === undefined
|
|
3343
3376
|
? {}
|
|
3344
3377
|
: {
|
|
@@ -3398,7 +3431,7 @@ function applyCreateWorld(base, changes, operation, context, signal) {
|
|
|
3398
3431
|
for (const file of world.files)
|
|
3399
3432
|
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType);
|
|
3400
3433
|
// Remove only unchanged outputs recorded by a previous generation, before indexing new IDs.
|
|
3401
|
-
for (const file of
|
|
3434
|
+
for (const file of planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined }).changes)
|
|
3402
3435
|
if (file.content === null)
|
|
3403
3436
|
addChange(changes, file.relativePath, null, operation.id, 'text/plain');
|
|
3404
3437
|
// Start from empty derived files so no base-game position or supply line survives.
|
|
@@ -3418,8 +3451,16 @@ function applyCreateWorld(base, changes, operation, context, signal) {
|
|
|
3418
3451
|
const positioned = currentIndex(base, changes);
|
|
3419
3452
|
for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
|
|
3420
3453
|
addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
|
|
3421
|
-
|
|
3454
|
+
const outputs = planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined });
|
|
3455
|
+
for (const file of outputs.changes)
|
|
3422
3456
|
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType ?? 'text/plain');
|
|
3457
|
+
worldOutputsByPlan.set(changes, {
|
|
3458
|
+
operationId: operation.id,
|
|
3459
|
+
written: outputs.written,
|
|
3460
|
+
retired: outputs.retired,
|
|
3461
|
+
released: outputs.released,
|
|
3462
|
+
...(world.realms === undefined ? {} : { realms: world.realms }),
|
|
3463
|
+
});
|
|
3423
3464
|
return [
|
|
3424
3465
|
...(reference?.diagnostics ?? []).map((message) => ({
|
|
3425
3466
|
code: 'MAP_REFERENCE_INTERPRETATION',
|
|
@@ -3448,6 +3489,24 @@ function applyCreateWorld(base, changes, operation, context, signal) {
|
|
|
3448
3489
|
details: { warningCount: world.warnings.length, samples: world.warnings.slice(0, 5) },
|
|
3449
3490
|
},
|
|
3450
3491
|
]),
|
|
3492
|
+
worldOutputsDiagnostic(operation.id, outputs),
|
|
3493
|
+
...worldTagDiagnostics(operation.id, tagCatalog, world, workspaceCountries !== undefined),
|
|
3494
|
+
...worldRealmDiagnostics(operation.id, world),
|
|
3495
|
+
...(outputs.released.length === 0
|
|
3496
|
+
? []
|
|
3497
|
+
: [
|
|
3498
|
+
{
|
|
3499
|
+
code: 'MAP_WORLD_OUTPUT_RELEASED',
|
|
3500
|
+
severity: 'info',
|
|
3501
|
+
category: 'map',
|
|
3502
|
+
operationId: operation.id,
|
|
3503
|
+
message: `${outputs.released.length} edited country file(s) of the previous world are left to the workspace and no longer recorded as generated`,
|
|
3504
|
+
details: {
|
|
3505
|
+
count: outputs.released.length,
|
|
3506
|
+
files: outputs.released.slice(0, WORLD_OUTPUT_PATH_SAMPLE),
|
|
3507
|
+
},
|
|
3508
|
+
},
|
|
3509
|
+
]),
|
|
3451
3510
|
{
|
|
3452
3511
|
code: 'MAP_WORLD_CREATED',
|
|
3453
3512
|
severity: 'info',
|
|
@@ -3470,6 +3529,107 @@ function applyCreateWorld(base, changes, operation, context, signal) {
|
|
|
3470
3529
|
]),
|
|
3471
3530
|
];
|
|
3472
3531
|
}
|
|
3532
|
+
/** Tag count observed to overflow the flag sheet of one 1.19 Windows build; never a block. */
|
|
3533
|
+
export const WORLD_TAG_COUNT_OBSERVED_LIMIT = 439;
|
|
3534
|
+
function worldTagDiagnostics(operationId, catalog, world, workspaceOwned) {
|
|
3535
|
+
const declared = new Set(catalog.static);
|
|
3536
|
+
const generated = workspaceOwned
|
|
3537
|
+
? 0
|
|
3538
|
+
: worldGeneratedTags(world).filter((tag) => !declared.has(tag)).length;
|
|
3539
|
+
const total = declared.size + generated + catalog.dynamic.length;
|
|
3540
|
+
const details = {
|
|
3541
|
+
static: declared.size + generated,
|
|
3542
|
+
generated,
|
|
3543
|
+
dynamic: catalog.dynamic.length,
|
|
3544
|
+
total,
|
|
3545
|
+
observedLimit: WORLD_TAG_COUNT_OBSERVED_LIMIT,
|
|
3546
|
+
};
|
|
3547
|
+
return [
|
|
3548
|
+
{
|
|
3549
|
+
code: 'MAP_WORLD_TAG_COUNT',
|
|
3550
|
+
severity: 'info',
|
|
3551
|
+
category: 'map',
|
|
3552
|
+
operationId,
|
|
3553
|
+
message: `After this write ${total} country tags are declared: ${details.static} static (${generated} written by the world) and ${details.dynamic} dynamic`,
|
|
3554
|
+
details,
|
|
3555
|
+
},
|
|
3556
|
+
...(total <= WORLD_TAG_COUNT_OBSERVED_LIMIT
|
|
3557
|
+
? []
|
|
3558
|
+
: [
|
|
3559
|
+
{
|
|
3560
|
+
code: 'MAP_WORLD_TAG_COUNT_HIGH',
|
|
3561
|
+
severity: 'warning',
|
|
3562
|
+
category: 'map',
|
|
3563
|
+
operationId,
|
|
3564
|
+
message: `${total} country tags exceed ${WORLD_TAG_COUNT_OBSERVED_LIMIT}, the count at which one measured game build overflowed its flag sheet`,
|
|
3565
|
+
details,
|
|
3566
|
+
},
|
|
3567
|
+
]),
|
|
3568
|
+
];
|
|
3569
|
+
}
|
|
3570
|
+
/** Tags of the country files a generated world writes. */
|
|
3571
|
+
function worldGeneratedTags(world) {
|
|
3572
|
+
const file = world.files.find(({ relativePath }) => relativePath === WORLD_COUNTRY_TAG_FILE);
|
|
3573
|
+
if (file === undefined)
|
|
3574
|
+
return [];
|
|
3575
|
+
return [
|
|
3576
|
+
...new Set([...file.content.toString('utf8').matchAll(/^([A-Z][A-Z0-9]{2}) =/gmu)].map((match) => match[1])),
|
|
3577
|
+
];
|
|
3578
|
+
}
|
|
3579
|
+
function worldRealmDiagnostics(operationId, world) {
|
|
3580
|
+
if (world.realms === undefined)
|
|
3581
|
+
return [];
|
|
3582
|
+
const { countries, unownedStates } = world.realms;
|
|
3583
|
+
return [
|
|
3584
|
+
{
|
|
3585
|
+
code: 'MAP_WORLD_REALMS_PLACED',
|
|
3586
|
+
severity: 'info',
|
|
3587
|
+
category: 'map',
|
|
3588
|
+
operationId,
|
|
3589
|
+
message: `Placed ${countries.length} workspace countries; their country files, histories, flags and names stay the workspace's`,
|
|
3590
|
+
details: {
|
|
3591
|
+
countries: countries
|
|
3592
|
+
.slice(0, 100)
|
|
3593
|
+
.map(({ tag, capitalState, states, landShare, targetShare }) => ({
|
|
3594
|
+
tag,
|
|
3595
|
+
capitalState,
|
|
3596
|
+
states: states.length,
|
|
3597
|
+
landShare,
|
|
3598
|
+
targetShare,
|
|
3599
|
+
})),
|
|
3600
|
+
truncated: countries.length > 100,
|
|
3601
|
+
},
|
|
3602
|
+
},
|
|
3603
|
+
...(unownedStates.length === 0
|
|
3604
|
+
? []
|
|
3605
|
+
: [
|
|
3606
|
+
{
|
|
3607
|
+
code: 'MAP_WORLD_STATE_UNOWNED',
|
|
3608
|
+
severity: 'info',
|
|
3609
|
+
category: 'map',
|
|
3610
|
+
operationId,
|
|
3611
|
+
message: `${unownedStates.length} state(s) have no owner, as the request asks; whether the game build accepts unowned land is for the mod to verify`,
|
|
3612
|
+
details: { count: unownedStates.length, states: unownedStates.slice(0, 200) },
|
|
3613
|
+
},
|
|
3614
|
+
]),
|
|
3615
|
+
];
|
|
3616
|
+
}
|
|
3617
|
+
function worldOutputsDiagnostic(operationId, outputs) {
|
|
3618
|
+
return {
|
|
3619
|
+
code: 'MAP_WORLD_OUTPUTS_RECONCILED',
|
|
3620
|
+
severity: 'info',
|
|
3621
|
+
category: 'map',
|
|
3622
|
+
operationId,
|
|
3623
|
+
message: `The world records ${outputs.written.length} generated file(s), retires ${outputs.retired.length} earlier output(s) it no longer writes and releases ${outputs.released.length} to the workspace`,
|
|
3624
|
+
details: {
|
|
3625
|
+
written: outputs.written.length,
|
|
3626
|
+
retired: outputs.retired.length,
|
|
3627
|
+
released: outputs.released.length,
|
|
3628
|
+
retiredFiles: outputs.retired.slice(0, WORLD_OUTPUT_PATH_SAMPLE),
|
|
3629
|
+
truncated: outputs.retired.length > WORLD_OUTPUT_PATH_SAMPLE,
|
|
3630
|
+
},
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3473
3633
|
function applyRegenerateMapPositions(base, changes, operation) {
|
|
3474
3634
|
const index = currentIndex(base, changes);
|
|
3475
3635
|
for (const file of planMapPositions(index, operation))
|
|
@@ -3567,6 +3727,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3567
3727
|
const allocations = [];
|
|
3568
3728
|
let expectedChangedBounds;
|
|
3569
3729
|
const manifestOperations = [];
|
|
3730
|
+
const worldOutputs = [];
|
|
3570
3731
|
const ids = new Set();
|
|
3571
3732
|
for (const [operationIndex, operation] of operations.entries()) {
|
|
3572
3733
|
yield* cancellationCheckpoint(signal, operationIndex);
|
|
@@ -3596,8 +3757,13 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3596
3757
|
const allocationsCheckpoint = allocations.length;
|
|
3597
3758
|
const boundsCheckpoint = expectedChangedBounds;
|
|
3598
3759
|
try {
|
|
3599
|
-
if (operation.kind === 'create_world')
|
|
3760
|
+
if (operation.kind === 'create_world') {
|
|
3600
3761
|
diagnostics.push(...applyCreateWorld(base, changes, operation, context, signal));
|
|
3762
|
+
const outputs = worldOutputsByPlan.get(changes);
|
|
3763
|
+
worldOutputsByPlan.delete(changes);
|
|
3764
|
+
if (outputs !== undefined)
|
|
3765
|
+
worldOutputs.push(outputs);
|
|
3766
|
+
}
|
|
3601
3767
|
else if (operation.kind === 'move_state_provinces')
|
|
3602
3768
|
applyMoveStateProvinces(base, changes, operation);
|
|
3603
3769
|
else if (operation.kind === 'update_state')
|
|
@@ -3674,6 +3840,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3674
3840
|
...(expectedChangedBounds === undefined ? {} : { expectedChangedBounds }),
|
|
3675
3841
|
finalIndex,
|
|
3676
3842
|
operations: manifestOperations,
|
|
3843
|
+
...(worldOutputs.length === 0 ? {} : { worldOutputs }),
|
|
3677
3844
|
};
|
|
3678
3845
|
}
|
|
3679
3846
|
export function planMapOperations(base, operations, signal, context = {}) {
|
|
@@ -248,8 +248,11 @@ export async function prepareReferenceWorld(resolver, workspaceId, operation, pr
|
|
|
248
248
|
return id === 0 ? undefined : id;
|
|
249
249
|
};
|
|
250
250
|
let countries;
|
|
251
|
-
if (operation.countries
|
|
251
|
+
if (Array.isArray(operation.countries))
|
|
252
252
|
countries = operation.countries;
|
|
253
|
+
// Workspace-owned countries are placed from their own points; the picture's colours are unused.
|
|
254
|
+
else if (operation.countries !== undefined)
|
|
255
|
+
countries = [];
|
|
253
256
|
else if (surface.inferredCountries.length > 1) {
|
|
254
257
|
countries = surface.inferredCountries.slice(0, 200).map((country, index) => ({
|
|
255
258
|
tag: `X${(index + 1).toString(36).toUpperCase().padStart(2, '0')}`,
|
|
@@ -1297,6 +1297,21 @@ export class AgentNudger {
|
|
|
1297
1297
|
writes.push(...(await this.geographyArtifacts(prepared, options.signal, budget)));
|
|
1298
1298
|
for (const [ordinal, imported] of prepared.imports.entries())
|
|
1299
1299
|
writes.push(...(await buildImportArtifacts(imported, ordinal)));
|
|
1300
|
+
if (plan.worldOutputs !== undefined)
|
|
1301
|
+
writes.push({
|
|
1302
|
+
name: 'map-world-outputs.json',
|
|
1303
|
+
mimeType: 'application/json',
|
|
1304
|
+
content: `${canonicalJson({ schemaVersion: 1, worlds: plan.worldOutputs.map(({ realms: _realms, ...outputs }) => outputs) })}\n`,
|
|
1305
|
+
provenance,
|
|
1306
|
+
});
|
|
1307
|
+
const realmWorlds = (plan.worldOutputs ?? []).flatMap(({ operationId, realms }) => realms === undefined ? [] : [{ operationId, ...realms }]);
|
|
1308
|
+
if (realmWorlds.length > 0)
|
|
1309
|
+
writes.push({
|
|
1310
|
+
name: 'map-world-realms.json',
|
|
1311
|
+
mimeType: 'application/json',
|
|
1312
|
+
content: `${canonicalJson({ schemaVersion: 1, worlds: realmWorlds })}\n`,
|
|
1313
|
+
provenance,
|
|
1314
|
+
});
|
|
1300
1315
|
const generatedWorlds = generatedWorldOperationIds(plan);
|
|
1301
1316
|
if (generatedWorlds.size > 0)
|
|
1302
1317
|
writes.push(...(await buildWorldPreviewArtifacts({
|