mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Hero refuses inline SVG illustrations; finish cannot record ship over an open phase; the comp-led path names its model tier
From the human review's most repeated pin ('terrible svg instead of asset', on every model) and from sessions that wrote 'ship' with the hero open. Icons, arrows, chevrons, and runtime data charts stay code; diagrams, notation, and leader lines are plates.
AI-assisted (Claude Code).
This commit is contained in:
@@ -71,7 +71,7 @@ const require = createRequire(import.meta.url);
|
||||
import { crop, createImage, blit, resize } from './lib/raster.mjs';
|
||||
import { structureScore } from './lib/image-metrics.mjs';
|
||||
import { compare, verdictFor, alignBuild, bestShift } from './comp-diff.mjs';
|
||||
import { textRegionCheck, chromeStripCheck, inventedInk, plateClipCheck } from './lib/hero-checks.mjs';
|
||||
import { textRegionCheck, chromeStripCheck, inventedInk, plateClipCheck, svgIllustrations } from './lib/hero-checks.mjs';
|
||||
import { SPEC_PATH, BUILD_DIR, loadSpec, plateReference } from './comp-spec.mjs';
|
||||
import { choiceStamped } from './font-match.mjs';
|
||||
|
||||
@@ -521,6 +521,15 @@ export function gateHero(state, { buildPath = HERO_REPRO, specPath = SPEC_PATH,
|
||||
if (artifactFile && fs.existsSync(artifactFile) && specForRefs) {
|
||||
const organic = organicClipRegions(artifactFile, specForRefs);
|
||||
for (const r of organic) reasons.push(`artifact draws an organic clip-path (${r.snippet}) inside raster region ${r.id}'s box; that region ships as its plate, never as a polygon`);
|
||||
// Inline SVG past an icon's budget is a drawing in code: the single most
|
||||
// repeated pin of the human review ("terrible svg instead of asset",
|
||||
// "lines that point nowhere"), on every model. Icons, arrows and
|
||||
// chevrons pass; diagrams, notation, and leader lines do not; those are
|
||||
// plates, or part of the plate they annotate.
|
||||
let svgs = [];
|
||||
try { svgs = svgIllustrations(fs.readFileSync(artifactFile, 'utf8')); } catch { svgs = []; }
|
||||
for (const v of svgs.slice(0, 6)) reasons.push(`artifact draws an illustration in inline SVG${v.label ? ` (${v.label})` : ''}: ${v.snippet}. Drawings, diagrams, notation, and leader lines are plates or belong to the plate they annotate; only icon-sized SVG (under 64px, a few paths) is code`);
|
||||
if (svgs.length > 6) reasons.push(`...and ${svgs.length - 6} more inline SVG illustrations`);
|
||||
}
|
||||
// Numbers a designer reads off the side-by-side: type set at the wrong
|
||||
// size, weight, colour, or place; a nav bar too tall; ink where the comp
|
||||
@@ -860,6 +869,14 @@ async function main() {
|
||||
if (cmd === 'finish') {
|
||||
const disposition = arg('disposition');
|
||||
if (!['ship', 'fix', 'rebuild', 'recapture'].includes(disposition)) { console.error('build-phase: finish --disposition ship|fix|rebuild|recapture'); process.exit(1); }
|
||||
// A ship cannot be recorded over an open phase. The model can still stop
|
||||
// talking, but it cannot write "ship" into the state with the hero open;
|
||||
// sessions did exactly that and summarised the build as complete.
|
||||
const openBefore = PHASES.filter((ph) => ph !== 'review' && state.phases[ph] && state.phases[ph].status !== 'closed' && state.phases[ph].status !== 'skipped');
|
||||
if (disposition === 'ship' && openBefore.length) {
|
||||
console.error(`build-phase: finish --disposition ship refused: ${openBefore.join(', ')} ${openBefore.length === 1 ? 'is' : 'are'} not closed (phase ${state.phase}). Record fix or rebuild, or close the phases first; a page shipped over an open hero is a page shipped against its own gate.`);
|
||||
process.exit(2);
|
||||
}
|
||||
state.finish = { disposition, at: now(), phaseAtFinish: state.phase };
|
||||
if (state.phase === 'review') { state.phases.review.status = 'closed'; state.phases.review.closedAt = now(); }
|
||||
saveState(state);
|
||||
|
||||
@@ -207,3 +207,40 @@ export function plateClipCheck(region, compCrop, buildCrop, { margin = 6 } = {})
|
||||
if (H - (a.y + a.h) >= margin && flush(H - (b.y + b.h))) sides.push('bottom');
|
||||
return { sides, comp: a, build: b };
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline SVG that is an illustration, not an icon. An icon is small (a
|
||||
* viewBox or box under `iconPx` on its long side) with a few paths; anything
|
||||
* with a real path budget is a drawing in code: a diagram, a rack of
|
||||
* carburetors, staff notation, leader lines with arrows, a "terrible svg
|
||||
* approximation of the asset". Those ship as plates or as part of the plate
|
||||
* they annotate. Returns one entry per offending <svg> with a snippet.
|
||||
*
|
||||
* `html` is the artifact source. `pathBudget` counts characters of path
|
||||
* data (d="..."), points, and polyline/polygon points across the element.
|
||||
*/
|
||||
export function svgIllustrations(html, { iconPx = 64, pathBudget = 480, maxPaths = 8 } = {}) {
|
||||
const out = [];
|
||||
const re = /<svg\b([^>]*)>([\s\S]*?)<\/svg>/gi;
|
||||
let m;
|
||||
while ((m = re.exec(html))) {
|
||||
const attrs = m[1], body = m[2];
|
||||
const paths = (body.match(/<path\b/gi) || []).length + (body.match(/<(polyline|polygon|line|circle|ellipse|rect)\b/gi) || []).length;
|
||||
let budget = 0;
|
||||
for (const d of body.matchAll(/\sd="([^"]*)"/g)) budget += d[1].length;
|
||||
for (const pts of body.matchAll(/\spoints="([^"]*)"/g)) budget += pts[1].length;
|
||||
const vb = /viewBox="\s*[-\d.]+\s+[-\d.]+\s+([\d.]+)\s+([\d.]+)/.exec(attrs);
|
||||
const w = /\swidth="([\d.]+)(px)?"/.exec(attrs), h = /\sheight="([\d.]+)(px)?"/.exec(attrs);
|
||||
const long = Math.max(vb ? Math.max(+vb[1], +vb[2]) : 0, w ? +w[1] : 0, h ? +h[1] : 0);
|
||||
const iconSized = long > 0 && long <= iconPx && paths <= maxPaths;
|
||||
const uses = /<use\b/i.test(body) && paths === 0; // a sprite reference
|
||||
if (uses) continue;
|
||||
if (iconSized && budget <= pathBudget) continue;
|
||||
if (budget <= pathBudget && paths <= maxPaths && long === 0 && !/<(text|image)\b/i.test(body)) continue; // a tiny inline glyph with no size hint
|
||||
if (budget > pathBudget || paths > maxPaths || (long > iconPx && paths > 0)) {
|
||||
const id = /\b(id|class|aria-label|data-region)="([^"]+)"/i.exec(attrs);
|
||||
out.push({ snippet: `<svg${attrs.slice(0, 80).replace(/\s+/g, ' ')}...> (${paths} shapes, ${budget} chars of path data${long ? `, ${long}px` : ''})`, label: id ? id[2] : null, paths, budget, long });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user