mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Fix: keep raster provenance through the finish-review fix loop (#588)
* Fix: keep raster provenance through the finish-review fix loop Three runs (two harnesses) showed the parent generating production rasters after the producer returned: no exact embedded prompt, no inventory row, orphan files. The asset contract in visualize.md was phase-scoped to the build while new-work.md's fix loop licensed "produce the named assets" with no rules attached. - visualize.md: name the provenance contract, require the exact tool payload, and scope it to the run, fix rounds and rebuilds included. - new-work.md: bind fix/rebuild rasters to the contract, add an embed-prompt --scan step before the verdict round, and extend the FINISH line to carry the condition through long builds. - embed-prompt.mjs: add --scan mode listing rasters missing a prompt (exit 3 when any), reusing the existing read path. AI-assisted change, prepared with Cursor under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Add cursor-control-8 comp vs final screenshots for PR evidence AI-assisted change (Cursor), prepared under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Add cursor-control-9 comp vs final screenshots for PR evidence AI-assisted change (Cursor), prepared under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Address review findings on the provenance gate - Hoist the provenance rule out of the fix disposition into its own paragraph binding rebuild and fix alike, gated before either round's result goes back for review or verdict (Bugbot: rebuild skipped the scan when its fresh review shipped). - A scan-flagged raster gets the record it is missing embedded, exact prompt for produced, origin for sourced/stock/pre-existing; deletion is reserved for abandoned rasters, never scan hits (Bugbot: gate hit non-generated assets on extensions). - Document the scan command with its required directory argument (Greptile: literal command exited before scanning). - Align the FINISH line on the provenance token. AI-assisted change (Cursor), prepared under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove evidence images from the diff; they live on the pr-evidence branch AI-assisted change (Cursor), prepared under maintainer direction. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -72,7 +72,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. <!-- rule:skill-decide-then-build -->
|
||||
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. <!-- rule:skill-decide-then-build -->
|
||||
|
||||
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. <!-- rule:skill-design-md-from-the-build -->
|
||||
|
||||
@@ -117,6 +117,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. <!-- rule:skill-verdict-bounds-the-finish -->
|
||||
|
||||
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 {{scripts_path}}/embed-prompt.mjs --scan <asset-dir...>` 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. <!-- rule:skill-late-raster-provenance -->
|
||||
|
||||
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. <!-- rule:skill-user-evidence-reopens-review -->
|
||||
|
||||
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. <!-- rule:skill-documenter-records-the-world -->
|
||||
|
||||
@@ -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 {{scripts_path}}/embed-prompt.mjs <image> --prompt "<the prompt used>"` 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 {{scripts_path}}/embed-prompt.mjs <image> --prompt "<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 <dir>` 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.
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// node embed-prompt.mjs <image> --prompt "the prompt text"
|
||||
// node embed-prompt.mjs <image> --prompt-file prompt.txt
|
||||
// node embed-prompt.mjs <image> --read
|
||||
// node embed-prompt.mjs --scan <dir...> # 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 `<image>.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);
|
||||
|
||||
Reference in New Issue
Block a user