diff --git a/skill/scripts/build-phase.mjs b/skill/scripts/build-phase.mjs index 3fba37724..623201f55 100644 --- a/skill/scripts/build-phase.mjs +++ b/skill/scripts/build-phase.mjs @@ -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(); diff --git a/skill/scripts/comp-diff.mjs b/skill/scripts/comp-diff.mjs index 75c68f377..1c5d3fd91 100644 --- a/skill/scripts/comp-diff.mjs +++ b/skill/scripts/comp-diff.mjs @@ -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(); diff --git a/skill/scripts/comp-spec.mjs b/skill/scripts/comp-spec.mjs index ae566bcf5..90a66d900 100644 --- a/skill/scripts/comp-spec.mjs +++ b/skill/scripts/comp-spec.mjs @@ -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();