Round-2 sim fixes: one plate rule, plate size from the gate's floor, missing means empty

plateVerdict() is shared by the plates gate and generate-image's PLATE-WARN
so they cannot disagree; --plate picks a frame that clears the 1.5x width
floor (a square region wider than 682px takes the 1536 landscape frame);
'missing' on text/chrome/control regions requires the build region to be
near-empty, so a 12px rule a few pixels off reads as contradicted or drift,
not missing; the responsive gate does not re-litigate a plate that passed
the hero; record hero after close does not inflate the attempt count.

AI-assisted (Claude).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-08-28 06:13:59 +05:00
committed by Abdul Wahab
co-authored by Claude
parent af4ac68805
commit bfa6014796
3 changed files with 46 additions and 10 deletions
+26 -6
View File
@@ -195,6 +195,25 @@ export function gateSpec(state, { specPath = SPEC_PATH } = {}) {
return { ok: true, reasons: [], summary: `${spec.regions.length} regions, ${plates} plates` };
}
/**
* The one plate rule, shared by the plates gate and generate-image's
* PLATE-WARN so they never disagree. `score` is compare().whole against the
* masked comp crop under cover alignment.
*/
export function plateVerdict(region, score) {
const isTexture = region.kind === 'texture';
const reasons = [];
if (isTexture) {
const effective = 0.5 * score.color + 0.5 * Math.min(1, score.detail / 0.6);
if (effective < PLATE_MIN) reasons.push(`scores ${(effective * 100).toFixed(0)}% as the material of region ${region.id} (color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); crop a clean patch of the comp region (comp-spec.mjs --crop ${region.id} --raw) and mirror-tile it, generate only when no clean patch exists`);
return { ok: reasons.length === 0, reasons, effective };
}
if (score.detailAdded > 0.45) reasons.push(`carries detail the comp region ${region.id} does not have (added-detail ${(score.detailAdded * 100).toFixed(0)}% of cells): noise, grain, or a busier subject where the comp is calm; regenerate from the crop reference without adding texture`);
if (score.structure < PLATE_STRUCTURE_MIN) reasons.push(`structure ${(score.structure * 100).toFixed(0)}% against the comp region ${region.id}: the composition of the plate is not the region's (different subject, orientation, or crop); regenerate with comp-spec.mjs --crop ${region.id} as the reference image`);
if (score.overall < PLATE_MIN) reasons.push(`scores ${(score.overall * 100).toFixed(0)}% against the comp region ${region.id} (structure ${(score.structure * 100).toFixed(0)}%, color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); regenerate with the crop as --ref and the comp-spec plate prompt`);
return { ok: reasons.length === 0, reasons, effective: score.overall };
}
export function gatePlates(state, { specPath = SPEC_PATH } = {}) {
const spec = loadSpec(specPath);
if (!spec) return { ok: false, reasons: ['no spec'] };
@@ -221,10 +240,8 @@ export function gatePlates(state, { specPath = SPEC_PATH } = {}) {
const ref = plateReference(comp, spec, r);
const res = compare({ comp: ref, build: img, align: 'cover', spec: null, kind: r.kind });
score = res.whole;
const effective = isTexture ? 0.5 * score.color + 0.5 * Math.min(1, score.detail / 0.6) : score.overall;
if (!isTexture && score.detailAdded > 0.45) reasons.push(`plate ${file} carries detail the comp region ${r.id} does not have (added-detail ${(score.detailAdded * 100).toFixed(0)}% of cells): noise, grain, or a busier subject where the comp is calm. Regenerate from the crop reference; do not add texture the comp does not show.`);
if (!isTexture && score.structure < PLATE_STRUCTURE_MIN) reasons.push(`plate ${file} has structure ${(score.structure * 100).toFixed(0)}% against the comp region ${r.id}: the composition of the plate is not the region's (a different subject, orientation, or crop). Regenerate with comp-spec.mjs --crop ${r.id} as the reference image; a plate that only shares the palette and busyness is not this plate.`);
if (effective < PLATE_MIN) reasons.push(`plate ${file} scores ${(effective * 100).toFixed(0)}% against the comp region ${r.id} (structure ${(score.structure * 100).toFixed(0)}%, color ${(score.color * 100).toFixed(0)}%, detail ${(score.detail * 100).toFixed(0)}%); it does not read as the same ${isTexture ? 'material' : 'region'}. Regenerate with the crop as --ref and the comp-spec plate prompt${isTexture ? ', or crop a clean patch of the comp region and tile it' : ''}.`);
const v = plateVerdict(r, score);
for (const reason of v.reasons) reasons.push(`plate ${file}: ${reason}`);
}
plates.push({ id: r.id, file, status: 'ok', size: `${img.width}x${img.height}`, score: score ? score.overall : null });
}
@@ -401,7 +418,10 @@ export function gateResponsive(state, { specPath = SPEC_PATH, min = RESPONSIVE_M
let report;
try { report = JSON.parse(res.stdout); } catch { return { ok: false, reasons: [`comp-diff failed on ${desktop}: ${res.stderr || res.stdout}`] }; }
const missing = report.regions.filter((r) => r.verdict === 'missing');
const contradictedDirection = report.regions.filter((r) => r.verdict === 'contradicted' && (r.kind === 'plate' || r.kind === 'image' || r.kind === 'text'));
// A plate placed and passed at the hero is not re-litigated at 1440: the
// rescale alone drops SSIM on a busy region. Text can still contradict
// (a wrapped headline is a different composition).
const contradictedDirection = report.regions.filter((r) => r.verdict === 'contradicted' && r.kind === 'text');
if (report.overall < min) reasons.push(`the desktop capture (${report.buildSize}) scores ${(report.overall * 100).toFixed(0)}% against the comp, under ${(min * 100).toFixed(0)}%: the first viewport does not survive a common desktop width. The hero passed at ${state.breakpoint || 'the comp size'}; the layout must hold from ~1280 up, not only at the comp's exact width (grid columns in fr / minmax, not fixed px that overflow and wrap).`);
for (const r of missing) reasons.push(`at desktop width, region ${r.id} is missing`);
for (const r of contradictedDirection) reasons.push(`at desktop width, region ${r.id} (${r.kind}) is contradicted (structure ${(r.score.structure * 100).toFixed(0)}%)`);
@@ -542,7 +562,7 @@ async function main() {
const which = process.argv[3];
if (which !== 'hero') { console.error('build-phase: record hero --build <png>'); process.exit(1); }
const gate = gateHero(state, { buildPath: arg('build', HERO_REPRO), min: arg('min') ? parseFloat(arg('min')) : HERO_MIN });
state.phases.hero.attempts += 1;
if (state.phases.hero.status !== 'closed') state.phases.hero.attempts += 1;
state.phases.hero.gate = { ...gate, at: now() };
saveState(state);
// record is the look, advance is the gate: on the plates-only capture,