mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
Address the Bugbot and Copilot findings on #599
- keyChroma re-encodes with the PNG's tEXt chunks intact (the embedded prompt survived generation but not keying)
- organic-clip-path counts relative curve commands too (path data letters are only commands, so the match is case-insensitive)
- buried-raster normalizes percentage alphas (parseFloat('80%') read as 80) and reads 4- and 8-digit hex alpha instead of treating #rrggbbaa as opaque
- the extension-injected-node skip in checkQuality runs before any finding is pushed (a low-opacity injected raster was recorded, then returned by the skip)
- fake-mode plates carry impeccable:fake tEXt and the plates gate's crop-identity refusal skips them (fake mode IS the crop by design; the refusal is for models shipping the comp's pixels as artwork)
Findings by cursor[bot] and Copilot on PR #599; detector engines rebuilt (build:browser, build:extension).
AI-assisted (Claude Code).
This commit is contained in:
@@ -2733,8 +2733,9 @@ function scanCssTextForOrganicClipPath(styleText) {
|
||||
const kind = m[1].toLowerCase();
|
||||
const body = m[2];
|
||||
if (kind === 'path') {
|
||||
// curves (C, S, Q, T, A) drawing a contour, not a rectilinear M/L/Z outline
|
||||
const curves = (body.match(/[CSQTA]/g) || []).length;
|
||||
// curves (C, S, Q, T, A, absolute or relative) drawing a contour, not a
|
||||
// rectilinear M/L/Z outline; letters in path data are only commands
|
||||
const curves = (body.match(/[CSQTA]/gi) || []).length;
|
||||
if (curves < 3) continue;
|
||||
findings.push({ id: 'organic-clip-path', snippet: `clip-path: path() with ${curves} curve segments`, selector: enclosingCssSelector(styleText, m.index) || undefined });
|
||||
continue;
|
||||
@@ -2780,11 +2781,21 @@ function scanCssTextForBuriedRaster(styleText) {
|
||||
const firstUrl = value.search(/url\(/i);
|
||||
const gradients = [...value.matchAll(/(?:linear|radial|conic)-gradient\([^()]*(?:\([^()]*\)[^()]*)*\)/gi)].filter((gm) => gm.index < firstUrl).map((gm) => gm[0]);
|
||||
let opaqueWash = false;
|
||||
// an alpha token normalized to 0..1: '0.8' -> 0.8, '80%' -> 0.8
|
||||
const alphaOf = (a) => { if (a == null) return 1; const v = parseFloat(a); return String(a).trim().endsWith('%') ? v / 100 : v; };
|
||||
for (const g of gradients) {
|
||||
const alphas = [...g.matchAll(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+))?\s*\)|hsla?\([^)]*?(?:,\s*([\d.]+%?))?\s*\)/gi)].map((a) => a[1] ?? a[2]);
|
||||
const hexOrNamed = /#[0-9a-f]{3,8}\b|\b(?:white|black|ivory|beige|linen|snow|cream)\b/i.test(g.replace(/rgba?\([^)]*\)|hsla?\([^)]*\)/gi, ''));
|
||||
if (!alphas.length && hexOrNamed) { opaqueWash = true; break; }
|
||||
if (alphas.length && alphas.every((a) => a == null || parseFloat(a) >= 0.9)) { opaqueWash = true; break; }
|
||||
const alphas = [...g.matchAll(/rgba?\(\s*[\d.]+%?\s*,?\s*[\d.]+%?\s*,?\s*[\d.]+%?\s*(?:[,/]\s*([\d.]+%?))?\s*\)|hsla?\([^)]*?(?:[,/]\s*([\d.]+%?))?\s*\)/gi)].map((a) => alphaOf(a[1] ?? a[2]));
|
||||
const stripped = g.replace(/rgba?\([^)]*\)|hsla?\([^)]*\)/gi, '');
|
||||
// hex stops: 4- and 8-digit forms carry their own alpha
|
||||
for (const h of stripped.matchAll(/#([0-9a-f]{3,8})\b/gi)) {
|
||||
const hex = h[1];
|
||||
if (hex.length === 4) alphas.push(parseInt(hex[3] + hex[3], 16) / 255);
|
||||
else if (hex.length === 8) alphas.push(parseInt(hex.slice(6), 16) / 255);
|
||||
else alphas.push(1);
|
||||
}
|
||||
const named = /\b(?:white|black|ivory|beige|linen|snow|cream)\b/i.test(stripped);
|
||||
if (named) alphas.push(1);
|
||||
if (alphas.length && alphas.every((a) => !Number.isFinite(a) || a >= 0.9)) { opaqueWash = true; break; }
|
||||
}
|
||||
if (!opaqueWash) continue;
|
||||
findings.push({ id: 'buried-raster', snippet: `raster under a near-opaque gradient wash: ${value.trim().slice(0, 90)}`, selector: enclosingCssSelector(styleText, m.index) || undefined });
|
||||
@@ -4442,6 +4453,16 @@ function checkQuality(opts) {
|
||||
// opacity never reaches the screen: the produced material ships as a
|
||||
// compliance token. The CSS-text scan catches the stylesheet form; this
|
||||
// catches computed opacity on the element itself (both engines).
|
||||
// Skip browser extension injected elements BEFORE any finding is pushed
|
||||
// (a low-opacity raster those hosts inject used to be recorded and then
|
||||
// returned by this very skip). Read the id via getAttribute whenever
|
||||
// `el.id` is not a string: on a <form> (and other [LegacyOverrideBuiltIns]
|
||||
// hosts) a named control like <input name="id"> shadows the builtin `id`
|
||||
// getter and returns the control element, whose `.startsWith` is undefined
|
||||
// and throws (issue #407 — every Shopify product form ships an
|
||||
// <input name="id">).
|
||||
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute?.('id') || '');
|
||||
if (elId.startsWith('claude-') || elId.startsWith('cic-')) return findings;
|
||||
{
|
||||
const op = parseFloat(style.opacity);
|
||||
if (Number.isFinite(op) && op < 0.15 && op >= 0) {
|
||||
@@ -4452,14 +4473,6 @@ function checkQuality(opts) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip browser extension injected elements. Read the id via getAttribute
|
||||
// whenever `el.id` is not a string: on a <form> (and other
|
||||
// [LegacyOverrideBuiltIns] hosts) a named control like <input name="id">
|
||||
// shadows the builtin `id` getter and returns the control element, whose
|
||||
// `.startsWith` is undefined and throws (issue #407 — every Shopify product
|
||||
// form ships an <input name="id">).
|
||||
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute?.('id') || '');
|
||||
if (elId.startsWith('claude-') || elId.startsWith('cic-')) return findings;
|
||||
|
||||
// --- Line length too long --- (browser-only: needs rect.width)
|
||||
if (rect && hasDirectText && QUALITY_TEXT_TAGS.has(tag) && rect.width > 0 && textLen > lineMax) {
|
||||
|
||||
+27
-14
@@ -1473,8 +1473,9 @@ function scanCssTextForOrganicClipPath(styleText) {
|
||||
const kind = m[1].toLowerCase();
|
||||
const body = m[2];
|
||||
if (kind === 'path') {
|
||||
// curves (C, S, Q, T, A) drawing a contour, not a rectilinear M/L/Z outline
|
||||
const curves = (body.match(/[CSQTA]/g) || []).length;
|
||||
// curves (C, S, Q, T, A, absolute or relative) drawing a contour, not a
|
||||
// rectilinear M/L/Z outline; letters in path data are only commands
|
||||
const curves = (body.match(/[CSQTA]/gi) || []).length;
|
||||
if (curves < 3) continue;
|
||||
findings.push({ id: 'organic-clip-path', snippet: `clip-path: path() with ${curves} curve segments`, selector: enclosingCssSelector(styleText, m.index) || undefined });
|
||||
continue;
|
||||
@@ -1520,11 +1521,21 @@ function scanCssTextForBuriedRaster(styleText) {
|
||||
const firstUrl = value.search(/url\(/i);
|
||||
const gradients = [...value.matchAll(/(?:linear|radial|conic)-gradient\([^()]*(?:\([^()]*\)[^()]*)*\)/gi)].filter((gm) => gm.index < firstUrl).map((gm) => gm[0]);
|
||||
let opaqueWash = false;
|
||||
// an alpha token normalized to 0..1: '0.8' -> 0.8, '80%' -> 0.8
|
||||
const alphaOf = (a) => { if (a == null) return 1; const v = parseFloat(a); return String(a).trim().endsWith('%') ? v / 100 : v; };
|
||||
for (const g of gradients) {
|
||||
const alphas = [...g.matchAll(/rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*(?:,\s*([\d.]+))?\s*\)|hsla?\([^)]*?(?:,\s*([\d.]+%?))?\s*\)/gi)].map((a) => a[1] ?? a[2]);
|
||||
const hexOrNamed = /#[0-9a-f]{3,8}\b|\b(?:white|black|ivory|beige|linen|snow|cream)\b/i.test(g.replace(/rgba?\([^)]*\)|hsla?\([^)]*\)/gi, ''));
|
||||
if (!alphas.length && hexOrNamed) { opaqueWash = true; break; }
|
||||
if (alphas.length && alphas.every((a) => a == null || parseFloat(a) >= 0.9)) { opaqueWash = true; break; }
|
||||
const alphas = [...g.matchAll(/rgba?\(\s*[\d.]+%?\s*,?\s*[\d.]+%?\s*,?\s*[\d.]+%?\s*(?:[,/]\s*([\d.]+%?))?\s*\)|hsla?\([^)]*?(?:[,/]\s*([\d.]+%?))?\s*\)/gi)].map((a) => alphaOf(a[1] ?? a[2]));
|
||||
const stripped = g.replace(/rgba?\([^)]*\)|hsla?\([^)]*\)/gi, '');
|
||||
// hex stops: 4- and 8-digit forms carry their own alpha
|
||||
for (const h of stripped.matchAll(/#([0-9a-f]{3,8})\b/gi)) {
|
||||
const hex = h[1];
|
||||
if (hex.length === 4) alphas.push(parseInt(hex[3] + hex[3], 16) / 255);
|
||||
else if (hex.length === 8) alphas.push(parseInt(hex.slice(6), 16) / 255);
|
||||
else alphas.push(1);
|
||||
}
|
||||
const named = /\b(?:white|black|ivory|beige|linen|snow|cream)\b/i.test(stripped);
|
||||
if (named) alphas.push(1);
|
||||
if (alphas.length && alphas.every((a) => !Number.isFinite(a) || a >= 0.9)) { opaqueWash = true; break; }
|
||||
}
|
||||
if (!opaqueWash) continue;
|
||||
findings.push({ id: 'buried-raster', snippet: `raster under a near-opaque gradient wash: ${value.trim().slice(0, 90)}`, selector: enclosingCssSelector(styleText, m.index) || undefined });
|
||||
@@ -3182,6 +3193,16 @@ function checkQuality(opts) {
|
||||
// opacity never reaches the screen: the produced material ships as a
|
||||
// compliance token. The CSS-text scan catches the stylesheet form; this
|
||||
// catches computed opacity on the element itself (both engines).
|
||||
// Skip browser extension injected elements BEFORE any finding is pushed
|
||||
// (a low-opacity raster those hosts inject used to be recorded and then
|
||||
// returned by this very skip). Read the id via getAttribute whenever
|
||||
// `el.id` is not a string: on a <form> (and other [LegacyOverrideBuiltIns]
|
||||
// hosts) a named control like <input name="id"> shadows the builtin `id`
|
||||
// getter and returns the control element, whose `.startsWith` is undefined
|
||||
// and throws (issue #407 — every Shopify product form ships an
|
||||
// <input name="id">).
|
||||
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute?.('id') || '');
|
||||
if (elId.startsWith('claude-') || elId.startsWith('cic-')) return findings;
|
||||
{
|
||||
const op = parseFloat(style.opacity);
|
||||
if (Number.isFinite(op) && op < 0.15 && op >= 0) {
|
||||
@@ -3192,14 +3213,6 @@ function checkQuality(opts) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip browser extension injected elements. Read the id via getAttribute
|
||||
// whenever `el.id` is not a string: on a <form> (and other
|
||||
// [LegacyOverrideBuiltIns] hosts) a named control like <input name="id">
|
||||
// shadows the builtin `id` getter and returns the control element, whose
|
||||
// `.startsWith` is undefined and throws (issue #407 — every Shopify product
|
||||
// form ships an <input name="id">).
|
||||
const elId = typeof el.id === 'string' ? el.id : (el.getAttribute?.('id') || '');
|
||||
if (elId.startsWith('claude-') || elId.startsWith('cic-')) return findings;
|
||||
|
||||
// --- Line length too long --- (browser-only: needs rect.width)
|
||||
if (rect && hasDirectText && QUALITY_TEXT_TAGS.has(tag) && rect.width > 0 && textLen > lineMax) {
|
||||
|
||||
@@ -299,7 +299,12 @@ export function gatePlates(state, { specPath = SPEC_PATH } = {}) {
|
||||
// upscaled past the size floor: the comp's grain, its neighbours'
|
||||
// edges, and its resolution ship as the artwork. Crops are never
|
||||
// plates; the crop is the reference the plate is generated from.
|
||||
if (!isTexture) {
|
||||
// A fake-mode plate (offline pipelines, eval fixtures) IS the crop by
|
||||
// design and says so in its tEXt; the identity refusal is for models
|
||||
// shipping the comp's pixels as artwork, not for the deterministic
|
||||
// stand-in.
|
||||
const isFake = img.text && img.text['impeccable:fake'] === '1';
|
||||
if (!isTexture && !isFake) {
|
||||
const raw = crop(comp, r.px.x, r.px.y, r.px.w, r.px.h);
|
||||
const same = structureScore(raw, resize(img, raw.width, raw.height));
|
||||
if (same >= 0.95) reasons.push(`plate ${file} is the comp crop of region ${r.id} (structure ${(same * 100).toFixed(0)}% against the raw region, a resample of the same pixels): a crop of the comp is never a plate; generate the plate from the crop as reference (generate-image.mjs --plate ${r.id})`);
|
||||
|
||||
@@ -248,7 +248,7 @@ if (plateId) {
|
||||
plateCtx = { spec, specPath, region, ref, refPath, out, size, prompt, comp, encodePng, resize, chroma: wantsChroma ? chromaColor : null };
|
||||
if (process.env.IMPECCABLE_IMAGE_GEN_FAKE) {
|
||||
const up = resize(ref, ref.width * 2, ref.height * 2);
|
||||
fs.writeFileSync(out, encodePng(up, { text: { 'impeccable:prompt': prompt } }));
|
||||
fs.writeFileSync(out, encodePng(up, { text: { 'impeccable:prompt': prompt, 'impeccable:fake': '1' } }));
|
||||
fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'fake', plate: region.id, refs: [refPath] }, null, 2));
|
||||
console.log(`PLATE: ${out} (${up.width}x${up.height}, fake 2x crop of region ${region.id}, $0.00, no API call)`);
|
||||
process.exit(0);
|
||||
@@ -296,7 +296,8 @@ async function keyChroma(file, keyHex) {
|
||||
const m = (r + b) / 2; img.data[i + 1] = Math.round(g * a + m * (1 - a));
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(file, encodePng(img));
|
||||
// keep the tEXt chunks (the embedded prompt written before keying)
|
||||
fs.writeFileSync(file, encodePng(img, { text: img.text && Object.keys(img.text).length ? img.text : null }));
|
||||
return keyed / (img.data.length / 4);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user