hoi4-agent-tools 4.5.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/hoi4_agent_tools/map/border-quality.js +25 -7
- package/dist/hoi4_agent_tools/map/derived.js +16 -3
- package/dist/hoi4_agent_tools/map/gameplay/crossing-validation.js +268 -0
- package/dist/hoi4_agent_tools/map/gameplay/crossings.js +961 -0
- package/dist/hoi4_agent_tools/map/gameplay/development.js +168 -0
- package/dist/hoi4_agent_tools/map/gameplay/economy.js +75 -0
- package/dist/hoi4_agent_tools/map/gameplay/facts.js +264 -0
- package/dist/hoi4_agent_tools/map/gameplay/inland-sea.js +66 -0
- package/dist/hoi4_agent_tools/map/gameplay/metrics.js +132 -0
- package/dist/hoi4_agent_tools/map/gameplay/names.js +419 -0
- package/dist/hoi4_agent_tools/map/gameplay/ports.js +244 -0
- package/dist/hoi4_agent_tools/map/gameplay/prepare.js +27 -0
- package/dist/hoi4_agent_tools/map/gameplay/rail-cost.js +77 -0
- package/dist/hoi4_agent_tools/map/gameplay/sites.js +61 -0
- package/dist/hoi4_agent_tools/map/geography/resolve.js +15 -4
- package/dist/hoi4_agent_tools/map/operations.js +109 -32
- package/dist/hoi4_agent_tools/map/relief/hydrology.js +316 -192
- package/dist/hoi4_agent_tools/map/relief/macro-relief.js +107 -0
- package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +52 -0
- package/dist/hoi4_agent_tools/map/service.js +78 -16
- package/dist/hoi4_agent_tools/map/state-restructure.js +378 -0
- package/dist/hoi4_agent_tools/map/territory-graph.js +238 -0
- package/dist/hoi4_agent_tools/map/territory-operations.js +944 -0
- package/dist/hoi4_agent_tools/map/territory-quality.js +189 -0
- package/dist/hoi4_agent_tools/map/territory.js +1108 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +78 -0
- package/dist/hoi4_agent_tools/map/validation.js +4 -0
- package/dist/hoi4_agent_tools/map/world.js +301 -107
- package/dist/hoi4_agent_tools/reference/known-mistakes.js +3 -0
- package/dist/hoi4_agent_tools/schemas/geography.js +12 -1
- package/dist/hoi4_agent_tools/schemas/map.js +206 -21
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/map.md +100 -4
- 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 +13 -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 +1355 -736
- 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
|
@@ -478,6 +478,112 @@ function applyVolcano(volcano, base, kind, mw, mh, scale, seed, targets) {
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
// ---------------------------------------------------------------------------------------------
|
|
481
|
+
/**
|
|
482
|
+
* A declared pass is authoritative over the other relief (ADR 0072). The saddle of a range's
|
|
483
|
+
* envelope is lowered by the range alone; a volcano cone, a plateau or a neighbouring range that
|
|
484
|
+
* overlaps the pass adds height back and the gap disappears. After everything is composed, the
|
|
485
|
+
* heights along the pass corridor are capped at the saddle level (base plus crest times the
|
|
486
|
+
* envelope), with a soft edge across the corridor, so the pass is lower than the crest on both
|
|
487
|
+
* sides wherever it lies.
|
|
488
|
+
*/
|
|
489
|
+
function carvePassSaddles(geography, tables, heights, kind, mw, mh) {
|
|
490
|
+
const aspect = mh / mw;
|
|
491
|
+
// The line of every range in cell units, one point per table entry, with its footprint.
|
|
492
|
+
const lines = new Map();
|
|
493
|
+
for (const range of geography.ranges) {
|
|
494
|
+
const table = tables.find((candidate) => candidate.id === range.id);
|
|
495
|
+
if (table === undefined)
|
|
496
|
+
continue;
|
|
497
|
+
const x = new Float64Array(table.count);
|
|
498
|
+
const y = new Float64Array(table.count);
|
|
499
|
+
let widest = 0;
|
|
500
|
+
for (let j = 0; j < table.count; j += 1) {
|
|
501
|
+
const point = sampleAlong(range.path, geography.frame, j / (table.count - 1));
|
|
502
|
+
x[j] = point.x * mw;
|
|
503
|
+
y[j] = point.y * mh;
|
|
504
|
+
widest = Math.max(widest, table.halfWidth[j]);
|
|
505
|
+
}
|
|
506
|
+
lines.set(range.id, { x, y, reach: Math.max(2, widest * mw) });
|
|
507
|
+
}
|
|
508
|
+
for (const range of geography.ranges) {
|
|
509
|
+
const table = tables.find((candidate) => candidate.id === range.id);
|
|
510
|
+
const own = lines.get(range.id);
|
|
511
|
+
if (table === undefined || own === undefined)
|
|
512
|
+
continue;
|
|
513
|
+
const lineX = own.x;
|
|
514
|
+
const lineY = own.y;
|
|
515
|
+
for (const entry of table.passes) {
|
|
516
|
+
if (entry.depth <= 0)
|
|
517
|
+
continue;
|
|
518
|
+
const k = Math.min(table.count - 1, Math.max(0, Math.round(entry.at * (table.count - 1))));
|
|
519
|
+
// Half-width of the corridor in cells: the range's footprint, at least three cells, plus a
|
|
520
|
+
// soft edge of three more.
|
|
521
|
+
const corridor = Math.max(2, table.halfWidth[k] * mw);
|
|
522
|
+
const edge = 2;
|
|
523
|
+
const entries = Math.ceil((entry.halfLength / table.length) * (table.count - 1));
|
|
524
|
+
const first = Math.max(0, k - entries);
|
|
525
|
+
const last = Math.min(table.count - 1, k + entries);
|
|
526
|
+
let minX = Infinity;
|
|
527
|
+
let maxX = -Infinity;
|
|
528
|
+
let minY = Infinity;
|
|
529
|
+
let maxY = -Infinity;
|
|
530
|
+
for (let j = first; j <= last; j += 1) {
|
|
531
|
+
minX = Math.min(minX, lineX[j]);
|
|
532
|
+
maxX = Math.max(maxX, lineX[j]);
|
|
533
|
+
minY = Math.min(minY, lineY[j]);
|
|
534
|
+
maxY = Math.max(maxY, lineY[j]);
|
|
535
|
+
}
|
|
536
|
+
const reach = Math.ceil(corridor + edge) + 1;
|
|
537
|
+
const x0 = Math.max(0, Math.floor(minX) - reach);
|
|
538
|
+
const x1 = Math.min(mw - 1, Math.ceil(maxX) + reach);
|
|
539
|
+
const y0 = Math.max(0, Math.floor(minY) - reach);
|
|
540
|
+
const y1 = Math.min(mh - 1, Math.ceil(maxY) + reach);
|
|
541
|
+
const baseLevel = table.base[k];
|
|
542
|
+
const crestLevel = table.crest[k];
|
|
543
|
+
for (let y = y0; y <= y1; y += 1)
|
|
544
|
+
for (let x = x0; x <= x1; x += 1) {
|
|
545
|
+
const cell = y * mw + x;
|
|
546
|
+
if (kind[cell] !== LAND)
|
|
547
|
+
continue;
|
|
548
|
+
// The nearest point of the range line, which must lie inside the pass window.
|
|
549
|
+
let nearest = -1;
|
|
550
|
+
let nearestDistance = Infinity;
|
|
551
|
+
for (let j = 0; j < table.count; j += 1) {
|
|
552
|
+
const distance = Math.hypot(lineX[j] - (x + 0.5), lineY[j] - (y + 0.5));
|
|
553
|
+
if (distance < nearestDistance) {
|
|
554
|
+
nearestDistance = distance;
|
|
555
|
+
nearest = j;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (nearest < first || nearest > last || nearestDistance > corridor + edge)
|
|
559
|
+
continue;
|
|
560
|
+
// A cell on the body of another range belongs to that range, not to this pass.
|
|
561
|
+
let onOther = false;
|
|
562
|
+
for (const [id, other] of lines) {
|
|
563
|
+
if (id === range.id)
|
|
564
|
+
continue;
|
|
565
|
+
for (let j = 0; j < other.x.length && !onOther; j += 1)
|
|
566
|
+
if (Math.hypot(other.x[j] - (x + 0.5), other.y[j] - (y + 0.5)) <= other.reach)
|
|
567
|
+
onOther = true;
|
|
568
|
+
if (onOther)
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
if (onOther)
|
|
572
|
+
continue;
|
|
573
|
+
const along = ((nearest - k) / (table.count - 1)) * table.length;
|
|
574
|
+
const s = (along * along) / (entry.halfLength * entry.halfLength);
|
|
575
|
+
if (s >= 1)
|
|
576
|
+
continue;
|
|
577
|
+
const bump = (1 - s) * (1 - s);
|
|
578
|
+
const cap = baseLevel + (crestLevel - baseLevel) * (1 - entry.depth * bump);
|
|
579
|
+
const weight = nearestDistance <= corridor ? 1 : 1 - smoothstep((nearestDistance - corridor) / edge);
|
|
580
|
+
if (cap < heights[cell])
|
|
581
|
+
heights[cell] = heights[cell] + (cap - heights[cell]) * weight;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
void aspect;
|
|
586
|
+
}
|
|
481
587
|
/** Build the macro relief of the model. */
|
|
482
588
|
export function synthesizeMacroRelief(input) {
|
|
483
589
|
const { geography, width, height, seed } = input;
|
|
@@ -519,6 +625,7 @@ export function synthesizeMacroRelief(input) {
|
|
|
519
625
|
heights[index] = Math.min(HEIGHT_CEILING, heights[index] + targets.delta[index]);
|
|
520
626
|
footprint[index] = Math.max(footprint[index], targets.footprint[index]);
|
|
521
627
|
}
|
|
628
|
+
carvePassSaddles(geography, ranges, heights, kind, mw, mh);
|
|
522
629
|
return {
|
|
523
630
|
width: mw,
|
|
524
631
|
height: mh,
|
|
@@ -85,6 +85,58 @@ function barrierField(width, height, kind, heights, riverBitmap, macro, signal)
|
|
|
85
85
|
}
|
|
86
86
|
return { width, height, strength, sources };
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Run the macro relief and the hydrology only, which is where a model's rivers fail to reach
|
|
90
|
+
* their mouths, and return their notes. The full-resolution heights, river pixels and fields are
|
|
91
|
+
* not built, so a model can be checked in a fraction of a write. A river that cannot reach its
|
|
92
|
+
* mouth throws as in `synthesizeTerrain`, with every failing river in its details.
|
|
93
|
+
*/
|
|
94
|
+
export function checkTerrainRivers(input) {
|
|
95
|
+
const { geography, width, height, seed, kind } = input;
|
|
96
|
+
const relief = { ...DEFAULT_RELIEF, ...input.relief };
|
|
97
|
+
let landPixels = 0;
|
|
98
|
+
for (let index = 0; index < width * height; index += 1)
|
|
99
|
+
if (kind[index] === LAND)
|
|
100
|
+
landPixels += 1;
|
|
101
|
+
const macro = synthesizeMacroRelief({
|
|
102
|
+
geography,
|
|
103
|
+
width,
|
|
104
|
+
height,
|
|
105
|
+
seed,
|
|
106
|
+
kind,
|
|
107
|
+
relief,
|
|
108
|
+
signal: input.signal,
|
|
109
|
+
});
|
|
110
|
+
const adjust = input.adjustHeight;
|
|
111
|
+
if (adjust !== undefined)
|
|
112
|
+
for (let y = 0; y < macro.height; y += 1)
|
|
113
|
+
for (let x = 0; x < macro.width; x += 1) {
|
|
114
|
+
const cell = y * macro.width + x;
|
|
115
|
+
if (macro.kind[cell] === LAND)
|
|
116
|
+
macro.heights[cell] = adjust(2 * x + 1, 2 * y + 1, macro.heights[cell]);
|
|
117
|
+
}
|
|
118
|
+
const hydrology = synthesizeHydrology({
|
|
119
|
+
geography,
|
|
120
|
+
macro,
|
|
121
|
+
seed,
|
|
122
|
+
density: input.rivers?.density,
|
|
123
|
+
count: input.rivers?.count,
|
|
124
|
+
rainfall: input.rainfall,
|
|
125
|
+
landPixels,
|
|
126
|
+
signal: input.signal,
|
|
127
|
+
});
|
|
128
|
+
const notes = [...hydrology.notes];
|
|
129
|
+
// The pass test of the write, on the macro grid: a declared pass must be a saddle lower than
|
|
130
|
+
// the crest on both sides. Only its error is reported, so a preview never blocks on a warning
|
|
131
|
+
// the full-resolution write would not raise.
|
|
132
|
+
const quantised = new Uint8Array(macro.width * macro.height);
|
|
133
|
+
for (let index = 0; index < quantised.length; index += 1)
|
|
134
|
+
quantised[index] = Math.max(0, Math.min(255, Math.round(hydrology.heights[index])));
|
|
135
|
+
for (const note of checkRangesAndFeatures(geography, macro.ranges, { width: macro.width, height: macro.height, kind: macro.kind, heights: quantised }, (peak) => peakHeight(peak, macro.scale), (peak) => peakHeight(peak, macro.scale)))
|
|
136
|
+
if (note.code === 'MAP_PASS_NOT_LOWER')
|
|
137
|
+
notes.push(note);
|
|
138
|
+
return notes;
|
|
139
|
+
}
|
|
88
140
|
/** Build relief, hydrology, normals and fields for a geography model on a map of the given size. */
|
|
89
141
|
export function synthesizeTerrain(input) {
|
|
90
142
|
const { geography, width, height, seed, kind } = input;
|
|
@@ -23,6 +23,7 @@ import { WORLD_OUTPUT_MANIFEST } from './world-outputs.js';
|
|
|
23
23
|
import { renderMap, renderMapDiff, } from './render.js';
|
|
24
24
|
import { validateMapAsync } from './validation.js';
|
|
25
25
|
import { buildWorldPreviewArtifacts, generatedWorldOperationIds } from './world-preview.js';
|
|
26
|
+
import { evaluateTerritoryQuality, territoryArtifactContent, } from './territory-quality.js';
|
|
26
27
|
/**
|
|
27
28
|
* Evidence of an imported reference: registered sheets, an exact or frame-fitted picture, or a
|
|
28
29
|
* geography model beside the picture. Plain pictures keep their previous output.
|
|
@@ -625,7 +626,7 @@ export class AgentNudger {
|
|
|
625
626
|
this.scanner = this.engine.scanner;
|
|
626
627
|
this.transactions = this.engine.transactions;
|
|
627
628
|
}
|
|
628
|
-
async scan(workspaceId, principal, signal, includeExternalScriptReferences = false, includeWorldContext = false, includeAppearance = false) {
|
|
629
|
+
async scan(workspaceId, principal, signal, includeExternalScriptReferences = false, includeWorldContext = false, includeAppearance = false, includeBorderInputs = false) {
|
|
629
630
|
signal?.throwIfAborted();
|
|
630
631
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
631
632
|
const sourceRoots = {
|
|
@@ -634,7 +635,9 @@ export class AgentNudger {
|
|
|
634
635
|
localisation: workspace.registration.roots.localisation,
|
|
635
636
|
};
|
|
636
637
|
const selectorSnapshot = await this.engine.scan(workspaceId, { patterns: defaultMapPatterns(sourceRoots) }, principal, signal);
|
|
637
|
-
const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map, includeAppearance
|
|
638
|
+
const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map, includeAppearance || includeBorderInputs
|
|
639
|
+
? BORDER_BITMAP_SELECTORS
|
|
640
|
+
: PROVINCE_BITMAP_SELECTORS);
|
|
638
641
|
const contentSnapshot = await this.engine.scan(workspaceId, { patterns: [...sourceTextPatterns(sourceRoots), ...bitmapPatterns] }, principal, signal);
|
|
639
642
|
const locatorSnapshot = await this.engine.scan(workspaceId, { patterns: ['**/*.asset'], rootKinds: ['mod', 'fixture'] }, principal, signal);
|
|
640
643
|
const externalScriptSnapshot = includeExternalScriptReferences
|
|
@@ -727,11 +730,11 @@ export class AgentNudger {
|
|
|
727
730
|
scriptReferences: { references: consistency.references, missing: consistency.missing },
|
|
728
731
|
};
|
|
729
732
|
}
|
|
730
|
-
async prepareOperations(workspaceId, operations, principal, signal) {
|
|
733
|
+
async prepareOperations(workspaceId, operations, principal, signal, synthesisCheck = false) {
|
|
731
734
|
signal?.throwIfAborted();
|
|
732
735
|
const snapshot = await this.scan(workspaceId, principal, signal, operations.some((operation) => operation.kind === 'renumber_map_entity' ||
|
|
733
736
|
operation.kind === 'remap_map_references' ||
|
|
734
|
-
'remapReferences' in operation), operations.some((operation) => operation.kind === 'create_world'));
|
|
737
|
+
'remapReferences' in operation), operations.some((operation) => operation.kind === 'create_world'), false, operations.some((operation) => operation.kind === 'assign_territory' || operation.kind === 'repartition_states'));
|
|
735
738
|
const referenceWorlds = new Map();
|
|
736
739
|
const referenceImages = [];
|
|
737
740
|
const referenceLoads = new Map();
|
|
@@ -796,28 +799,56 @@ export class AgentNudger {
|
|
|
796
799
|
const plan = await planMapOperationsAsync(snapshot.index, operations, signal, {
|
|
797
800
|
referenceWorlds,
|
|
798
801
|
geographyModels,
|
|
802
|
+
...(synthesisCheck ? { synthesisCheck: true } : {}),
|
|
799
803
|
});
|
|
800
804
|
// Model diagnostics (including warnings of a model that is otherwise usable) travel with the plan.
|
|
801
805
|
for (const geography of geographyModels.values())
|
|
802
806
|
plan.diagnostics.push(...geography.diagnostics);
|
|
807
|
+
// Border naturalness of what would be written, for the operations that ask for it.
|
|
808
|
+
const territoryQuality = await evaluateTerritoryQuality(plan, signal);
|
|
809
|
+
if (territoryQuality !== undefined) {
|
|
810
|
+
plan.diagnostics.push(...territoryQuality.diagnostics);
|
|
811
|
+
plan.blockers.push(...territoryQuality.blockers);
|
|
812
|
+
for (const report of plan.territory ?? []) {
|
|
813
|
+
const found = territoryQuality.operations.find(({ operationId }) => operationId === report.operationId);
|
|
814
|
+
if (found !== undefined)
|
|
815
|
+
report.borderQuality.verdicts = found.layers.map(({ layer, report: measured, check }) => ({
|
|
816
|
+
layer,
|
|
817
|
+
status: measured.status,
|
|
818
|
+
enrichment: measured.barrier.enrichment,
|
|
819
|
+
naturalShare: measured.natural.share,
|
|
820
|
+
straightFraction: measured.straightness.fraction,
|
|
821
|
+
passed: check.passed,
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
803
825
|
plan.diagnostics.push(...importDiagnostics);
|
|
804
826
|
const newWorld = operations.some(({ kind }) => kind === 'create_world');
|
|
805
|
-
|
|
806
|
-
|
|
827
|
+
// A new world replaces the whole map: findings of the map it replaces are not its baseline,
|
|
828
|
+
// so every finding of the generated map is judged on its own and belongs to the world
|
|
829
|
+
// operation (ADR 0070).
|
|
830
|
+
const baselineValidation = newWorld
|
|
807
831
|
? { passed: true, diagnostics: [], checks: [] }
|
|
808
832
|
: await validateMapAsync(snapshot.index, signal === undefined ? {} : { signal });
|
|
809
833
|
// A new world has nothing to compare its bitmap format and pixels with.
|
|
810
834
|
const worldOperation = operations.find((operation) => operation.kind === 'create_world');
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
: {
|
|
820
|
-
|
|
835
|
+
// A world that failed to generate left the workspace map unchanged; validating that map would
|
|
836
|
+
// report only its old findings, so the generation blocker stands alone.
|
|
837
|
+
const worldFailed = worldOperation !== undefined &&
|
|
838
|
+
(plan.blockers.some(({ operationId }) => operationId === worldOperation.id) ||
|
|
839
|
+
synthesisCheck);
|
|
840
|
+
const rawValidation = worldFailed
|
|
841
|
+
? { passed: true, diagnostics: [], checks: [] }
|
|
842
|
+
: await validateMapAsync(plan.finalIndex, {
|
|
843
|
+
...(newWorld ? {} : { baseline: snapshot.index }),
|
|
844
|
+
...(worldOperation?.kind === 'create_world' && worldOperation.edge !== undefined
|
|
845
|
+
? { edgeMode: worldOperation.edge.mode ?? 'sea-band' }
|
|
846
|
+
: {}),
|
|
847
|
+
...(signal === undefined ? {} : { signal }),
|
|
848
|
+
...(plan.expectedChangedBounds === undefined
|
|
849
|
+
? {}
|
|
850
|
+
: { expectedChangedBounds: plan.expectedChangedBounds }),
|
|
851
|
+
});
|
|
821
852
|
const attributedValidation = {
|
|
822
853
|
...rawValidation,
|
|
823
854
|
diagnostics: attributeMapValidationDiagnostics({
|
|
@@ -833,6 +864,7 @@ export class AgentNudger {
|
|
|
833
864
|
validation: validationWithPlan(attributedValidation, plan),
|
|
834
865
|
referenceImages,
|
|
835
866
|
geography: [...geographyModels.values()],
|
|
867
|
+
territory: { reports: plan.territory ?? [], quality: territoryQuality },
|
|
836
868
|
imports,
|
|
837
869
|
};
|
|
838
870
|
}
|
|
@@ -992,6 +1024,25 @@ export class AgentNudger {
|
|
|
992
1024
|
}, prepared);
|
|
993
1025
|
return { ...diff, ...planned };
|
|
994
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Run the relief and river synthesis of every `create_world` with `synthesisCheck` and report
|
|
1029
|
+
* the model's rivers that cannot reach their mouths, without generating or writing anything.
|
|
1030
|
+
*/
|
|
1031
|
+
async checkSynthesis(input) {
|
|
1032
|
+
const prepared = await this.prepareOperations(input.workspaceId, input.operations, input.principal, input.signal, true);
|
|
1033
|
+
const writes = await this.geographyArtifacts(prepared, input.signal, new RenderBudget());
|
|
1034
|
+
const artifacts = await this.artifacts.withAtomicChunkedWrites(this.resolver.get(input.workspaceId, input.principal), writes, (stored) => Promise.resolve([...stored]), input.signal);
|
|
1035
|
+
return {
|
|
1036
|
+
plan: prepared.plan,
|
|
1037
|
+
geography: prepared.geography,
|
|
1038
|
+
diagnostics: sortDiagnostics([
|
|
1039
|
+
...prepared.plan.diagnostics,
|
|
1040
|
+
...prepared.geography.flatMap(({ diagnostics }) => diagnostics),
|
|
1041
|
+
]),
|
|
1042
|
+
artifacts,
|
|
1043
|
+
filesScanned: mapSourceEvidence(prepared.snapshot).map(({ displayPath }) => displayPath),
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
995
1046
|
/** Build validated offline evidence without planning or applying a write transaction. */
|
|
996
1047
|
async previewRewriteWithDiff(input) {
|
|
997
1048
|
const prepared = await this.prepareOperations(input.workspaceId, input.operations, input.principal, input.signal);
|
|
@@ -1295,6 +1346,17 @@ export class AgentNudger {
|
|
|
1295
1346
|
provenance: comparisonProvenance('before'),
|
|
1296
1347
|
});
|
|
1297
1348
|
writes.push(...(await this.geographyArtifacts(prepared, options.signal, budget)));
|
|
1349
|
+
if (prepared.territory.reports.length > 0 || prepared.territory.quality !== undefined)
|
|
1350
|
+
writes.push({
|
|
1351
|
+
name: 'map-territory-report.json',
|
|
1352
|
+
mimeType: 'application/json',
|
|
1353
|
+
content: `${canonicalJson(territoryArtifactContent(prepared.territory.reports, prepared.territory.quality))}\n`,
|
|
1354
|
+
provenance: {
|
|
1355
|
+
...provenance,
|
|
1356
|
+
kind: 'map-territory-report',
|
|
1357
|
+
schemaVersion: 'map-territory.v1',
|
|
1358
|
+
},
|
|
1359
|
+
});
|
|
1298
1360
|
for (const [ordinal, imported] of prepared.imports.entries())
|
|
1299
1361
|
writes.push(...(await buildImportArtifacts(imported, ordinal)));
|
|
1300
1362
|
if (plan.worldOutputs !== undefined)
|