This commit is contained in:
33333-33333 2026-08-11 21:51:07 +09:00
commit de7c6c32bd
123 changed files with 13201 additions and 6213 deletions

View file

@ -87,7 +87,6 @@ const showLabelsInput = document.getElementById("showLabels");
const showSeamDiagnosticsInput = document.getElementById("showSeamDiagnostics");
const modeGrid = document.getElementById("modeGrid");
const mainLegendGrid = document.getElementById("mainLegendGrid");
const floatingLegendGrid = document.getElementById("floatingLegendGrid");
const statsEl = document.getElementById("stats");
const advancedGenerationStatsEl = document.getElementById("advancedGenerationStats");
const advancedGenerationHistoryEl = document.getElementById("advancedGenerationHistory");
@ -128,8 +127,9 @@ let patchGeometryPreviewCache = null;
let patchWorkerEpoch = 0;
let patchWorkerConstructorCount = 0;
let patchSearchSeries = { contextId: null, consumedCandidateIds: new Set(), nextVariant: null };
const PATCH_SEARCH_DEFAULT_LIMIT = 3;
const PATCH_SEARCH_LARGE_LIMIT = 2;
const PATCH_SEARCH_BATCH_SIZE = 3;
const PATCH_SEARCH_DEFAULT_LIMIT = 12;
const PATCH_SEARCH_LARGE_LIMIT = 12;
const PATCH_WORKER_STALL_MS = 120_000;
const PATCH_NON_COOPERATIVE_DEADLINE_MS = 300_000;
let generationWorker = null;
@ -718,6 +718,14 @@ function commitPendingPatch({ redrawAfter = true } = {}) {
const acceptedPatch = state.pendingPatch;
const applyWorker = acceptedPatch.worker && acceptedPatch.result?.applyToken ? patchWorker : null;
state.world = acceptedPatch.world;
// Seam diagnostics are a preview aid. Once the patch is applied, disable the
// magenta/red dashed overlay in both the committed source data and UI state so
// it cannot remain stuck on the map after the selection overlay disappears.
if (state.world?.sourceMap?.patchSeamDiagnostics) {
state.world.sourceMap.patchSeamDiagnostics.enabled = false;
}
state.showSeamDiagnostics = false;
if (showSeamDiagnosticsInput) showSeamDiagnosticsInput.checked = false;
const committedRevision = advanceCommittedRevision({ preservePatchWorker: !!applyWorker });
state.map = state.world.sourceMap || state.map;
state.lastPatchResult = state.pendingPatch.result || state.world.lastPatchResult || state.lastPatchResult;
@ -1236,7 +1244,7 @@ function seamDiagnosticRows() {
{ label: "Candidate mode", value: d.patchGenerationMode || "-", sub: modeSub },
{ label: "Candidate window", value: `${Number(d.candidateWidth || 0)}×${Number(d.candidateHeight || 0)}`, sub: `${formatPercent(Number(d.candidateAreaRatio || 0) * 100)} of full candidate area` },
...(d.qualityPolicyVersion ? [
{ label: "Expansion quality gate", value: d.qualityHardPass ? "PASS" : "BEST AVAILABLE", sub: `${d.qualityPolicyVersion} · score ${Number(d.qualityScore || 0).toFixed(3)} · selected variant ${Number(d.qualitySelectedVariant || 0)}` },
{ label: "Expansion quality gate", value: d.qualityHardPass ? "PASS" : "FAIL", sub: `${d.qualityPolicyVersion} · score ${Number(d.qualityScore || 0).toFixed(3)} · selected variant ${Number(d.qualitySelectedVariant || 0)}` },
{ label: "Candidate land quality", value: formatPercent(Number(d.qualityLandRatio || 0) * 100), sub: `${d.qualityTerrainType || "-"} · ${formatPercent(Number(d.qualityDevelopableRatio || 0) * 100)} developable · ${formatPercent(Number(d.qualityLargestComponentRatio || 0) * 100)} largest component` },
{ label: "Candidate place density", value: Number(d.qualityLabelCount || 0).toLocaleString(), sub: `${Number(d.qualitySettlementCount || 0)} settlements · ${Number(d.qualityLabelDensityPer1000 || 0).toFixed(2)} labels / 1000 land cells` },
{ label: "Merged patch quality", value: d.qualityFinalHardPass ? "PASS" : "WARNING", sub: `${formatPercent(Number(d.qualityFinalOwnedLandRatio || 0) * 100)} land in owned interior · ${Number(d.qualityFinalLabelCount || 0)} labels / ${Number(d.qualityFinalSettlementCount || 0)} settlements` },
@ -2036,7 +2044,6 @@ function renderLegendGrid(container, rows) {
function renderLegend() {
const rows = legendRowsForMode(state.mode);
renderLegendGrid(mainLegendGrid, rows);
renderLegendGrid(floatingLegendGrid, rows.slice(0, 5));
}
function buildHoverEntities(map) {
@ -2246,13 +2253,49 @@ function updateTooltip(event) {
if (entity?.population) lines.splice(1, 0, `Population: ${entity.population.toLocaleString()}`);
tooltipEl.innerHTML = lines.join("<br>");
const margin = 8;
const offset = 14;
const maxLeft = Math.max(margin, rect.width - tooltipEl.offsetWidth - margin);
const maxTop = Math.max(margin, rect.height - tooltipEl.offsetHeight - margin);
const desiredLeft = event.clientX - rect.left + offset;
const desiredTop = event.clientY - rect.top + offset;
tooltipEl.style.left = `${Math.min(Math.max(margin, desiredLeft), maxLeft)}px`;
tooltipEl.style.top = `${Math.min(Math.max(margin, desiredTop), maxTop)}px`;
const gap = 18;
const cursorX = event.clientX - rect.left;
const cursorY = event.clientY - rect.top;
const tooltipWidth = tooltipEl.offsetWidth;
const tooltipHeight = tooltipEl.offsetHeight;
const maxLeft = Math.max(margin, rect.width - tooltipWidth - margin);
const maxTop = Math.max(margin, rect.height - tooltipHeight - margin);
// Keep following the cursor all the way to the bottom. Near the lower edge,
// prefer moving to the cursor's left/right while pinning the tooltip to the
// bottom margin; the old vertical flip made it appear to stop moving well
// before the cursor reached the bottom of the map.
const roomRight = rect.width - cursorX - gap;
const roomLeft = cursorX - gap;
const canRight = roomRight >= tooltipWidth;
const canLeft = roomLeft >= tooltipWidth;
let left = canRight ? cursorX + gap
: canLeft ? cursorX - gap - tooltipWidth
: Math.min(Math.max(margin, cursorX + gap), maxLeft);
let top = Math.min(Math.max(margin, cursorY + 10), maxTop);
left = Math.min(Math.max(margin, left), maxLeft);
// Cursor exclusion is a hard invariant. If horizontal separation is not
// available (very narrow viewport), then and only then move vertically.
const exclusion = 12;
const overlapsCursor = () => cursorX >= left - exclusion && cursorX <= left + tooltipWidth + exclusion
&& cursorY >= top - exclusion && cursorY <= top + tooltipHeight + exclusion;
if (overlapsCursor()) {
const leftAlt = cursorX - gap - tooltipWidth;
const rightAlt = cursorX + gap;
if (leftAlt >= margin) left = leftAlt;
else if (rightAlt + tooltipWidth <= rect.width - margin) left = rightAlt;
}
if (overlapsCursor()) {
const above = cursorY - gap - tooltipHeight;
const below = cursorY + gap;
if (above >= margin) top = above;
else if (below + tooltipHeight <= rect.height - margin) top = below;
}
left = Math.min(Math.max(margin, left), maxLeft);
top = Math.min(Math.max(margin, top), maxTop);
tooltipEl.style.left = `${left}px`;
tooltipEl.style.top = `${top}px`;
tooltipEl.classList.add("visible");
}
@ -2443,7 +2486,7 @@ function patchSearchContextId(world, rect, terrainType, requestedPatchMode, reso
world?.height || 0,
world?.originX || 0,
world?.originY || 0,
"production-search-v2|single-explicit-production-candidate-v2",
"quality-batched-production-search-v4|initial-quality-oracle-transport-parity-v3",
].join("|");
}
@ -2489,7 +2532,7 @@ function buildPatchCandidatePlan(world, rect, terrainType, requestedPatchMode, r
function consumePatchSearchAttempts(contextId, attempts = []) {
if (patchSearchSeries.contextId !== contextId) return;
for (const attempt of attempts) {
if (attempt?.status !== "rejected" && attempt?.status !== "success") continue;
if (attempt?.status !== "rejected" && attempt?.status !== "evaluated" && attempt?.status !== "success") continue;
const candidateId = `${contextId}|${normalizePatchVariant(attempt.variant)}|${normalizePatchVariant(attempt.seed)}`;
patchSearchSeries.consumedCandidateIds.add(candidateId);
}
@ -3059,6 +3102,8 @@ function runPatchInWorker(world, rect, options, operation = null) {
totalCandidateCount: operation.candidateLimit || operation.candidatePlan.length,
reuseCommittedMirror,
resolvedPatchMode: operation.resolvedPatchMode,
selectBestCandidate: operation.selectBestCandidate === true,
draftSelection: operation.draftSelection === true,
candidatePlan: operation.candidatePlan,
} : null,
});
@ -3199,6 +3244,41 @@ async function generatePatchPreviewWorld(baseWorld, rect, options, operation) {
operation.executionAttempt = infrastructureRetries + 1;
continue;
}
// r9 quality search is batched: only three drafts are evaluated at a time.
// A batch that contains no publishable, fully finalized candidate advances
// to the next three variants without ever exposing a draft or a quality-
// rejected production result. Structural/invariant failures remain terminal.
const contentBatchExhausted = execution.result?.searchStatus === "exhausted"
&& ["patch-search-exhausted", "patch-draft-search-exhausted"].includes(String(execution.result?.code || ""));
const allCandidatePlan = Array.isArray(operation.allCandidatePlan) ? operation.allCandidatePlan : operation.candidatePlan;
const nextCandidateIndex = Math.max(0, Number(operation.nextCandidateIndex || 0));
if (contentBatchExhausted && nextCandidateIndex < allCandidatePlan.length) {
operation.completedAttemptSummaries ||= [];
for (const attempt of execution.result?.searchAttempts || []) {
const identity = `${attempt?.candidateId || `${attempt?.variant}:${attempt?.seed}`}|${attempt?.status || "unknown"}`;
if (!operation.completedAttemptSummaries.some((entry) => `${entry?.candidateId || `${entry?.variant}:${entry?.seed}`}|${entry?.status || "unknown"}` === identity)) {
operation.completedAttemptSummaries.push({ ...attempt });
}
}
const batchSize = Math.max(1, Number(operation.candidateBatchSize || PATCH_SEARCH_BATCH_SIZE));
const batchEnd = Math.min(allCandidatePlan.length, nextCandidateIndex + batchSize);
operation.candidatePlan = allCandidatePlan.slice(nextCandidateIndex, batchEnd);
operation.nextCandidateIndex = batchEnd;
operation.candidateBatchOrdinal = Math.max(1, Number(operation.candidateBatchOrdinal || 1)) + 1;
operation.executionAttempt = Math.max(1, Number(operation.executionAttempt || 1)) + 1;
if (progressStageEl) {
const firstOrdinal = operation.candidatePlan[0]?.candidateOrdinal || (nextCandidateIndex + 1);
const lastOrdinal = operation.candidatePlan[operation.candidatePlan.length - 1]?.candidateOrdinal || batchEnd;
progressStageEl.textContent = `Quality batch ${operation.candidateBatchOrdinal}: candidates ${firstOrdinal}-${lastOrdinal}/${allCandidatePlan.length}; previous batch did not meet the production quality floor...`;
}
recordDiagnosticLog("info", "Patch quality search continuing", `No publishable candidate in quality batch ${operation.candidateBatchOrdinal - 1}; evaluating the next batch.`, {
completedCandidates: nextCandidateIndex,
remainingCandidates: allCandidatePlan.length - nextCandidateIndex,
nextVariants: operation.candidatePlan.map((candidate) => candidate.variant >>> 0),
});
continue;
}
job = execution;
} catch (error) {
executionRecord.endedAt = performance.now();
@ -3311,8 +3391,10 @@ async function generateSelectedPatch(kind = "Patch preview") {
patchMode,
resolvedPatchMode,
candidateWindowSignature,
generatorPolicyVersion: "production-search-v2",
qualityPolicyVersion: "single-explicit-production-candidate-v2",
generatorPolicyVersion: "quality-batched-production-search-v4",
qualityPolicyVersion: "initial-quality-oracle-transport-parity-v3",
selectBestCandidate: true,
draftSelection: true,
requestedVariant: variant,
requestedSeed: seed,
currentVariant: variant,
@ -3322,7 +3404,11 @@ async function generateSelectedPatch(kind = "Patch preview") {
completedAttemptSummaries: [],
executions: [],
candidateLimit: searchPlan.candidateLimit,
candidatePlan: searchPlan.plan,
candidateBatchSize: PATCH_SEARCH_BATCH_SIZE,
allCandidatePlan: searchPlan.plan,
candidatePlan: searchPlan.plan.slice(0, PATCH_SEARCH_BATCH_SIZE),
nextCandidateIndex: Math.min(PATCH_SEARCH_BATCH_SIZE, searchPlan.plan.length),
candidateBatchOrdinal: 1,
estimatedTileCount: searchPlan.estimatedTileCount,
includeSeamVisualization: state.showSeamDiagnostics,
selectionRect: validation.rect,
@ -3333,7 +3419,7 @@ async function generateSelectedPatch(kind = "Patch preview") {
state.patchBusyVariant = variant;
state.patchStatusMessage = "";
updatePatchControls();
setProgressVisible(true, `Searching up to ${searchPlan.plan.length} complete candidate${searchPlan.plan.length === 1 ? "" : "s"} from variant ${variant}...`);
setProgressVisible(true, `Generating quality batch 1 (${operation.candidatePlan.length} lightweight drafts, up to ${searchPlan.plan.length} candidates); only fully finalized candidates can be previewed...`);
await nextFrame();
try {
if (!isPatchOperationCurrent(operation)) return;
@ -3342,12 +3428,10 @@ async function generateSelectedPatch(kind = "Patch preview") {
patchMode,
seed,
variant,
// Expansion previews use the same complete production pipeline as initial
// generation. A quality rejection is shown for this exact variant; only
// the explicit Alternative action requests another complete candidate.
// Three lightweight drafts are ranked first. Only the top draft normally
// runs administration, final transport, merge repair, seam audit, delta,
// and hash construction. Structural/invariant failures remain fatal.
maxQualityRetries: 0,
// One attempt still runs the complete production pipeline. Additional
// complete candidates are generated only if the strict final gate fails.
qualityTerrainAttempts: 1,
acceptBestAvailableQuality: false,
includeSeamVisualization: operation.includeSeamVisualization,
@ -3423,20 +3507,17 @@ async function generateSelectedPatch(kind = "Patch preview") {
inputDispatchMs: job.dispatchMs, inputMirrorReused: job.mirrorReused, estimatedTileCount: operation.estimatedTileCount,
});
state.patchStatusMessage = !previewDelta.identical
? `Candidate ${result.candidateOrdinal || 1}/${result.candidateCount || searchPlan.plan.length}, variant ${actualVariant}, is displayed (render revision ${job.world.renderRevision}); ${previewDelta.changedCells.toLocaleString()} cells and ${previewDelta.featureLayersChanged.toLocaleString()} feature layers differ from the committed map.`
? `Highest-quality candidate ${result.candidateOrdinal || 1}/${result.candidateCount || searchPlan.plan.length}, variant ${actualVariant}, is displayed (render revision ${job.world.renderRevision}); ${previewDelta.changedCells.toLocaleString()} cells and ${previewDelta.featureLayersChanged.toLocaleString()} feature layers differ from the committed map.`
: `Variant ${actualVariant} completed but is identical to the committed map in the audited change scope.`;
updatePatchControls();
const modeText = `${result.patchMode || patchMode} / ${result.patchGenerationMode}`;
const quality = result.candidateQuality;
const qualityText = quality
? ` / quality ${quality.hardPass ? "PASS" : "best available"} ${Number(quality.score || 0).toFixed(3)} / selected terrain variant ${Number(quality.selectedVariant || 0)}`
? ` / quality ${quality.hardPass ? "PASS" : "FAIL"} ${Number(quality.score || 0).toFixed(3)} / selected terrain variant ${Number(quality.selectedVariant || 0)}`
: "";
const retryText = job.qualityWorkerRetryCount ? ` / quality retries ${job.qualityWorkerRetryCount}` : "";
const searchText = ` / candidate ${result.candidateOrdinal || 1}/${result.candidateCount || searchPlan.plan.length} / complete attempts ${searchAttempts.length || 1}`;
const searchText = ` / best candidate ${result.candidateOrdinal || 1}/${result.candidateCount || searchPlan.plan.length} / evaluated ${searchAttempts.length || 1}`;
finishProgress(`Preview variant ${actualVariant} displayed${job.worker ? " from worker" : ""}: ${previewDelta.changedCells.toLocaleString()} changed cells + ${previewDelta.featureLayersChanged.toLocaleString()} changed feature layers / end-to-end ${formatMs(performance.now() - patchStartedAt)} / dispatch ${formatMs(job.dispatchMs || 0)} / render ${formatMs(renderMs)}${searchText}${retryText}${qualityText} / ${modeText}.`, result.patchTimings || [], 1400);
if (result.candidateQuality && !result.candidateQuality.hardPass) {
recordDiagnosticLog("warning", "Expansion quality floor not fully met", `Selected the best available production candidate (score ${Number(result.candidateQuality.score || 0).toFixed(3)}).`, { terrainType, variant: actualVariant, requestedVariant: variant, candidateQuality: result.candidateQuality });
}
} catch (error) {
if (error?.name === "AbortError") {
if (requestId === patchRequestSeq) {