mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-14 15:16:35 +03:00
Hero gate reads type, strips, and invented ink as numbers; every region needs a note; missing beats the text relaxation
From the first human review of sweep-3 builds (pins on 12 samples): fonts at the wrong size, weight, colour, or place; nav bars too tall; kickers and dividers the comp does not have; a footer strip pushed off the frame that read as drift on ground colour alone; a drawing filed as chrome with no note to catch it. - lib/hero-checks.mjs: textRegionCheck (cap height, line count, ink density, ink colour, first-line offset vs the comp crop, measured fresh), chromeStripCheck (first rule row), inventedInk (build energy over a calm comp cell and neighbourhood). Wired into gateHero as reasons; invented ink vetoes at 4% of cells. - comp-diff verdictFor: detailRaw < 0.15 is missing whatever the palette. - comp-spec: every region carries a note. - font-fingerprint: the tall-line filter takes its median over lines with real mass, so two display lines above a small line are not 'tall'. AI-assisted (Claude Code).
This commit is contained in:
+15
-15
@@ -48,7 +48,7 @@ describe('comp-spec', () => {
|
||||
it('measures regions with palette, pixel box, medium, and plate path', () => {
|
||||
const comp = makeComp();
|
||||
const spec = measureRegions(comp, { allowUncovered: true, regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0', note: 'navy masthead bar' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4', note: 'noise plate' },
|
||||
] }, 'comp.png');
|
||||
assert.equal(spec.regions.length, 2);
|
||||
@@ -76,18 +76,18 @@ describe('comp-spec', () => {
|
||||
|
||||
it('refuses a code region that covers a column of the comp, unless container', () => {
|
||||
const comp = makeComp();
|
||||
assert.throws(() => measureRegions(comp, { allowUncovered: true, regions: [{ id: 'parts-column', kind: 'chrome', grid: 'G0:J9' }] }, 'c.png'), /covers 40% of the comp/);
|
||||
const ok = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'parts-column', kind: 'chrome', grid: 'G0:J9', container: true }] }, 'c.png');
|
||||
assert.throws(() => measureRegions(comp, { allowUncovered: true, regions: [{ id: 'parts-column', kind: 'chrome', grid: 'G0:J9', note: 'right column of parts' }] }, 'c.png'), /covers 40% of the comp/);
|
||||
const ok = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'parts-column', kind: 'chrome', grid: 'G0:J9', container: true, note: 'right column of parts' }] }, 'c.png');
|
||||
assert.equal(ok.regions[0].kind, 'chrome');
|
||||
const plate = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'art', kind: 'plate', grid: 'G0:J9' }] }, 'c.png');
|
||||
const plate = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'art', kind: 'plate', grid: 'G0:J9', note: 'noise plate' }] }, 'c.png');
|
||||
assert.equal(plate.regions[0].medium, 'raster');
|
||||
});
|
||||
|
||||
it('refuses a regions file that leaves comp ink unnamed, unless allowUncovered', () => {
|
||||
const comp = makeComp();
|
||||
// only the masthead named: the headline, plate, and list are ink no region covers
|
||||
assert.throws(() => measureRegions(comp, { regions: [{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' }] }, 'c.png'), /carry ink no region names/);
|
||||
const spec = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' }] }, 'c.png');
|
||||
assert.throws(() => measureRegions(comp, { regions: [{ id: 'masthead', kind: 'chrome', grid: 'A0:J0', note: 'navy masthead bar' }] }, 'c.png'), /carry ink no region names/);
|
||||
const spec = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'masthead', kind: 'chrome', grid: 'A0:J0', note: 'navy masthead bar' }] }, 'c.png');
|
||||
assert.ok(spec.uncoveredInkCells.length > 3);
|
||||
});
|
||||
|
||||
@@ -155,10 +155,10 @@ describe('build-phase state machine (CLI)', () => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'build-phase-'));
|
||||
fs.writeFileSync(path.join(dir, 'comp.png'), encodePng(makeComp()));
|
||||
fs.writeFileSync(path.join(dir, 'regions.json'), JSON.stringify({ regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'headline', kind: 'text', grid: 'A1:D2' },
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0', note: 'navy masthead bar' },
|
||||
{ id: 'headline', kind: 'text', grid: 'A1:D2', note: 'two-line block headline' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4', note: 'noise plate' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9', container: true },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9', container: true, note: 'ruled list rows' },
|
||||
] }));
|
||||
});
|
||||
after(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} });
|
||||
@@ -242,7 +242,7 @@ describe('build-phase state machine (CLI)', () => {
|
||||
fs.writeFileSync(path.join(dir, '.impeccable', 'review', 'hero-repro.png'), encodePng(flat));
|
||||
// no source references the plate yet: refused before any diff runs
|
||||
let res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
assert.equal(res.status, 2, res.stdout);
|
||||
assert.equal(res.status, 2, res.stdout + res.stderr);
|
||||
assert.match(res.stdout, /not referenced by any source file/);
|
||||
fs.writeFileSync(path.join(dir, 'index.html'), '<img src="assets/plates/art.png" alt="">');
|
||||
res = run(PHASE_SCRIPT, ['advance'], dir);
|
||||
@@ -285,8 +285,8 @@ describe('build-phase state machine (CLI)', () => {
|
||||
fillRect(comp, 40, 300, 160, 40, [19, 33, 48, 255]);
|
||||
fs.writeFileSync(path.join(d4, 'comp.png'), encodePng(comp));
|
||||
fs.writeFileSync(path.join(d4, 'regions.json'), JSON.stringify({ allowUncovered: true, regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'cta', kind: 'control', box: { x: 0, y: 0.5, w: 0.5, h: 0.5 } },
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0', note: 'navy masthead bar' },
|
||||
{ id: 'cta', kind: 'control', box: { x: 0, y: 0.5, w: 0.5, h: 0.5 }, note: 'black CTA button' },
|
||||
] }));
|
||||
run(PHASE_SCRIPT, ['start', '--comp', 'comp.png', '--artifact', 'index.html'], d4);
|
||||
run(SPEC_SCRIPT, ['--comp', 'comp.png', '--regions', 'regions.json'], d4);
|
||||
@@ -312,9 +312,9 @@ describe('build-phase state machine (CLI)', () => {
|
||||
const comp = makeComp();
|
||||
fs.writeFileSync(path.join(d3, 'comp.png'), encodePng(comp));
|
||||
fs.writeFileSync(path.join(d3, 'regions.json'), JSON.stringify({ allowUncovered: true, regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9', container: true },
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0', note: 'navy masthead bar' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4', note: 'noise plate' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9', container: true, note: 'ruled list rows' },
|
||||
] }));
|
||||
run(PHASE_SCRIPT, ['start', '--comp', 'comp.png', '--artifact', 'index.html'], d3);
|
||||
run(SPEC_SCRIPT, ['--comp', 'comp.png', '--regions', 'regions.json'], d3);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createImage, fillRect, drawText } from '../skill/scripts/lib/raster.mjs';
|
||||
import { textRegionCheck, chromeStripCheck, inventedInk, ruleRows, inkColor } from '../skill/scripts/lib/hero-checks.mjs';
|
||||
|
||||
const paper = [235, 232, 220, 255], ink = [20, 20, 20, 255];
|
||||
function textCrop({ scale = 4, lines = 2, color = ink, y0 = 20, x0 = 12, w = 460, h = 200 } = {}) {
|
||||
const img = createImage(w, h, paper);
|
||||
for (let i = 0; i < lines; i++) drawText(img, 'KEEP OLD IRON', x0, y0 + i * (scale * 9), color, scale);
|
||||
return img;
|
||||
}
|
||||
|
||||
describe('hero-checks: text regions', () => {
|
||||
const region = { id: 'headline', kind: 'text', type: { chosen: { family: 'Six Caps', weight: 400, fontSizePx: 40 } } };
|
||||
it('says nothing when the build sets the type as the comp does', () => {
|
||||
const { findings } = textRegionCheck(region, textCrop(), textCrop());
|
||||
assert.deepEqual(findings, []);
|
||||
});
|
||||
it('names a cap-height miss with the ranked face and size', () => {
|
||||
const { findings } = textRegionCheck(region, textCrop({ scale: 4 }), textCrop({ scale: 6, lines: 2 }));
|
||||
assert.ok(findings.some((f) => /cap height .*px in the build, .*px in the comp/.test(f) && /Six Caps 400 at 40px/.test(f)), findings.join('\n'));
|
||||
});
|
||||
it('names a different line count', () => {
|
||||
const { findings } = textRegionCheck(region, textCrop({ lines: 3 }), textCrop({ lines: 2 }));
|
||||
assert.ok(findings.some((f) => /2 lines in the build, 3 in the comp/.test(f)), findings.join('\n'));
|
||||
});
|
||||
it('names a colour change and a vertical shift', () => {
|
||||
const { findings } = textRegionCheck(region, textCrop(), textCrop({ color: [180, 40, 30, 255], y0: 80 }));
|
||||
assert.ok(findings.some((f) => /ink is #/.test(f)), findings.join('\n'));
|
||||
assert.ok(findings.some((f) => /starts 60px lower/.test(f)), findings.join('\n'));
|
||||
});
|
||||
it('stays quiet on rotated or unmeasurable comp crops', () => {
|
||||
const blank = createImage(300, 300, paper);
|
||||
assert.deepEqual(textRegionCheck(region, blank, textCrop()).findings, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hero-checks: chrome strips and invented ink', () => {
|
||||
it('reads a strip height off its rule', () => {
|
||||
const mk = (ruleY) => { const img = createImage(800, 100, paper); fillRect(img, 0, ruleY, 800, 2, ink); drawText(img, 'THREADS GARAGE', 20, 12, ink, 2); return img; };
|
||||
assert.deepEqual(ruleRows(mk(60)), [59]); // the edge row above the rule
|
||||
const { findings } = chromeStripCheck({ id: 'masthead', kind: 'chrome' }, mk(44), mk(70));
|
||||
assert.ok(findings.some((f) => /43px into the box in the comp and 69px in the build/.test(f)), findings.join('\n'));
|
||||
assert.deepEqual(chromeStripCheck({ id: 'masthead', kind: 'chrome' }, mk(44), mk(47)).findings, []);
|
||||
});
|
||||
it('lists cells where the build carries ink over a calm comp', () => {
|
||||
const comp = createImage(1000, 1000, paper);
|
||||
const build = createImage(1000, 1000, paper);
|
||||
drawText(build, 'SECTION KICKER', 20, 20, ink, 3);
|
||||
fillRect(build, 100, 500, 800, 2, ink);
|
||||
const r = inventedInk(comp, build);
|
||||
assert.ok(r.cells.length >= 3, `cells ${r.cells.length}`);
|
||||
assert.ok(r.cells.some((c) => c.row === 0), 'the kicker row');
|
||||
assert.deepEqual(inventedInk(comp, comp).cells, []);
|
||||
});
|
||||
it('inkColor separates ink from ground', () => {
|
||||
const c = inkColor(textCrop({ color: [180, 40, 30, 255] }));
|
||||
assert.ok(c.ink && /^#[0-9a-f]{6}$/.test(c.ink.hex));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user