diff --git a/.agents/skills/impeccable/reference/new-work.md b/.agents/skills/impeccable/reference/new-work.md index ae0c88b28..c723c9e91 100644 --- a/.agents/skills/impeccable/reference/new-work.md +++ b/.agents/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .agents/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.agents/skills/impeccable/reference/visualize.md b/.agents/skills/impeccable/reference/visualize.md index 460aebf7d..155aec227 100644 --- a/.agents/skills/impeccable/reference/visualize.md +++ b/.agents/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .agents/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .agents/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.agents/skills/impeccable/scripts/embed-prompt.mjs b/.agents/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.agents/skills/impeccable/scripts/embed-prompt.mjs +++ b/.agents/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.claude/skills/impeccable/reference/new-work.md b/.claude/skills/impeccable/reference/new-work.md index 9c62404ba..9fefa623a 100644 --- a/.claude/skills/impeccable/reference/new-work.md +++ b/.claude/skills/impeccable/reference/new-work.md @@ -70,7 +70,7 @@ Your measured rendition prior: warm, bookish, family, and child-facing subjects ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -115,6 +115,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.claude/skills/impeccable/reference/visualize.md b/.claude/skills/impeccable/reference/visualize.md index 8ea7ef507..f7dc0fd96 100644 --- a/.claude/skills/impeccable/reference/visualize.md +++ b/.claude/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.claude/skills/impeccable/scripts/embed-prompt.mjs b/.claude/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.claude/skills/impeccable/scripts/embed-prompt.mjs +++ b/.claude/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.cursor/skills/impeccable/reference/new-work.md b/.cursor/skills/impeccable/reference/new-work.md index a6b415425..0afbdab2e 100644 --- a/.cursor/skills/impeccable/reference/new-work.md +++ b/.cursor/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .cursor/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.cursor/skills/impeccable/reference/visualize.md b/.cursor/skills/impeccable/reference/visualize.md index 07997f610..c9b4e48af 100644 --- a/.cursor/skills/impeccable/reference/visualize.md +++ b/.cursor/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .cursor/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .cursor/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.cursor/skills/impeccable/scripts/embed-prompt.mjs b/.cursor/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.cursor/skills/impeccable/scripts/embed-prompt.mjs +++ b/.cursor/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.gemini/skills/impeccable/reference/new-work.md b/.gemini/skills/impeccable/reference/new-work.md index 45faefc81..d6af1d041 100644 --- a/.gemini/skills/impeccable/reference/new-work.md +++ b/.gemini/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .gemini/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.gemini/skills/impeccable/reference/visualize.md b/.gemini/skills/impeccable/reference/visualize.md index 242d69d44..4e32349ad 100644 --- a/.gemini/skills/impeccable/reference/visualize.md +++ b/.gemini/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .gemini/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .gemini/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.gemini/skills/impeccable/scripts/embed-prompt.mjs b/.gemini/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.gemini/skills/impeccable/scripts/embed-prompt.mjs +++ b/.gemini/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.github/skills/impeccable/reference/new-work.md b/.github/skills/impeccable/reference/new-work.md index dc32680f9..97e113fa8 100644 --- a/.github/skills/impeccable/reference/new-work.md +++ b/.github/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .github/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.github/skills/impeccable/reference/visualize.md b/.github/skills/impeccable/reference/visualize.md index c77dfad5d..1f70ec6e0 100644 --- a/.github/skills/impeccable/reference/visualize.md +++ b/.github/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .github/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .github/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.github/skills/impeccable/scripts/embed-prompt.mjs b/.github/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.github/skills/impeccable/scripts/embed-prompt.mjs +++ b/.github/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.grok/skills/impeccable/reference/new-work.md b/.grok/skills/impeccable/reference/new-work.md index a638ae8e5..8ed1a6673 100644 --- a/.grok/skills/impeccable/reference/new-work.md +++ b/.grok/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .grok/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.grok/skills/impeccable/reference/visualize.md b/.grok/skills/impeccable/reference/visualize.md index a464eee24..863213349 100644 --- a/.grok/skills/impeccable/reference/visualize.md +++ b/.grok/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .grok/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .grok/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.grok/skills/impeccable/scripts/embed-prompt.mjs b/.grok/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.grok/skills/impeccable/scripts/embed-prompt.mjs +++ b/.grok/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.hermes/skills/impeccable/reference/new-work.md b/.hermes/skills/impeccable/reference/new-work.md index 00084b0e9..413f56212 100644 --- a/.hermes/skills/impeccable/reference/new-work.md +++ b/.hermes/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .hermes/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.hermes/skills/impeccable/reference/visualize.md b/.hermes/skills/impeccable/reference/visualize.md index 349e62729..50a060011 100644 --- a/.hermes/skills/impeccable/reference/visualize.md +++ b/.hermes/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .hermes/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .hermes/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.hermes/skills/impeccable/scripts/embed-prompt.mjs b/.hermes/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.hermes/skills/impeccable/scripts/embed-prompt.mjs +++ b/.hermes/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.kiro/skills/impeccable/reference/new-work.md b/.kiro/skills/impeccable/reference/new-work.md index 2c9d1316b..a021797b6 100644 --- a/.kiro/skills/impeccable/reference/new-work.md +++ b/.kiro/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .kiro/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.kiro/skills/impeccable/reference/visualize.md b/.kiro/skills/impeccable/reference/visualize.md index 5ef07a05b..c7b8141a4 100644 --- a/.kiro/skills/impeccable/reference/visualize.md +++ b/.kiro/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .kiro/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .kiro/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.kiro/skills/impeccable/scripts/embed-prompt.mjs b/.kiro/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.kiro/skills/impeccable/scripts/embed-prompt.mjs +++ b/.kiro/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.opencode/skills/impeccable/reference/new-work.md b/.opencode/skills/impeccable/reference/new-work.md index ae3f0c49b..32f5b9bb0 100644 --- a/.opencode/skills/impeccable/reference/new-work.md +++ b/.opencode/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .opencode/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.opencode/skills/impeccable/reference/visualize.md b/.opencode/skills/impeccable/reference/visualize.md index 3e0a6530b..f8d733328 100644 --- a/.opencode/skills/impeccable/reference/visualize.md +++ b/.opencode/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .opencode/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .opencode/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.opencode/skills/impeccable/scripts/embed-prompt.mjs b/.opencode/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.opencode/skills/impeccable/scripts/embed-prompt.mjs +++ b/.opencode/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.pi/skills/impeccable/reference/new-work.md b/.pi/skills/impeccable/reference/new-work.md index 836490040..7f572166a 100644 --- a/.pi/skills/impeccable/reference/new-work.md +++ b/.pi/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .pi/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.pi/skills/impeccable/reference/visualize.md b/.pi/skills/impeccable/reference/visualize.md index 472042f6c..f99dc97aa 100644 --- a/.pi/skills/impeccable/reference/visualize.md +++ b/.pi/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .pi/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .pi/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.pi/skills/impeccable/scripts/embed-prompt.mjs b/.pi/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.pi/skills/impeccable/scripts/embed-prompt.mjs +++ b/.pi/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.qoder/skills/impeccable/reference/new-work.md b/.qoder/skills/impeccable/reference/new-work.md index 1d8c3c2df..8ac54a407 100644 --- a/.qoder/skills/impeccable/reference/new-work.md +++ b/.qoder/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .qoder/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.qoder/skills/impeccable/reference/visualize.md b/.qoder/skills/impeccable/reference/visualize.md index f4460abd8..756f6b926 100644 --- a/.qoder/skills/impeccable/reference/visualize.md +++ b/.qoder/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .qoder/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .qoder/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.qoder/skills/impeccable/scripts/embed-prompt.mjs b/.qoder/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.qoder/skills/impeccable/scripts/embed-prompt.mjs +++ b/.qoder/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.rovodev/skills/impeccable/reference/new-work.md b/.rovodev/skills/impeccable/reference/new-work.md index 6a9496e09..501645e7d 100644 --- a/.rovodev/skills/impeccable/reference/new-work.md +++ b/.rovodev/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .rovodev/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.rovodev/skills/impeccable/reference/visualize.md b/.rovodev/skills/impeccable/reference/visualize.md index 3a27f7f4f..a2213cd45 100644 --- a/.rovodev/skills/impeccable/reference/visualize.md +++ b/.rovodev/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .rovodev/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .rovodev/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.rovodev/skills/impeccable/scripts/embed-prompt.mjs b/.rovodev/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.rovodev/skills/impeccable/scripts/embed-prompt.mjs +++ b/.rovodev/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.trae-cn/skills/impeccable/reference/new-work.md b/.trae-cn/skills/impeccable/reference/new-work.md index 0af329e54..6e47a77c5 100644 --- a/.trae-cn/skills/impeccable/reference/new-work.md +++ b/.trae-cn/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .trae-cn/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae-cn/skills/impeccable/reference/visualize.md b/.trae-cn/skills/impeccable/reference/visualize.md index aede0f6b5..9b22bfcce 100644 --- a/.trae-cn/skills/impeccable/reference/visualize.md +++ b/.trae-cn/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .trae-cn/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .trae-cn/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.trae-cn/skills/impeccable/scripts/embed-prompt.mjs b/.trae-cn/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.trae-cn/skills/impeccable/scripts/embed-prompt.mjs +++ b/.trae-cn/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.trae/skills/impeccable/reference/new-work.md b/.trae/skills/impeccable/reference/new-work.md index 691cf1d30..fdade3806 100644 --- a/.trae/skills/impeccable/reference/new-work.md +++ b/.trae/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .trae/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.trae/skills/impeccable/reference/visualize.md b/.trae/skills/impeccable/reference/visualize.md index 848abd2a5..1a325bab6 100644 --- a/.trae/skills/impeccable/reference/visualize.md +++ b/.trae/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .trae/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .trae/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.trae/skills/impeccable/scripts/embed-prompt.mjs b/.trae/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.trae/skills/impeccable/scripts/embed-prompt.mjs +++ b/.trae/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/.vibe/skills/impeccable/reference/new-work.md b/.vibe/skills/impeccable/reference/new-work.md index c7d380cdc..d9561a45f 100644 --- a/.vibe/skills/impeccable/reference/new-work.md +++ b/.vibe/skills/impeccable/reference/new-work.md @@ -68,7 +68,7 @@ Calibration: AI-generated interfaces cluster around a few looks regardless of su ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -113,6 +113,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .vibe/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/.vibe/skills/impeccable/reference/visualize.md b/.vibe/skills/impeccable/reference/visualize.md index dbdd3ee97..0c97bb519 100644 --- a/.vibe/skills/impeccable/reference/visualize.md +++ b/.vibe/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .vibe/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .vibe/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/.vibe/skills/impeccable/scripts/embed-prompt.mjs b/.vibe/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/.vibe/skills/impeccable/scripts/embed-prompt.mjs +++ b/.vibe/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file); diff --git a/plugin/skills/impeccable/reference/new-work.md b/plugin/skills/impeccable/reference/new-work.md index 9c62404ba..9fefa623a 100644 --- a/plugin/skills/impeccable/reference/new-work.md +++ b/plugin/skills/impeccable/reference/new-work.md @@ -70,7 +70,7 @@ Your measured rendition prior: warm, bookish, family, and child-facing subjects ## 5. Record the decision -Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. +Before code, state the chosen direction as a contract in the artifact's opening comment, five short blocks, 150 words at most, in a form that survives the production build: an HTML comment in the emitted markup, never only a templating-frontmatter comment, placed as the first child of the document's body in the root layout, never inside a slotted or child component (some compilers, Astro among them, strip a slot's leading comment while keeping deeper ones). After the first production build, grep the built output for the seed key; a contract the build erased is a contract nobody can audit. THESIS: the one idea this surface owns and the category-default arrangement it refuses. OWN-WORLD: the palette and component language, specific enough to be recognizable with all content removed. STORY: what the visitor understands, believes, and does. FIRST VIEWPORT: the exact composition, what is where and at what scale, and where the primary action sits. FORM: the chosen form, its position on your ordered list, and the seed key the script printed. Close with one more line, FINISH: the run's exit condition, verbatim "unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance". The comment tops the artifact you re-open on every edit, the one reminder that survives a long build: a page that looks complete with the FINISH line undischarged is not done, it is abandoned at the finish line. If a block reads like a mood, the direction is not decided yet; the finishing review audits the render against this contract. On a new or replacement world, DESIGN.md is written at finish, from the built world, by the shipped documenter (section 7); a rulebook written before the build gets defended against reality instead of describing it, and hands the design-system detector an unstable target. A new world shipped with no DESIGN.md is still an incomplete run. An ordinary extension does not rewrite DESIGN.md. @@ -115,6 +115,8 @@ Then spawn the shipped finish reviewer, `impeccable-finish-reviewer` (`impeccabl Act on the disposition word; there are exactly four. **recapture**: the evidence failed, not the build. Recapture what the return names under the capture-validity rules, then run a full review over the new evidence. A review conducted on invalid evidence binds nothing, and a verdict pass may never follow it. **rebuild**: fidelity failed wholesale, not in patches. Skip the fix batch and execute the rebuild immediately: re-derive the named regions, produce the named assets, and send the result back for a fresh full review, never a verdict pass; a rebuild replaces regions wholesale, so the whole matrix runs again over the recaptures. Tell the user what is happening rather than asking permission to fix a failure. Consult the user only on a second rebuild directive, both verdicts on the table, or when rebuilding would discard content the user approved. **ship**: nothing is owed; report the verdict at its scope and continue to the documenter. **fix**: apply the material fixes in one batch, rebuild once, and recapture the same viewports over the same files. A recapture measures positions, loading, and overflow; it cannot measure whether a fix reached the quality the finding named, so send the recaptured screenshots back to the same reviewer for a verdict scoring every material fix resolved, partial, or unresolved (through the harness's agent continuation; without one, run the scoring fresh from [degraded/finish-reviewer.md](degraded/finish-reviewer.md)'s Verdict Pass). Fixes scored partial or unresolved get another batch, recapture, and verdict. Two rounds is the budget an unattended run ends at; an attended session's ceiling belongs to the user, so when the second verdict still lists open items, put the table in front of them and let them choose between shipping as it stands and funding another round. Whoever decides, stop the moment a round resolves nothing, and the reviewer's findings are the only list you work from, never your own re-opened hunt. Do not run a second detector. +A rebuild and a fix round share one asset rule: a raster either round creates or replaces is still asset work under [visualize.md](visualize.md)'s Produce section and keeps its **provenance** like every build raster, and a raster the round abandons is deleted in the same batch. Before either round's result goes back for review or verdict, run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --scan ` over the directories the artifact's rasters ship from and clear every file it reports by embedding what it is missing: the exact generation prompt for a produced raster, the origin for a sourced, stock, or pre-existing one. The scan only reads; deletion is reserved for rasters the round abandoned, never for a file the scan flagged. + Report the final verdict under the reviewer's own disposition word and at its actual scope. A verdict pass scores the listed fixes and nothing else: "the reviewer scored all three fixes resolved" is a claim it supports, "no material issues remain" is not. A table with open material findings is never announced as a pass, never softened, and never dressed as whole-surface approval when only a fix list was scored. When the user answers a ship with evidence against it, their own screenshot, a named mismatch with the comp, that evidence outranks every capture you made: put their material in the packet and spawn a fresh reviewer for a new full review. Patching inline and self-certifying is how a rejected page ships twice. Then spawn the shipped documenter, `impeccable-documenter` (`impeccable_documenter` in codex), with the project root, the artifact path, the direction contract, PRODUCT.md, the [document.md](document.md) reference path, and the boundary to write at; it records DESIGN.md and the sidecar from the built world, ground truth over intention; without subagents the pass runs from [degraded/documenter.md](degraded/documenter.md). The documenter runs after the last correction lands: when any fix round follows the documentation, re-run the documenter over the changed surface, because a DESIGN.md describing a layout that no longer exists turns defects into system guidance. A clean detector pass is not finished; finished is the contract kept, the comp honored, the review closed, and the system recorded. diff --git a/plugin/skills/impeccable/reference/visualize.md b/plugin/skills/impeccable/reference/visualize.md index 8ea7ef507..f7dc0fd96 100644 --- a/plugin/skills/impeccable/reference/visualize.md +++ b/plugin/skills/impeccable/reference/visualize.md @@ -43,7 +43,9 @@ The comp is a north star, not something to trace, and know what that allows: tra ## Produce only the assets the build needs -Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` so the intent lives inside the file and survives copies between machines and harnesses; `--read` recovers it from any impeccable-generated image. +Generation context is part of the asset: a build composed by a thread that never saw the prompts places assets it does not understand. Prefer generating build-critical imagery in the build thread when the budget allows; when a subagent produces assets instead, every asset carries its prompt, and the builder reads those prompts before composing. The carrier is uniform across harnesses: after generating any image with any tool, native or `generate-image.mjs` (which does it automatically), run `node .claude/skills/impeccable/scripts/embed-prompt.mjs --prompt ""` with the exact string the generation tool received, pasted whole, so the intent lives inside the file and survives copies between machines and harnesses; a summary reconstructed from memory records an asset that was never made. `--read` recovers the prompt from any impeccable-generated image, and `--scan ` lists every raster in a directory still missing one. The embedded prompt plus the asset's row in the written inventory is the raster's **provenance**, and every raster the artifact references carries it; a sourced, stock, or pre-existing raster with no generation prompt embeds its origin instead. + +Provenance is owed for the run, not the build phase: a raster created or replaced later, in a fix batch or a reviewer's rebuild, is produced under this same section, prompt embedded and inventory row added, because the inventory is how the next thread knows what ships. A raster a fix abandons or supersedes is deleted from the assets directory in the same batch; an unreferenced raster with no record is a provenance leak, not a spare. When the harness runs subagents, spawn the shipped asset producer every time, even when the inventory's produce bucket looks empty: its manifest is the independent second opinion on your media, and the runs that skipped the spawn are the runs whose cotton became CSS. An honestly empty manifest costs one cheap spawn; a wrongly empty produce bucket costs the build its materials. Use `impeccable-asset-producer` (`impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent"): give it the approved comp, output paths, required dimensions and formats, transparency needs, crop notes, and what must remain semantic code. Without subagents, produce the minimum required assets in the current thread by the book: load [degraded/asset-producer.md](degraded/asset-producer.md) and follow it inline, with whatever generation exists. diff --git a/plugin/skills/impeccable/scripts/embed-prompt.mjs b/plugin/skills/impeccable/scripts/embed-prompt.mjs index 72a03b1ca..a5e7614c2 100644 --- a/plugin/skills/impeccable/scripts/embed-prompt.mjs +++ b/plugin/skills/impeccable/scripts/embed-prompt.mjs @@ -5,6 +5,7 @@ // node embed-prompt.mjs --prompt "the prompt text" // node embed-prompt.mjs --prompt-file prompt.txt // node embed-prompt.mjs --read +// node embed-prompt.mjs --scan # list rasters missing a prompt; exit 3 when any // // Formats: PNG (tEXt chunk, keyword "impeccable:prompt"), JPEG (COM segment). // WebP and anything else fall back to a `.json` sidecar; --read checks @@ -21,8 +22,49 @@ 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); + 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) { + 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; + const rasters = []; + const walk = (p, isRoot) => { + const stat = fs.statSync(p); + if (stat.isDirectory()) { + const base = p.replace(/\/+$/, '').split('/').pop(); + // Skip installed deps and hidden dirs found during the walk, but honor a + // hidden dir the caller passed explicitly (e.g. .impeccable/mocks). + if (!isRoot && (base === 'node_modules' || base.startsWith('.'))) return; + for (const entry of fs.readdirSync(p)) walk(`${p.replace(/\/+$/, '')}/${entry}`, false); + } else if (RASTER.test(p)) { + rasters.push(p); + } + }; + for (const target of targets) { + if (!fs.existsSync(target)) { console.error(`embed-prompt: no such path ${target}`); process.exit(1); } + walk(target, true); + } + let missing = 0; + for (const raster of rasters) { + if (promptOf(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); +} + if (!file || !fs.existsSync(file)) { console.error('embed-prompt: image file required'); process.exit(1); } const buf = fs.readFileSync(file);