2764 lines
130 KiB
JavaScript
2764 lines
130 KiB
JavaScript
import {
|
|
buildRawPatchTerrainScoutRequest,
|
|
capturePatchTransactionSnapshot,
|
|
evaluatePatchDraftCandidate,
|
|
generatePatch,
|
|
generatePatchAsync,
|
|
preparePatchOperationContext,
|
|
restorePatchTransactionSnapshot,
|
|
} from "./mapPatch.js";
|
|
import { collectTransferableBuffers } from "./transferUtils.js";
|
|
import { applyCommittedWorldDelta, hashCommittedWorld } from "./committedWorldDelta.js";
|
|
|
|
export { applyCommittedWorldDelta as applyCommittedMirrorDelta, hashCommittedWorld } from "./committedWorldDelta.js";
|
|
|
|
let persistentCommittedMirror = null;
|
|
let persistentCommittedRevision = -1;
|
|
let pendingMirrorBootstrap = null;
|
|
const pendingApplyDeltas = new Map();
|
|
|
|
const rawCandidateWorkerSlots = [];
|
|
const rawCandidateWorkerTerminations = [];
|
|
let rawCandidateTaskSerial = 0;
|
|
|
|
function normalizeWorkerTermination(value) {
|
|
return Promise.resolve(value).catch(() => undefined);
|
|
}
|
|
|
|
function destroyRawCandidateWorkerSlot(index, reason = "Raw patch candidate worker was terminated.") {
|
|
const slot = rawCandidateWorkerSlots[index];
|
|
if (!slot) return rawCandidateWorkerTerminations[index] || Promise.resolve();
|
|
rawCandidateWorkerSlots[index] = null;
|
|
const active = slot.active;
|
|
slot.active = null;
|
|
if (active) {
|
|
const error = new Error(reason);
|
|
error.code = "raw-candidate-worker-terminated";
|
|
active.reject(error);
|
|
}
|
|
let termination;
|
|
try {
|
|
termination = normalizeWorkerTermination(slot.worker?.terminate?.());
|
|
} catch {
|
|
termination = Promise.resolve();
|
|
}
|
|
const trackedTermination = termination.finally(() => {
|
|
if (rawCandidateWorkerTerminations[index] === trackedTermination) rawCandidateWorkerTerminations[index] = null;
|
|
});
|
|
rawCandidateWorkerTerminations[index] = trackedTermination;
|
|
return trackedTermination;
|
|
}
|
|
|
|
async function ensureRawCandidateWorkerSlot(index) {
|
|
if (rawCandidateWorkerTerminations[index]) await rawCandidateWorkerTerminations[index];
|
|
const existing = rawCandidateWorkerSlots[index];
|
|
if (existing?.worker) return existing;
|
|
if (typeof Worker !== "function") {
|
|
const error = new Error("Nested module workers are not available in this environment.");
|
|
error.code = "raw-candidate-worker-unavailable";
|
|
throw error;
|
|
}
|
|
const worker = new Worker(new URL("./patchCandidateWorker.js", import.meta.url), { type: "module" });
|
|
const slot = { worker, active: null, index };
|
|
worker.onmessage = (event) => {
|
|
const message = event.data || {};
|
|
const active = slot.active;
|
|
if (!active || Number(message.id) !== active.id) return;
|
|
if (message.type === active.progressType) {
|
|
active.onProgress?.(active.request, message.progress || {});
|
|
return;
|
|
}
|
|
if (message.type !== active.resultType) return;
|
|
slot.active = null;
|
|
if (!message.ok) {
|
|
const error = new Error(message.error || "Raw patch candidate generation failed.");
|
|
error.code = message.code || "raw-patch-candidate-error";
|
|
error.stack = message.stack || error.stack;
|
|
active.reject(error);
|
|
return;
|
|
}
|
|
active.resolve(message.candidate);
|
|
};
|
|
worker.onerror = (event) => {
|
|
const active = slot.active;
|
|
slot.active = null;
|
|
const error = new Error(event?.message || "Raw patch candidate worker failed.");
|
|
error.code = "raw-candidate-worker-error";
|
|
if (active) active.reject(error);
|
|
// Do not reuse a worker that raised an uncaught error. The next task in
|
|
// this lane waits for termination before constructing a replacement.
|
|
destroyRawCandidateWorkerSlot(index, error.message);
|
|
};
|
|
rawCandidateWorkerSlots[index] = slot;
|
|
return slot;
|
|
}
|
|
|
|
async function runRawHelperTask(slotIndex, request, onProgress, kind = "candidate") {
|
|
const slot = await ensureRawCandidateWorkerSlot(slotIndex);
|
|
if (slot.active) {
|
|
const error = new Error(`Raw candidate worker slot ${slotIndex} is unexpectedly busy.`);
|
|
error.code = "raw-candidate-worker-busy";
|
|
throw error;
|
|
}
|
|
const id = ++rawCandidateTaskSerial;
|
|
const terrainScoutOnly = kind === "terrain-scout";
|
|
const draftOnly = kind === "draft";
|
|
const resultType = terrainScoutOnly ? "raw-patch-terrain-scout-result" : draftOnly ? "raw-patch-draft-result" : "raw-patch-candidate-result";
|
|
const progressType = terrainScoutOnly ? "raw-patch-terrain-scout-progress" : draftOnly ? "raw-patch-draft-progress" : "raw-patch-candidate-progress";
|
|
return new Promise((resolve, reject) => {
|
|
slot.active = { id, request, onProgress, resolve, reject, resultType, progressType };
|
|
try {
|
|
slot.worker.postMessage({
|
|
type: terrainScoutOnly ? "generate-raw-patch-terrain-scout" : draftOnly ? "generate-raw-patch-draft" : "generate-raw-patch-candidate",
|
|
id,
|
|
taskId: request.taskId || `${terrainScoutOnly ? "raw-terrain-scout" : draftOnly ? "raw-draft" : "raw-candidate"}-${id}`,
|
|
seed: request.seed >>> 0,
|
|
mapOptions: request.mapOptions || {},
|
|
});
|
|
} catch (error) {
|
|
slot.active = null;
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function runRawCandidateTask(slotIndex, request, onProgress) {
|
|
return runRawHelperTask(slotIndex, request, onProgress, "candidate");
|
|
}
|
|
|
|
async function runRawDraftTask(slotIndex, request, onProgress) {
|
|
return runRawHelperTask(slotIndex, request, onProgress, "draft");
|
|
}
|
|
|
|
async function runRawTerrainScoutTask(slotIndex, request, onProgress) {
|
|
return runRawHelperTask(slotIndex, request, onProgress, "terrain-scout");
|
|
}
|
|
|
|
async function precomputeRawCandidateBatch(requests, onProgress) {
|
|
if (!Array.isArray(requests) || !requests.length) return [];
|
|
if (requests.length > 2) throw new Error(`Raw candidate batch exceeds the two-worker limit (${requests.length}).`);
|
|
try {
|
|
return await Promise.all(requests.map((request, index) => runRawCandidateTask(index, request, onProgress)));
|
|
} catch (error) {
|
|
// A failed child may have left its module worker in an unknown state. Reset
|
|
// the bounded pool before the coordinator falls back to serial generation.
|
|
await Promise.all(rawCandidateWorkerSlots.map((slot, index) => slot?.active ? destroyRawCandidateWorkerSlot(index) : null));
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function precomputeRawDraftBatch(requests, onProgress) {
|
|
if (!Array.isArray(requests) || !requests.length) return [];
|
|
if (requests.length > 2) throw new Error(`Raw draft batch exceeds the two-worker limit (${requests.length}).`);
|
|
try {
|
|
return await Promise.all(requests.map((request, index) => runRawDraftTask(index, request, onProgress)));
|
|
} catch (error) {
|
|
await Promise.all(rawCandidateWorkerSlots.map((slot, index) => slot?.active ? destroyRawCandidateWorkerSlot(index) : null));
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function precomputeRawTerrainScoutBatch(requests, onProgress) {
|
|
if (!Array.isArray(requests) || !requests.length) return [];
|
|
if (requests.length > 2) throw new Error(`Raw terrain-scout batch exceeds the two-worker limit (${requests.length}).`);
|
|
try {
|
|
return await Promise.all(requests.map((request, index) => runRawTerrainScoutTask(index, request, onProgress)));
|
|
} catch (error) {
|
|
await Promise.all(rawCandidateWorkerSlots.map((slot, index) => slot?.active ? destroyRawCandidateWorkerSlot(index) : null));
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Continuously feed at most two production helpers instead of waiting for
|
|
// fixed pairs to finish. Merge order remains strictly deterministic in
|
|
// mapPatch.js. Callers choose a bounded sliding window; production uses one
|
|
// resident result per lane so transferred candidate graphs cannot accumulate.
|
|
export function scheduleRawCandidateSequence(requests, onProgress, options = {}) {
|
|
const list = Array.isArray(requests) ? requests : [];
|
|
const count = list.length;
|
|
const laneCount = Math.max(1, Math.min(2, Math.floor(options.parallelism || 2), Math.max(1, count)));
|
|
const windowSize = Math.max(laneCount, Math.min(count || laneCount, Math.floor(options.windowSize || (laneCount + 1))));
|
|
const recycleWorkers = options.recycleWorkers === true;
|
|
// Keep each helper for a small bounded run. Reusing it forever lets
|
|
// generateMap's module-local/cache state accumulate; recycling after every
|
|
// tile creates excessive isolate churn on max-range selections. Three
|
|
// complete candidates per lane is the bounded middle ground.
|
|
const recycleEvery = recycleWorkers
|
|
? Math.max(1, Math.min(4, Math.floor(options.recycleEvery || 3)))
|
|
: Infinity;
|
|
const controls = list.map(() => {
|
|
let resolve;
|
|
let reject;
|
|
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
// The coordinator can stop after an earlier failure. Register a rejection
|
|
// observer now so later cancelled items cannot become unhandled rejections.
|
|
promise.catch(() => undefined);
|
|
return { promise, resolve, reject, settled: false };
|
|
});
|
|
const released = new Uint8Array(count);
|
|
let releasedPrefix = 0;
|
|
let nextToSchedule = 0;
|
|
let cancelled = false;
|
|
let cancelError = null;
|
|
const gateWaiters = new Set();
|
|
|
|
const wake = () => {
|
|
for (const resolve of gateWaiters) resolve();
|
|
gateWaiters.clear();
|
|
};
|
|
const waitForGate = () => new Promise((resolve) => gateWaiters.add(resolve));
|
|
const allowedEnd = () => Math.min(count, releasedPrefix + windowSize);
|
|
const takeNext = () => {
|
|
if (cancelled || nextToSchedule >= count) return -1;
|
|
if (nextToSchedule >= allowedEnd()) return -2;
|
|
return nextToSchedule++;
|
|
};
|
|
const failAll = (error) => {
|
|
if (cancelled) return;
|
|
cancelled = true;
|
|
cancelError = error instanceof Error ? error : new Error(String(error || "Raw candidate sequence cancelled."));
|
|
for (const control of controls) {
|
|
if (!control?.settled) {
|
|
control.settled = true;
|
|
const reject = control.reject;
|
|
// Promise capability functions keep their Promise reachable. Clear
|
|
// both callbacks as soon as the public Promise has settled so a
|
|
// transferred raw candidate cannot remain retained through scheduler
|
|
// bookkeeping after the coordinator drops publicPromises[index].
|
|
control.resolve = null;
|
|
control.reject = null;
|
|
reject?.(cancelError);
|
|
}
|
|
}
|
|
for (let index = 0; index < laneCount; index++) {
|
|
if (rawCandidateWorkerSlots[index]?.active) destroyRawCandidateWorkerSlot(index, cancelError.message);
|
|
}
|
|
wake();
|
|
};
|
|
|
|
const lane = async (slotIndex) => {
|
|
let tasksSinceRecycle = 0;
|
|
while (!cancelled) {
|
|
const index = takeNext();
|
|
if (index === -1) return;
|
|
if (index === -2) {
|
|
await waitForGate();
|
|
continue;
|
|
}
|
|
try {
|
|
let candidate = await runRawCandidateTask(slotIndex, list[index], onProgress);
|
|
if (cancelled) return;
|
|
const control = controls[index];
|
|
if (!control.settled) {
|
|
control.settled = true;
|
|
const resolve = control.resolve;
|
|
control.resolve = null;
|
|
control.reject = null;
|
|
resolve?.(candidate);
|
|
}
|
|
// The Promise result owns the transferred graph now. Do not keep an
|
|
// additional lane-local reference while waiting for the next window.
|
|
candidate = null;
|
|
// Reclaim helper-local generator state before it can grow without
|
|
// paying module-worker startup/teardown cost on every single tile.
|
|
tasksSinceRecycle++;
|
|
if (recycleWorkers && tasksSinceRecycle >= recycleEvery) {
|
|
await destroyRawCandidateWorkerSlot(slotIndex, `Raw candidate helper recycled after ${tasksSinceRecycle} completed tiles.`);
|
|
tasksSinceRecycle = 0;
|
|
}
|
|
} catch (error) {
|
|
failAll(error);
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Keep the fulfilled Promise graph only in the public array. Once the
|
|
// coordinator nulls an entry after merge, scheduler bookkeeping must not keep
|
|
// the transferred candidate reachable for the rest of the sequence.
|
|
const publicPromises = controls.map((control) => control.promise);
|
|
for (const control of controls) control.promise = null;
|
|
const lanes = count ? Array.from({ length: laneCount }, (_, index) => lane(index)) : [];
|
|
const done = Promise.allSettled(lanes).then(() => undefined);
|
|
|
|
return {
|
|
promises: publicPromises,
|
|
release(index) {
|
|
const i = Math.floor(Number(index));
|
|
if (i < 0 || i >= count || released[i]) return;
|
|
released[i] = 1;
|
|
while (releasedPrefix < count && released[releasedPrefix]) releasedPrefix++;
|
|
wake();
|
|
},
|
|
cancel(reason = "Raw candidate sequence cancelled.") {
|
|
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
if (!error.code) error.code = "raw-candidate-sequence-cancelled";
|
|
failAll(error);
|
|
return done;
|
|
},
|
|
done,
|
|
async dispose(reason = "Raw candidate sequence disposed.") {
|
|
await done;
|
|
await Promise.allSettled(Array.from({ length: laneCount }, (_, index) =>
|
|
rawCandidateWorkerSlots[index] ? destroyRawCandidateWorkerSlot(index, reason) : rawCandidateWorkerTerminations[index]
|
|
).filter(Boolean));
|
|
},
|
|
get cancelled() { return cancelled; },
|
|
get error() { return cancelError; },
|
|
laneCount,
|
|
windowSize,
|
|
};
|
|
}
|
|
|
|
export async function shutdownRawCandidateWorkers() {
|
|
const pending = [];
|
|
for (let index = 0; index < rawCandidateWorkerSlots.length; index++) {
|
|
if (rawCandidateWorkerSlots[index]) pending.push(destroyRawCandidateWorkerSlot(index, "Raw candidate operation completed."));
|
|
else if (rawCandidateWorkerTerminations[index]) pending.push(rawCandidateWorkerTerminations[index]);
|
|
}
|
|
if (pending.length) await Promise.allSettled(pending);
|
|
}
|
|
|
|
function isMirrorSyncMessage(type) {
|
|
return typeof type === "string" && type.startsWith("patch-mirror-sync-");
|
|
}
|
|
|
|
function safeMirrorKey(key, label = "key") {
|
|
const normalized = String(key ?? "");
|
|
if (!normalized || normalized === "__proto__" || normalized === "prototype" || normalized === "constructor") {
|
|
throw new Error(`Invalid mirror ${label}: ${normalized || "<empty>"}.`);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function defineMirrorEntry(target, key, value) {
|
|
Object.defineProperty(target, key, {
|
|
value,
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
function sameMirrorKeys(actual, expected) {
|
|
const actualKeys = Object.keys(actual || {});
|
|
if (!Array.isArray(expected) || actualKeys.length !== expected.length) return false;
|
|
const actualSet = new Set(actualKeys);
|
|
return expected.every((key) => actualSet.has(key));
|
|
}
|
|
|
|
function validateCompletedMirrorBootstrap(bootstrap) {
|
|
if (!bootstrap?.world || !bootstrap?.manifest) throw new Error("Mirror bootstrap is incomplete.");
|
|
const { world, manifest } = bootstrap;
|
|
if (!sameMirrorKeys(world.fields, manifest.fieldKeys)) throw new Error("Mirror field manifest is incomplete.");
|
|
if (!sameMirrorKeys(world.sourceMap, manifest.sourceKeys)) throw new Error("Mirror sourceMap manifest is incomplete.");
|
|
const rootActual = Object.keys(world).filter((key) => key !== "fields" && key !== "sourceMap" && key !== "generatedMask");
|
|
const rootExpected = Array.isArray(manifest.rootKeys) ? manifest.rootKeys : [];
|
|
if (rootActual.length !== rootExpected.length || !rootExpected.every((key) => rootActual.includes(key))) {
|
|
throw new Error("Mirror root manifest is incomplete.");
|
|
}
|
|
if (!!manifest.hasGeneratedMask !== !!world.generatedMask) throw new Error("Mirror generatedMask manifest is incomplete.");
|
|
const width = Number(world.width);
|
|
const height = Number(world.height);
|
|
const area = width * height;
|
|
if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0 || !Number.isSafeInteger(area) || area <= 0) {
|
|
throw new Error("Mirror dimensions are invalid.");
|
|
}
|
|
for (const key of manifest.fieldKeys || []) {
|
|
const field = world.fields[key];
|
|
if (!ArrayBuffer.isView(field) || field.length !== area) {
|
|
throw new Error(`Mirror field ${key} has invalid storage (${field?.length ?? "missing"} != ${area}).`);
|
|
}
|
|
}
|
|
if (manifest.hasGeneratedMask && (!ArrayBuffer.isView(world.generatedMask) || world.generatedMask.length !== area)) {
|
|
throw new Error(`Mirror generatedMask has invalid storage (${world.generatedMask?.length ?? "missing"} != ${area}).`);
|
|
}
|
|
for (const [key, childKeys] of Object.entries(manifest.expandedSourceObjects || {})) {
|
|
if (!world.sourceMap[key] || !sameMirrorKeys(world.sourceMap[key], childKeys)) {
|
|
throw new Error(`Mirror sourceMap.${key} manifest is incomplete.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleMirrorSyncMessage(incoming) {
|
|
if (!isMirrorSyncMessage(incoming?.type)) return false;
|
|
const type = incoming.type;
|
|
const syncId = String(incoming.syncId || "");
|
|
const sequence = Number(incoming.sequence || 0);
|
|
const acknowledge = (ok, error = null, extra = {}) => {
|
|
self.postMessage({
|
|
id: incoming.id,
|
|
type: "patch-mirror-sync-ack",
|
|
syncId,
|
|
sequence,
|
|
stage: type,
|
|
ok,
|
|
error,
|
|
...extra,
|
|
});
|
|
};
|
|
try {
|
|
if (!syncId || !Number.isSafeInteger(sequence) || sequence <= 0) throw new Error("Invalid mirror synchronization envelope.");
|
|
if (type === "patch-mirror-sync-start") {
|
|
const manifest = incoming.manifest || {};
|
|
if (!Array.isArray(manifest.rootKeys) || !Array.isArray(manifest.fieldKeys) || !Array.isArray(manifest.sourceKeys)) {
|
|
throw new Error("Mirror synchronization manifest is invalid.");
|
|
}
|
|
pendingMirrorBootstrap = {
|
|
id: incoming.id,
|
|
syncId,
|
|
committedRevision: Number(incoming.committedRevision ?? -1),
|
|
lastSequence: sequence,
|
|
manifest,
|
|
world: { fields: {}, sourceMap: {} },
|
|
};
|
|
acknowledge(true);
|
|
return true;
|
|
}
|
|
const bootstrap = pendingMirrorBootstrap;
|
|
if (!bootstrap || bootstrap.syncId !== syncId || bootstrap.id !== incoming.id) throw new Error("Mirror synchronization session is unavailable or stale.");
|
|
if (sequence <= bootstrap.lastSequence) throw new Error(`Mirror synchronization sequence did not advance (${sequence} <= ${bootstrap.lastSequence}).`);
|
|
bootstrap.lastSequence = sequence;
|
|
if (type === "patch-mirror-sync-root") {
|
|
const key = safeMirrorKey(incoming.key, "root key");
|
|
if (!bootstrap.manifest.rootKeys.includes(key)) throw new Error(`Unexpected mirror root key ${key}.`);
|
|
defineMirrorEntry(bootstrap.world, key, incoming.value);
|
|
} else if (type === "patch-mirror-sync-field") {
|
|
const key = safeMirrorKey(incoming.key, "field key");
|
|
if (!bootstrap.manifest.fieldKeys.includes(key)) throw new Error(`Unexpected mirror field ${key}.`);
|
|
defineMirrorEntry(bootstrap.world.fields, key, incoming.value);
|
|
} else if (type === "patch-mirror-sync-generated-mask") {
|
|
if (!bootstrap.manifest.hasGeneratedMask) throw new Error("Unexpected mirror generatedMask.");
|
|
bootstrap.world.generatedMask = incoming.value;
|
|
} else if (type === "patch-mirror-sync-source") {
|
|
const key = safeMirrorKey(incoming.key, "source key");
|
|
if (!bootstrap.manifest.sourceKeys.includes(key)) throw new Error(`Unexpected mirror source key ${key}.`);
|
|
if (Object.prototype.hasOwnProperty.call(bootstrap.manifest.expandedSourceObjects || {}, key)) {
|
|
throw new Error(`Mirror source key ${key} must be synchronized by child entries.`);
|
|
}
|
|
defineMirrorEntry(bootstrap.world.sourceMap, key, incoming.value);
|
|
} else if (type === "patch-mirror-sync-source-object-start") {
|
|
const key = safeMirrorKey(incoming.key, "source object key");
|
|
if (!Object.prototype.hasOwnProperty.call(bootstrap.manifest.expandedSourceObjects || {}, key)) {
|
|
throw new Error(`Unexpected expanded mirror source key ${key}.`);
|
|
}
|
|
defineMirrorEntry(bootstrap.world.sourceMap, key, {});
|
|
} else if (type === "patch-mirror-sync-source-object-entry") {
|
|
const key = safeMirrorKey(incoming.key, "source object key");
|
|
const childKey = safeMirrorKey(incoming.childKey, "source child key");
|
|
const expectedChildren = bootstrap.manifest.expandedSourceObjects?.[key];
|
|
if (!Array.isArray(expectedChildren) || !expectedChildren.includes(childKey) || !bootstrap.world.sourceMap[key]) {
|
|
throw new Error(`Unexpected mirror source child ${key}.${childKey}.`);
|
|
}
|
|
defineMirrorEntry(bootstrap.world.sourceMap[key], childKey, incoming.value);
|
|
} else if (type === "patch-mirror-sync-finish") {
|
|
validateCompletedMirrorBootstrap(bootstrap);
|
|
persistentCommittedMirror = bootstrap.world;
|
|
persistentCommittedRevision = bootstrap.committedRevision;
|
|
pendingMirrorBootstrap = null;
|
|
pendingApplyDeltas.clear();
|
|
acknowledge(true, null, { mirrorCommittedRevision: persistentCommittedRevision });
|
|
return true;
|
|
} else if (type === "patch-mirror-sync-abort") {
|
|
pendingMirrorBootstrap = null;
|
|
acknowledge(true);
|
|
return true;
|
|
} else {
|
|
throw new Error(`Unknown mirror synchronization stage ${type}.`);
|
|
}
|
|
acknowledge(true);
|
|
} catch (error) {
|
|
if (type === "patch-mirror-sync-start" || pendingMirrorBootstrap?.syncId === syncId) pendingMirrorBootstrap = null;
|
|
acknowledge(false, error?.message || String(error));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function valuesEqual(a, b) {
|
|
return a === b || (Number.isNaN(a) && Number.isNaN(b));
|
|
}
|
|
|
|
function exactStructuredEqual(a, b) {
|
|
if (valuesEqual(a, b)) return true;
|
|
if (a == null || b == null || typeof a !== "object" || typeof b !== "object") return false;
|
|
if (ArrayBuffer.isView(a) || ArrayBuffer.isView(b)) {
|
|
if (!ArrayBuffer.isView(a) || !ArrayBuffer.isView(b)
|
|
|| a.constructor !== b.constructor || a.length !== b.length) return false;
|
|
for (let index = 0; index < a.length; index++) if (!valuesEqual(a[index], b[index])) return false;
|
|
return true;
|
|
}
|
|
if (Array.isArray(a) || Array.isArray(b)) {
|
|
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
for (let index = 0; index < a.length; index++) if (!exactStructuredEqual(a[index], b[index])) return false;
|
|
// Generated paths use one enumerable array annotation. Compare it directly
|
|
// instead of allocating Object.keys arrays for every coordinate tuple.
|
|
return a.patchGenerated === b.patchGenerated;
|
|
}
|
|
if (a instanceof Date || b instanceof Date) return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
if (a instanceof Map || b instanceof Map) {
|
|
if (!(a instanceof Map) || !(b instanceof Map) || a.size !== b.size) return false;
|
|
for (const [key, value] of a) if (!b.has(key) || !exactStructuredEqual(value, b.get(key))) return false;
|
|
return true;
|
|
}
|
|
if (a instanceof Set || b instanceof Set) {
|
|
if (!(a instanceof Set) || !(b instanceof Set) || a.size !== b.size) return false;
|
|
for (const value of a) if (!b.has(value)) return false;
|
|
return true;
|
|
}
|
|
const aKeys = Object.keys(a);
|
|
const bKeys = Object.keys(b);
|
|
if (aKeys.length !== bKeys.length) return false;
|
|
for (let index = 0; index < aKeys.length; index++) {
|
|
const key = aKeys[index];
|
|
if (key !== bKeys[index] || !exactStructuredEqual(a[key], b[key])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isDensePlainArray(value) {
|
|
if (!Array.isArray(value)) return false;
|
|
const keys = Object.keys(value);
|
|
if (keys.length !== value.length) return false;
|
|
for (let index = 0; index < keys.length; index++) if (keys[index] !== String(index)) return false;
|
|
return true;
|
|
}
|
|
|
|
function buildExactArraySplice(base, next, { cloneValues = true } = {}) {
|
|
if (!isDensePlainArray(base) || !isDensePlainArray(next)) return null;
|
|
const commonLimit = Math.min(base.length, next.length);
|
|
let start = 0;
|
|
while (start < commonLimit && exactStructuredEqual(base[start], next[start])) start++;
|
|
if (start === base.length && start === next.length) return { unchanged: true };
|
|
let suffix = 0;
|
|
while (suffix < commonLimit - start
|
|
&& exactStructuredEqual(base[base.length - 1 - suffix], next[next.length - 1 - suffix])) suffix++;
|
|
const items = next.slice(start, next.length - suffix);
|
|
return {
|
|
start,
|
|
deleteCount: base.length - start - suffix,
|
|
items: cloneValues ? structuredClone(items) : items,
|
|
};
|
|
}
|
|
|
|
function buildExactObjectDelta(base = {}, next = {}, { cloneValues = true } = {}) {
|
|
const set = {};
|
|
const arraySplices = {};
|
|
for (const [key, value] of Object.entries(next || {})) {
|
|
if (!Object.prototype.hasOwnProperty.call(base || {}, key)) {
|
|
set[key] = cloneValues ? structuredClone(value) : value;
|
|
continue;
|
|
}
|
|
if (isDensePlainArray(base[key]) && isDensePlainArray(value)) {
|
|
const splice = buildExactArraySplice(base[key], value, { cloneValues });
|
|
if (splice && !splice.unchanged) arraySplices[key] = splice;
|
|
continue;
|
|
}
|
|
if (!exactStructuredEqual(base[key], value)) set[key] = cloneValues ? structuredClone(value) : value;
|
|
}
|
|
const removed = Object.keys(base || {}).filter((key) => !Object.prototype.hasOwnProperty.call(next || {}, key));
|
|
return { set, arraySplices, removed };
|
|
}
|
|
|
|
function buildTypedRowDelta(base, next, width) {
|
|
if (!ArrayBuffer.isView(next)) return null;
|
|
if (!ArrayBuffer.isView(base) || base.constructor?.name !== next.constructor?.name || base.length !== next.length) {
|
|
return { constructorName: next.constructor.name, length: next.length, replace: new next.constructor(next) };
|
|
}
|
|
const rows = [];
|
|
const rowWidth = Math.max(1, Number(width || next.length));
|
|
for (let rowStart = 0; rowStart < next.length; rowStart += rowWidth) {
|
|
const rowEnd = Math.min(next.length, rowStart + rowWidth);
|
|
let first = -1;
|
|
let last = -1;
|
|
for (let index = rowStart; index < rowEnd; index++) {
|
|
if (valuesEqual(base[index], next[index])) continue;
|
|
if (first < 0) first = index;
|
|
last = index;
|
|
}
|
|
if (first >= 0) rows.push({ start: first, values: new next.constructor(next.subarray(first, last + 1)) });
|
|
}
|
|
return rows.length ? { constructorName: next.constructor.name, length: next.length, rows } : null;
|
|
}
|
|
|
|
export function buildCommittedMirrorDelta(baseWorld, nextWorld) {
|
|
if (!baseWorld || !nextWorld) throw new Error("Cannot build a committed mirror delta without both worlds.");
|
|
const fieldNames = new Set([...Object.keys(baseWorld.fields || {}), ...Object.keys(nextWorld.fields || {})]);
|
|
const fields = {};
|
|
for (const name of fieldNames) {
|
|
const next = nextWorld.fields?.[name];
|
|
if (!next) {
|
|
fields[name] = { remove: true };
|
|
continue;
|
|
}
|
|
const delta = buildTypedRowDelta(baseWorld.fields?.[name], next, nextWorld.width);
|
|
if (delta) fields[name] = delta;
|
|
}
|
|
const generatedMask = buildTypedRowDelta(baseWorld.generatedMask, nextWorld.generatedMask, nextWorld.width);
|
|
const baseMeta = {};
|
|
const nextMeta = {};
|
|
for (const [key, value] of Object.entries(baseWorld)) {
|
|
if (key !== "fields" && key !== "generatedMask" && key !== "sourceMap") baseMeta[key] = value;
|
|
}
|
|
for (const [key, value] of Object.entries(nextWorld)) {
|
|
if (key !== "fields" && key !== "generatedMask" && key !== "sourceMap") nextMeta[key] = value;
|
|
}
|
|
const exactDeltas = structuredClone({
|
|
sourceMapDelta: buildExactObjectDelta(baseWorld.sourceMap || {}, nextWorld.sourceMap || {}, { cloneValues: false }),
|
|
metaDelta: buildExactObjectDelta(baseMeta, nextMeta, { cloneValues: false }),
|
|
});
|
|
return {
|
|
width: nextWorld.width,
|
|
height: nextWorld.height,
|
|
fields,
|
|
generatedMask,
|
|
sourceMapDelta: exactDeltas.sourceMapDelta,
|
|
metaDelta: exactDeltas.metaDelta,
|
|
};
|
|
}
|
|
|
|
export function buildMainThreadTransferDelta(delta) {
|
|
if (!delta) return null;
|
|
// Raster row buffers are transferred to the main thread and therefore need
|
|
// a Worker-side copy so the retained Apply-ACK delta is not detached. Plain
|
|
// metadata is already cloned once by postMessage; cloning that graph here as
|
|
// well only doubled paths, points, histories and diagnostics at peak memory.
|
|
const raster = structuredClone({
|
|
fields: delta.fields || {},
|
|
generatedMask: delta.generatedMask || null,
|
|
});
|
|
return {
|
|
...delta,
|
|
fields: raster.fields,
|
|
generatedMask: raster.generatedMask,
|
|
};
|
|
}
|
|
|
|
function markPreviewChange(changeTracker, worldIndex, category = 0) {
|
|
if (!changeTracker?.mask) return;
|
|
const width = changeTracker.worldWidth;
|
|
const x = worldIndex % width;
|
|
const y = Math.floor(worldIndex / width);
|
|
const rect = changeTracker.rect;
|
|
if (x < rect.x0 || y < rect.y0 || x >= rect.x1 || y >= rect.y1) return;
|
|
const localIndex = (y - rect.y0) * changeTracker.width + (x - rect.x0);
|
|
changeTracker.mask[localIndex] |= 1 | category;
|
|
}
|
|
|
|
function markAllPreviewChanges(changeTracker, category = 0) {
|
|
if (!changeTracker?.mask) return;
|
|
const flags = 1 | category;
|
|
changeTracker.mask.fill(flags);
|
|
}
|
|
|
|
function buildTypedRowDeltaFromTransaction(snapshot, name, next, changeTracker = null) {
|
|
if (!ArrayBuffer.isView(next)) return null;
|
|
const baseRef = snapshot?.fieldRefs?.get(name);
|
|
if (!ArrayBuffer.isView(baseRef) || baseRef.constructor?.name !== next.constructor?.name || baseRef.length !== next.length) {
|
|
const category = PREVIEW_TERRAIN_FIELDS.has(name) ? 2 : PREVIEW_ADMIN_FIELDS.has(name) ? 4 : 0;
|
|
markAllPreviewChanges(changeTracker, category);
|
|
return { constructorName: next.constructor.name, length: next.length, replace: new next.constructor(next) };
|
|
}
|
|
const localEntry = snapshot?.fields?.get(name) || null;
|
|
const rect = snapshot?.fieldRect || null;
|
|
const width = Math.max(1, Number(snapshot?.width || next.length));
|
|
const rows = [];
|
|
const scanLocalRect = localEntry && rect;
|
|
const startY = scanLocalRect ? Math.max(0, rect.y0) : 0;
|
|
const endY = scanLocalRect ? Math.min(snapshot.height, rect.y1) : Math.ceil(next.length / width);
|
|
for (let y = startY; y < endY; y++) {
|
|
const rowStart = y * width;
|
|
const rowEnd = Math.min(next.length, rowStart + width);
|
|
const scanStart = scanLocalRect ? Math.max(rowStart, rowStart + rect.x0) : rowStart;
|
|
const scanEnd = scanLocalRect ? Math.min(rowEnd, rowStart + rect.x1) : rowEnd;
|
|
let first = -1;
|
|
let last = -1;
|
|
for (let index = scanStart; index < scanEnd; index++) {
|
|
const x = index - rowStart;
|
|
let oldValue = baseRef[index];
|
|
if (localEntry && rect && x >= rect.x0 && x < rect.x1 && y >= rect.y0 && y < rect.y1) {
|
|
oldValue = localEntry.data[(y - rect.y0) * snapshot.fieldWidth + (x - rect.x0)];
|
|
}
|
|
if (valuesEqual(oldValue, next[index])) continue;
|
|
const category = PREVIEW_TERRAIN_FIELDS.has(name) ? 2 : PREVIEW_ADMIN_FIELDS.has(name) ? 4 : 0;
|
|
markPreviewChange(changeTracker, index, category);
|
|
if (first < 0) first = index;
|
|
last = index;
|
|
}
|
|
if (first >= 0) rows.push({ start: first, values: new next.constructor(next.subarray(first, last + 1)) });
|
|
}
|
|
return rows.length ? { constructorName: next.constructor.name, length: next.length, rows } : null;
|
|
}
|
|
|
|
export function buildCommittedMirrorDeltaFromTransaction(snapshot, nextWorld) {
|
|
if (!snapshot || snapshot.lightweight || !nextWorld) throw new Error("A full patch transaction snapshot is required.");
|
|
const fieldNames = new Set([...(snapshot.fieldNames || []), ...Object.keys(nextWorld.fields || {})]);
|
|
const fields = {};
|
|
const snapshotRect = snapshot.fieldRect || { x0: 0, y0: 0, x1: nextWorld.width, y1: nextWorld.height };
|
|
const changeRect = {
|
|
x0: Math.max(0, Math.floor(snapshotRect.x0 || 0)),
|
|
y0: Math.max(0, Math.floor(snapshotRect.y0 || 0)),
|
|
x1: Math.min(nextWorld.width, Math.ceil(snapshotRect.x1 || 0)),
|
|
y1: Math.min(nextWorld.height, Math.ceil(snapshotRect.y1 || 0)),
|
|
};
|
|
const changeTracker = {
|
|
rect: changeRect,
|
|
width: Math.max(0, changeRect.x1 - changeRect.x0),
|
|
worldWidth: nextWorld.width,
|
|
mask: null,
|
|
};
|
|
changeTracker.mask = new Uint8Array(changeTracker.width * Math.max(0, changeRect.y1 - changeRect.y0));
|
|
for (const name of fieldNames) {
|
|
const next = nextWorld.fields?.[name];
|
|
if (!next) {
|
|
if (snapshot.fieldNames?.has(name)) {
|
|
fields[name] = { remove: true };
|
|
const category = PREVIEW_TERRAIN_FIELDS.has(name) ? 2 : PREVIEW_ADMIN_FIELDS.has(name) ? 4 : 0;
|
|
markAllPreviewChanges(changeTracker, category);
|
|
}
|
|
continue;
|
|
}
|
|
const delta = buildTypedRowDeltaFromTransaction(snapshot, name, next, changeTracker);
|
|
if (delta) fields[name] = delta;
|
|
}
|
|
const baseMeta = {};
|
|
const nextMeta = {};
|
|
for (const [key, value] of Object.entries(nextWorld)) {
|
|
if (key !== "fields" && key !== "generatedMask" && key !== "sourceMap") {
|
|
baseMeta[key] = value;
|
|
nextMeta[key] = value;
|
|
}
|
|
}
|
|
Object.assign(baseMeta, {
|
|
width: snapshot.width,
|
|
height: snapshot.height,
|
|
originX: snapshot.originX,
|
|
originY: snapshot.originY,
|
|
sourceWidth: snapshot.sourceWidth,
|
|
sourceHeight: snapshot.sourceHeight,
|
|
generatedRects: snapshot.generatedRects,
|
|
lastPatchResult: snapshot.lastPatchResult,
|
|
patchGenerationSerial: snapshot.patchGenerationSerial,
|
|
seaLevel: snapshot.seaLevel,
|
|
});
|
|
const rawSourceMapDelta = buildExactObjectDelta(snapshot.sourceMap || {}, nextWorld.sourceMap || {}, { cloneValues: false });
|
|
const rawMetaDelta = buildExactObjectDelta(baseMeta, nextMeta, { cloneValues: false });
|
|
// Transaction restore replaces world/sourceMap roots; it does not mutate the
|
|
// accepted arrays and diagnostic objects referenced by these deltas. Adopt
|
|
// that completed graph directly. Cloning it here duplicated all changed
|
|
// metadata immediately before rollback and the later postMessage clone.
|
|
const exactDeltas = { sourceMapDelta: rawSourceMapDelta, metaDelta: rawMetaDelta };
|
|
let changedCells = 0;
|
|
let terrainChangedCells = 0;
|
|
let adminChangedCells = 0;
|
|
for (const flags of changeTracker.mask) {
|
|
if (flags & 1) changedCells++;
|
|
if (flags & 2) terrainChangedCells++;
|
|
if (flags & 4) adminChangedCells++;
|
|
}
|
|
const changedSourceKeys = new Set([
|
|
...Object.keys(rawSourceMapDelta.set || {}),
|
|
...Object.keys(rawSourceMapDelta.arraySplices || {}),
|
|
...(rawSourceMapDelta.removed || []),
|
|
]);
|
|
let featureLayersChanged = 0;
|
|
for (const key of PREVIEW_FEATURE_KEYS) if (changedSourceKeys.has(key)) featureLayersChanged++;
|
|
return {
|
|
width: nextWorld.width,
|
|
height: nextWorld.height,
|
|
fields,
|
|
generatedMask: buildTypedRowDelta(
|
|
snapshot.generatedMask || snapshot.generatedMaskRef,
|
|
nextWorld.generatedMask,
|
|
nextWorld.width
|
|
),
|
|
sourceMapDelta: exactDeltas.sourceMapDelta,
|
|
metaDelta: exactDeltas.metaDelta,
|
|
previewDelta: {
|
|
changedCells,
|
|
terrainChangedCells,
|
|
adminChangedCells,
|
|
featureLayersChanged,
|
|
identical: changedCells === 0 && featureLayersChanged === 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
function nowMs() {
|
|
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
}
|
|
|
|
function compactQuality(quality) {
|
|
if (!quality || typeof quality !== "object") return null;
|
|
return {
|
|
hardPass: quality.hardPass !== false,
|
|
score: Number(quality.score || 0),
|
|
selectedVariant: Number.isFinite(quality.selectedVariant) ? quality.selectedVariant >>> 0 : null,
|
|
terrainType: quality.terrainType || quality.terrain?.terrainType || null,
|
|
terrain: quality.terrain ? {
|
|
hardPass: quality.terrain.hardPass !== false,
|
|
landRatio: Number(quality.terrain.landRatio || 0),
|
|
developableRatio: Number(quality.terrain.developableRatio || 0),
|
|
largestComponentRatio: Number(quality.terrain.largestComponentRatio || 0),
|
|
} : null,
|
|
human: quality.human ? {
|
|
hardPass: quality.human.hardPass !== false,
|
|
settlementCount: Number(quality.human.settlementCount || 0),
|
|
labelCount: Number(quality.human.labelCount || 0),
|
|
roadPaths: Number(quality.human.roadPaths || 0),
|
|
railPaths: Number(quality.human.railPaths || 0),
|
|
} : null,
|
|
finalMerge: quality.finalMerge ? {
|
|
hardPass: quality.finalMerge.hardPass !== false,
|
|
ownedCells: Number(quality.finalMerge.ownedCells || 0),
|
|
ownedLandCells: Number(quality.finalMerge.ownedLandCells || 0),
|
|
ownedLandRatio: Number(quality.finalMerge.ownedLandRatio || 0),
|
|
landFloor: Number(quality.finalMerge.landFloor || 0),
|
|
settlementCount: Number(quality.finalMerge.settlementCount || 0),
|
|
minFinalSettlements: Number(quality.finalMerge.minFinalSettlements || 0),
|
|
preMergeSettlementCount: Number.isFinite(quality.finalMerge.preMergeSettlementCount)
|
|
? Number(quality.finalMerge.preMergeSettlementCount) : null,
|
|
candidateMinimumSettlements: Number(quality.finalMerge.candidateMinimumSettlements || 0),
|
|
labelCount: Number(quality.finalMerge.labelCount || 0),
|
|
minFinalLabels: Number(quality.finalMerge.minFinalLabels || 0),
|
|
preMergeLabelCount: Number.isFinite(quality.finalMerge.preMergeLabelCount)
|
|
? Number(quality.finalMerge.preMergeLabelCount) : null,
|
|
candidateMinimumLabels: Number(quality.finalMerge.candidateMinimumLabels || 0),
|
|
adminCenters: Number(quality.finalMerge.counts?.adminCenters || 0),
|
|
minFinalAdminCenters: Number(quality.finalMerge.minFinalAdminCenters || 0),
|
|
transportRequired: quality.finalMerge.transportRequired === true,
|
|
roadPaths: Number(quality.finalMerge.roadPaths || 0),
|
|
railPaths: Number(quality.finalMerge.railPaths || 0),
|
|
transportHierarchyPass: quality.finalMerge.transportHierarchyPass !== false,
|
|
transportTopology: quality.finalMerge.transportTopology ? structuredClone(quality.finalMerge.transportTopology) : null,
|
|
prefectureCoherence: quality.finalMerge.prefectureCoherence ? structuredClone(quality.finalMerge.prefectureCoherence) : null,
|
|
transportClasses: quality.finalMerge.transportClasses
|
|
? Object.fromEntries(Object.entries(quality.finalMerge.transportClasses).map(([key, value]) => [key, {
|
|
paths: Number(value?.paths || 0),
|
|
cells: Number(value?.cells || 0),
|
|
pathsPer1000Land: Number(value?.pathsPer1000Land || 0),
|
|
cellsPer1000Land: Number(value?.cellsPer1000Land || 0),
|
|
}]))
|
|
: null,
|
|
transportRequirements: quality.finalMerge.transportRequirements
|
|
? Object.fromEntries(Object.entries(quality.finalMerge.transportRequirements).map(([key, value]) => [key, {
|
|
// The producer field is `demand`; the old compact diagnostic read a
|
|
// nonexistent `required` property, making every required trunk class
|
|
// appear optional and allowing regression tests to silently skip it.
|
|
demand: value?.demand === true,
|
|
required: value?.demand === true,
|
|
minPaths: Number(value?.minPaths || 0),
|
|
minCells: Number(value?.minCells || 0),
|
|
expectedPaths: Number(value?.expectedPaths || 0),
|
|
expectedCells: Number(value?.expectedCells || 0),
|
|
}]))
|
|
: null,
|
|
rectangularCoastHardPass: quality.finalMerge.rectangularCoastHardPass !== false,
|
|
rectangularCoastRun: Number(quality.finalMerge.rectangularCoastCut?.maxAxisAlignedRun || 0),
|
|
rectangularCoastLongestRun: quality.finalMerge.rectangularCoastCut?.longestRun
|
|
? { ...quality.finalMerge.rectangularCoastCut.longestRun }
|
|
: null,
|
|
rectangularCoastCutScope: quality.finalMerge.rectangularCoastCutScope || null,
|
|
score: Number(quality.finalMerge.score || 0),
|
|
} : null,
|
|
};
|
|
}
|
|
|
|
function compactSeam(diagnostics) {
|
|
if (!diagnostics || typeof diagnostics !== "object") return null;
|
|
return {
|
|
hardPass: diagnostics.hardPass !== false,
|
|
status: diagnostics.status || null,
|
|
gateReasons: [...(diagnostics.gateReasons || [])],
|
|
roadPortalsBroken: Number(diagnostics.roadPortalsBroken || 0),
|
|
railPortalsBroken: Number(diagnostics.railPortalsBroken || 0),
|
|
prefectureSeamBreakEdges: Number(diagnostics.prefectureSeamBreakEdges || 0),
|
|
adminSeamBreakEdges: Number(diagnostics.adminSeamBreakEdges || 0),
|
|
landToSeaCells: Number(diagnostics.landToSeaCells || 0),
|
|
transportLandToSeaConflicts: Number(diagnostics.transportLandToSeaConflicts || 0),
|
|
duplicateBoundaryPairs: Number(diagnostics.duplicateBoundaryPairs || 0),
|
|
expansionFootprintEscapedCells: Number(diagnostics.expansionFootprintEscapedCells || 0),
|
|
maxEstablishedFrontierElevationJump: Number(diagnostics.maxEstablishedFrontierElevationJump || 0),
|
|
humanTerrainReconciliation: diagnostics.humanTerrainReconciliation ? {
|
|
checked: Number(diagnostics.humanTerrainReconciliation.checked || 0),
|
|
alreadyValid: Number(diagnostics.humanTerrainReconciliation.alreadyValid || 0),
|
|
relocated: Number(diagnostics.humanTerrainReconciliation.relocated || 0),
|
|
dropped: Number(diagnostics.humanTerrainReconciliation.dropped || 0),
|
|
stagedOutsideOwnership: Number(diagnostics.humanTerrainReconciliation.stagedOutsideOwnership || 0),
|
|
byLayer: diagnostics.humanTerrainReconciliation.byLayer
|
|
? Object.fromEntries(Object.entries(diagnostics.humanTerrainReconciliation.byLayer).map(([key, value]) => [key, {
|
|
checked: Number(value?.checked || 0),
|
|
alreadyValid: Number(value?.alreadyValid || 0),
|
|
relocated: Number(value?.relocated || 0),
|
|
dropped: Number(value?.dropped || 0),
|
|
stagedOutsideOwnership: Number(value?.stagedOutsideOwnership || 0),
|
|
}]))
|
|
: {},
|
|
} : null,
|
|
};
|
|
}
|
|
|
|
function isInvariantFailure(result) {
|
|
if (result?.tileResult && isInvariantFailure(result.tileResult)) return true;
|
|
const code = String(result?.code || "");
|
|
const reasons = result?.seamDiagnostics?.gateReasons || [];
|
|
return code === "patch-candidate-coverage-incomplete"
|
|
|| code === "patch-generated-footprint-write-escape"
|
|
|| code === "patch-invariant-breach"
|
|
|| reasons.includes("generated-footprint-write-escape")
|
|
|| Number(result?.candidateUnmappedActiveCells || 0) > 0;
|
|
}
|
|
|
|
function isContentRejection(result) {
|
|
const code = String(result?.code || "");
|
|
if (isInvariantFailure(result)) return false;
|
|
if ((code === "patch-large-tile-failed" || code === "patch-large-regeneration-tile-failed") && result?.tileResult) {
|
|
return isContentRejection(result.tileResult);
|
|
}
|
|
return code === "patch-quality-gate-failed"
|
|
|| code === "patch-seam-gate-failed"
|
|
|| code === "patch-large-final-seam-failed";
|
|
}
|
|
|
|
function normalizeCandidateResult(result) {
|
|
if (!result?.ok || result?.seamDiagnostics?.hardPass !== false) return result;
|
|
return {
|
|
...result,
|
|
ok: false,
|
|
code: "patch-seam-gate-failed",
|
|
reason: `Candidate failed final seam continuity (${(result.seamDiagnostics.gateReasons || []).join(", ") || "unknown seam failure"}).`,
|
|
rolledBack: true,
|
|
};
|
|
}
|
|
|
|
function summarizeAttempt(result, candidate, candidateOrdinal, wallMs, status, executionAttempt = 1) {
|
|
return {
|
|
candidateId: candidate.candidateId || `${candidate.variant >>> 0}:${candidate.seed >>> 0}`,
|
|
candidateOrdinal,
|
|
executionAttempt: Math.max(1, Number(executionAttempt || 1)),
|
|
variant: candidate.variant >>> 0,
|
|
seed: candidate.seed >>> 0,
|
|
status,
|
|
ok: result?.ok === true,
|
|
code: result?.code || null,
|
|
reason: result?.reason || null,
|
|
patchMode: result?.patchMode || null,
|
|
tileCount: Number(result?.tileCount || 0),
|
|
wallMs: Math.round(wallMs * 10) / 10,
|
|
candidateQuality: compactQuality(result?.candidateQuality),
|
|
seamDiagnostics: compactSeam(result?.seamDiagnostics),
|
|
patchTimings: (result?.patchTimings || []).map((entry) => ({
|
|
key: entry.key || null,
|
|
label: entry.label || entry.key || "Stage",
|
|
ms: Number(entry.ms || 0),
|
|
})),
|
|
};
|
|
}
|
|
|
|
function candidateQualityScore(result) {
|
|
const quality = result?.candidateQuality || null;
|
|
// Ordinary production candidates already expose the combined ranking score
|
|
// in candidateQuality.score. Tiled candidates explicitly promote the final
|
|
// whole-selection merge audit to the authoritative score instead.
|
|
const candidates = quality?.qualityAuthority === "whole-selection-post-merge"
|
|
? [quality?.finalMerge?.score, quality?.score, quality?.final?.score]
|
|
: [quality?.score, quality?.final?.score, quality?.finalMerge?.score];
|
|
for (const value of candidates) {
|
|
const score = Number(value);
|
|
if (Number.isFinite(score)) return score;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function retainProductionTerrainDraft(draft) {
|
|
if (!draft?.terrain) return null;
|
|
return {
|
|
terrain: draft.terrain,
|
|
generationTimings: (draft.generationTimings || []).filter((row) => row?.key === "terrain").map((row) => ({ ...row })),
|
|
generationTotalMs: Number((draft.generationTimings || []).find((row) => row?.key === "terrain")?.ms || 0),
|
|
baseSeed: draft.baseSeed >>> 0,
|
|
effectiveSeed: draft.effectiveSeed >>> 0,
|
|
generationContext: { ...(draft.generationContext || {}) },
|
|
terrainDraftOnly: true,
|
|
residentDraftSource: true,
|
|
};
|
|
}
|
|
|
|
function draftCandidateScore(result) {
|
|
const score = Number(result?.score ?? result?.candidateQuality?.score);
|
|
return Number.isFinite(score) ? score : -Infinity;
|
|
}
|
|
|
|
function draftCandidateUpperBound(result) {
|
|
const value = Number(result?.qualityUpperBound ?? result?.candidateQuality?.qualityUpperBound);
|
|
if (Number.isFinite(value)) return Math.max(0, Math.min(1, value));
|
|
return 1;
|
|
}
|
|
|
|
function draftAdmissibleHardReject(result) {
|
|
return result?.admissibleHardReject === true || result?.candidateQuality?.admissibleHardReject === true;
|
|
}
|
|
|
|
function compactDraftQuality(result) {
|
|
const quality = result?.candidateQuality || null;
|
|
if (!quality) return null;
|
|
return {
|
|
score: draftCandidateScore(result),
|
|
qualityUpperBound: draftCandidateUpperBound(result),
|
|
hardPass: quality.hardPass !== false,
|
|
admissibleHardReject: draftAdmissibleHardReject(result),
|
|
earlyRejectStage: result?.earlyRejectStage || null,
|
|
draftProxy: true,
|
|
terrain: quality.terrain ? {
|
|
terrainType: quality.terrain.terrainType || null,
|
|
score: Number(quality.terrain.score || 0),
|
|
landRatio: Number(quality.terrain.landRatio || 0),
|
|
developableRatio: Number(quality.terrain.developableRatio || 0),
|
|
largestComponentRatio: Number(quality.terrain.largestComponentRatio || 0),
|
|
frontierLandRate: Number(quality.terrain.frontierLandRate || 0),
|
|
} : null,
|
|
human: quality.human ? {
|
|
score: Number(quality.human.score || 0),
|
|
settlementCount: Number(quality.human.settlementCount || 0),
|
|
labelCount: Number(quality.human.labelCount || 0),
|
|
roadPaths: Number(quality.human.roadPaths || 0),
|
|
railPaths: Number(quality.human.railPaths || 0),
|
|
} : null,
|
|
};
|
|
}
|
|
|
|
function rankedDraftCandidates(rows = []) {
|
|
return [...rows].sort((a, b) => {
|
|
const scoreDelta = draftCandidateScore(b.draftResult) - draftCandidateScore(a.draftResult);
|
|
if (Math.abs(scoreDelta) > 1e-12) return scoreDelta;
|
|
const hardDelta = Number(b.draftResult?.candidateQuality?.hardPass !== false) - Number(a.draftResult?.candidateQuality?.hardPass !== false);
|
|
if (hardDelta) return hardDelta;
|
|
return a.candidateOrdinal - b.candidateOrdinal;
|
|
});
|
|
}
|
|
|
|
function candidateSeamIssueCount(result) {
|
|
const seam = result?.seamDiagnostics || null;
|
|
if (!seam) return 0;
|
|
return (seam.gateReasons || []).length
|
|
+ Number(seam.roadPortalsBroken || 0)
|
|
+ Number(seam.railPortalsBroken || 0)
|
|
+ Number(seam.prefectureSeamBreakEdges || 0)
|
|
+ Number(seam.transportLandToSeaConflicts || 0);
|
|
}
|
|
|
|
function isBetterCandidate(result, candidateOrdinal, best) {
|
|
if (!best) return true;
|
|
const score = candidateQualityScore(result);
|
|
const bestScore = candidateQualityScore(best.result);
|
|
if (Math.abs(score - bestScore) > 1e-12) return score > bestScore;
|
|
const seamPass = result?.seamDiagnostics?.hardPass !== false;
|
|
const bestSeamPass = best.result?.seamDiagnostics?.hardPass !== false;
|
|
if (seamPass !== bestSeamPass) return seamPass;
|
|
const issues = candidateSeamIssueCount(result);
|
|
const bestIssues = candidateSeamIssueCount(best.result);
|
|
if (issues !== bestIssues) return issues < bestIssues;
|
|
const jump = Number(result?.seamDiagnostics?.maxEstablishedFrontierElevationJump || 0);
|
|
const bestJump = Number(best.result?.seamDiagnostics?.maxEstablishedFrontierElevationJump || 0);
|
|
if (Math.abs(jump - bestJump) > 1e-12) return jump < bestJump;
|
|
return candidateOrdinal < best.candidateOrdinal;
|
|
}
|
|
|
|
function finalizeBestCandidate(best, attempts, candidatePlan, candidateCount) {
|
|
if (!best) return null;
|
|
const selectedId = best.candidate.candidateId || `${best.candidate.variant >>> 0}:${best.candidate.seed >>> 0}`;
|
|
for (const attempt of attempts) {
|
|
const attemptId = attempt.candidateId || `${attempt.variant >>> 0}:${attempt.seed >>> 0}`;
|
|
attempt.selected = attemptId === selectedId;
|
|
if (attempt.selected) attempt.status = "success";
|
|
}
|
|
const last = candidatePlan[candidatePlan.length - 1] || best.candidate;
|
|
return {
|
|
...best.result,
|
|
ok: true,
|
|
searchAttempts: attempts,
|
|
searchStatus: "succeeded",
|
|
candidateOrdinal: best.candidateOrdinal,
|
|
candidateCount,
|
|
actualVariant: best.candidate.variant >>> 0,
|
|
actualSeed: best.candidate.seed >>> 0,
|
|
nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0,
|
|
bestOfCandidates: true,
|
|
evaluatedCandidateCount: attempts.filter((attempt) => attempt.status === "evaluated" || attempt.status === "success").length,
|
|
selectionScore: candidateQualityScore(best.result),
|
|
};
|
|
}
|
|
|
|
const PREVIEW_TERRAIN_FIELDS = new Set(["elevation", "slope", "sea", "landMask", "plain", "landuse", "populationDensity"]);
|
|
const PREVIEW_ADMIN_FIELDS = new Set(["adminId", "municipalityId", "prefectureRegionId", "prefectureMask", "humanRegionMask"]);
|
|
const PREVIEW_FEATURE_KEYS = [
|
|
"villages", "markets", "castles", "ports", "modernCities", "satelliteCities", "stations",
|
|
"interchanges", "industrialZones", "logisticsParks", "newTowns", "adminCenters", "externalGateways",
|
|
"prefectureRegions", "premodernRoads", "minorRoads", "nationalRoads", "ringRoads", "externalRoads",
|
|
"railways", "branchRailways", "ringRailways", "externalRailways", "expressways", "externalExpressways",
|
|
"mainRivers", "tributaryRivers", "smallStreams", "prefectureBorder", "regionalPrefectureBorders", "adminBorders",
|
|
];
|
|
|
|
function computePreviewDelta(baseWorld, previewWorld, rectLike, onProgress = () => {}) {
|
|
const rect = rectLike?.transportReachRect || rectLike?.repairRect || rectLike?.writeRect || rectLike || null;
|
|
if (!baseWorld || !previewWorld || !rect) return null;
|
|
const x0 = Math.max(0, Math.floor(rect.x0 || 0));
|
|
const y0 = Math.max(0, Math.floor(rect.y0 || 0));
|
|
const x1 = Math.min(Math.max(baseWorld.width || 0, previewWorld.width || 0), Math.ceil(rect.x1 || 0));
|
|
const y1 = Math.min(Math.max(baseWorld.height || 0, previewWorld.height || 0), Math.ceil(rect.y1 || 0));
|
|
const fieldNames = new Set([...Object.keys(baseWorld.fields || {}), ...Object.keys(previewWorld.fields || {})]);
|
|
let changedCells = 0;
|
|
let terrainChangedCells = 0;
|
|
let adminChangedCells = 0;
|
|
const totalRows = Math.max(0, y1 - y0) + PREVIEW_FEATURE_KEYS.length;
|
|
for (let y = y0; y < y1; y++) {
|
|
for (let x = x0; x < x1; x++) {
|
|
const bi = x >= 0 && y >= 0 && x < baseWorld.width && y < baseWorld.height ? y * baseWorld.width + x : -1;
|
|
const pi = x >= 0 && y >= 0 && x < previewWorld.width && y < previewWorld.height ? y * previewWorld.width + x : -1;
|
|
if (bi < 0 || pi < 0) continue;
|
|
let terrainChanged = false;
|
|
let adminChanged = false;
|
|
let cellChanged = false;
|
|
for (const name of fieldNames) {
|
|
const a = baseWorld.fields?.[name]?.[bi];
|
|
const b = previewWorld.fields?.[name]?.[pi];
|
|
if (a === b || (Number.isNaN(a) && Number.isNaN(b))) continue;
|
|
cellChanged = true;
|
|
if (PREVIEW_TERRAIN_FIELDS.has(name)) terrainChanged = true;
|
|
if (PREVIEW_ADMIN_FIELDS.has(name)) adminChanged = true;
|
|
}
|
|
if (cellChanged) changedCells++;
|
|
if (terrainChanged) terrainChangedCells++;
|
|
if (adminChanged) adminChangedCells++;
|
|
}
|
|
if ((y - y0) % 16 === 15 || y + 1 === y1) onProgress(y - y0 + 1, totalRows, "raster");
|
|
}
|
|
let featureLayersChanged = 0;
|
|
for (let index = 0; index < PREVIEW_FEATURE_KEYS.length; index++) {
|
|
const key = PREVIEW_FEATURE_KEYS[index];
|
|
if (!exactStructuredEqual(baseWorld.sourceMap?.[key] || [], previewWorld.sourceMap?.[key] || [])) featureLayersChanged++;
|
|
onProgress(Math.max(0, y1 - y0) + index + 1, totalRows, "features");
|
|
}
|
|
return {
|
|
changedCells,
|
|
terrainChangedCells,
|
|
adminChangedCells,
|
|
featureLayersChanged,
|
|
identical: changedCells === 0 && featureLayersChanged === 0,
|
|
};
|
|
}
|
|
|
|
export function runPatchCandidateSearch(message, dependencies = {}) {
|
|
const { id, world, rect, options, search } = message || {};
|
|
const cloneWorld = dependencies.cloneWorld || ((value) => structuredClone(value));
|
|
const generateCandidate = dependencies.generateCandidate || generatePatch;
|
|
const clock = dependencies.now || nowMs;
|
|
const publishProgress = dependencies.onProgress || (() => {});
|
|
const transactional = dependencies.transactional === true;
|
|
const selectBestCandidate = search?.selectBestCandidate === true;
|
|
const candidatePlan = Array.isArray(search?.candidatePlan) && search.candidatePlan.length
|
|
? search.candidatePlan
|
|
: [{ variant: options?.variant || 0, seed: options?.seed || world?.seed || 0 }];
|
|
const candidateCount = Math.max(candidatePlan.length, Number(search?.totalCandidateCount || candidatePlan.length));
|
|
const searchId = search?.searchId || `patch-${id}`;
|
|
const workerEpoch = Number(search?.workerEpoch || 0);
|
|
const executionAttempt = Math.max(1, Number(search?.executionAttempt || 1));
|
|
let eventSeq = 0;
|
|
let phaseOrdinal = 0;
|
|
let currentPhase = null;
|
|
const workUnits = new Map();
|
|
const emitProgress = (progress = {}, candidateOrdinal = 0) => {
|
|
const phase = String(progress.phase || progress.key || "patch");
|
|
if (phase !== currentPhase) {
|
|
currentPhase = phase;
|
|
phaseOrdinal++;
|
|
}
|
|
const hasCompleted = progress.completed != null;
|
|
const hasTotal = progress.total != null;
|
|
const completed = Number(progress.completed);
|
|
const total = Number(progress.total);
|
|
const boundedWork = hasCompleted || hasTotal;
|
|
const explicitWorkUnitId = progress.workUnitId != null && String(progress.workUnitId).length > 0;
|
|
const workUnitId = String(explicitWorkUnitId ? progress.workUnitId : (progress.key || phase));
|
|
if (boundedWork) {
|
|
if (!hasCompleted || !hasTotal || !Number.isFinite(completed) || !Number.isFinite(total)) {
|
|
const error = new Error(`Incomplete bounded progress for ${phase}/${workUnitId}: ${String(progress.completed)}/${String(progress.total)}`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
if (!explicitWorkUnitId) {
|
|
const error = new Error(`Bounded progress is missing an explicit workUnitId for ${phase}/${workUnitId}.`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
if (completed < 0 || total < 0 || completed > total) {
|
|
const error = new Error(`Invalid progress bounds for ${phase}/${workUnitId}: ${completed}/${total}`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
// workUnitId is globally unique within one candidate-search operation.
|
|
// Do not reset its monotonicity merely because a display phase changed in
|
|
// between heartbeats; that would hide real restarts/runaway producers.
|
|
const unitKey = workUnitId;
|
|
const previous = workUnits.get(unitKey);
|
|
if (previous && (total !== previous.total || completed < previous.completed)) {
|
|
const error = new Error(`Non-monotonic progress for ${phase}/${workUnitId}: ${completed}/${total} after ${previous.completed}/${previous.total}`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
const workerNow = nowMs();
|
|
const startedAtWorker = previous?.startedAtWorker ?? workerNow;
|
|
const lastAdvancedAtWorker = !previous || completed > previous.completed
|
|
? workerNow
|
|
: (previous.lastAdvancedAtWorker ?? startedAtWorker);
|
|
workUnits.set(unitKey, { completed, total, phase, startedAtWorker, lastAdvancedAtWorker });
|
|
}
|
|
eventSeq++;
|
|
publishProgress({
|
|
id,
|
|
type: "progress",
|
|
progress: {
|
|
...progress,
|
|
searchId,
|
|
operationId: search?.operationId || searchId,
|
|
candidateOrdinal,
|
|
candidateCount,
|
|
executionAttempt,
|
|
workerEpoch,
|
|
committedRevision: Number(search?.committedRevision || 0),
|
|
eventSeq,
|
|
counter: eventSeq,
|
|
phase,
|
|
phaseOrdinal,
|
|
workUnitId,
|
|
boundedWork,
|
|
startedAtWorker: boundedWork ? workUnits.get(workUnitId)?.startedAtWorker : undefined,
|
|
lastAdvancedAtWorker: boundedWork ? workUnits.get(workUnitId)?.lastAdvancedAtWorker : undefined,
|
|
cooperative: progress.nonCooperative !== true,
|
|
},
|
|
});
|
|
};
|
|
try {
|
|
const attempts = [];
|
|
let terminalResult = null;
|
|
let successWorld = null;
|
|
let successDelta = null;
|
|
let successTargetHash = null;
|
|
let bestCandidate = null;
|
|
emitProgress({ status: "start", key: "search", phase: "search", workUnitId: "candidate-search", label: selectBestCandidate
|
|
? `Evaluating ${candidateCount} complete candidates and selecting the highest-quality result`
|
|
: `Searching ${candidateCount} complete candidate${candidateCount === 1 ? "" : "s"}`, completed: 0, total: candidateCount });
|
|
for (let index = 0; index < candidatePlan.length; index++) {
|
|
const candidate = candidatePlan[index];
|
|
const candidateOrdinal = Math.max(1, Number(candidate.candidateOrdinal || index + 1));
|
|
emitProgress({
|
|
status: "start",
|
|
key: transactional ? "candidate-transaction" : "candidate-clone",
|
|
phase: transactional ? "candidate-transaction" : "candidate-clone",
|
|
label: transactional
|
|
? `Candidate ${candidateOrdinal}/${candidateCount}: capturing exact rollback state`
|
|
: `Candidate ${candidateOrdinal}/${candidateCount}: preparing immutable baseline`,
|
|
variant: candidate.variant >>> 0,
|
|
workUnitId: "candidate-search",
|
|
// candidateOrdinal is global across the full search, whereas `index` is
|
|
// local to this Worker execution. Recovery can restart with candidates
|
|
// 2..N after candidate 1 was already rejected, so using `index` here
|
|
// made the shared search work unit move backward (2/3 -> 1/3).
|
|
completed: Math.max(0, candidateOrdinal - 1),
|
|
total: candidateCount,
|
|
nonCooperative: true,
|
|
}, candidateOrdinal);
|
|
let candidateWorld;
|
|
let transaction = null;
|
|
try {
|
|
if (transactional) {
|
|
transaction = capturePatchTransactionSnapshot(world, {
|
|
lightweight: false,
|
|
isolateSourceMap: true,
|
|
copyGeneratedMask: String(search?.resolvedPatchMode || "").toLowerCase() !== "regeneration",
|
|
});
|
|
candidateWorld = world;
|
|
} else {
|
|
candidateWorld = cloneWorld(world);
|
|
}
|
|
} catch (error) {
|
|
const failed = {
|
|
ok: false,
|
|
code: transactional ? "candidate-transaction-failed" : "candidate-clone-failed",
|
|
reason: error?.message || String(error),
|
|
};
|
|
attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, 0, "infrastructure-error", executionAttempt));
|
|
terminalResult = {
|
|
...failed,
|
|
searchAttempts: attempts,
|
|
searchStatus: "infrastructure-error",
|
|
nextVariant: candidate.variant >>> 0,
|
|
};
|
|
break;
|
|
}
|
|
const startedAt = clock();
|
|
let result;
|
|
try {
|
|
result = generateCandidate(candidateWorld, rect, {
|
|
...(options || {}),
|
|
seed: candidate.seed >>> 0,
|
|
variant: candidate.variant >>> 0,
|
|
maxQualityRetries: 0,
|
|
// The production Worker mutates its committed mirror transactionally
|
|
// and restores it after extracting the accepted delta. Direct callers
|
|
// retain the immutable-clone reference path for independent tests.
|
|
_workerOwnedPreview: !transactional,
|
|
_externalTransactionSnapshot: transaction,
|
|
onProgress: (progress) => {
|
|
// Producer workUnitIds are invocation-local. The same complete
|
|
// pipeline is executed again when a content-rejected candidate
|
|
// advances to the next Variant, so namespace every explicit inner
|
|
// unit by candidate ordinal before the search-wide monotonicity
|
|
// validator sees it. Do not invent an ID for a bounded producer
|
|
// that omitted one; emitProgress must still reject that protocol
|
|
// violation instead of masking it.
|
|
const rawWorkUnitId = progress?.workUnitId != null && String(progress.workUnitId).length > 0
|
|
? String(progress.workUnitId)
|
|
: null;
|
|
emitProgress({
|
|
...progress,
|
|
workUnitId: rawWorkUnitId ? `candidate-${candidateOrdinal}/${rawWorkUnitId}` : progress?.workUnitId,
|
|
variant: candidate.variant >>> 0,
|
|
seed: candidate.seed >>> 0,
|
|
}, candidateOrdinal);
|
|
},
|
|
});
|
|
if (!selectBestCandidate) result = normalizeCandidateResult(result);
|
|
} catch (error) {
|
|
if (transaction) restorePatchTransactionSnapshot(world, transaction);
|
|
if (error?.code === "worker-progress-invariant") throw error;
|
|
const wallMs = clock() - startedAt;
|
|
const failed = {
|
|
ok: false,
|
|
code: "candidate-execution-error",
|
|
reason: error?.message || String(error),
|
|
stack: error?.stack || "",
|
|
};
|
|
attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "execution-error", executionAttempt));
|
|
terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "execution-error", nextVariant: candidate.variant >>> 0 };
|
|
break;
|
|
}
|
|
const wallMs = clock() - startedAt;
|
|
if (result?.ok) {
|
|
let candidateDelta = null;
|
|
let candidateTargetHash = null;
|
|
if (transaction) {
|
|
try {
|
|
candidateDelta = buildCommittedMirrorDeltaFromTransaction(transaction, candidateWorld);
|
|
candidateTargetHash = hashCommittedWorld(candidateWorld);
|
|
} catch (error) {
|
|
restorePatchTransactionSnapshot(world, transaction);
|
|
const failed = {
|
|
ok: false,
|
|
code: "candidate-delta-build-failed",
|
|
reason: error?.message || String(error),
|
|
};
|
|
attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "infrastructure-error", executionAttempt));
|
|
terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0 };
|
|
break;
|
|
}
|
|
restorePatchTransactionSnapshot(world, transaction);
|
|
}
|
|
if (!selectBestCandidate) {
|
|
successDelta = candidateDelta;
|
|
successTargetHash = candidateTargetHash;
|
|
attempts.push(summarizeAttempt(result, candidate, candidateOrdinal, wallMs, "success", executionAttempt));
|
|
result.searchAttempts = attempts;
|
|
result.searchStatus = "succeeded";
|
|
result.candidateOrdinal = candidateOrdinal;
|
|
result.candidateCount = candidateCount;
|
|
result.actualVariant = candidate.variant >>> 0;
|
|
result.actualSeed = candidate.seed >>> 0;
|
|
result.nextVariant = ((candidate.variant >>> 0) + 1) >>> 0;
|
|
if (successDelta?.previewDelta) result.previewDelta = { ...successDelta.previewDelta };
|
|
terminalResult = result;
|
|
successWorld = transactional ? null : candidateWorld;
|
|
emitProgress({ status: "done", key: "search", phase: "search", workUnitId: "candidate-search", label: `Candidate ${candidateOrdinal}/${candidateCount} accepted`, variant: candidate.variant >>> 0, completed: candidateOrdinal, total: candidateCount }, candidateOrdinal);
|
|
break;
|
|
}
|
|
|
|
if (candidateDelta?.previewDelta) result.previewDelta = { ...candidateDelta.previewDelta };
|
|
const attemptSummary = summarizeAttempt(result, candidate, candidateOrdinal, wallMs, "evaluated", executionAttempt);
|
|
attemptSummary.selectionScore = candidateQualityScore(result);
|
|
attempts.push(attemptSummary);
|
|
if (isBetterCandidate(result, candidateOrdinal, bestCandidate)) {
|
|
bestCandidate = {
|
|
result, candidate, candidateOrdinal,
|
|
world: transactional ? null : candidateWorld,
|
|
delta: candidateDelta,
|
|
targetHash: candidateTargetHash,
|
|
};
|
|
}
|
|
emitProgress({
|
|
status: "evaluated",
|
|
key: "candidate-evaluated",
|
|
phase: "candidate-result",
|
|
workUnitId: "candidate-search",
|
|
label: `Candidate ${candidateOrdinal}/${candidateCount} evaluated (quality ${candidateQualityScore(result).toFixed(3)}); continuing best-of-${candidateCount} selection`,
|
|
variant: candidate.variant >>> 0,
|
|
completed: candidateOrdinal,
|
|
total: candidateCount,
|
|
attemptSummary,
|
|
}, candidateOrdinal);
|
|
candidateWorld = null;
|
|
transaction = null;
|
|
result = null;
|
|
continue;
|
|
}
|
|
|
|
const invariant = isInvariantFailure(result);
|
|
const contentRejected = isContentRejection(result);
|
|
const status = invariant ? "invariant-breach" : contentRejected ? "rejected" : "failed";
|
|
const attemptSummary = summarizeAttempt(result, candidate, candidateOrdinal, wallMs, status, executionAttempt);
|
|
attempts.push(attemptSummary);
|
|
if (transaction) restorePatchTransactionSnapshot(world, transaction);
|
|
emitProgress({
|
|
status: contentRejected ? "rejected" : "error",
|
|
key: contentRejected ? "candidate-rejected" : "candidate-failed",
|
|
phase: "candidate-result",
|
|
label: contentRejected
|
|
? `Candidate ${candidateOrdinal}/${candidateCount} rejected; searching the next complete candidate`
|
|
: `Candidate ${candidateOrdinal}/${candidateCount} stopped: ${result?.reason || result?.code || "failure"}`,
|
|
variant: candidate.variant >>> 0,
|
|
workUnitId: "candidate-search",
|
|
completed: candidateOrdinal,
|
|
total: candidateCount,
|
|
code: result?.code || null,
|
|
attemptSummary,
|
|
}, candidateOrdinal);
|
|
if (!contentRejected) {
|
|
terminalResult = {
|
|
...(result || { ok: false }),
|
|
searchAttempts: attempts,
|
|
searchStatus: invariant ? "invariant-breach" : "failed",
|
|
nextVariant: candidate.variant >>> 0,
|
|
};
|
|
break;
|
|
}
|
|
// The rejected full world is no longer observable. Drop the last strong
|
|
// references before cloning the next Variant so the browser may reclaim
|
|
// its field buffers instead of retaining multiple complete candidates.
|
|
candidateWorld = null;
|
|
transaction = null;
|
|
result = null;
|
|
}
|
|
|
|
if (!terminalResult && selectBestCandidate && bestCandidate) {
|
|
terminalResult = finalizeBestCandidate(bestCandidate, attempts, candidatePlan, candidateCount);
|
|
successWorld = bestCandidate.world;
|
|
successDelta = bestCandidate.delta;
|
|
successTargetHash = bestCandidate.targetHash;
|
|
if (successDelta?.previewDelta) terminalResult.previewDelta = { ...successDelta.previewDelta };
|
|
emitProgress({
|
|
status: "done", key: "search", phase: "search", workUnitId: "candidate-search",
|
|
label: `Selected candidate ${bestCandidate.candidateOrdinal}/${candidateCount} with highest quality ${candidateQualityScore(bestCandidate.result).toFixed(3)}`,
|
|
variant: bestCandidate.candidate.variant >>> 0, completed: candidateCount, total: candidateCount,
|
|
}, bestCandidate.candidateOrdinal);
|
|
}
|
|
|
|
if (!terminalResult) {
|
|
const last = candidatePlan[candidatePlan.length - 1];
|
|
terminalResult = {
|
|
ok: false,
|
|
code: "patch-search-exhausted",
|
|
reason: `All ${candidateCount} complete production candidates were rejected.`,
|
|
searchStatus: "exhausted",
|
|
searchAttempts: attempts,
|
|
nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0,
|
|
candidateCount,
|
|
};
|
|
emitProgress({ status: "done", key: "search-exhausted", phase: "search", workUnitId: "candidate-search", label: terminalResult.reason, completed: candidateCount, total: candidateCount });
|
|
}
|
|
|
|
if (successWorld && terminalResult?.ok) {
|
|
emitProgress({
|
|
status: "start",
|
|
key: "preview-delta",
|
|
phase: "preview-delta",
|
|
label: "Auditing preview changes",
|
|
}, terminalResult.candidateOrdinal || 0);
|
|
terminalResult.previewDelta = computePreviewDelta(world, successWorld, terminalResult.rects || rect, (completed, total, part) => {
|
|
emitProgress({
|
|
status: "step",
|
|
key: `preview-delta:${part}`,
|
|
phase: "preview-delta",
|
|
label: part === "raster" ? "Auditing preview raster changes" : "Auditing preview feature changes",
|
|
workUnitId: "preview-delta-audit",
|
|
completed,
|
|
total,
|
|
}, terminalResult.candidateOrdinal || 0);
|
|
});
|
|
emitProgress({
|
|
status: "done",
|
|
key: "preview-delta",
|
|
phase: "preview-delta",
|
|
label: "Preview change audit complete",
|
|
}, terminalResult.candidateOrdinal || 0);
|
|
}
|
|
|
|
// Return only the accepted candidate world. Rejected candidate clones and
|
|
// the immutable worker baseline remain worker-local and become collectible.
|
|
return {
|
|
id,
|
|
ok: true,
|
|
world: successWorld,
|
|
transactionDelta: successDelta,
|
|
targetHash: successTargetHash,
|
|
result: terminalResult,
|
|
searchId,
|
|
workerEpoch,
|
|
eventSeq,
|
|
};
|
|
} catch (error) {
|
|
return { id, ok: false, code: error?.code || "candidate-search-error", error: error?.message || String(error), stack: error?.stack || "", searchId, workerEpoch, eventSeq };
|
|
}
|
|
}
|
|
|
|
|
|
export async function runPatchCandidateSearchAsync(message, dependencies = {}) {
|
|
const { id, world, rect, options, search } = message || {};
|
|
const cloneWorld = dependencies.cloneWorld || ((value) => structuredClone(value));
|
|
const generateCandidate = dependencies.generateCandidate || generatePatchAsync;
|
|
const clock = dependencies.now || nowMs;
|
|
const publishProgress = dependencies.onProgress || (() => {});
|
|
const transactional = dependencies.transactional === true;
|
|
const selectBestCandidate = search?.selectBestCandidate === true;
|
|
const candidatePlan = Array.isArray(search?.candidatePlan) && search.candidatePlan.length
|
|
? search.candidatePlan
|
|
: [{ variant: options?.variant || 0, seed: options?.seed || world?.seed || 0 }];
|
|
const candidateCount = Math.max(candidatePlan.length, Number(search?.totalCandidateCount || candidatePlan.length));
|
|
const searchId = search?.searchId || `patch-${id}`;
|
|
const workerEpoch = Number(search?.workerEpoch || 0);
|
|
const executionAttempt = Math.max(1, Number(search?.executionAttempt || 1));
|
|
let eventSeq = 0;
|
|
let phaseOrdinal = 0;
|
|
let currentPhase = null;
|
|
const workUnits = new Map();
|
|
const emitProgress = (progress = {}, candidateOrdinal = 0) => {
|
|
const phase = String(progress.phase || progress.key || "patch");
|
|
if (phase !== currentPhase) {
|
|
currentPhase = phase;
|
|
phaseOrdinal++;
|
|
}
|
|
const hasCompleted = progress.completed != null;
|
|
const hasTotal = progress.total != null;
|
|
const completed = Number(progress.completed);
|
|
const total = Number(progress.total);
|
|
const boundedWork = hasCompleted || hasTotal;
|
|
const explicitWorkUnitId = progress.workUnitId != null && String(progress.workUnitId).length > 0;
|
|
const workUnitId = String(explicitWorkUnitId ? progress.workUnitId : (progress.key || phase));
|
|
if (boundedWork) {
|
|
if (!hasCompleted || !hasTotal || !Number.isFinite(completed) || !Number.isFinite(total)) {
|
|
const error = new Error(`Incomplete bounded progress for ${phase}/${workUnitId}: ${String(progress.completed)}/${String(progress.total)}`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
if (!explicitWorkUnitId) {
|
|
const error = new Error(`Bounded progress is missing an explicit workUnitId for ${phase}/${workUnitId}.`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
if (completed < 0 || total < 0 || completed > total) {
|
|
const error = new Error(`Invalid progress bounds for ${phase}/${workUnitId}: ${completed}/${total}`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
// workUnitId is globally unique within one candidate-search operation.
|
|
// Do not reset its monotonicity merely because a display phase changed in
|
|
// between heartbeats; that would hide real restarts/runaway producers.
|
|
const unitKey = workUnitId;
|
|
const previous = workUnits.get(unitKey);
|
|
if (previous && (total !== previous.total || completed < previous.completed)) {
|
|
const error = new Error(`Non-monotonic progress for ${phase}/${workUnitId}: ${completed}/${total} after ${previous.completed}/${previous.total}`);
|
|
error.code = "worker-progress-invariant";
|
|
throw error;
|
|
}
|
|
const workerNow = nowMs();
|
|
const startedAtWorker = previous?.startedAtWorker ?? workerNow;
|
|
const lastAdvancedAtWorker = !previous || completed > previous.completed
|
|
? workerNow
|
|
: (previous.lastAdvancedAtWorker ?? startedAtWorker);
|
|
workUnits.set(unitKey, { completed, total, phase, startedAtWorker, lastAdvancedAtWorker });
|
|
}
|
|
eventSeq++;
|
|
publishProgress({
|
|
id,
|
|
type: "progress",
|
|
progress: {
|
|
...progress,
|
|
searchId,
|
|
operationId: search?.operationId || searchId,
|
|
candidateOrdinal,
|
|
candidateCount,
|
|
executionAttempt,
|
|
workerEpoch,
|
|
committedRevision: Number(search?.committedRevision || 0),
|
|
eventSeq,
|
|
counter: eventSeq,
|
|
phase,
|
|
phaseOrdinal,
|
|
workUnitId,
|
|
boundedWork,
|
|
startedAtWorker: boundedWork ? workUnits.get(workUnitId)?.startedAtWorker : undefined,
|
|
lastAdvancedAtWorker: boundedWork ? workUnits.get(workUnitId)?.lastAdvancedAtWorker : undefined,
|
|
cooperative: progress.nonCooperative !== true,
|
|
},
|
|
});
|
|
};
|
|
try {
|
|
if (selectBestCandidate && search?.draftSelection === true) {
|
|
const attempts = [];
|
|
const draftRows = [];
|
|
const evaluateDraft = dependencies.evaluateDraftCandidate || evaluatePatchDraftCandidate;
|
|
const prepareContext = dependencies.prepareOperationContext || preparePatchOperationContext;
|
|
let operationContext;
|
|
try {
|
|
operationContext = prepareContext(world, rect, options || {});
|
|
if (!operationContext?.ok) {
|
|
return {
|
|
id, ok: true, world: null, transactionDelta: null, targetHash: null,
|
|
result: {
|
|
ok: false,
|
|
code: operationContext?.code || "patch-draft-context-failed",
|
|
reason: operationContext?.reason || "Could not prepare patch operation context.",
|
|
searchStatus: "failed",
|
|
searchAttempts: attempts,
|
|
candidateCount,
|
|
},
|
|
searchId, workerEpoch, eventSeq,
|
|
};
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
id, ok: true, world: null, transactionDelta: null, targetHash: null,
|
|
result: {
|
|
ok: false, code: "patch-draft-context-failed", reason: error?.message || String(error),
|
|
searchStatus: "infrastructure-error", searchAttempts: attempts, candidateCount,
|
|
},
|
|
searchId, workerEpoch, eventSeq,
|
|
};
|
|
}
|
|
|
|
const draftBatchTotal = candidatePlan.length;
|
|
const draftBatchFirstOrdinal = Math.max(1, Number(candidatePlan[0]?.candidateOrdinal || 1));
|
|
const draftBatchLastOrdinal = Math.max(draftBatchFirstOrdinal, Number(candidatePlan[candidatePlan.length - 1]?.candidateOrdinal || draftBatchFirstOrdinal));
|
|
const draftRankingWorkUnitId = `draft-ranking-${draftBatchFirstOrdinal}-${draftBatchLastOrdinal}`;
|
|
emitProgress({
|
|
status: "start", key: "draft-ranking", phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: `Scouting terrain batch ${draftBatchFirstOrdinal}-${draftBatchLastOrdinal} with admissible pruning; only complete production finalists can publish`,
|
|
completed: 0, total: draftBatchTotal,
|
|
});
|
|
|
|
// r10: generate regular-size drafts on two resident nested-worker lanes.
|
|
// Typed-array ownership is transferred back to this coordinator, so the
|
|
// helpers remain resident without retaining duplicate candidate heaps.
|
|
const parallelDrafts = new Map();
|
|
let parallelDraftRequests = null;
|
|
let parallelDraftGeneration = false;
|
|
let parallelDraftFallbackReason = null;
|
|
if (typeof dependencies.precomputeRawTerrainScoutBatch === "function"
|
|
&& search?.parallelDrafts !== false
|
|
&& candidatePlan.length > 1) {
|
|
try {
|
|
const requests = candidatePlan.map((candidate, index) => buildRawPatchTerrainScoutRequest(world, rect, {
|
|
...(options || {}),
|
|
seed: candidate.seed >>> 0,
|
|
variant: candidate.variant >>> 0,
|
|
maxQualityRetries: 0,
|
|
includeSeamVisualization: false,
|
|
_preparedPatchContext: operationContext,
|
|
_draftProxy: true,
|
|
taskId: `candidate-draft-${candidate.candidateOrdinal || index + 1}`,
|
|
}));
|
|
if (requests.every((request) => request?.ok && request.reusableForFull === true)) {
|
|
parallelDraftGeneration = true;
|
|
parallelDraftRequests = requests;
|
|
// Prime only the first two resident lanes. Later candidates are fed
|
|
// after earlier transferred drafts have been evaluated/released, so
|
|
// the coordinator never retains three complete draft graphs at once.
|
|
const requestBatch = requests.slice(0, 2);
|
|
const candidateBatch = candidatePlan.slice(0, requestBatch.length);
|
|
const drafts = await dependencies.precomputeRawTerrainScoutBatch(requestBatch, (request, progress) => {
|
|
const localIndex = requestBatch.indexOf(request);
|
|
const candidate = candidateBatch[Math.max(0, localIndex)] || candidateBatch[0];
|
|
const ordinal = Math.max(1, Number(candidate?.candidateOrdinal || localIndex + 1));
|
|
emitProgress({
|
|
...progress,
|
|
phase: `draft-lane-${String(progress?.phase || progress?.key || "generation")}`,
|
|
workUnitId: progress?.workUnitId ? `draft-lane-${ordinal}/${progress.workUnitId}` : undefined,
|
|
variant: candidate?.variant >>> 0,
|
|
seed: candidate?.seed >>> 0,
|
|
draftOnly: true,
|
|
parallelDraftLane: true,
|
|
}, ordinal);
|
|
});
|
|
for (let index = 0; index < drafts.length; index++) {
|
|
const candidate = candidateBatch[index];
|
|
const candidateId = candidate?.candidateId || `${candidate?.variant >>> 0}:${candidate?.seed >>> 0}`;
|
|
parallelDrafts.set(candidateId, drafts[index]);
|
|
}
|
|
} else {
|
|
parallelDraftFallbackReason = "selection-not-regular-production-size";
|
|
}
|
|
} catch (error) {
|
|
parallelDrafts.clear();
|
|
parallelDraftGeneration = false;
|
|
parallelDraftFallbackReason = error?.message || error?.code || "parallel-draft-failed";
|
|
emitProgress({
|
|
status: "warning", key: "draft-lane-fallback", phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: "Parallel terrain-scout lanes were unavailable; continuing with deterministic serial terrain scouting",
|
|
completed: 0, total: draftBatchTotal,
|
|
code: error?.code || "parallel-draft-fallback",
|
|
});
|
|
}
|
|
}
|
|
|
|
for (let index = 0; index < candidatePlan.length; index++) {
|
|
const candidate = candidatePlan[index];
|
|
const candidateOrdinal = Math.max(1, Number(candidate.candidateOrdinal || index + 1));
|
|
const candidateId = candidate.candidateId || `${candidate.variant >>> 0}:${candidate.seed >>> 0}`;
|
|
const startedAt = clock();
|
|
let draftResult;
|
|
if (parallelDraftGeneration && !parallelDrafts.has(candidateId) && parallelDraftRequests?.[index]) {
|
|
try {
|
|
const [draft] = await dependencies.precomputeRawTerrainScoutBatch([parallelDraftRequests[index]], (request, progress) => {
|
|
emitProgress({
|
|
...progress,
|
|
phase: `draft-lane-${String(progress?.phase || progress?.key || "generation")}`,
|
|
workUnitId: progress?.workUnitId ? `draft-lane-${candidateOrdinal}/${progress.workUnitId}` : undefined,
|
|
variant: candidate.variant >>> 0,
|
|
seed: candidate.seed >>> 0,
|
|
draftOnly: true,
|
|
parallelDraftLane: true,
|
|
}, candidateOrdinal);
|
|
});
|
|
if (draft) parallelDrafts.set(candidateId, draft);
|
|
} catch (error) {
|
|
parallelDraftFallbackReason = error?.message || error?.code || "parallel-draft-feed-failed";
|
|
}
|
|
}
|
|
try {
|
|
draftResult = await evaluateDraft(world, rect, {
|
|
...(options || {}),
|
|
seed: candidate.seed >>> 0,
|
|
variant: candidate.variant >>> 0,
|
|
maxQualityRetries: 0,
|
|
includeSeamVisualization: false,
|
|
_preparedPatchContext: operationContext,
|
|
_terrainScoutOnly: true,
|
|
_precomputedTerrainDraftCandidate: parallelDrafts.get(candidateId) || null,
|
|
onProgress: (progress) => {
|
|
const rawWorkUnitId = progress?.workUnitId != null && String(progress.workUnitId).length > 0
|
|
? String(progress.workUnitId)
|
|
: null;
|
|
emitProgress({
|
|
...progress,
|
|
phase: `draft-${String(progress?.phase || progress?.key || "generation")}`,
|
|
workUnitId: rawWorkUnitId ? `draft-${candidateOrdinal}/${rawWorkUnitId}` : progress?.workUnitId,
|
|
variant: candidate.variant >>> 0,
|
|
seed: candidate.seed >>> 0,
|
|
draftOnly: true,
|
|
}, candidateOrdinal);
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const wallMs = clock() - startedAt;
|
|
const failed = { ok: false, code: "candidate-draft-error", reason: error?.message || String(error) };
|
|
const summary = summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "draft-failed", executionAttempt);
|
|
summary.draftOnly = true;
|
|
attempts.push(summary);
|
|
emitProgress({
|
|
status: "error", key: "draft-candidate-failed", phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: `Draft ${candidateOrdinal}/${candidateCount} failed; continuing with remaining drafts`,
|
|
completed: index + 1, total: draftBatchTotal, variant: candidate.variant >>> 0, code: failed.code,
|
|
}, candidateOrdinal);
|
|
continue;
|
|
} finally {
|
|
parallelDrafts.delete(candidateId);
|
|
}
|
|
const wallMs = clock() - startedAt;
|
|
if (!draftResult?.ok || !Number.isFinite(draftCandidateScore(draftResult))) {
|
|
const failed = draftResult?.ok === false
|
|
? draftResult
|
|
: { ok: false, code: "candidate-draft-score-invalid", reason: "Draft ranking did not produce a finite score." };
|
|
const summary = summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "draft-failed", executionAttempt);
|
|
summary.draftOnly = true;
|
|
attempts.push(summary);
|
|
emitProgress({
|
|
status: "error", key: "draft-candidate-failed", phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: `Draft ${candidateOrdinal}/${candidateCount} could not be ranked`,
|
|
completed: index + 1, total: draftBatchTotal, variant: candidate.variant >>> 0, code: failed.code || null,
|
|
}, candidateOrdinal);
|
|
continue;
|
|
}
|
|
|
|
const score = draftCandidateScore(draftResult);
|
|
const qualityUpperBound = draftCandidateUpperBound(draftResult);
|
|
const hardPruned = draftAdmissibleHardReject(draftResult);
|
|
const summary = summarizeAttempt(draftResult, candidate, candidateOrdinal, wallMs, hardPruned ? "pruned" : "evaluated", executionAttempt);
|
|
summary.draftOnly = true;
|
|
summary.draftQuality = compactDraftQuality(draftResult);
|
|
summary.selectionScore = score;
|
|
summary.qualityUpperBound = qualityUpperBound;
|
|
summary.admissibleHardReject = hardPruned;
|
|
summary.earlyRejectStage = draftResult.earlyRejectStage || null;
|
|
summary.fullFinalized = false;
|
|
summary.fullGenerationPasses = 0;
|
|
attempts.push(summary);
|
|
|
|
const row = {
|
|
candidate, candidateOrdinal, candidateId,
|
|
draftResult: {
|
|
ok: true,
|
|
score,
|
|
qualityUpperBound,
|
|
admissibleHardReject: hardPruned,
|
|
earlyRejectStage: draftResult.earlyRejectStage || null,
|
|
candidateQuality: draftResult.candidateQuality || null,
|
|
reusableForFull: draftResult.reusableForFull === true,
|
|
},
|
|
precomputedDraft: draftResult.reusableForFull === true ? retainProductionTerrainDraft(draftResult.precomputedDraft) : null,
|
|
};
|
|
draftRows.push(row);
|
|
draftResult.precomputedDraft = null;
|
|
// Before feeding the next resident-lane result, retain only the strongest
|
|
// already-scored draft. A dropped candidate remains fully reproducible
|
|
// from its deterministic seed/variant if Branch-and-Bound later needs it.
|
|
if (index < candidatePlan.length - 1) {
|
|
const seenWithDraft = draftRows.filter((entry) => entry.precomputedDraft);
|
|
if (seenWithDraft.length > 1) {
|
|
seenWithDraft.sort((a, b) => {
|
|
const scoreDelta = draftCandidateScore(b.draftResult) - draftCandidateScore(a.draftResult);
|
|
if (Math.abs(scoreDelta) > 1e-12) return scoreDelta;
|
|
const boundDelta = draftCandidateUpperBound(b.draftResult) - draftCandidateUpperBound(a.draftResult);
|
|
if (Math.abs(boundDelta) > 1e-12) return boundDelta;
|
|
return a.candidateOrdinal - b.candidateOrdinal;
|
|
});
|
|
for (const discard of seenWithDraft.slice(1)) discard.precomputedDraft = null;
|
|
}
|
|
}
|
|
emitProgress({
|
|
status: hardPruned ? "pruned" : "evaluated",
|
|
key: hardPruned ? "draft-candidate-pruned" : "draft-candidate-evaluated",
|
|
phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: hardPruned
|
|
? `Terrain scout ${candidateOrdinal}/${candidateCount} rejected at ${row.draftResult.earlyRejectStage || "immutable terrain safety"}`
|
|
: `Terrain scout ${candidateOrdinal}/${candidateCount} ranked at ${score.toFixed(3)} (final upper ${qualityUpperBound.toFixed(3)})`,
|
|
completed: index + 1, total: draftBatchTotal, variant: candidate.variant >>> 0,
|
|
attemptSummary: summary, draftOnly: true,
|
|
}, candidateOrdinal);
|
|
draftResult = null;
|
|
}
|
|
|
|
const rankedAll = rankedDraftCandidates(draftRows);
|
|
const ranked = rankedAll.filter((row) => !draftAdmissibleHardReject(row.draftResult));
|
|
if (!ranked.length) {
|
|
const last = candidatePlan[candidatePlan.length - 1];
|
|
const result = {
|
|
ok: false,
|
|
code: "patch-draft-search-exhausted",
|
|
reason: `All ${candidatePlan.length} candidates in this quality batch were proven unable to satisfy the full production contract before finalization.`,
|
|
searchStatus: "exhausted",
|
|
searchAttempts: attempts,
|
|
nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0,
|
|
candidateCount,
|
|
draftSelection: true,
|
|
};
|
|
emitProgress({
|
|
status: "done", key: "draft-ranking-exhausted", phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: result.reason, completed: draftBatchTotal, total: draftBatchTotal,
|
|
});
|
|
return { id, ok: true, world: null, transactionDelta: null, targetHash: null, result, searchId, workerEpoch, eventSeq };
|
|
}
|
|
|
|
// Bound peak retained draft memory at two candidates: the rank-1 proxy and
|
|
// the strongest remaining admissible upper bound. Any later contender can
|
|
// deterministically regenerate its draft if Branch-and-Bound proves it is
|
|
// still capable of winning after the first full evaluation.
|
|
const retainedDrafts = new Map();
|
|
const keepRows = [ranked[0]];
|
|
const upperBoundRunner = ranked.slice(1).sort((a, b) => {
|
|
const boundDelta = draftCandidateUpperBound(b.draftResult) - draftCandidateUpperBound(a.draftResult);
|
|
if (Math.abs(boundDelta) > 1e-12) return boundDelta;
|
|
return draftCandidateScore(b.draftResult) - draftCandidateScore(a.draftResult);
|
|
})[0];
|
|
if (upperBoundRunner) keepRows.push(upperBoundRunner);
|
|
const keepIds = new Set(keepRows.map((row) => row.candidateId));
|
|
for (const row of draftRows) {
|
|
if (keepIds.has(row.candidateId) && row.precomputedDraft) retainedDrafts.set(row.candidateId, { draft: row.precomputedDraft });
|
|
row.precomputedDraft = null;
|
|
}
|
|
|
|
emitProgress({
|
|
status: "done", key: "draft-ranking", phase: "draft-ranking", workUnitId: draftRankingWorkUnitId,
|
|
label: `Terrain scouting complete; admissible Branch-and-Bound starts from candidate ${ranked[0].candidateOrdinal}/${candidateCount}`,
|
|
completed: draftBatchTotal, total: draftBatchTotal,
|
|
}, ranked[0].candidateOrdinal);
|
|
|
|
let fullGenerationPassCount = 0;
|
|
const fullFinalizedCandidateIds = new Set();
|
|
const fullComparedCandidateOrdinals = [];
|
|
let branchBoundPrunedCount = rankedAll.length - ranked.length;
|
|
const branchBoundPrunedCandidateOrdinals = rankedAll
|
|
.filter((row) => draftAdmissibleHardReject(row.draftResult))
|
|
.map((row) => row.candidateOrdinal);
|
|
|
|
const recordFullAttempt = (row, result, wallMs, status) => {
|
|
const prior = attempts.find((entry) => entry.candidateId === row.candidateId);
|
|
const previousPasses = Number(prior?.fullGenerationPasses || 0);
|
|
const previousFullWallMs = Number(prior?.fullWallMs || 0);
|
|
const draftQuality = prior?.draftQuality || compactDraftQuality(row.draftResult);
|
|
const summary = summarizeAttempt(result, row.candidate, row.candidateOrdinal, wallMs, status, executionAttempt);
|
|
if (prior) {
|
|
Object.assign(prior, summary, {
|
|
draftQuality,
|
|
draftOnly: false,
|
|
fullFinalized: true,
|
|
draftSelectionScore: row.draftResult.score,
|
|
qualityUpperBound: draftCandidateUpperBound(row.draftResult),
|
|
selected: false,
|
|
});
|
|
prior.fullGenerationPasses = previousPasses + 1;
|
|
prior.fullWallMs = Math.round((previousFullWallMs + wallMs) * 10) / 10;
|
|
prior.wallMs = prior.fullWallMs;
|
|
}
|
|
fullFinalizedCandidateIds.add(row.candidateId);
|
|
if (!fullComparedCandidateOrdinals.includes(row.candidateOrdinal)) fullComparedCandidateOrdinals.push(row.candidateOrdinal);
|
|
return prior;
|
|
};
|
|
|
|
const executeFullCandidate = async (row, { preserveSuccess = false, label = null, materializationPass = false } = {}) => {
|
|
const { candidate, candidateOrdinal, candidateId } = row;
|
|
fullGenerationPassCount++;
|
|
const passOrdinal = fullGenerationPassCount;
|
|
const finalistWorkUnitId = `finalist-${candidateOrdinal}-pass-${passOrdinal}-generation`;
|
|
emitProgress({
|
|
status: "start", key: "finalist-generation", phase: "finalist-generation", workUnitId: finalistWorkUnitId,
|
|
label: label || `Finalizing draft ${candidateOrdinal}/${candidateCount}`,
|
|
completed: 0, total: 1, variant: candidate.variant >>> 0,
|
|
materializationPass,
|
|
}, candidateOrdinal);
|
|
|
|
let candidateWorld;
|
|
let transaction = null;
|
|
try {
|
|
if (transactional) {
|
|
transaction = capturePatchTransactionSnapshot(world, {
|
|
lightweight: false,
|
|
isolateSourceMap: true,
|
|
copyGeneratedMask: String(search?.resolvedPatchMode || "").toLowerCase() !== "regeneration",
|
|
});
|
|
candidateWorld = world;
|
|
} else {
|
|
candidateWorld = cloneWorld(world);
|
|
}
|
|
} catch (error) {
|
|
const failed = { ok: false, code: transactional ? "candidate-transaction-failed" : "candidate-clone-failed", reason: error?.message || String(error) };
|
|
return {
|
|
terminal: { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", candidateCount, draftSelection: true },
|
|
};
|
|
}
|
|
|
|
const startedAt = clock();
|
|
let result;
|
|
try {
|
|
result = await generateCandidate(candidateWorld, rect, {
|
|
...(options || {}),
|
|
seed: candidate.seed >>> 0,
|
|
variant: candidate.variant >>> 0,
|
|
maxQualityRetries: 0,
|
|
_workerOwnedPreview: !transactional,
|
|
_externalTransactionSnapshot: transaction,
|
|
_preparedPatchContext: operationContext,
|
|
// Draft features are ranking-only and are never publishable. Reuse
|
|
// only the exact terrain field; geography, settlements, admin, and
|
|
// transport rerun with full production parity.
|
|
_precomputedDraftCandidate: null,
|
|
_precomputedTerrainDraftCandidate: retainedDrafts.get(candidateId)?.draft || null,
|
|
_precomputeRawCandidateBatch: dependencies.precomputeRawCandidateBatch,
|
|
_precomputeRawCandidateSequence: dependencies.precomputeRawCandidateSequence,
|
|
_rawCandidateParallelism: Math.max(1, Math.min(2, Number(dependencies.rawCandidateParallelism || 2))),
|
|
onProgress: (progress) => {
|
|
const rawWorkUnitId = progress?.workUnitId != null && String(progress.workUnitId).length > 0
|
|
? String(progress.workUnitId)
|
|
: null;
|
|
emitProgress({
|
|
...progress,
|
|
workUnitId: rawWorkUnitId ? `finalist-${candidateOrdinal}-pass-${passOrdinal}/${rawWorkUnitId}` : progress?.workUnitId,
|
|
variant: candidate.variant >>> 0,
|
|
seed: candidate.seed >>> 0,
|
|
finalist: true,
|
|
materializationPass,
|
|
}, candidateOrdinal);
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (transaction) restorePatchTransactionSnapshot(world, transaction);
|
|
if (error?.code === "worker-progress-invariant") throw error;
|
|
const wallMs = clock() - startedAt;
|
|
const failed = { ok: false, code: "candidate-execution-error", reason: error?.message || String(error), stack: error?.stack || "" };
|
|
recordFullAttempt(row, failed, wallMs, "execution-error");
|
|
return {
|
|
terminal: { ...failed, searchAttempts: attempts, searchStatus: "execution-error", nextVariant: candidate.variant >>> 0, candidateCount, draftSelection: true },
|
|
};
|
|
}
|
|
const wallMs = clock() - startedAt;
|
|
|
|
if (!result?.ok) {
|
|
if (transaction) restorePatchTransactionSnapshot(world, transaction);
|
|
const invariant = isInvariantFailure(result);
|
|
const contentRejected = isContentRejection(result);
|
|
recordFullAttempt(row, result, wallMs, invariant ? "invariant-breach" : contentRejected ? "rejected" : "failed");
|
|
emitProgress({
|
|
status: contentRejected ? "rejected" : "error",
|
|
key: contentRejected ? "finalist-content-rejected" : "finalist-generation-failed",
|
|
phase: "finalist-generation", workUnitId: finalistWorkUnitId,
|
|
label: contentRejected
|
|
? `Finalist ${candidateOrdinal}/${candidateCount} was content-rejected`
|
|
: `Finalist ${candidateOrdinal}/${candidateCount} failed`,
|
|
completed: 1, total: 1, variant: candidate.variant >>> 0, code: result?.code || null,
|
|
}, candidateOrdinal);
|
|
if (!contentRejected || invariant) {
|
|
return {
|
|
terminal: {
|
|
...(result || { ok: false }),
|
|
searchAttempts: attempts,
|
|
searchStatus: invariant ? "invariant-breach" : "failed",
|
|
nextVariant: candidate.variant >>> 0,
|
|
candidateCount,
|
|
draftSelection: true,
|
|
},
|
|
};
|
|
}
|
|
return { row, result, contentRejected: true, wallMs, restored: true };
|
|
}
|
|
|
|
recordFullAttempt(row, result, wallMs, "evaluated");
|
|
emitProgress({
|
|
status: "done", key: "finalist-generation", phase: "finalist-generation", workUnitId: finalistWorkUnitId,
|
|
label: materializationPass
|
|
? `Materialized selected candidate ${candidateOrdinal}/${candidateCount}`
|
|
: `Full evaluation complete for candidate ${candidateOrdinal}/${candidateCount}`,
|
|
completed: 1, total: 1, variant: candidate.variant >>> 0,
|
|
materializationPass,
|
|
}, candidateOrdinal);
|
|
|
|
if (transaction && !preserveSuccess) {
|
|
restorePatchTransactionSnapshot(world, transaction);
|
|
return { row, result, candidateWorld: null, transaction: null, restored: true, wallMs };
|
|
}
|
|
return { row, result, candidateWorld, transaction, restored: false, wallMs };
|
|
};
|
|
|
|
const captureWinnerArtifact = (row, execution) => {
|
|
if (!execution?.result?.ok) return { ok: false, code: "candidate-artifact-result-missing" };
|
|
if (!transactional) {
|
|
// Non-transactional test/in-process callers already generated into an
|
|
// isolated clone. Retain that exact full-production world instead of
|
|
// regenerating the winner after comparing later challengers.
|
|
return { ok: true, world: execution.candidateWorld || null, delta: null, targetHash: null };
|
|
}
|
|
if (!execution?.transaction || execution.restored) {
|
|
return { ok: false, code: "candidate-materialization-state-missing", reason: "Candidate is not materialized on the transactional mirror." };
|
|
}
|
|
try {
|
|
// Keep a bounded replay state for a provisional winner, not the final
|
|
// committed Apply artifact. This avoids an expensive winner
|
|
// rematerialization if a later challenger loses, while the official
|
|
// committed delta/hash are still built exactly once for the final
|
|
// selected candidate at publication time.
|
|
return {
|
|
ok: true,
|
|
provisionalDelta: buildCommittedMirrorDeltaFromTransaction(execution.transaction, execution.candidateWorld),
|
|
world: null,
|
|
};
|
|
} catch (error) {
|
|
return { ok: false, code: "candidate-provisional-state-build-failed", reason: error?.message || String(error) };
|
|
}
|
|
};
|
|
|
|
const publishSuccessfulCandidate = (row, execution, selectionReason) => {
|
|
const { candidate, candidateOrdinal, candidateId } = row;
|
|
let successDelta = null;
|
|
let successTargetHash = null;
|
|
let successWorldArtifact = execution?.winnerArtifact?.world || null;
|
|
if (transactional) {
|
|
const buildWinnerDelta = dependencies.buildCommittedDeltaFromTransaction || buildCommittedMirrorDeltaFromTransaction;
|
|
const hashWinnerWorld = dependencies.hashCommittedWorld || hashCommittedWorld;
|
|
let publishTransaction = execution?.transaction || null;
|
|
let materializedFromCache = false;
|
|
try {
|
|
if (execution?.winnerArtifact?.provisionalDelta && (!publishTransaction || execution.restored)) {
|
|
publishTransaction = capturePatchTransactionSnapshot(world, {
|
|
lightweight: false,
|
|
isolateSourceMap: true,
|
|
copyGeneratedMask: String(search?.resolvedPatchMode || "").toLowerCase() !== "regeneration",
|
|
});
|
|
applyCommittedWorldDelta(world, execution.winnerArtifact.provisionalDelta);
|
|
materializedFromCache = true;
|
|
}
|
|
if (!publishTransaction || (execution.restored && !materializedFromCache)) {
|
|
throw new Error("Selected candidate is not materialized on the transactional mirror.");
|
|
}
|
|
// r11.6: only the final selected candidate constructs the public
|
|
// committed delta and whole-world hash. Provisional winners retain
|
|
// a replay state only, so multi-finalist comparison no longer forces
|
|
// a full winner regeneration and still preserves the r6 delta/hash
|
|
// single-build contract.
|
|
successDelta = buildWinnerDelta(publishTransaction, world);
|
|
successTargetHash = hashWinnerWorld(world);
|
|
} catch (error) {
|
|
if (publishTransaction) restorePatchTransactionSnapshot(world, publishTransaction);
|
|
const failed = { ok: false, code: "candidate-delta-build-failed", reason: error?.message || String(error) };
|
|
return {
|
|
terminal: { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0, candidateCount, draftSelection: true },
|
|
};
|
|
}
|
|
if (publishTransaction) restorePatchTransactionSnapshot(world, publishTransaction);
|
|
if (execution?.transaction === publishTransaction) execution.restored = true;
|
|
}
|
|
|
|
const result = execution.result;
|
|
const prior = attempts.find((entry) => entry.candidateId === candidateId);
|
|
const draftQuality = prior?.draftQuality || compactDraftQuality(row.draftResult);
|
|
if (prior) {
|
|
prior.status = "success";
|
|
prior.ok = true;
|
|
prior.selected = true;
|
|
prior.draftQuality = draftQuality;
|
|
prior.draftOnly = false;
|
|
prior.fullFinalized = true;
|
|
prior.draftSelectionScore = row.draftResult.score;
|
|
}
|
|
for (const attempt of attempts) if (attempt !== prior) attempt.selected = false;
|
|
const last = candidatePlan[candidatePlan.length - 1] || candidate;
|
|
result.searchAttempts = attempts;
|
|
result.searchStatus = "succeeded";
|
|
result.candidateOrdinal = candidateOrdinal;
|
|
result.candidateCount = candidateCount;
|
|
result.actualVariant = candidate.variant >>> 0;
|
|
result.actualSeed = candidate.seed >>> 0;
|
|
result.nextVariant = (((last?.variant || 0) >>> 0) + 1) >>> 0;
|
|
result.bestOfCandidates = true;
|
|
result.evaluatedCandidateCount = draftRows.length;
|
|
result.selectionScore = candidateQualityScore(result);
|
|
result.draftSelectionScore = row.draftResult.score;
|
|
result.draftSelection = {
|
|
enabled: true,
|
|
policy: "admissible-terrain-scout-branch-and-bound-two-lane-v2",
|
|
finalistCandidateOrdinal: candidateOrdinal,
|
|
finalistVariant: candidate.variant >>> 0,
|
|
draftCount: draftRows.length,
|
|
fullCandidateCount: fullFinalizedCandidateIds.size,
|
|
fullGenerationPassCount,
|
|
reusedWinningDraft: false,
|
|
reusedTerrainDraft: !!retainedDrafts.get(candidateId)?.draft,
|
|
fullProductionFromTerrainOnly: true,
|
|
parallelDraftGeneration, // legacy metadata name retained for UI compatibility
|
|
parallelDraftLaneCount: parallelDraftGeneration ? 2 : 0,
|
|
parallelTerrainScoutGeneration: parallelDraftGeneration,
|
|
terrainScoutLaneCount: parallelDraftGeneration ? 2 : 0,
|
|
parallelDraftFallbackReason,
|
|
branchBoundPrunedCount,
|
|
branchBoundPrunedCandidateOrdinals: branchBoundPrunedCandidateOrdinals.slice(),
|
|
selectionReason,
|
|
fullComparedCandidateOrdinals: fullComparedCandidateOrdinals.slice(),
|
|
ranking: rankedAll.map((entry, rank) => ({
|
|
rank: rank + 1,
|
|
candidateOrdinal: entry.candidateOrdinal,
|
|
variant: entry.candidate.variant >>> 0,
|
|
seed: entry.candidate.seed >>> 0,
|
|
score: entry.draftResult.score,
|
|
qualityUpperBound: draftCandidateUpperBound(entry.draftResult),
|
|
admissibleHardReject: draftAdmissibleHardReject(entry.draftResult),
|
|
})),
|
|
};
|
|
if (successDelta?.previewDelta) result.previewDelta = { ...successDelta.previewDelta };
|
|
return {
|
|
payload: {
|
|
id, ok: true,
|
|
world: transactional ? null : (successWorldArtifact || execution.candidateWorld),
|
|
transactionDelta: successDelta,
|
|
targetHash: successTargetHash,
|
|
result, searchId, workerEpoch, eventSeq,
|
|
},
|
|
};
|
|
};
|
|
|
|
let provisionalBest = null;
|
|
const remaining = ranked.slice();
|
|
while (remaining.length) {
|
|
const row = remaining.shift();
|
|
const currentBestScore = provisionalBest ? candidateQualityScore(provisionalBest.result) : -Infinity;
|
|
const upperBound = draftCandidateUpperBound(row.draftResult);
|
|
// Strict '<' preserves the existing deterministic seam/tie-break rules:
|
|
// an equal-score candidate can still win on seam quality, so equality is
|
|
// never pruned.
|
|
if (provisionalBest && upperBound < currentBestScore - 1e-12) {
|
|
branchBoundPrunedCount++;
|
|
branchBoundPrunedCandidateOrdinals.push(row.candidateOrdinal);
|
|
const prior = attempts.find((entry) => entry.candidateId === row.candidateId);
|
|
if (prior) {
|
|
prior.status = "bound-pruned";
|
|
prior.branchBoundBestScore = currentBestScore;
|
|
prior.qualityUpperBound = upperBound;
|
|
}
|
|
emitProgress({
|
|
status: "pruned", key: "branch-bound-pruned", phase: "finalist-generation",
|
|
workUnitId: `branch-bound-${row.candidateOrdinal}`,
|
|
label: `Candidate ${row.candidateOrdinal}/${candidateCount} cannot beat ${currentBestScore.toFixed(3)} (upper ${upperBound.toFixed(3)})`,
|
|
variant: row.candidate.variant >>> 0,
|
|
nonCooperative: true,
|
|
}, row.candidateOrdinal);
|
|
continue;
|
|
}
|
|
|
|
const execution = await executeFullCandidate(row, {
|
|
preserveSuccess: true,
|
|
label: `Fully evaluating candidate ${row.candidateOrdinal}/${candidateCount} (upper ${upperBound.toFixed(3)})`,
|
|
});
|
|
if (execution.terminal) {
|
|
return { id, ok: true, world: null, transactionDelta: null, targetHash: null, result: execution.terminal, searchId, workerEpoch, eventSeq };
|
|
}
|
|
if (execution.contentRejected || !execution.result?.ok) continue;
|
|
|
|
if (!provisionalBest || isBetterCandidate(execution.result, row.candidateOrdinal, {
|
|
result: provisionalBest.result,
|
|
candidateOrdinal: provisionalBest.row.candidateOrdinal,
|
|
})) {
|
|
// Capture the exact full-production state while it is live. If a later
|
|
// challenger does not beat it, r11.5 used to rerun the entire large
|
|
// candidate solely to reconstruct the winner. A bounded delta/hash
|
|
// (or isolated clone for non-transactional tests) removes that extra
|
|
// full generation pass without changing candidate comparison.
|
|
const winnerArtifact = captureWinnerArtifact(row, execution);
|
|
if (!winnerArtifact.ok) {
|
|
if (execution.transaction && !execution.restored) {
|
|
restorePatchTransactionSnapshot(world, execution.transaction);
|
|
execution.restored = true;
|
|
}
|
|
return { id, ok: true, world: null, transactionDelta: null, targetHash: null, result: {
|
|
ok: false,
|
|
code: winnerArtifact.code || "candidate-delta-build-failed",
|
|
reason: winnerArtifact.reason || "Could not retain provisional winner state.",
|
|
searchAttempts: attempts,
|
|
searchStatus: "infrastructure-error",
|
|
candidateCount,
|
|
draftSelection: true,
|
|
}, searchId, workerEpoch, eventSeq };
|
|
}
|
|
execution.winnerArtifact = winnerArtifact;
|
|
provisionalBest = { row, result: execution.result, winnerArtifact };
|
|
}
|
|
|
|
const bestScore = candidateQualityScore(provisionalBest.result);
|
|
const hasPotentialChallenger = remaining.some((future) => draftCandidateUpperBound(future.draftResult) >= bestScore - 1e-12);
|
|
const currentIsBest = provisionalBest.row.candidateId === row.candidateId;
|
|
if (currentIsBest && !hasPotentialChallenger) {
|
|
// Prune all remaining candidates now for diagnostics, then publish the
|
|
// live transaction without an avoidable rematerialization pass.
|
|
for (const future of remaining) {
|
|
branchBoundPrunedCount++;
|
|
branchBoundPrunedCandidateOrdinals.push(future.candidateOrdinal);
|
|
const prior = attempts.find((entry) => entry.candidateId === future.candidateId);
|
|
if (prior) {
|
|
prior.status = "bound-pruned";
|
|
prior.branchBoundBestScore = bestScore;
|
|
prior.qualityUpperBound = draftCandidateUpperBound(future.draftResult);
|
|
}
|
|
}
|
|
remaining.length = 0;
|
|
const published = publishSuccessfulCandidate(row, execution, "admissible-bound-winner-live");
|
|
if (published.terminal) return { id, ok: true, world: null, transactionDelta: null, targetHash: null, result: published.terminal, searchId, workerEpoch, eventSeq };
|
|
return published.payload;
|
|
}
|
|
|
|
// Another candidate can still win, or this candidate did not beat the
|
|
// previous best. Roll back the live mirror before the next challenger.
|
|
if (execution.transaction && !execution.restored) {
|
|
restorePatchTransactionSnapshot(world, execution.transaction);
|
|
execution.restored = true;
|
|
}
|
|
}
|
|
|
|
if (provisionalBest) {
|
|
const published = publishSuccessfulCandidate(provisionalBest.row, {
|
|
result: provisionalBest.result,
|
|
winnerArtifact: provisionalBest.winnerArtifact,
|
|
candidateWorld: provisionalBest.winnerArtifact?.world || null,
|
|
transaction: null,
|
|
restored: true,
|
|
}, "admissible-bound-cached-winner");
|
|
if (published.terminal) return { id, ok: true, world: null, transactionDelta: null, targetHash: null, result: published.terminal, searchId, workerEpoch, eventSeq };
|
|
return published.payload;
|
|
}
|
|
|
|
const last = candidatePlan[candidatePlan.length - 1];
|
|
const terminal = {
|
|
ok: false, code: "patch-search-exhausted",
|
|
reason: `All ${candidatePlan.length} candidates in this quality batch failed or were proven unable to satisfy the full production quality gate.`,
|
|
searchStatus: "exhausted", searchAttempts: attempts, candidateCount, draftSelection: true,
|
|
nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0,
|
|
};
|
|
return { id, ok: true, world: null, transactionDelta: null, targetHash: null, result: terminal, searchId, workerEpoch, eventSeq };
|
|
}
|
|
|
|
const attempts = [];
|
|
let terminalResult = null;
|
|
let successWorld = null;
|
|
let successDelta = null;
|
|
let successTargetHash = null;
|
|
let bestCandidate = null;
|
|
emitProgress({ status: "start", key: "search", phase: "search", workUnitId: "candidate-search", label: selectBestCandidate
|
|
? `Evaluating ${candidateCount} complete candidates and selecting the highest-quality result`
|
|
: `Searching ${candidateCount} complete candidate${candidateCount === 1 ? "" : "s"}`, completed: 0, total: candidateCount });
|
|
for (let index = 0; index < candidatePlan.length; index++) {
|
|
const candidate = candidatePlan[index];
|
|
const candidateOrdinal = Math.max(1, Number(candidate.candidateOrdinal || index + 1));
|
|
emitProgress({
|
|
status: "start",
|
|
key: transactional ? "candidate-transaction" : "candidate-clone",
|
|
phase: transactional ? "candidate-transaction" : "candidate-clone",
|
|
label: transactional
|
|
? `Candidate ${candidateOrdinal}/${candidateCount}: capturing exact rollback state`
|
|
: `Candidate ${candidateOrdinal}/${candidateCount}: preparing immutable baseline`,
|
|
variant: candidate.variant >>> 0,
|
|
workUnitId: "candidate-search",
|
|
// candidateOrdinal is global across the full search, whereas `index` is
|
|
// local to this Worker execution. Recovery can restart with candidates
|
|
// 2..N after candidate 1 was already rejected, so using `index` here
|
|
// made the shared search work unit move backward (2/3 -> 1/3).
|
|
completed: Math.max(0, candidateOrdinal - 1),
|
|
total: candidateCount,
|
|
nonCooperative: true,
|
|
}, candidateOrdinal);
|
|
let candidateWorld;
|
|
let transaction = null;
|
|
try {
|
|
if (transactional) {
|
|
transaction = capturePatchTransactionSnapshot(world, {
|
|
lightweight: false,
|
|
isolateSourceMap: true,
|
|
copyGeneratedMask: String(search?.resolvedPatchMode || "").toLowerCase() !== "regeneration",
|
|
});
|
|
candidateWorld = world;
|
|
} else {
|
|
candidateWorld = cloneWorld(world);
|
|
}
|
|
} catch (error) {
|
|
const failed = {
|
|
ok: false,
|
|
code: transactional ? "candidate-transaction-failed" : "candidate-clone-failed",
|
|
reason: error?.message || String(error),
|
|
};
|
|
attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, 0, "infrastructure-error", executionAttempt));
|
|
terminalResult = {
|
|
...failed,
|
|
searchAttempts: attempts,
|
|
searchStatus: "infrastructure-error",
|
|
nextVariant: candidate.variant >>> 0,
|
|
};
|
|
break;
|
|
}
|
|
const startedAt = clock();
|
|
let result;
|
|
try {
|
|
result = await generateCandidate(candidateWorld, rect, {
|
|
...(options || {}),
|
|
seed: candidate.seed >>> 0,
|
|
variant: candidate.variant >>> 0,
|
|
maxQualityRetries: 0,
|
|
// The production Worker mutates its committed mirror transactionally
|
|
// and restores it after extracting the accepted delta. Direct callers
|
|
// retain the immutable-clone reference path for independent tests.
|
|
_workerOwnedPreview: !transactional,
|
|
_externalTransactionSnapshot: transaction,
|
|
_precomputeRawCandidateBatch: dependencies.precomputeRawCandidateBatch,
|
|
_precomputeRawCandidateSequence: dependencies.precomputeRawCandidateSequence,
|
|
_rawCandidateParallelism: Math.max(1, Math.min(2, Number(dependencies.rawCandidateParallelism || 2))),
|
|
onProgress: (progress) => {
|
|
// Producer workUnitIds are invocation-local. The same complete
|
|
// pipeline is executed again when a content-rejected candidate
|
|
// advances to the next Variant, so namespace every explicit inner
|
|
// unit by candidate ordinal before the search-wide monotonicity
|
|
// validator sees it. Do not invent an ID for a bounded producer
|
|
// that omitted one; emitProgress must still reject that protocol
|
|
// violation instead of masking it.
|
|
const rawWorkUnitId = progress?.workUnitId != null && String(progress.workUnitId).length > 0
|
|
? String(progress.workUnitId)
|
|
: null;
|
|
emitProgress({
|
|
...progress,
|
|
workUnitId: rawWorkUnitId ? `candidate-${candidateOrdinal}/${rawWorkUnitId}` : progress?.workUnitId,
|
|
variant: candidate.variant >>> 0,
|
|
seed: candidate.seed >>> 0,
|
|
}, candidateOrdinal);
|
|
},
|
|
});
|
|
if (!selectBestCandidate) result = normalizeCandidateResult(result);
|
|
} catch (error) {
|
|
if (transaction) restorePatchTransactionSnapshot(world, transaction);
|
|
if (error?.code === "worker-progress-invariant") throw error;
|
|
const wallMs = clock() - startedAt;
|
|
const failed = {
|
|
ok: false,
|
|
code: "candidate-execution-error",
|
|
reason: error?.message || String(error),
|
|
stack: error?.stack || "",
|
|
};
|
|
attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "execution-error", executionAttempt));
|
|
terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "execution-error", nextVariant: candidate.variant >>> 0 };
|
|
break;
|
|
}
|
|
const wallMs = clock() - startedAt;
|
|
if (result?.ok) {
|
|
let candidateDelta = null;
|
|
let candidateTargetHash = null;
|
|
if (transaction) {
|
|
try {
|
|
candidateDelta = buildCommittedMirrorDeltaFromTransaction(transaction, candidateWorld);
|
|
candidateTargetHash = hashCommittedWorld(candidateWorld);
|
|
} catch (error) {
|
|
restorePatchTransactionSnapshot(world, transaction);
|
|
const failed = {
|
|
ok: false,
|
|
code: "candidate-delta-build-failed",
|
|
reason: error?.message || String(error),
|
|
};
|
|
attempts.push(summarizeAttempt(failed, candidate, candidateOrdinal, wallMs, "infrastructure-error", executionAttempt));
|
|
terminalResult = { ...failed, searchAttempts: attempts, searchStatus: "infrastructure-error", nextVariant: candidate.variant >>> 0 };
|
|
break;
|
|
}
|
|
restorePatchTransactionSnapshot(world, transaction);
|
|
}
|
|
if (!selectBestCandidate) {
|
|
successDelta = candidateDelta;
|
|
successTargetHash = candidateTargetHash;
|
|
attempts.push(summarizeAttempt(result, candidate, candidateOrdinal, wallMs, "success", executionAttempt));
|
|
result.searchAttempts = attempts;
|
|
result.searchStatus = "succeeded";
|
|
result.candidateOrdinal = candidateOrdinal;
|
|
result.candidateCount = candidateCount;
|
|
result.actualVariant = candidate.variant >>> 0;
|
|
result.actualSeed = candidate.seed >>> 0;
|
|
result.nextVariant = ((candidate.variant >>> 0) + 1) >>> 0;
|
|
if (successDelta?.previewDelta) result.previewDelta = { ...successDelta.previewDelta };
|
|
terminalResult = result;
|
|
successWorld = transactional ? null : candidateWorld;
|
|
emitProgress({ status: "done", key: "search", phase: "search", workUnitId: "candidate-search", label: `Candidate ${candidateOrdinal}/${candidateCount} accepted`, variant: candidate.variant >>> 0, completed: candidateOrdinal, total: candidateCount }, candidateOrdinal);
|
|
break;
|
|
}
|
|
|
|
if (candidateDelta?.previewDelta) result.previewDelta = { ...candidateDelta.previewDelta };
|
|
const attemptSummary = summarizeAttempt(result, candidate, candidateOrdinal, wallMs, "evaluated", executionAttempt);
|
|
attemptSummary.selectionScore = candidateQualityScore(result);
|
|
attempts.push(attemptSummary);
|
|
if (isBetterCandidate(result, candidateOrdinal, bestCandidate)) {
|
|
bestCandidate = {
|
|
result, candidate, candidateOrdinal,
|
|
world: transactional ? null : candidateWorld,
|
|
delta: candidateDelta,
|
|
targetHash: candidateTargetHash,
|
|
};
|
|
}
|
|
emitProgress({
|
|
status: "evaluated",
|
|
key: "candidate-evaluated",
|
|
phase: "candidate-result",
|
|
workUnitId: "candidate-search",
|
|
label: `Candidate ${candidateOrdinal}/${candidateCount} evaluated (quality ${candidateQualityScore(result).toFixed(3)}); continuing best-of-${candidateCount} selection`,
|
|
variant: candidate.variant >>> 0,
|
|
completed: candidateOrdinal,
|
|
total: candidateCount,
|
|
attemptSummary,
|
|
}, candidateOrdinal);
|
|
candidateWorld = null;
|
|
transaction = null;
|
|
result = null;
|
|
continue;
|
|
}
|
|
|
|
const invariant = isInvariantFailure(result);
|
|
const contentRejected = isContentRejection(result);
|
|
const status = invariant ? "invariant-breach" : contentRejected ? "rejected" : "failed";
|
|
const attemptSummary = summarizeAttempt(result, candidate, candidateOrdinal, wallMs, status, executionAttempt);
|
|
attempts.push(attemptSummary);
|
|
if (transaction) restorePatchTransactionSnapshot(world, transaction);
|
|
emitProgress({
|
|
status: contentRejected ? "rejected" : "error",
|
|
key: contentRejected ? "candidate-rejected" : "candidate-failed",
|
|
phase: "candidate-result",
|
|
label: contentRejected
|
|
? `Candidate ${candidateOrdinal}/${candidateCount} rejected; searching the next complete candidate`
|
|
: `Candidate ${candidateOrdinal}/${candidateCount} stopped: ${result?.reason || result?.code || "failure"}`,
|
|
variant: candidate.variant >>> 0,
|
|
workUnitId: "candidate-search",
|
|
completed: candidateOrdinal,
|
|
total: candidateCount,
|
|
code: result?.code || null,
|
|
attemptSummary,
|
|
}, candidateOrdinal);
|
|
if (!contentRejected) {
|
|
terminalResult = {
|
|
...(result || { ok: false }),
|
|
searchAttempts: attempts,
|
|
searchStatus: invariant ? "invariant-breach" : "failed",
|
|
nextVariant: candidate.variant >>> 0,
|
|
};
|
|
break;
|
|
}
|
|
// The rejected full world is no longer observable. Drop the last strong
|
|
// references before cloning the next Variant so the browser may reclaim
|
|
// its field buffers instead of retaining multiple complete candidates.
|
|
candidateWorld = null;
|
|
transaction = null;
|
|
result = null;
|
|
}
|
|
|
|
if (!terminalResult && selectBestCandidate && bestCandidate) {
|
|
terminalResult = finalizeBestCandidate(bestCandidate, attempts, candidatePlan, candidateCount);
|
|
successWorld = bestCandidate.world;
|
|
successDelta = bestCandidate.delta;
|
|
successTargetHash = bestCandidate.targetHash;
|
|
if (successDelta?.previewDelta) terminalResult.previewDelta = { ...successDelta.previewDelta };
|
|
emitProgress({
|
|
status: "done", key: "search", phase: "search", workUnitId: "candidate-search",
|
|
label: `Selected candidate ${bestCandidate.candidateOrdinal}/${candidateCount} with highest quality ${candidateQualityScore(bestCandidate.result).toFixed(3)}`,
|
|
variant: bestCandidate.candidate.variant >>> 0, completed: candidateCount, total: candidateCount,
|
|
}, bestCandidate.candidateOrdinal);
|
|
}
|
|
|
|
if (!terminalResult) {
|
|
const last = candidatePlan[candidatePlan.length - 1];
|
|
terminalResult = {
|
|
ok: false,
|
|
code: "patch-search-exhausted",
|
|
reason: `All ${candidateCount} complete production candidates were rejected.`,
|
|
searchStatus: "exhausted",
|
|
searchAttempts: attempts,
|
|
nextVariant: (((last?.variant || 0) >>> 0) + 1) >>> 0,
|
|
candidateCount,
|
|
};
|
|
emitProgress({ status: "done", key: "search-exhausted", phase: "search", workUnitId: "candidate-search", label: terminalResult.reason, completed: candidateCount, total: candidateCount });
|
|
}
|
|
|
|
if (successWorld && terminalResult?.ok) {
|
|
emitProgress({
|
|
status: "start",
|
|
key: "preview-delta",
|
|
phase: "preview-delta",
|
|
label: "Auditing preview changes",
|
|
}, terminalResult.candidateOrdinal || 0);
|
|
terminalResult.previewDelta = computePreviewDelta(world, successWorld, terminalResult.rects || rect, (completed, total, part) => {
|
|
emitProgress({
|
|
status: "step",
|
|
key: `preview-delta:${part}`,
|
|
phase: "preview-delta",
|
|
label: part === "raster" ? "Auditing preview raster changes" : "Auditing preview feature changes",
|
|
workUnitId: "preview-delta-audit",
|
|
completed,
|
|
total,
|
|
}, terminalResult.candidateOrdinal || 0);
|
|
});
|
|
emitProgress({
|
|
status: "done",
|
|
key: "preview-delta",
|
|
phase: "preview-delta",
|
|
label: "Preview change audit complete",
|
|
}, terminalResult.candidateOrdinal || 0);
|
|
}
|
|
|
|
// Return only the accepted candidate world. Rejected candidate clones and
|
|
// the immutable worker baseline remain worker-local and become collectible.
|
|
return {
|
|
id,
|
|
ok: true,
|
|
world: successWorld,
|
|
transactionDelta: successDelta,
|
|
targetHash: successTargetHash,
|
|
result: terminalResult,
|
|
searchId,
|
|
workerEpoch,
|
|
eventSeq,
|
|
};
|
|
} catch (error) {
|
|
return { id, ok: false, code: error?.code || "candidate-search-error", error: error?.message || String(error), stack: error?.stack || "", searchId, workerEpoch, eventSeq };
|
|
}
|
|
}
|
|
|
|
if (typeof self !== "undefined") {
|
|
self.onmessage = async (event) => {
|
|
const incoming = event.data || {};
|
|
if (handleMirrorSyncMessage(incoming)) return;
|
|
if (incoming.type === "patch-apply-discard") {
|
|
if (incoming.applyToken) pendingApplyDeltas.delete(incoming.applyToken);
|
|
return;
|
|
}
|
|
if (incoming.type === "patch-apply-ack") {
|
|
const pending = pendingApplyDeltas.get(incoming.applyToken);
|
|
let ok = false;
|
|
let error = null;
|
|
try {
|
|
if (!pending) throw new Error("Pending patch delta is unavailable.");
|
|
if (persistentCommittedRevision !== Number(incoming.baseCommittedRevision)) {
|
|
throw new Error(`Committed mirror revision conflict (${persistentCommittedRevision} != ${incoming.baseCommittedRevision}).`);
|
|
}
|
|
applyCommittedWorldDelta(persistentCommittedMirror, pending.delta, { consumeMetadata: true });
|
|
const mirrorHash = hashCommittedWorld(persistentCommittedMirror);
|
|
if (mirrorHash !== pending.targetHash) {
|
|
throw new Error(`Committed mirror hash mismatch (${mirrorHash} != ${pending.targetHash}).`);
|
|
}
|
|
persistentCommittedRevision = Number(incoming.committedRevision);
|
|
pendingApplyDeltas.delete(incoming.applyToken);
|
|
ok = true;
|
|
} catch (applyError) {
|
|
error = applyError?.message || String(applyError);
|
|
persistentCommittedMirror = null;
|
|
persistentCommittedRevision = -1;
|
|
pendingApplyDeltas.clear();
|
|
}
|
|
self.postMessage({
|
|
type: "patch-apply-ack-result",
|
|
ackId: incoming.ackId,
|
|
applyToken: incoming.applyToken,
|
|
ok,
|
|
error,
|
|
mirrorHash: ok ? pending?.targetHash || null : null,
|
|
mirrorCommittedRevision: persistentCommittedRevision,
|
|
});
|
|
return;
|
|
}
|
|
const requestedRevision = Number(incoming.search?.committedRevision ?? -1);
|
|
if (incoming.world) {
|
|
persistentCommittedMirror = incoming.world;
|
|
persistentCommittedRevision = requestedRevision;
|
|
pendingMirrorBootstrap = null;
|
|
pendingApplyDeltas.clear();
|
|
} else if (!incoming.search?.reuseCommittedMirror || !persistentCommittedMirror || persistentCommittedRevision !== requestedRevision) {
|
|
self.postMessage({
|
|
id: incoming.id,
|
|
ok: false,
|
|
error: "Committed worker mirror is unavailable or stale.",
|
|
code: "worker-mirror-stale",
|
|
searchId: incoming.search?.searchId || null,
|
|
workerEpoch: Number(incoming.search?.workerEpoch || 0),
|
|
});
|
|
return;
|
|
}
|
|
const payload = await runPatchCandidateSearchAsync({ ...incoming, world: persistentCommittedMirror }, {
|
|
onProgress: (message) => self.postMessage(message),
|
|
transactional: true,
|
|
precomputeRawCandidateBatch,
|
|
precomputeRawDraftBatch,
|
|
precomputeRawTerrainScoutBatch,
|
|
precomputeRawCandidateSequence: scheduleRawCandidateSequence,
|
|
rawCandidateParallelism: 2,
|
|
});
|
|
// Helper workers are scoped to one candidate-search operation. Releasing
|
|
// them here prevents a completed/rejected large search from retaining two
|
|
// full generator heaps while the coordinator waits for Apply/Alternative.
|
|
await shutdownRawCandidateWorkers();
|
|
payload.mirrorCommittedRevision = persistentCommittedRevision;
|
|
if (payload.ok && payload.result?.ok && (payload.transactionDelta || payload.world)) {
|
|
payload.eventSeq = Number(payload.eventSeq || 0) + 1;
|
|
self.postMessage({
|
|
id: incoming.id,
|
|
type: "progress",
|
|
progress: {
|
|
searchId: payload.searchId,
|
|
operationId: incoming.search?.operationId || payload.searchId,
|
|
candidateOrdinal: Number(payload.result?.candidateOrdinal || 0),
|
|
candidateCount: Number(payload.result?.candidateCount || incoming.search?.totalCandidateCount || 0),
|
|
executionAttempt: Math.max(1, Number(incoming.search?.executionAttempt || 1)),
|
|
workerEpoch: payload.workerEpoch,
|
|
committedRevision: requestedRevision,
|
|
eventSeq: payload.eventSeq,
|
|
counter: payload.eventSeq,
|
|
phase: "mirror-delta-build",
|
|
phaseOrdinal: Number.MAX_SAFE_INTEGER - 1,
|
|
workUnitId: "mirror-delta-build",
|
|
boundedWork: false,
|
|
cooperative: false,
|
|
nonCooperative: true,
|
|
status: "start",
|
|
key: "mirror-delta-build",
|
|
label: "Building transactional Apply delta",
|
|
},
|
|
});
|
|
const applyToken = `${payload.searchId}:${payload.result.candidateOrdinal || 0}:${payload.result.actualVariant ?? payload.result.variant ?? 0}:${requestedRevision}`;
|
|
try {
|
|
const delta = payload.transactionDelta || buildCommittedMirrorDelta(persistentCommittedMirror, payload.world);
|
|
const targetHash = payload.targetHash || hashCommittedWorld(payload.world);
|
|
const mainThreadDelta = buildMainThreadTransferDelta(delta);
|
|
pendingApplyDeltas.clear();
|
|
pendingApplyDeltas.set(applyToken, { delta, baseCommittedRevision: requestedRevision, targetHash });
|
|
// The main thread needs one transferable copy to materialize the
|
|
// preview. Keep the original delta Worker-local for the later Apply ACK.
|
|
payload.worldDelta = mainThreadDelta;
|
|
payload.transactionDelta = null;
|
|
payload.targetHash = null;
|
|
payload.world = null;
|
|
payload.result.applyToken = applyToken;
|
|
payload.result.acceptedWorldHash = targetHash;
|
|
} catch (deltaError) {
|
|
pendingApplyDeltas.delete(applyToken);
|
|
payload.transactionDelta = null;
|
|
payload.targetHash = null;
|
|
payload.world = null;
|
|
payload.result = {
|
|
...payload.result,
|
|
ok: false,
|
|
code: "candidate-delta-build-failed",
|
|
reason: deltaError?.message || String(deltaError),
|
|
searchStatus: "infrastructure-error",
|
|
applyToken: null,
|
|
applyDeltaError: deltaError?.message || String(deltaError),
|
|
};
|
|
}
|
|
}
|
|
if (payload.ok) {
|
|
payload.eventSeq = Number(payload.eventSeq || 0) + 1;
|
|
self.postMessage({
|
|
id: incoming.id,
|
|
type: "progress",
|
|
progress: {
|
|
searchId: payload.searchId,
|
|
operationId: incoming.search?.operationId || payload.searchId,
|
|
candidateOrdinal: Number(payload.result?.candidateOrdinal || 0),
|
|
candidateCount: Number(payload.result?.candidateCount || incoming.search?.totalCandidateCount || 0),
|
|
executionAttempt: Math.max(1, Number(incoming.search?.executionAttempt || 1)),
|
|
workerEpoch: payload.workerEpoch,
|
|
committedRevision: requestedRevision,
|
|
eventSeq: payload.eventSeq,
|
|
counter: payload.eventSeq,
|
|
phase: "result-serialization",
|
|
phaseOrdinal: Number.MAX_SAFE_INTEGER,
|
|
workUnitId: "result-serialization",
|
|
boundedWork: false,
|
|
cooperative: false,
|
|
nonCooperative: true,
|
|
status: "start",
|
|
key: "result-serialization",
|
|
label: "Serializing accepted patch result",
|
|
},
|
|
});
|
|
}
|
|
// Accepted transactional results keep all large typed payloads under the
|
|
// world delta. Traversing the complete diagnostic/result graph here caused
|
|
// a long no-progress pause at the final stage without finding additional
|
|
// buffers. Legacy full-world payloads still transfer their world buffers.
|
|
// Only the copied raster delta buffers are transferable. Metadata in
|
|
// worldDelta deliberately shares the Worker-retained ACK graph until
|
|
// postMessage clones it; transferring a nested typed metadata buffer would
|
|
// detach the retained delta and make Apply fail.
|
|
const transferRoot = payload.worldDelta
|
|
? { fields: payload.worldDelta.fields, generatedMask: payload.worldDelta.generatedMask }
|
|
: (payload.world || null);
|
|
const transfer = payload.ok && transferRoot ? Array.from(collectTransferableBuffers(transferRoot)) : [];
|
|
self.postMessage(payload, transfer);
|
|
};
|
|
}
|