From bae0c24f45fd2a4b07089d0d87d86755123be3a5 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 17 Aug 2026 12:14:28 -0700 Subject: [PATCH] Hero readings go advisory after three unchanged attempts; overall shows a decimal near the floor; a single link is not a strip One cf6 session spent 27 attempts on the same three readings and read '72% < 72%'. AI-assisted (Claude Code). --- skill/scripts/build-phase.mjs | 23 +++++++++++++++++++---- skill/scripts/lib/hero-checks.mjs | 7 +++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/skill/scripts/build-phase.mjs b/skill/scripts/build-phase.mjs index 50e6abdc1..c42b20a89 100644 --- a/skill/scripts/build-phase.mjs +++ b/skill/scripts/build-phase.mjs @@ -408,7 +408,7 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH, const compAspect = cw / ch, buildAspect = bw / bh; if (bw < cw * 0.9 || Math.abs(buildAspect - compAspect) / compAspect > 0.08) reasons.push(`hero capture is ${bw}x${bh}; the comp is ${cw}x${ch}. Capture the first viewport at the comp's own dimensions (viewport ${cw}x${ch}, not full page) into ${buildPath}.`); } - if (report.overall < min) reasons.push(`hero overall ${(report.overall * 100).toFixed(0)}% < ${(min * 100).toFixed(0)}% (structure ${(report.scores.structure * 100).toFixed(0)}%, color ${(report.scores.color * 100).toFixed(0)}%, detail ${(report.scores.detail * 100).toFixed(0)}%)`); + if (report.overall < min) reasons.push(`hero overall ${(report.overall * 100).toFixed(1)}% < ${(min * 100).toFixed(0)}% (structure ${(report.scores.structure * 100).toFixed(0)}%, color ${(report.scores.color * 100).toFixed(0)}%, detail ${(report.scores.detail * 100).toFixed(0)}%)`); if (report.scores.colorIntersection != null && report.scores.colorIntersection < 0.2) reasons.push(`the palette is not the comp's (color intersection ${(report.scores.colorIntersection * 100).toFixed(0)}%): comp ${(report.palette.comp || []).slice(0, 3).map((c) => c.hex).join(' ')} vs build ${(report.palette.build || []).slice(0, 3).map((c) => c.hex).join(' ')}. Use the spec's sampled palette values, not a rendition of them.`); // A texture band that shares its box with a text/control region carries // that region's ink in the comp crop; when the overlapping ink regions are @@ -500,6 +500,7 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH, // size, weight, colour, or place; a nav bar too tall; ink where the comp // has none (a kicker, a divider, a second row). Each was a pin in the // first human review of builds the region scores had passed. + const advisories = []; let readings = null; try { readings = heroReadings(state, specForRefs, buildPath); } catch (e) { reasons.push(`hero readings errored (${e.message}); the region scores above stand`); } if (readings) { @@ -507,10 +508,20 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH, // pass (the worst first: size, then lines, then colour and place) const order = (f) => (/cap height/.test(f) ? 0 : /lines? in the build/.test(f) ? 1 : /heavier|lighter/.test(f) ? 2 : /ink is/.test(f) ? 3 : 4); const text = [...readings.text].sort((a, b) => order(a) - order(b)); - const kept = text.slice(0, 8); - if (kept.length) reasons.push(`READINGS, each one CSS edit (${text.length > kept.length ? `${kept.length} of ${text.length}, the rest after these` : `${kept.length}`}):`); + // A reading that has come back unchanged three attempts running is one + // the session is not acting on (or cannot: a measured "rule" that is + // really an underline the layout does not have). It stays in the message + // as advisory and stops blocking; the fresh readings still do. One + // session spent 27 attempts on the same three lines. + const seen = state.phases.hero.readingsSeen || (state.phases.hero.readingsSeen = {}); + const stale = (f) => { const k = f.replace(/\s+/g, ' ').trim(); seen[k] = (seen[k] || 0) + 1; return seen[k] > 3; }; + const all = [...text, ...readings.chrome]; + const fresh = all.filter((f) => !stale(f)); + const advisory = all.filter((f) => !fresh.includes(f)); + const kept = fresh.slice(0, 8); + if (kept.length) reasons.push(`READINGS, each one CSS edit (${fresh.length > kept.length ? `${kept.length} of ${fresh.length}, the rest after these` : `${kept.length}`}):`); for (const f of kept) reasons.push(f); - for (const f of readings.chrome) reasons.push(f); + if (advisory.length) advisories.push(...advisory.map((f) => `(advisory, unchanged for 3+ attempts) ${f}`)); if (readings.invented && readings.invented.fraction >= INVENTED_MIN) { const cells = readings.invented.cells.map((c) => c.label); reasons.push(`the build carries ink in ${cells.length} grid cells where the comp is calm (${cells.slice(0, 12).join(', ')}${cells.length > 12 ? ', ...' : ''}); nothing exists on the page that the comp does not show (a kicker, an extra nav item, a divider, a second row of controls); remove it or name it in a stated decision after the hero passes`); @@ -529,6 +540,7 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH, worst: worstRegions.map((r) => `${r.id} ${r.verdict} ${(r.score.overall * 100).toFixed(0)}%`), worstIds: worstRegions.map((r) => r.id), worstCrops: worstRegions.map((r) => ({ id: r.id, verdict: r.verdict, score: r.score, file: path.join(regionDir, `${r.id}.png`) })), + advisories, regionVerdicts: Object.fromEntries(report.regions.map((r) => [r.id, r.verdict])), }; } @@ -777,6 +789,7 @@ async function main() { if (plateRows.length) console.log(`PLATES ${plateRows.map(([id, v]) => `${id}:${v}`).join(' ')}`); console.log(`${gate.ok ? 'PASS' : 'FAIL'} ${gate.summary || ''} (record: nothing advanced)`); for (const r of gate.reasons) console.log(` - ${r}`); + if (gate.advisories) for (const a of gate.advisories) console.log(` ${a}`); if (gate.worst) console.log(` worst: ${gate.worst.join('; ')}`); if (gate.sideBySide) console.log(` open ${gate.sideBySide}`); process.exit(gate.ok ? 0 : 2); @@ -796,10 +809,12 @@ async function main() { 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.advisories && res.gate.advisories.length) for (const a of res.gate.advisories) console.log(` ${a}`); if (res.gate && res.gate.sideBySide) console.log(` then ${res.gate.sideBySide} for the whole viewport`); process.exit(2); } console.log(`ADVANCED ${res.phase} -> ${res.next}${res.forced ? ' (FORCED; recorded)' : ''}${res.gate.summary ? ` ${res.gate.summary}` : ''}`); + if (res.gate && res.gate.advisories && res.gate.advisories.length) for (const a of res.gate.advisories) console.log(` ${a}`); console.log(`NEXT ${nextInstruction(state)}`); return; } diff --git a/skill/scripts/lib/hero-checks.mjs b/skill/scripts/lib/hero-checks.mjs index e9cd6b4c9..9fde90cba 100644 --- a/skill/scripts/lib/hero-checks.mjs +++ b/skill/scripts/lib/hero-checks.mjs @@ -112,6 +112,13 @@ export function chromeStripCheck(region, compCrop, buildCrop) { const findings = []; const strip = compCrop.height <= compCrop.width * 0.35; if (!strip) return { findings }; + // a control that is one link or one button, not a bar across its box, has + // no strip height to compare (its underline read as a 'rule' for 27 + // attempts in one session) + if (region.kind === 'control') { + const ib = inkBox(compCrop); + if (!ib || ib.w < compCrop.width * 0.6) return { findings }; + } const ra = ruleRows(compCrop), rb = ruleRows(buildCrop); if (ra.length && rb.length) { // the rule that closes the strip is the first one from the top (a grid