mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
comp-spec refuses a code region larger than a quarter of the comp
A session named seven regions for a page with three plates, a table, a note, callouts and a spine, so the hero gate could name nothing and the score sat at 70. A code region is one element; a column is a container of several. AI-assisted (Claude Code).
This commit is contained in:
@@ -101,7 +101,7 @@ When an approved comp exists, it is a spatial contract, not a mood board: only t
|
||||
Then, in order, each closed by `node {{scripts_path}}/build-phase.mjs advance` (every script below lives under `{{scripts_path}}/` and runs with `node`; exit 2 means the gate failed and printed why; fix that and advance again; write nothing for a later phase while an earlier gate is open):
|
||||
|
||||
0. **comps.** The comp round from [visualize.md](visualize.md): three compositional comps of the requested surface at its own viewport under `.impeccable/mocks/`, each with a prompt sidecar, put in front of the user; the chosen one's sidecar gets `"approved": true`. The gate counts them and reads the approval; a `start --comp` skips this phase because it already happened.
|
||||
1. **spec.** Measure the comp: `comp-spec.mjs --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws), and run `comp-spec.mjs --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `comp-spec.mjs --print` is the build's reference from here on. Type is measured, not guessed: `font-match.mjs --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `font-match.mjs --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user chose an icon library), and genuine defects in the comp such as spelling errors. <!-- rule:skill-comp-spec -->
|
||||
1. **spec.** Measure the comp: `comp-spec.mjs --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws), and run `comp-spec.mjs --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `comp-spec.mjs --print` is the build's reference from here on. Type is measured, not guessed: `font-match.mjs --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `font-match.mjs --rank <region> --text "..."` takes its candidates from a fingerprint index of the Google Fonts catalog (the nearest faces to the crop's shape) plus any names you pass with `--candidates`, renders them at that cap height with the region's words, and ranks them by fingerprint distance (its `USE` line is the CSS; its proof sheet shows the comp over the top three); with no browser resolvable it records the catalog's nearest face and says the size is estimated, which is still the choice to build on. Do not install a browser to rank, and never write a `chosen` face into the spec by hand: the gate accepts only what font-match wrote. The spec gate refuses to close until the lead text region is measured and ranked. A region note that describes painted material (a diagram, drawing, photograph, texture) under a code kind is refused at the spec: reclassify it as a plate, or reword the note if code really draws it. The script refuses a regions file that leaves comp ink unnamed (callouts, a parts table, a notes block): what is never named can never be missing, so everything the comp shows gets a region. It also refuses a `text` / `control` / `chrome` region larger than a quarter of the comp: that is a column, not an element, and a column scored as one region hides the plates, tables, and notes inside it. Name each element inside it (`container: true` only when it truly is one undivided element). Anything not in the spec does not exist on the page: no borders, rules, containers, or chrome the comp does not show. Only three concessions exist: fonts (the closest obtainable face), icons (exact match unless the user chose an icon library), and genuine defects in the comp such as spelling errors. <!-- rule:skill-comp-spec -->
|
||||
2. **plates.** Every raster region ships as a plate: an illustration, photo, or figure regenerated at asset resolution from its comp crop, UI text removed, at its `plate` path (ink on flat ground is generated on a chroma key and keyed to alpha, so it sits on the page's own ground rather than a second paper); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `generate-image.mjs --plate <id>` does one region end to end and scores it against the crop; a harness-native image tool takes the crop (`comp-spec.mjs --crop <id>`) as its input image and `comp-spec.mjs --plate-prompt <id>` as its prompt, then `embed-prompt.mjs`. With parallel subagents, spawn the shipped asset producer (`impeccable-asset-producer`; `impeccable_asset_producer` in codex; `/impeccable-asset-producer` in Cursor; on GitHub Copilot say "Use the impeccable-asset-producer agent") with the spec path and let it produce them all; without subagents, produce them here. A crop of the comp is a reference, never a shipping pixel. The gate checks every plate exists, is at least 1.5x the region's size, and reads as the region. Page code waits for this gate: a page written before its plates exist is a page that draws its material in CSS. A single-file deliverable changes nothing here: the plate is produced the same way and inlined as a data URI. `--force` exists for one case only, the user downgrading the comp's authority in words you quote in `--reason`; the script refuses every other reason. <!-- rule:skill-plates-before-page -->
|
||||
3. **hero.** Build only the first viewport, at the comp's own dimensions, the comp's words copied verbatim (the user approved that comp with those words; rewording is a stated decision after the hero passes, never a silent one inside it), every text region sized from its measured cap height and set in its ranked face, plates first: place every plate at its spec box (`object-fit: cover`, an `<img>`, a background image, or an inlined data URI named for it) before any text or control, capture into `.impeccable/review/hero-repro.png`, run `build-phase.mjs record hero` once so you see the plate regions read as match before any text exists, then lay the semantic layer over the plates from the spec's palette and boxes and advance. The gate first refuses while any plate is unreferenced by the source, then runs `comp-diff.mjs`, writes `.impeccable/review/diff/hero/` (side-by-side, heatmap, one paired crop per region, `report.json`), and passes at 72% overall with no region missing. When it fails, open the region crops it lists, in order, before editing: a region scored `missing` needs its material, `contradicted` needs its structure re-derived from the spec box, `drift` is where size and spacing edits belong; the gate refuses a third attempt that only nudges values on the same region. This is where the run's ambition is won or lost, and a retry here costs minutes where a rebuild verdict at the finish costs the run. <!-- rule:skill-hero-gate -->
|
||||
4. **sections.** Build the rest of the surface inside the spec's system: the same corner language, line weights, and palette, and nothing the comp never shows. Where the comp does not cover a region, it inherits the recorded system.
|
||||
|
||||
@@ -90,6 +90,9 @@ function paletteOf(img) {
|
||||
/** Words in a region note that name painted material rather than code-drawn UI. */
|
||||
export const PAINTED_NOTE = /\b(diagram|drawing|drawn|illustrat\w*|figure|schematic|exploded|photo\w*|picture|painting|painted|render\w*|artwork|engraving|etching|linework|line art|texture\w*|grain|paper|fabric|halftone|watercolou?r|sketch\w*|blueprint|technical geometry|carburetor geometry|product shot|hero image|3d)\b/i;
|
||||
|
||||
/** A text/control/chrome region larger than this fraction of the comp is a column, not an element. */
|
||||
export const MAX_CODE_REGION_AREA = 0.25;
|
||||
|
||||
function energyOf(img) {
|
||||
const g = detailGrid(img, 4, 4, 256);
|
||||
let s = 0; for (const v of g.cells) s += v;
|
||||
@@ -141,6 +144,16 @@ export function measureRegions(comp, regionsInput, compPath) {
|
||||
throw new Error(`region ${raw.id} is kind "${kind}" but its note describes painted material ("${raw.note}"). Anything drawn, photographed, or textured ships as a raster plate: set kind to plate (illustration, diagram, figure), image (photograph), or texture (ground). If the note is wrong and code really draws it (a table, a rule, a chrome bar), reword the note or set "codeDrawn": true on the region.`);
|
||||
}
|
||||
const box = raw.box && typeof raw.box.x === 'number' ? raw.box : gridToBox(raw.grid);
|
||||
// A code region is one element the page draws: a headline, a table, a
|
||||
// button, a bar. A "chrome" region covering a third of the comp is a
|
||||
// column, and a column scored as one region hides everything inside it
|
||||
// (a session named seven regions for a page with three plates, a table,
|
||||
// a note, callouts and a spine, and the hero gate could name nothing).
|
||||
// Raster regions may be as large as the material; a texture is a sample.
|
||||
const area = box.w * box.h;
|
||||
if (!RASTER_KINDS.has(kind) && kind !== 'band' && area > MAX_CODE_REGION_AREA && !raw.container) {
|
||||
throw new Error(`region ${raw.id} (${kind}) covers ${Math.round(area * 100)}% of the comp; a code region is one element (a headline, a table, a control, a rule, a bar), and one this large is a column holding several. Name each element inside it as its own region (every illustration or photo as a plate), or set "container": true on the region if it truly is one undivided element.`);
|
||||
}
|
||||
const px = { x: Math.round(box.x * comp.width), y: Math.round(box.y * comp.height), w: Math.round(box.w * comp.width), h: Math.round(box.h * comp.height) };
|
||||
const c = crop(comp, px.x, px.y, px.w, px.h);
|
||||
const energy = energyOf(c);
|
||||
|
||||
@@ -74,6 +74,15 @@ describe('comp-spec', () => {
|
||||
assert.equal(plate.regions[0].medium, 'raster');
|
||||
});
|
||||
|
||||
it('refuses a code region that covers a column of the comp, unless container', () => {
|
||||
const comp = makeComp();
|
||||
assert.throws(() => measureRegions(comp, { allowUncovered: true, regions: [{ id: 'parts-column', kind: 'chrome', grid: 'G0:J9' }] }, 'c.png'), /covers 40% of the comp/);
|
||||
const ok = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'parts-column', kind: 'chrome', grid: 'G0:J9', container: true }] }, 'c.png');
|
||||
assert.equal(ok.regions[0].kind, 'chrome');
|
||||
const plate = measureRegions(comp, { allowUncovered: true, regions: [{ id: 'art', kind: 'plate', grid: 'G0:J9' }] }, 'c.png');
|
||||
assert.equal(plate.regions[0].medium, 'raster');
|
||||
});
|
||||
|
||||
it('refuses a regions file that leaves comp ink unnamed, unless allowUncovered', () => {
|
||||
const comp = makeComp();
|
||||
// only the masthead named: the headline, plate, and list are ink no region covers
|
||||
@@ -149,7 +158,7 @@ describe('build-phase state machine (CLI)', () => {
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'headline', kind: 'text', grid: 'A1:D2' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4', note: 'noise plate' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9', container: true },
|
||||
] }));
|
||||
});
|
||||
after(() => { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} });
|
||||
@@ -305,7 +314,7 @@ describe('build-phase state machine (CLI)', () => {
|
||||
fs.writeFileSync(path.join(d3, 'regions.json'), JSON.stringify({ allowUncovered: true, regions: [
|
||||
{ id: 'masthead', kind: 'chrome', grid: 'A0:J0' },
|
||||
{ id: 'art', kind: 'plate', grid: 'F1:J4' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9' },
|
||||
{ id: 'list', kind: 'control', grid: 'A5:J9', container: true },
|
||||
] }));
|
||||
run(PHASE_SCRIPT, ['start', '--comp', 'comp.png', '--artifact', 'index.html'], d3);
|
||||
run(SPEC_SCRIPT, ['--comp', 'comp.png', '--regions', 'regions.json'], d3);
|
||||
|
||||
Reference in New Issue
Block a user