Compare commits

..
Author SHA1 Message Date
Paul Bakaus 7871a9e392 Use parsed PNG terminator offsets
Avoid mistaking IEND bytes inside metadata payloads for the real terminator, with a regression fixture covering that valid PNG shape.

AI-assisted: prepared by OpenAI Codex while resolving an automated review under maintainer pbakaus authorization.
2026-08-23 12:03:02 -07:00
Paul Bakaus b3abdc30a6 Centralize image prompt parsing
Share image-format detection and PNG chunk parsing across read, scan, and replacement paths while preserving the CLI contract.

AI-assisted: prepared by OpenAI Codex under maintainer pbakaus scheduled-refactor authorization.
2026-08-23 11:55:22 -07:00
4 changed files with 110 additions and 433 deletions
+7 -155
View File
@@ -42,7 +42,6 @@ function shouldRunPageAnalyzers(content, filePath) {
}
const JS_SOURCE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
const STYLESHEET_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
const REGEX_PREFIX_KEYWORDS = new Set(['await', 'case', 'default', 'delete', 'do', 'else', 'in', 'instanceof', 'new', 'of', 'return', 'throw', 'typeof', 'void', 'yield']);
const BLOCK_BRACE_PREFIX_KEYWORDS = new Set(['do', 'else', 'finally', 'try']);
@@ -257,153 +256,6 @@ function stripCssComments(content) {
return content.replace(/\/\*[\s\S]*?\*\//g, comment => comment.replace(/[^\n]/g, ' '));
}
function blankHtmlComments(text) {
return text.replace(/<!--[\s\S]*?-->/g, comment => comment.replace(/[^\n]/g, ' '));
}
function blankCssLineCommentsInStyleBlocks(text) {
const re = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
let output = '';
let lastIndex = 0;
let match;
while ((match = re.exec(text)) !== null) {
const inner = match[1];
const openLength = match[0].length - inner.length - '</style>'.length;
output += text.slice(lastIndex, match.index);
output += match[0].slice(0, openLength);
output += blankCssLineComments(inner);
output += match[0].slice(openLength + inner.length);
lastIndex = re.lastIndex;
}
return output + text.slice(lastIndex);
}
function blankHtmlAndCssCommentsOutsideScripts(text) {
const re = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
let output = '';
let lastIndex = 0;
let match;
while ((match = re.exec(text)) !== null) {
output += blankCssLineCommentsInStyleBlocks(stripCssComments(blankHtmlComments(text.slice(lastIndex, match.index))));
output += match[0];
lastIndex = re.lastIndex;
}
return output + blankCssLineCommentsInStyleBlocks(stripCssComments(blankHtmlComments(text.slice(lastIndex))));
}
function blankCssLineComments(text) {
let output = '';
let state = 'code';
let urlDepth = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
const next = text[i + 1];
if (state === 'line') {
if (char === '\n') {
output += '\n';
state = 'code';
} else {
output += ' ';
}
continue;
}
if (state === 'single' || state === 'double') {
output += char;
if (char === '\\' && next) {
output += next;
i++;
} else if ((state === 'single' && char === "'") || (state === 'double' && char === '"')) {
state = 'code';
}
continue;
}
const prev = output.length ? output[output.length - 1] : '';
if (char === '/' && next === '/' && urlDepth === 0 && prev !== ':' && prev !== '(' && prev !== '\\') {
output += ' ';
i++;
state = 'line';
continue;
}
if (char === "'") state = 'single';
else if (char === '"') state = 'double';
if (char === '(') {
const behind = output.replace(/\s+$/, '');
if (urlDepth > 0 || /url$/i.test(behind)) urlDepth++;
} else if (char === ')' && urlDepth) {
urlDepth--;
}
output += char;
}
return output;
}
function findAstroFrontmatterClose(text) {
if (!text.startsWith('---')) return -1;
let cursor = text.indexOf('\n');
if (cursor === -1) return -1;
cursor += 1;
while (cursor < text.length) {
if (text[cursor - 1] === '\n' && text.startsWith('---', cursor)) {
let end = cursor + 3;
while (text[end] === ' ' || text[end] === '\t') end++;
if (end >= text.length || text[end] === '\n' || text[end] === '\r') return cursor - 1;
}
const char = text[cursor];
const next = text[cursor + 1];
if (char === "'" || char === '"') {
const close = findQuotedStringEnd(text, cursor, char);
if (close === -1) return -1;
cursor = close + 1;
continue;
}
if (char === '`') {
const close = findTemplateLiteralEnd(text, cursor);
if (close === -1) return -1;
cursor = close + 1;
continue;
}
if (char === '/' && next === '/') {
const lineEnd = text.indexOf('\n', cursor);
if (lineEnd === -1) return -1;
cursor = lineEnd;
continue;
}
if (char === '/' && next === '*') {
const commentEnd = text.indexOf('*/', cursor + 2);
if (commentEnd === -1) return -1;
cursor = commentEnd + 2;
continue;
}
if (char === '/' && next !== '/' && next !== '*') {
const close = findRegexLiteralEnd(text, cursor);
if (close !== -1) {
cursor = close + 1;
continue;
}
}
cursor++;
}
return -1;
}
function blankAstroFrontmatterComments(text) {
const close = findAstroFrontmatterClose(text);
if (close === -1) return text;
return stripJsComments(text.slice(0, close)) + text.slice(close);
}
function blankCommentsForMatchers(text, ext) {
if (PAGE_ANALYZER_EXTS.has(ext)) {
const withFrontmatter = ext === '.astro' ? blankAstroFrontmatterComments(text) : text;
return blankHtmlAndCssCommentsOutsideScripts(withFrontmatter);
}
if (STYLESHEET_EXTS.has(ext)) {
const withoutBlocks = stripCssComments(text);
return ext === '.css' ? withoutBlocks : blankCssLineComments(withoutBlocks);
}
return text;
}
function firstOverusedGoogleFont(text) {
return extractGoogleFontFamilies(text).find(f => OVERUSED_FONTS.has(f)) || '';
}
@@ -1176,13 +1028,14 @@ function detectText(content, filePath, options = {}) {
const ext = extFromFilePath(filePath);
const commentStrippedSource = JS_SOURCE_EXTS.has(ext) ? stripJsComments(content, {
jsx: ext === '.js' || ext === '.jsx' || ext === '.tsx',
}) : blankCommentsForMatchers(content, ext);
}) : content;
const source = stripCssInJsComments(commentStrippedSource, ext);
const lines = source.split('\n');
// Run regex matchers on the full file content (catches Tailwind classes, inline styles)
// Enable block context for CSS files where related properties span multiple lines
findings.push(...runRegexMatchers(lines, filePath, 0, STYLESHEET_EXTS.has(ext) || null, {
const cssLike = new Set(['.css', '.scss', '.sass', '.less']);
findings.push(...runRegexMatchers(lines, filePath, 0, cssLike.has(ext) || null, {
profile,
phase: 'source',
}));
@@ -1197,7 +1050,7 @@ function detectText(content, filePath, options = {}) {
scanCssTextForPseudoStripe(text).map(hit =>
finding(hit.id, filePath, hit.snippet, lineOffset + text.slice(0, hit.index).split('\n').length));
if (STYLESHEET_EXTS.has(ext)) {
if (cssLike.has(ext)) {
findings.push(...scanInsetStripeCss(content, filePath));
findings.push(...pseudoStripeFindings(content, 0));
}
@@ -1225,8 +1078,7 @@ function detectText(content, filePath, options = {}) {
}, () => extractStyleBlocks(content, ext))
: extractStyleBlocks(content, ext);
for (const block of styleBlocks) {
const blockContent = blankCssLineComments(stripCssComments(block.content));
const blockLines = blockContent.split('\n');
const blockLines = block.content.split('\n');
findings.push(...runRegexMatchers(blockLines, filePath, block.startLine - 1, true, {
profile,
phase: 'style-block',
@@ -1237,8 +1089,8 @@ function detectText(content, filePath, options = {}) {
// 1-based, so the offset is startLine - 2; startLine - 1 double-counted and
// reported every selector one line low. runRegexMatchers keeps startLine - 1
// because it indexes its split lines from zero.
findings.push(...scanInsetStripeCss(blockContent, filePath, block.startLine - 2));
findings.push(...pseudoStripeFindings(blockContent, block.startLine - 2));
findings.push(...scanInsetStripeCss(block.content, filePath, block.startLine - 2));
findings.push(...pseudoStripeFindings(block.content, block.startLine - 2));
}
// Extract and scan CSS-in-JS template literals
+47 -56
View File
@@ -21,22 +21,24 @@ import zlib from 'node:zlib';
const KEYWORD = 'impeccable:prompt';
const args = process.argv.slice(2);
const file = args.find(a => !a.startsWith('--'));
const readMode = args.includes('--read');
const scanMode = args.includes('--scan');
const argOf = (name) => { const i = args.indexOf(name); return i !== -1 ? args[i + 1] : null; };
function promptOf(imagePath) {
const b = fs.readFileSync(imagePath);
let prompt = null;
if (b.length > 8 && b.readUInt32BE(0) === 0x89504e47) prompt = readPngText(b);
else if (b.length > 3 && b[0] === 0xff && b[1] === 0xd8) prompt = readJpegCom(b);
function imageType(buffer) {
if (buffer.length > 8 && buffer.readUInt32BE(0) === 0x89504e47) return 'png';
if (buffer.length > 3 && buffer[0] === 0xff && buffer[1] === 0xd8) return 'jpeg';
return null;
}
function readPrompt(imagePath, buffer = fs.readFileSync(imagePath)) {
const type = imageType(buffer);
let prompt = type === 'png' ? parsePng(buffer).prompt : type === 'jpeg' ? readJpegCom(buffer) : null;
if (prompt == null && fs.existsSync(`${imagePath}.json`)) {
try { prompt = JSON.parse(fs.readFileSync(`${imagePath}.json`, 'utf8')).prompt ?? null; } catch { /* stays null */ }
}
return prompt;
}
if (scanMode) {
if (args.includes('--scan')) {
const targets = args.filter(a => !a.startsWith('--'));
if (targets.length === 0) { console.error('embed-prompt: --scan needs at least one directory'); process.exit(1); }
const RASTER = /\.(png|jpe?g|webp)$/i;
@@ -59,7 +61,7 @@ if (scanMode) {
}
let missing = 0;
for (const raster of rasters) {
if (promptOf(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
if (readPrompt(raster) == null) { console.log(`MISSING: ${raster}`); missing++; }
}
console.log(`SCAN: ${rasters.length} raster${rasters.length === 1 ? '' : 's'}, ${missing} missing`);
process.exit(missing > 0 ? 3 : 0);
@@ -68,8 +70,7 @@ if (scanMode) {
if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); }
const buf = fs.readFileSync(file);
const isPng = buf.length > 8 && buf.readUInt32BE(0) === 0x89504e47;
const isJpeg = buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8;
const type = imageType(buf);
const crcTable = (() => {
const t = new Uint32Array(256);
@@ -87,22 +88,26 @@ function pngChunk(type, data) {
return out;
}
function readPngText(b) {
let off = 8;
while (off + 12 <= b.length) {
const len = b.readUInt32BE(off);
const type = b.toString('ascii', off + 4, off + 8);
if (type === 'tEXt' || type === 'zTXt') {
const data = b.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
if (nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD) {
if (type === 'tEXt') return data.toString('utf8', nul + 1);
return zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
}
function parsePng(buffer) {
const chunks = [];
let prompt = null;
let offset = 8;
while (offset + 12 <= buffer.length) {
const length = buffer.readUInt32BE(offset);
const type = buffer.toString('ascii', offset + 4, offset + 8);
const data = buffer.subarray(offset + 8, offset + 8 + length);
const nul = data.indexOf(0);
const promptChunk = (type === 'tEXt' || type === 'zTXt')
&& nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (prompt == null && promptChunk) {
prompt = type === 'tEXt'
? data.toString('utf8', nul + 1)
: zlib.inflateSync(data.subarray(nul + 2)).toString('utf8');
}
off += 12 + len;
chunks.push({ offset, type, promptChunk, bytes: buffer.subarray(offset, offset + 12 + length) });
offset += 12 + length;
}
return null;
return { chunks, prompt };
}
function readJpegCom(b) {
@@ -121,48 +126,34 @@ function readJpegCom(b) {
}
const sidecar = `${file}.json`;
if (readMode) {
let prompt = null;
if (isPng) prompt = readPngText(buf);
else if (isJpeg) prompt = readJpegCom(buf);
if (prompt == null && fs.existsSync(sidecar)) {
try { prompt = JSON.parse(fs.readFileSync(sidecar, 'utf8')).prompt ?? null; } catch { /* fall through */ }
}
if (args.includes('--read')) {
const prompt = readPrompt(file, buf);
if (prompt == null) { console.error('embed-prompt: no embedded prompt found'); process.exit(2); }
console.log(prompt);
process.exit(0);
}
const prompt = argOf('--prompt') ?? (argOf('--prompt-file') ? fs.readFileSync(argOf('--prompt-file'), 'utf8') : null);
const promptFile = argOf('--prompt-file');
const prompt = argOf('--prompt') ?? (promptFile ? fs.readFileSync(promptFile, 'utf8') : null);
if (!prompt) { console.error('embed-prompt: --prompt or --prompt-file required'); process.exit(1); }
if (isPng) {
if (type === 'png') {
// Insert (or replace) our tEXt chunk immediately before IEND.
const iend = buf.indexOf(Buffer.from('IEND', 'ascii')) - 4;
const { chunks, prompt: existingPrompt } = parsePng(buf);
const iend = chunks.find((chunk) => chunk.type === 'IEND')?.offset ?? -1;
if (iend < 8) { console.error('embed-prompt: malformed PNG'); process.exit(1); }
// Drop any existing chunk with our keyword to keep embedding idempotent.
let body = buf.subarray(8, iend);
const existing = readPngText(buf);
if (existing != null) {
const parts = [];
let off = 8;
while (off + 12 <= buf.length && off < iend + 12) {
const len = buf.readUInt32BE(off);
const type = buf.toString('ascii', off + 4, off + 8);
const chunk = buf.subarray(off, off + 12 + len);
const data = buf.subarray(off + 8, off + 8 + len);
const nul = data.indexOf(0);
const ours = (type === 'tEXt' || type === 'zTXt') && nul !== -1 && data.toString('latin1', 0, nul) === KEYWORD;
if (!ours && type !== 'IEND') parts.push(chunk);
off += 12 + len;
}
body = Buffer.concat(parts).subarray(8 * 0); // parts exclude signature
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), pngChunk('IEND', Buffer.alloc(0))]));
} else {
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, iend), pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')])), buf.subarray(iend)]));
}
const replacing = existingPrompt != null;
const body = replacing
? Buffer.concat(chunks
.filter((chunk) => chunk.offset < iend && !chunk.promptChunk)
.map((chunk) => chunk.bytes))
: buf.subarray(8, iend);
const promptChunk = pngChunk('tEXt', Buffer.concat([Buffer.from(KEYWORD, 'latin1'), Buffer.from([0]), Buffer.from(prompt, 'utf8')]));
const end = replacing ? pngChunk('IEND', Buffer.alloc(0)) : buf.subarray(iend);
fs.writeFileSync(file, Buffer.concat([buf.subarray(0, 8), body, promptChunk, end]));
console.log(`EMBEDDED: ${file} (png tEXt, ${prompt.length} chars)`);
} else if (isJpeg) {
} else if (type === 'jpeg') {
const seg = Buffer.from(`${KEYWORD}\0${prompt}`, 'utf8');
if (seg.length + 2 > 0xffff) { console.error('embed-prompt: prompt too long for a JPEG segment'); process.exit(1); }
const com = Buffer.alloc(4 + seg.length);
-222
View File
@@ -382,228 +382,6 @@ describe('detectText — broken images in source comments', () => {
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('ignores img tags in Astro style block comments', () => {
const source = [
'---',
'const title = "Hero";',
'---',
'<style>',
' /*',
' * Example markup: <img src="">',
' */',
' .hero { color: red; }',
'</style>',
].join('\n');
const findings = detectText(source, 'hero.astro');
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('ignores img tags in Astro HTML comments', () => {
const source = [
'---',
'const title = "Hero";',
'---',
'<!-- <img src="" alt="Comment-only image" /> -->',
'<img src="/logo.png" alt="Logo" />',
].join('\n');
const findings = detectText(source, 'hero.astro');
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('ignores img tags in Astro frontmatter line comments', () => {
const source = [
'---',
'// <img src="" alt="Comment-only image" />',
'const site = "https://example.com";',
'---',
'<img src="/logo.png" alt="Logo" />',
].join('\n');
const findings = detectText(source, 'hero.astro');
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('ignores img tags in CSS block comments', () => {
const source = [
'/*',
' * Example markup: <img src="">',
' */',
'.hero { color: red; }',
].join('\n');
const findings = detectText(source, 'hero.css');
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('still detects real img tags after an HTML comment in Astro', () => {
const source = [
'---',
'const title = "Hero";',
'---',
'<!-- decorative only -->',
'<img src="" alt="Empty source" />',
].join('\n');
const findings = detectText(source, 'hero.astro');
const broken = findings.filter(r => r.antipattern === 'broken-image');
expect(broken).toHaveLength(1);
expect(broken[0].line).toBe(5);
});
test('does not blank https URLs in Astro frontmatter', () => {
const source = [
'---',
'const site = "https://example.com/logo.png";',
'---',
'<img src="" alt="Empty source" />',
].join('\n');
const findings = detectText(source, 'hero.astro');
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
});
test('keeps same-line img visible after a bare https URL in Astro markup', () => {
const source = '<p>https://example.com <img src="" alt="Empty source" /></p>';
const findings = detectText(source, 'hero.astro');
expect(findings.filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
});
test('preserves line numbers after comment blanking in Astro', () => {
const source = [
'---',
'const title = "Hero";',
'---',
'<!-- <img src="" alt="Comment-only image" /> -->',
'<p>Intro copy</p>',
'<img src="" alt="Empty source" />',
].join('\n');
const findings = detectText(source, 'hero.astro');
const broken = findings.filter(r => r.antipattern === 'broken-image');
expect(broken).toHaveLength(1);
expect(broken[0].line).toBe(6);
});
test('does not treat comment markers inside script strings as markup comments', () => {
const htmlDelimiters = [
'<script>const open = "<!--";</script>',
'<img>',
'<script>const close = "-->";</script>',
].join('\n');
const cssDelimiters = [
'<script>const open = "/*";</script>',
'<img>',
'<script>const close = "*/";</script>',
].join('\n');
for (const filePath of ['hero.astro', 'hero.vue', 'hero.svelte']) {
expect(detectText(htmlDelimiters, filePath).filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
expect(detectText(cssDelimiters, filePath).filter(r => r.antipattern === 'broken-image')).toHaveLength(1);
}
});
test('ignores preprocessor line comments in stylesheets', () => {
const source = '// font-family: Inter\n.hero { color: red; }';
for (const filePath of ['hero.scss', 'hero.sass', 'hero.less']) {
expect(detectText(source, filePath).filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
}
});
test('still detects live font-family after a preprocessor line comment', () => {
const source = '// skip this\n.hero { font-family: Inter; }';
const findings = detectText(source, 'hero.scss').filter(r => r.antipattern === 'overused-font');
expect(findings).toHaveLength(1);
expect(findings[0].line).toBe(2);
});
test('does not blank https URLs in SCSS', () => {
const source = '.hero { background: url(https://example.com/i.png); }\n.hero { font-family: Inter; }';
const findings = detectText(source, 'hero.scss').filter(r => r.antipattern === 'overused-font');
expect(findings).toHaveLength(1);
expect(findings[0].line).toBe(2);
});
test('ignores frontmatter comments after a --- line inside a template literal', () => {
const source = [
'---',
'const md = `',
'---',
'`;',
'// <img src="" alt="Comment-only image" />',
'---',
'<div>ok</div>',
].join('\n');
expect(detectText(source, 'hero.astro').filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('ignores preprocessor line comments in component style blocks', () => {
const source = [
'<style lang="scss">',
'// font-family: Inter',
'.hero { color: red; }',
'</style>',
].join('\n');
for (const filePath of ['hero.astro', 'hero.vue', 'hero.svelte']) {
expect(detectText(source, filePath).filter(r => r.antipattern === 'overused-font')).toHaveLength(0);
}
});
test('still detects live font-family after a style-block line comment', () => {
const source = [
'<style lang="scss">',
'// skip this',
'.hero { font-family: Inter; }',
'</style>',
].join('\n');
const findings = detectText(source, 'hero.vue').filter(r => r.antipattern === 'overused-font');
expect(findings).toHaveLength(1);
expect(findings[0].line).toBe(3);
});
test('ignores frontmatter comments after a regex literal that contains quotes', () => {
const source = [
'---',
'const re = /["\']/;',
'// <img src="" alt="Comment-only image" />',
'---',
'<div>ok</div>',
].join('\n');
expect(detectText(source, 'hero.astro').filter(r => r.antipattern === 'broken-image')).toHaveLength(0);
});
test('keeps live font-family after a protocol-relative URL in SCSS', () => {
const sources = [
'.hero { background: url( //cdn.example.com/i.png); font-family: Inter; }',
'.hero { background: url(#{$prefix}//cdn.example.com/i.png); font-family: Inter; }',
];
for (const source of sources) {
expect(detectText(source, 'hero.scss').filter(r => r.antipattern === 'overused-font')).toHaveLength(1);
}
expect(detectText(
'.hero { background: url(@{prefix}//cdn.example.com/i.png); font-family: Inter; }',
'hero.less',
).filter(r => r.antipattern === 'overused-font')).toHaveLength(1);
});
});
describe('detectText — CSS borders', () => {
+56
View File
@@ -28,6 +28,7 @@ import { runUserBot } from './new-work-e2e/user-bot.mjs';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const SERVE = path.join(ROOT, 'skill', 'scripts', 'serve-question.mjs');
const GENERATE = path.join(ROOT, 'skill', 'scripts', 'generate-image.mjs');
const EMBED_PROMPT = path.join(ROOT, 'skill', 'scripts', 'embed-prompt.mjs');
const CATALOG_DIR = path.join(ROOT, 'tests', 'fixtures', 'concept-catalog');
let playwright;
@@ -120,6 +121,10 @@ function spawnSyncGen(prompt, out, size = null) {
});
}
function spawnSyncEmbed(args) {
return spawnSync(process.execPath, [EMBED_PROMPT, ...args], { encoding: 'utf8' });
}
// --------------------------------------------------------------------------
// serve-question interactive cycles
// --------------------------------------------------------------------------
@@ -1013,6 +1018,57 @@ describe('new-work-e2e: fake image generation', () => {
}
});
it('reads, scans, and idempotently replaces the prompt embedded in a PNG', () => {
const cwd = mkdtempSync(path.join(tmpdir(), 'new-work-img-'));
try {
const image = makeFakeImage(cwd, 'synthetic IEND source prompt', 'comp.png');
const original = readFileSync(image);
assert.ok(original.indexOf(Buffer.from('IEND')) < original.lastIndexOf(Buffer.from('IEND')),
'the fixture carries IEND bytes in metadata before the real terminator chunk');
const first = spawnSyncEmbed([image, '--prompt', 'first production prompt']);
assert.equal(first.status, 0, first.stderr);
assert.equal(spawnSyncEmbed([image, '--read']).stdout.trim(), 'first production prompt');
const scan = spawnSyncEmbed(['--scan', cwd]);
assert.equal(scan.status, 0, scan.stderr);
assert.match(scan.stdout, /SCAN: 1 raster, 0 missing/);
const second = spawnSyncEmbed([image, '--prompt', 'replacement production prompt']);
assert.equal(second.status, 0, second.stderr);
assert.equal(spawnSyncEmbed([image, '--read']).stdout.trim(), 'replacement production prompt');
const bytes = readFileSync(image);
assert.equal(bytes.toString().match(/impeccable:prompt/g)?.length, 1,
're-embedding replaces the existing metadata instead of accumulating chunks');
assert.ok(bytes.includes(Buffer.from('SYNTHETIC')),
'replacing the prompt preserves unrelated PNG metadata');
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it('reads JPEG comments and sidecar fallbacks through the same scan contract', () => {
const cwd = mkdtempSync(path.join(tmpdir(), 'new-work-img-'));
try {
const jpeg = path.join(cwd, 'reference.jpg');
const webp = path.join(cwd, 'reference.webp');
writeFileSync(jpeg, Buffer.from([0xff, 0xd8, 0xff, 0xda, 0x00, 0x02]));
writeFileSync(webp, Buffer.from('RIFF placeholder WEBP'));
assert.equal(spawnSyncEmbed([jpeg, '--prompt', 'jpeg prompt']).status, 0);
assert.equal(spawnSyncEmbed([webp, '--prompt', 'sidecar prompt']).status, 0);
assert.equal(spawnSyncEmbed([jpeg, '--read']).stdout.trim(), 'jpeg prompt');
assert.equal(spawnSyncEmbed([webp, '--read']).stdout.trim(), 'sidecar prompt');
const scan = spawnSyncEmbed(['--scan', cwd]);
assert.equal(scan.status, 0, scan.stderr);
assert.match(scan.stdout, /SCAN: 2 rasters, 0 missing/);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it('the SVG variant carries the readable prompt text and SYNTHETIC COMP label', () => {
const cwd = mkdtempSync(path.join(tmpdir(), 'new-work-img-'));
try {