diff --git a/skill/scripts/build-phase.mjs b/skill/scripts/build-phase.mjs index 820006d3c..bd6873e44 100644 --- a/skill/scripts/build-phase.mjs +++ b/skill/scripts/build-phase.mjs @@ -523,7 +523,16 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH, // the numbers are the edit list: keep it short enough to act on in one // 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)); + // sibling regions (row-1 ... row-8, item-a / item-b) with the same kind + // of finding are one edit: fold them into one line naming the siblings + const folded = new Map(); + for (const f of readings.text) { + const m = /^text ([a-z0-9]+(?:-[a-z0-9]+)*?)(?:-(?:\d+|[a-z]))?: (cap height|\d+ lines? in the build|the face renders|ink is|its first line|it starts|line pitch)/i.exec(f); + const key = m ? `${m[1]}|${m[2].replace(/\d+/g, 'N')}` : f; + if (!folded.has(key)) folded.set(key, { first: f, ids: [] }); + const idm = /^text ([^:]+):/.exec(f); if (idm) folded.get(key).ids.push(idm[1]); + } + const text = [...folded.values()].map((v) => (v.ids.length > 1 ? `${v.first} (also ${v.ids.slice(1).join(', ')})` : v.first)).sort((a, b) => order(a) - order(b)); // 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 diff --git a/skill/scripts/lib/hero-checks.mjs b/skill/scripts/lib/hero-checks.mjs index a37d09a06..394c37fb1 100644 --- a/skill/scripts/lib/hero-checks.mjs +++ b/skill/scripts/lib/hero-checks.mjs @@ -81,11 +81,15 @@ export function textRegionCheck(region, compCrop, buildCrop, { capTol = 0.22, mi if (r > 1.25) findings.push(`text ${region.id}: the face renders ${Math.round((r - 1) * 100)}% heavier than the comp's (ink density ${bfp.densTall.toFixed(2)} vs ${comp.densTall.toFixed(2)}); drop a weight step or use the ranked face`); else if (r < 0.75) findings.push(`text ${region.id}: the face renders ${Math.round((1 - r) * 100)}% lighter than the comp's (ink density ${bfp.densTall.toFixed(2)} vs ${comp.densTall.toFixed(2)}); raise a weight step or use the ranked face`); } - // colour: dominant ink of each crop - const ca = inkColor(compCrop), cb = inkColor(buildCrop); - if (ca && cb && ca.ink && cb.ink) { - const d = deltaE(ca.ink.lab, cb.ink.lab); - if (d > 22) findings.push(`text ${region.id}: ink is ${cb.ink.hex} in the build, ${ca.ink.hex} in the comp; use the comp's colour`); + // colour: dominant ink of each crop. Small type on a ruled or grainy + // ground (a track row across staff lines at cap 14) has no reliable ink + // cluster; the reading fired both ways on neighbouring rows of one list. + if (comp.capHeightPx >= 16) { + const ca = inkColor(compCrop), cb = inkColor(buildCrop); + if (ca && cb && ca.ink && cb.ink) { + const d = deltaE(ca.ink.lab, cb.ink.lab); + if (d > 22) findings.push(`text ${region.id}: ink is ${cb.ink.hex} in the build, ${ca.ink.hex} in the comp; use the comp's colour`); + } } // vertical placement inside the box: top of ink const ba = inkBox(compCrop), bb = inkBox(buildCrop);