Hero gate: text and chrome regions read as drift once structure and palette hold; scripts run through symlinks

Three simulated builds under the previous gate reached 78-83% overall with
the exploded plate placed and the table right, then spent 12-20 attempts
chasing 'contradicted' verdicts on a headline set in a substitute face and
on 50px chrome strips whose detail was paper grain. Text with structure
above the floor and its palette intact is drift; chrome and controls with
structure and palette held are drift. The adversarial set (swapped
columns, mirror, sepia, noise plate, tile shuffle) still fails.

isMain uses realpath on both sides so a skill mounted through a symlink
(Cursor, worktrees, staged evals) still runs its CLIs.

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 d9a155be8c
commit 24773eeedf
3 changed files with 31 additions and 4 deletions
+7 -2
View File
@@ -531,7 +531,7 @@ async function main() {
if (res.gate && res.gate.worstCrops && res.gate.worstCrops.length) {
console.log(' LOOK FIRST, in this order, before editing anything (comp on the left, your build on the right):');
for (const c of res.gate.worstCrops) console.log(` ${c.file} ${c.id}: ${c.verdict} ${(c.score.overall * 100).toFixed(0)}% (structure ${(c.score.structure * 100).toFixed(0)}%, color ${(c.score.color * 100).toFixed(0)}%, detail ${(c.score.detail * 100).toFixed(0)}%)`);
console.log(' A region scored missing needs its material (a plate placed, or produced), not a value change; contradicted needs its structure re-derived from the spec box; drift is where padding and size edits belong.');
console.log(' A region scored missing needs its material (a plate placed, or produced), not a value change; contradicted needs its structure re-derived from the spec box; drift is where padding and size edits belong. When a thin chrome strip (masthead, breadcrumb, table header) is the worst region, check its box height in the spec against the comp first: a strip one grid row tall in the spec but 53px in the comp compares your build against ground it never had.');
}
for (const r of res.reasons) console.log(` - ${r}`);
if (res.gate && res.gate.sideBySide) console.log(` then ${res.gate.sideBySide} for the whole viewport`);
@@ -554,5 +554,10 @@ async function main() {
process.exit(1);
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
// realpath on both sides: a skill mounted through a symlink (Cursor, a
// worktree, an eval stage) must still run as a CLI.
const isMain = (() => {
try { return !!process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); }
catch { return !!process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); }
})();
if (isMain) main();
+17 -1
View File
@@ -39,6 +39,7 @@
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { decodePng, encodePng } from './lib/png.mjs';
import { crop, resize, fit, blit, createImage, fillRect, strokeRect, drawLabel } from './lib/raster.mjs';
import { structureScore, colorScore, detailScore, diffMap, horizontalBands, bandScore, dominantColors, toGray, blurGray, ssimShifted } from './lib/image-metrics.mjs';
@@ -139,6 +140,16 @@ export function verdictFor(s, kind = null) {
// the weighted mean says; painted regions with invented detail likewise.
if (s.structure < 0.3) return 'contradicted';
if (painted && (s.structure < 0.45 || s.detailAdded > 0.4)) return 'contradicted';
// Text is set in a substitute face at a slightly different metric almost
// always, and blurred SSIM reads glyph shape; a text region with its
// structure above the swap floor and its palette intact is drift at worst.
// Chasing it past that point is what burned eight to thirteen hero attempts
// per build in the first simulated round.
if (kind === 'text' && s.color >= 0.5) return s.overall >= 0.8 ? 'match' : 'drift';
// Chrome and controls are thin strips whose "detail" is mostly ground grain
// (a paper texture the build renders flatter, a scanline). When their
// structure and palette hold, low detail is drift, not contradiction.
if ((kind === 'chrome' || kind === 'control') && s.structure >= 0.5 && s.color >= 0.5) return s.overall >= 0.8 ? 'match' : 'drift';
if (s.overall >= 0.8) return 'match';
if (s.overall >= 0.6) return 'drift';
return 'contradicted';
@@ -333,5 +344,10 @@ async function main() {
}
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
// realpath on both sides: a skill mounted through a symlink (Cursor, a
// worktree, an eval stage) must still run as a CLI.
const isMain = (() => {
try { return !!process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); }
catch { return !!process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); }
})();
if (isMain) main();
+7 -1
View File
@@ -37,6 +37,7 @@
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { decodePng, encodePng } from './lib/png.mjs';
import { crop, resize, fillRect, strokeRect, drawLabel, drawText } from './lib/raster.mjs';
import { dominantColors, horizontalBands, detailGrid } from './lib/image-metrics.mjs';
@@ -320,5 +321,10 @@ async function main() {
console.log(printSpec(spec));
}
const isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname);
// realpath on both sides: a skill mounted through a symlink (Cursor, a
// worktree, an eval stage) must still run as a CLI.
const isMain = (() => {
try { return !!process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); }
catch { return !!process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname); }
})();
if (isMain) main();