Make plate candidate checks non-destructive and align reference exclusions

Add exact pixel region coordinates and native command guidance. Apply UI exclusions symmetrically after cover alignment while retaining raw asset provenance checks. Verify unchanged selected assets, build state, missing-art rejection, and comp-copy rejection.

AI-assisted implementation and validation by Codex under maintainer direction.
This commit is contained in:
Paul Bakaus
2026-09-17 18:44:27 -07:00
parent 66b996a3eb
commit 37e1757a5d
7 changed files with 168 additions and 10 deletions
+111 -5
View File
@@ -19,7 +19,7 @@ use regex::Regex;
use serde_json::{json, Map, Value};
use crate::comp_diff::{align_build, best_shift, build_report, compare, write_artifacts, write_region_artifacts, CompareResult, Score};
use crate::comp_spec::{load_spec, prepare_plate_reference, BUILD_DIR, SPEC_PATH};
use crate::comp_spec::{load_spec, prepare_plate_reference, PlateReference, BUILD_DIR, SPEC_PATH};
use crate::font_match::choice_stamped;
use crate::entry_capture::{CapturedEntry, EntryRenderer, EntryRequest, EntryStage};
use crate::util::{self, arg, flag, round, to_fixed};
@@ -469,13 +469,36 @@ fn plate_verdict(region: &Value, score: &Score) -> (bool, Vec<String>) {
(reasons.is_empty(), reasons)
}
fn score_plate_reference(reference: &PlateReference, build: &Image, kind: Option<&str>) -> Score {
let mut aligned = align_build(&reference.image, build, "cover");
// These pixels belong to foreground UI, not the underlying artwork. Use
// the same exclusion on both sides, in reference coordinates AFTER cover
// alignment. Provenance checks below still inspect the unmasked asset.
for exclusion in &reference.excluded_regions {
let p = &exclusion["cropPx"];
let rect = r::clamp_rect(&aligned, p["x"].as_f64().unwrap_or(0.), p["y"].as_f64().unwrap_or(0.),
p["w"].as_f64().unwrap_or(0.), p["h"].as_f64().unwrap_or(0.));
for y in rect.y..rect.y + rect.h {
let start = (y * aligned.width + rect.x) * 4;
let end = start + rect.w * 4;
aligned.data[start..end].copy_from_slice(&reference.image.data[start..end]);
}
}
crate::comp_diff::score_pair(&reference.image, &aligned, kind)
}
fn gate_plates(io: &Io) -> Gate {
let s = self_cmd(io);
let Some(spec) = load_spec(&abs(io, SPEC_PATH)) else {
return Gate::fail(vec!["no spec".into()]);
};
gate_plates_for(io, &spec, None)
}
fn gate_plates_for(io: &Io, spec: &Value, only_id: Option<&str>) -> Gate {
let s = self_cmd(io);
let regions = spec_regions(&spec);
let raster_regions: Vec<Value> = regions.iter().filter(|r| r.get("medium").and_then(Value::as_str) == Some("raster")).cloned().collect();
let raster_regions: Vec<Value> = regions.iter().filter(|r| r.get("medium").and_then(Value::as_str) == Some("raster")
&& only_id.is_none_or(|id| r["id"] == id)).cloned().collect();
if raster_regions.is_empty() {
let mut g = Gate::ok("no plates owed".into());
g.plates = Some(vec![]);
@@ -549,7 +572,7 @@ fn gate_plates(io: &Io) -> Gate {
if let Some(issue) = reference.issue(&id) {
reasons.push(format!("plate {file}: {issue}"));
} else {
let score = compare(refimg, &build, None, "cover", "", kind).whole;
let score = score_plate_reference(&reference, &build, kind);
score_val = Some(score.overall);
let (_, vreasons) = plate_verdict(rr, &score);
for reason in vreasons {
@@ -2388,7 +2411,7 @@ fn next_instruction(io: &Io, state: &Value) -> String {
"Measure the comp: {s} comp-spec --comp {comp} --grid, open {}, write regions.json (every illustration, photo, texture as its own plate region; every text block its own text region), run {s} comp-spec --comp {comp} --regions regions.json. Then measure the type: {s} font-match --measure <id> for each text region (cap height, width class, weight class) and {s} font-match --rank <lead text region> --text \"<its first words>\" to choose the headline face by metrics (the USE line is the CSS; with no browser it records the catalog's nearest face, which is the choice; do not install one, and do not write a chosen face into the spec by hand). Then {s} build-phase advance.",
format!("{BUILD_DIR}/comp-grid.png")
),
"plates" => format!("Produce every plate in the spec ({s} comp-spec --print lists them). For each illustration, photo, or figure, run {s} comp-spec --crop <id> --out <crop.png> and save {s} comp-spec --plate-prompt <id> to a prompt file. For an isolated figure or object on the page ground, add --background transparent to that plate-prompt command. Prefer the harness image tool with the crop as reference and that prompt; request native transparent PNG for cutouts. With the API fallback, run {s} generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high; add --background transparent for cutouts. Create the output directory first and choose a supported size matching the region's aspect at least 1.5x its pixel size. generate-image embeds the prompt; after a harness generation run {s} embed-prompt <plate.png> --prompt-file <prompt.txt>. Preserve white paint, fine edges, and interior holes; verify alpha and inspect the cutout on light and dark grounds. Do not chroma-key native transparent output. Keep photos and textures opaque. Place cutouts with a plain <img> over the page's own ground; inspect glass and other translucent material carefully. Textures (paper, cloth, grain): crop a clean patch from {s} comp-spec --crop <id> --raw and mirror-tile it to the plate size; generate only when no clean patch exists. The gate scores a texture against its whole region box, so draw its region around clean ground. Then {s} build-phase advance scores all plates against their comp regions. A pass does not replace visual inspection of placement, scale, and alpha. Write no page code before this passes."),
"plates" => format!("Produce every plate in the spec ({s} comp-spec --print lists them). For each illustration, photo, or figure, run {s} comp-spec --crop <id> --out <crop.png> and save {s} comp-spec --plate-prompt <id> to a prompt file. For an isolated figure or object on the page ground, add --background transparent to that plate-prompt command. Prefer the harness image tool with the crop as reference and that prompt; request native transparent PNG for cutouts. With the API fallback, run {s} generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high; add --background transparent for cutouts. Create the output directory first and choose a supported size matching the region's aspect at least 1.5x its pixel size. generate-image embeds the prompt; after a harness generation run {s} embed-prompt <plate.png> --prompt-file <prompt.txt>. Preserve white paint, fine edges, and interior holes; verify alpha and inspect the cutout on light and dark grounds. Do not chroma-key native transparent output. Keep photos and textures opaque. Place cutouts with a plain <img> over the page's own ground; inspect glass and other translucent material carefully. Textures (paper, cloth, grain): crop a clean patch from {s} comp-spec --crop <id> --raw and mirror-tile it to the plate size; generate only when no clean patch exists. The gate scores a texture against its whole region box, so draw its region around clean ground. Keep candidate crops in separate files. Test each with {s} build-phase check-plate <id> --candidate <png> --json; this does not replace the selected asset or advance state. Inspect the candidate before explicitly selecting it at the spec plate path. Then {s} build-phase advance scores all selected plates against their comp regions. A pass does not replace visual inspection of placement, scale, and alpha. Write no page code before this passes."),
"hero" => format!(
"Run {s} build-phase scaffold first: it writes the measured layout as CSS custom properties (.impeccable/build/scaffold/layout.css, --r-<id>-x/y/w/h in % of the comp, plus cap height, font-size, family, and weight where measured) and a reference page with every region at its box. Bind those numbers to your own markup (an element per region, its box from the properties); the reference is a check, not the page, and overlapping boxes are overlapping boxes. Build only the first viewport at {}. Copy the comp's words verbatim in this phase (headline, labels, table cells, footer): the user approved that comp with those words, and rewriting is a later, stated decision, never a silent one here. Set every text region's font-size from its measured cap height and its face from the ranking. Plates first: place every plate at its spec box ({s} comp-spec --print lists boxes as percentages of the viewport) with object-fit: cover before writing a line of text or a control, capture into {HERO_REPRO}, and run {s} build-phase record hero (not advance) once so you see the plate regions read as match before text exists; then lay the semantic layer (text, controls, rules) over the plates from the spec's palette and boxes, capture, advance. When it fails, open the region crops it lists first, in order, then fix; do not build past the hero until it passes.",
bp.unwrap_or("the comp size")
@@ -2405,6 +2428,58 @@ fn next_instruction(io: &Io, state: &Value) -> String {
mod transparency_guidance_tests {
use super::*;
#[test]
fn candidate_check_never_replaces_selection_or_persists_gate_receipts() {
let dir = std::env::temp_dir().join(format!("plate-candidate-{}", std::process::id()));
std::fs::create_dir_all(dir.join(BUILD_DIR)).unwrap();
let image = r::create_image(64,64,[20,70,110,255]);
let bytes = png_io::encode_png(&image, &[]).unwrap();
let spec = json!({"comp":"comp.png","regions":[{"id":"ground","kind":"texture","medium":"raster",
"plate":"selected.png","px":{"x":0,"y":0,"w":64,"h":64},"palette":[{"hex":"#14466e"}]}]}).to_string();
for (path, data) in [("comp.png",bytes.as_slice()),("candidate.png",bytes.as_slice()),
("selected.png",b"selected asset must survive".as_slice()),(SPEC_PATH,spec.as_bytes()),
(".impeccable/build/state.json",b"{\"phase\":\"plates\",\"attempts\":7}".as_slice())] {
std::fs::write(dir.join(path),data).unwrap();
}
let before = ["comp.png","candidate.png","selected.png",SPEC_PATH,".impeccable/build/state.json"]
.map(|path| (path,std::fs::read(dir.join(path)).unwrap()));
let (mut io, output) = Io::captured("",dir.clone(),Default::default());
assert_eq!(run(&["check-plate","ground","--candidate","candidate.png","--json"].map(String::from), &mut io, &no_organic_scan),0);
let report: Value = serde_json::from_slice(&output.stdout.borrow()).unwrap();
assert_eq!(report["stateChanged"],false);
assert_eq!(report["plates"][0]["file"],"candidate.png");
assert_eq!(report["plates"][0]["status"],"ok");
for (path, bytes) in before { assert_eq!(std::fs::read(dir.join(path)).unwrap(),bytes); }
assert_eq!(run(&["check-plate","unknown","--candidate","candidate.png"].map(String::from), &mut io, &no_organic_scan),1);
// A candidate still goes through the exact same anti-copy validation.
let mut photo: Value = serde_json::from_str(&spec).unwrap();
photo["regions"][0]["kind"] = json!("image");
std::fs::write(dir.join(SPEC_PATH),photo.to_string()).unwrap();
let (mut io, output) = Io::captured("",dir.clone(),Default::default());
assert_eq!(run(&["check-plate","ground","--candidate","candidate.png","--json"].map(String::from), &mut io, &no_organic_scan),2);
assert!(String::from_utf8(output.stdout.borrow().clone()).unwrap().contains("comp crop"));
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn plate_score_excludes_the_same_occluded_pixels_on_both_sides() {
let mut comp = r::create_image(128, 64, [30, 80, 120, 255]);
r::fill_rect(&mut comp, 10., 8., 55., 48., [210., 140., 70., 255.]);
let region = json!({"id":"photo","kind":"image","medium":"raster",
"px":{"x":0,"y":0,"w":128,"h":64},"palette":[{"hex":"#1e5078"}]});
let spec = json!({"regions":[region,{"id":"label","kind":"text",
"px":{"x":80,"y":0,"w":48,"h":24}}]});
let reference = prepare_plate_reference(&comp, &spec, &spec["regions"][0]);
let original = score_plate_reference(&reference, &comp, Some("image"));
let mut hidden_change = comp.clone();
r::fill_rect(&mut hidden_change, 80., 0., 48., 24., [255., 0., 190., 255.]);
let hidden = score_plate_reference(&reference, &hidden_change, Some("image"));
assert_eq!(original.to_json(), hidden.to_json());
let missing = r::create_image(128, 64, [30, 80, 120, 255]);
let missing = score_plate_reference(&reference, &missing, Some("image"));
assert!(!plate_verdict(&spec["regions"][0], &missing).0);
}
#[test]
fn control_lettering_gets_the_same_measurements_as_text_without_reclassification() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../comp/tests/fixtures");
@@ -2704,10 +2779,41 @@ pub fn run(argv: &[String], io: &mut Io, organic_scan: OrganicScan) -> i32 {
pub fn run_with_renderer(argv: &[String],io: &mut Io,organic_scan: OrganicScan,renderer: Option<&dyn EntryRenderer>) -> i32 {
let cmd = argv.first().map(String::as_str);
if cmd.is_none() || flag(argv, "help") {
io.out("CANDIDATE CHECK: build-phase check-plate <region-id> --candidate <png> [--json] validates a separate file with the normal plate gate; never replaces the selected asset, records approval, or advances the phase.\n");
io.err("usage: build-phase.mjs start --comp <png> [--breakpoint WxH] [--artifact <entry file>] [--session-id <id>] | status [--json] | completion [--session-id <id>] | advance [--force --reason \"...\"] | record hero --build <png> | scaffold | note \"<text>\" | finish --disposition <word>\n");
return 1;
}
let cmd = cmd.unwrap();
if cmd == "check-plate" {
let Some(id) = argv.get(1).filter(|id| !id.starts_with('-')) else {
io.err("usage: build-phase check-plate <region-id> --candidate <png> [--json]\n");
return 1;
};
let Some(candidate) = arg(argv, "candidate").filter(|path| !path.is_empty()) else {
io.err("build-phase: check-plate needs --candidate <png>; it never replaces the selected plate\n");
return 1;
};
let Some(mut spec) = load_spec(&abs(io, SPEC_PATH)) else {
io.err("build-phase: no measured spec; run comp-spec --help for region coordinates\n");
return 1;
};
let region = spec["regions"].as_array_mut().and_then(|regions| regions.iter_mut().find(|r| r["id"] == id.as_str()));
let Some(region) = region.filter(|r| r["medium"] == "raster") else {
io.err(&format!("build-phase: {id} is not a measured raster region\n"));
return 1;
};
region["plate"] = json!(candidate);
let gate = gate_plates_for(io, &spec, Some(id));
if flag(argv, "json") {
io.out(&format!("{}\n", util::json_pretty(&json!({"ok":gate.ok,"candidate":candidate,
"region":id,"reasons":gate.reasons,"plates":gate.plates,"stateChanged":false}))));
} else {
io.out(&format!("{} {id}: {candidate} (candidate only; selected plate and build state unchanged)\n",
if gate.ok { "PASS" } else { "FAIL" }));
for reason in &gate.reasons { io.out(&format!(" - {reason}\n")); }
}
return if gate.ok { 0 } else { 2 };
}
if cmd == "completion" {
let state = load_state(io);
let session_id = arg(argv, "session-id").or_else(|| io.env("IMPECCABLE_SESSION_ID"))
+40 -2
View File
@@ -456,12 +456,26 @@ pub fn measure_regions(comp: &Image, regions_input: &Value, comp_path: &str) ->
}
}
// box: explicit raw.box (x is number) else gridToBox(raw.grid)
let has_box = raw
let has_normalized_box = raw
.get("box")
.and_then(|b| b.get("x"))
.map(|x| x.is_number())
.unwrap_or(false);
let mut boxf: (f64, f64, f64, f64) = if has_box {
let has_pixel_box = raw.get("pixelBox").is_some();
let has_box = has_normalized_box || has_pixel_box;
let mut boxf: (f64, f64, f64, f64) = if has_pixel_box {
if raw.get("box").is_some() || raw.get("grid").is_some() {
return Err(format!("region {id}: use pixelBox, box, or grid, not multiple coordinate formats"));
}
let b = &raw["pixelBox"];
let coords: Option<Vec<f64>> = ["x", "y", "w", "h"].iter().map(|key| b[*key].as_f64()).collect();
let Some(v) = coords else { return Err(format!("region {id}: pixelBox requires numeric x, y, w, h in original comp pixels")); };
if v.iter().any(|v| !v.is_finite() || v.fract() != 0.) || v[0] < 0. || v[1] < 0.
|| v[2] <= 0. || v[3] <= 0. || v[0] + v[2] > w || v[1] + v[3] > h {
return Err(format!("region {id}: pixelBox must use whole pixels within the {w}x{h} comp with positive width and height"));
}
(v[0] / w, v[1] / h, v[2] / w, v[3] / h)
} else if has_normalized_box {
let b = raw.get("box").unwrap();
(
b.get("x").and_then(Value::as_f64).unwrap_or(0.0),
@@ -882,6 +896,7 @@ fn resolve(io: &Io, p: &str) -> PathBuf {
pub fn run(argv: &[String], io: &mut Io) -> i32 {
let spec_path = arg_or(argv, "spec", SPEC_PATH).to_string();
if flag(argv, "help") || argv.is_empty() {
io.out("REGION COORDINATES: use one of grid (coarse inclusive cells), box {x,y,w,h} (fractions of the comp, 0..1), or pixelBox {x,y,w,h} (whole pixels in the original comp). Use exact bounds when an element ends inside a grid cell; do not include neighbouring content.\n");
io.out("usage: comp-spec.mjs --comp <png> --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp <png> --regions <json> measure regions -> .impeccable/build/spec.json\n regions json: { \"regions\": [ { \"id\": \"art\", \"kind\": \"plate|image|texture|text|control|chrome\", \"grid\": \"E0:J4\", \"note\": \"...\" } ] }\n comp-spec.mjs --comp <png> --auto band regions when you have no regions file\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop <id> [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt <id> [--background transparent|opaque|auto] the regeneration prompt for a raster region\n");
return 0;
}
@@ -1018,6 +1033,7 @@ pub fn run(argv: &[String], io: &mut Io) -> i32 {
io.out(" { \"regions\": [ { \"id\": \"exploded-plate\", \"kind\": \"plate\", \"grid\": \"E0:H4\", \"note\": \"exploded carburetor drawing\" }, { \"id\": \"masthead\", \"kind\": \"chrome\", \"grid\": \"A0:J0\", \"note\": \"navy bar\" } ] }\n");
io.out(" kind: plate | image | texture (painted material: every illustration, photograph, figure, product object, texture; each ships as a raster plate) or text | control | chrome (code draws it). grid: <colrow>:<colrow>, A0 top-left to J9 bottom-right, inclusive.\n");
io.out(" A texture region is a clean sample cell of the material (ground with no ink on it), not the whole band it covers; the page tiles it. Ink that sits on the material gets its own text/control region.\n");
io.out(" For exact edges, replace grid with pixelBox: {\"x\": <left>, \"y\": <top>, \"w\": <width>, \"h\": <height>} in original comp pixels, or box with fractions 0..1. Grid cells are approximate; an asset crop must not include the next section. comp-spec --help lists the command forms.\n");
return 0;
}
@@ -1113,6 +1129,28 @@ mod reference_tests {
(comp, region)
}
#[test]
fn exact_pixel_box_excludes_the_neighbouring_section() {
let mut comp = r::create_image(301, 101, [20, 70, 110, 255]);
r::fill_rect(&mut comp, 0., 61., 301., 40., [240., 180., 10., 255.]);
let input = json!({"allowUncovered":true,"regions":[{"id":"photo","kind":"image",
"note":"Wide photograph","bleed":true,"pixelBox":{"x":0,"y":0,"w":301,"h":61}}]});
let spec = measure_regions(&comp, &input, "comp.png").unwrap();
assert_eq!(spec["regions"][0]["px"], json!({"x":0,"y":0,"w":301,"h":61}));
let reference = prepare_plate_reference(&comp, &spec, &spec["regions"][0]);
assert_eq!((reference.image.width, reference.image.height), (301, 61));
assert!(reference.image.data.chunks_exact(4).all(|px| px == [20,70,110,255]));
for bad in [json!({"x":0,"y":0,"w":302,"h":61}), json!({"x":0.5,"y":0,"w":300,"h":61}),
json!({"x":0,"y":0,"w":0,"h":61}), json!({"x":0,"y":0,"w":301})] {
let mut broken = input.clone();
broken["regions"][0]["pixelBox"] = bad;
assert!(measure_regions(&comp, &broken, "comp.png").unwrap_err().contains("pixelBox"));
}
let mut ambiguous = input;
ambiguous["regions"][0]["grid"] = json!("A0:J5");
assert!(measure_regions(&comp, &ambiguous, "comp.png").unwrap_err().contains("multiple"));
}
#[test]
fn container_background_preserves_art_but_foreground_control_still_masks() {
let (comp, region) = fixture();
+3
View File
@@ -450,11 +450,14 @@ retain their local-development trust behavior. See [bundle signing](BUNDLE-SIGNI
Ported from the former `skill/scripts/{comp-spec,comp-diff,font-match,build-phase}.mjs` (+ `lib/{png,raster,image-metrics,font-fingerprint,font-index,hero-checks}.mjs`) into the engine; invoked as `{{scripts_path}}/impeccable <verb>`. All four resolve paths against the process cwd. Printed commands spell the launcher via `IMPECCABLE_SELF` (default `impeccable`), so they name `{{scripts_path}}/impeccable <verb>`, never `node …mjs`. ISO `createdAt`/`startedAt` timestamps in stdout and written JSON are the only run-dependent output.
- **`comp-spec`** — turns an approved comp into a measured build spec. `--comp <png> --grid` writes `.impeccable/build/comp-grid.png` (10x10 labeled grid) and prints PALETTE/BANDS/NEXT; `--comp <png> --regions <json>` measures regions into `.impeccable/build/spec.json` (region box, sampled palette, medium, aspect, detail energy, plate path for raster kinds) and prints the spec; `--comp <png> --auto` derives band regions; `--print` prints the compact spec; `--crop <id> [--out f] [--scale n] [--raw]` writes a reference crop; `--plate-prompt <id>` prints the regeneration prompt. `--spec <path>` overrides the spec path (default `.impeccable/build/spec.json`). Validation refusals (stderr, exit 1) are the JS strings verbatim: a region with no id / duplicate id / no note, a code-kind region whose note names painted material, a code region over 25% of the comp, a grid span that is not `<colrow>:<colrow>`, uncovered ink cells without `allowUncovered`. spec.json is byte-identical to the JS output.
Region inputs support three coordinate representations: inclusive `grid` cells, normalized `box: {x,y,w,h}`, or `pixelBox: {x,y,w,h}` in whole original-comp pixels. Pixel boxes cannot be combined with the other formats and must be positive-sized and contained in the comp; they avoid snapping an asset boundary to a neighbouring grid cell. Foreground UI excluded from a plate reference is excluded at the same aligned coordinates from the candidate during scoring; unmasked asset bytes still undergo provenance checks.
- **`comp-diff`** — `--comp <png> --build <png> [--spec spec.json] [--out-dir dir] [--align top|stretch|cover] [--label name] [--threshold t] [--json] [--no-files]`. Scores structure / color / detail / bands and per-region verdicts (`match`/`drift`/`missing`/`contradicted`); writes `side-by-side.png`, `heatmap.png`, `regions/<id>.png`, and `report.json` under `--out-dir` (unless `--no-files`); prints the text summary or, with `--json`, the report. Exit 0 measured, 1 usage/unreadable input, 3 below `--threshold`. The JSON report and text summary are byte-identical to the JS.
- **`font-match`** — `--measure <text-region-id>` fingerprints the comp crop of a text region (cap height, width/weight class, shape vector), records it on the region's `type` block in the spec, and prints the MEASURE line (pure; byte-identical to the JS). `--rank <id> [--candidates "Family:700,…"] [--text "…"] [--transform …] [--category …]` additionally renders candidate faces in a headless browser and ranks them by fingerprint distance, writing a stamped `chosen` face onto the region and a proof sheet under `.impeccable/build/font-match/`. **Browser**: an installed Chrome discovered and driven over CDP (the same browser the URL engine uses; the JS used Playwright/Puppeteer). With no browser resolvable, the catalog's nearest face is recorded (source `catalog`, estimated size) or, with no catalog either, the MEASURE line stands — matching the JS fallbacks; the sha1 `chosen` stamp is byte-identical. Screenshots vary by Chrome version, so the rendered ranking is not byte-stable.
- **`font-index` catalog (paid moat)** — resolved at run time, never committed to the engine repo: `IMPECCABLE_CATALOG_DIR/font-index.json` first, then the skill's shipped `IMPECCABLE_SKILL_DIR/scripts/data/font-index.json`; absent → the built-in per-width shortlist stands in (the JS degraded path).
- **`build-phase`** — the comp-led build state machine at `.impeccable/build/state.json`. `start --comp <png> | --direction <key>` (opens the phases; reads comp dimensions for the breakpoint), `status [--json]`, `advance [--force --reason "…"]` (runs the current phase's gate; exit 2 on gate failure, state unchanged), `record hero --build <png>`, `scaffold`, `note "<text>"`, `finish --disposition ship|fix|rebuild|recapture`. Phases and gates (`comps`, `spec`, `plates`, `hero`, `sections`, `motion`, `responsive`, `review`) are unchanged from the JS; the hero/responsive gates call comp-diff in-process (the JS spawned it). The organic-clip-path CSS scan is the engine's own rule (`organic-clip-path`), injected into the gate; `--force` is refused unless `--reason` quotes the user downgrading the comp (the JS `forceAllowed` logic verbatim).
`build-phase check-plate <region-id> --candidate <png> [--json]` runs the normal plate gate on a separate candidate, retaining the full spec for reference exclusions. Exit 0 means pass, 2 means gate failure, 1 means invalid input. It does not write files, select an asset, record an approval, or advance build state. JSON includes `ok`, `region`, `candidate`, `reasons`, `plates`, and `stateChanged: false`.
---
## 2. Context and utility verbs
+11
View File
@@ -180,3 +180,14 @@ only neighbouring pixels leaves the measured crop identical.
## Recorded 2026-09-17: reference-bound typography reuse
- `comp-spec-regions`: the written spec adds `compSha256`, a SHA-256 of decoded dimensions and pixels. This binds retained typography to the exact reference when regions are remeasured. Structured comparison verified that only this field changed; stdout, stderr, exit status, regions, palettes and bounds are identical. The failing/passing regression separately verifies preservation and invalidation.
## Recorded 2026-09-17: exact reference bounds and non-destructive plate candidates
Reviewed the three CLI differences before updating their goldens:
`comp-spec-grid` appends coordinate guidance, `comp-spec-usage` explains grid,
normalized box and exact pixelBox units, and `build-phase-usage` advertises the
read-only candidate check. Only those stdout strings changed. Existing image
files, measurements, exit codes and frozen function vectors were not replaced.
The plate gate applies reference UI exclusions symmetrically after alignment;
regressions separately verify hidden-pixel invariance, visible missing-art
rejection, raw comp-copy rejection and unchanged candidate-check state.
+1 -1
View File
@@ -1,5 +1,5 @@
{
"stdout": "",
"stdout": "CANDIDATE CHECK: build-phase check-plate <region-id> --candidate <png> [--json] validates a separate file with the normal plate gate; never replaces the selected asset, records approval, or advances the phase.\n",
"stderr": "usage: build-phase.mjs start --comp <png> [--breakpoint WxH] [--artifact <entry file>] [--session-id <id>] | status [--json] | completion [--session-id <id>] | advance [--force --reason \"...\"] | record hero --build <png> | scaffold | note \"<text>\" | finish --disposition <word>\n",
"exit": 1,
"signal": null,
+1 -1
View File
@@ -1,5 +1,5 @@
{
"stdout": "GRID .impeccable/build/comp-grid.png (768x512 comp; cells A0 top-left to J9 bottom-right)\nPALETTE #f7e8e8(69%) #182838(14%) #9a9aa5(7%) #c5c5d1(7%) #7c7c82(4%)\nBANDS 6% 9% 14% 18% 20% 24% 47% 51% 54% 58% 60% 64% 66% 69% 72% 75% 78% 81% 84% 88% 91% 98%\nNEXT open the grid image, then write regions.json in exactly this shape and run --regions regions.json:\n { \"regions\": [ { \"id\": \"exploded-plate\", \"kind\": \"plate\", \"grid\": \"E0:H4\", \"note\": \"exploded carburetor drawing\" }, { \"id\": \"masthead\", \"kind\": \"chrome\", \"grid\": \"A0:J0\", \"note\": \"navy bar\" } ] }\n kind: plate | image | texture (painted material: every illustration, photograph, figure, product object, texture; each ships as a raster plate) or text | control | chrome (code draws it). grid: <colrow>:<colrow>, A0 top-left to J9 bottom-right, inclusive.\n A texture region is a clean sample cell of the material (ground with no ink on it), not the whole band it covers; the page tiles it. Ink that sits on the material gets its own text/control region.\n",
"stdout": "GRID .impeccable/build/comp-grid.png (768x512 comp; cells A0 top-left to J9 bottom-right)\nPALETTE #f7e8e8(69%) #182838(14%) #9a9aa5(7%) #c5c5d1(7%) #7c7c82(4%)\nBANDS 6% 9% 14% 18% 20% 24% 47% 51% 54% 58% 60% 64% 66% 69% 72% 75% 78% 81% 84% 88% 91% 98%\nNEXT open the grid image, then write regions.json in exactly this shape and run --regions regions.json:\n { \"regions\": [ { \"id\": \"exploded-plate\", \"kind\": \"plate\", \"grid\": \"E0:H4\", \"note\": \"exploded carburetor drawing\" }, { \"id\": \"masthead\", \"kind\": \"chrome\", \"grid\": \"A0:J0\", \"note\": \"navy bar\" } ] }\n kind: plate | image | texture (painted material: every illustration, photograph, figure, product object, texture; each ships as a raster plate) or text | control | chrome (code draws it). grid: <colrow>:<colrow>, A0 top-left to J9 bottom-right, inclusive.\n A texture region is a clean sample cell of the material (ground with no ink on it), not the whole band it covers; the page tiles it. Ink that sits on the material gets its own text/control region.\n For exact edges, replace grid with pixelBox: {\"x\": <left>, \"y\": <top>, \"w\": <width>, \"h\": <height>} in original comp pixels, or box with fractions 0..1. Grid cells are approximate; an asset crop must not include the next section. comp-spec --help lists the command forms.\n",
"stderr": "",
"exit": 0,
"signal": null,
+1 -1
View File
@@ -1,5 +1,5 @@
{
"stdout": "usage: comp-spec.mjs --comp <png> --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp <png> --regions <json> measure regions -> .impeccable/build/spec.json\n regions json: { \"regions\": [ { \"id\": \"art\", \"kind\": \"plate|image|texture|text|control|chrome\", \"grid\": \"E0:J4\", \"note\": \"...\" } ] }\n comp-spec.mjs --comp <png> --auto band regions when you have no regions file\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop <id> [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt <id> [--background transparent|opaque|auto] the regeneration prompt for a raster region\n",
"stdout": "REGION COORDINATES: use one of grid (coarse inclusive cells), box {x,y,w,h} (fractions of the comp, 0..1), or pixelBox {x,y,w,h} (whole pixels in the original comp). Use exact bounds when an element ends inside a grid cell; do not include neighbouring content.\nusage: comp-spec.mjs --comp <png> --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp <png> --regions <json> measure regions -> .impeccable/build/spec.json\n regions json: { \"regions\": [ { \"id\": \"art\", \"kind\": \"plate|image|texture|text|control|chrome\", \"grid\": \"E0:J4\", \"note\": \"...\" } ] }\n comp-spec.mjs --comp <png> --auto band regions when you have no regions file\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop <id> [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt <id> [--background transparent|opaque|auto] the regeneration prompt for a raster region\n",
"stderr": "",
"exit": 0,
"signal": null,