mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
Make region mapping discoverable and validate repeated review groups
AI-assisted implementation by Codex under maintainer direction. Preserve map relationships through measurement and share structural review-group validation between inspection and component review.
This commit is contained in:
@@ -549,6 +549,9 @@ pub fn measure_regions(comp: &Image, regions_input: &Value, comp_path: &str) ->
|
||||
if raw.get("snap").and_then(Value::as_bool) == Some(false) {
|
||||
obj.insert("snap".into(), json!(false));
|
||||
}
|
||||
for key in ["parentId", "reviewGroup"] {
|
||||
if let Some(value) = raw.get(key) { obj.insert(key.into(), value.clone()); }
|
||||
}
|
||||
if let Some(cb) = cover_box {
|
||||
obj.insert("coverBox".into(), box_json(cb));
|
||||
}
|
||||
@@ -908,7 +911,12 @@ pub fn region_source_issue(io: &Io, spec: &Value) -> Option<String> {
|
||||
|
||||
pub fn run(argv: &[String], io: &mut Io) -> i32 {
|
||||
let spec_path = arg_or(argv, "spec", SPEC_PATH).to_string();
|
||||
if flag(argv, "schema") {
|
||||
io.out(include_str!("region-map.schema.json"));
|
||||
return 0;
|
||||
}
|
||||
if flag(argv, "help") || argv.is_empty() {
|
||||
io.out("MAP WORKFLOW: open --grid, author regions.json, run --regions regions.json --inspect-map, inspect its crops, then correct the map. Stop here for a mapping-only task.\nSCHEMA: comp-spec --schema lists required fields, coordinates, parentId and reviewGroup. Default inspection output is concise; --json prints the full report.\n");
|
||||
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 [--out f] write a band draft; refine into elements before --regions\n comp-spec.mjs --comp <png> --regions <json> --inspect-map [--out-dir dir] [--json] inspect all crops and masks without writing a spec\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;
|
||||
@@ -1105,6 +1113,11 @@ pub fn run(argv: &[String], io: &mut Io) -> i32 {
|
||||
io.err("comp-spec: this is an automatic draft, not a measured element map; refine its bands into the visible elements before removing the draft flag\n");
|
||||
return 1;
|
||||
}
|
||||
let group_issues = impeccable_comp::review_groups::issues(regions_input["regions"].as_array().unwrap_or(&Vec::new()));
|
||||
if !group_issues.is_empty() {
|
||||
for (id, message) in group_issues { io.err(&format!("comp-spec: region {id}: {message}\n")); }
|
||||
return 1;
|
||||
}
|
||||
let mut spec = match measure_regions(&comp, ®ions_input, comp_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
@@ -1157,6 +1170,17 @@ fn preserve_typography(spec: &mut Value, previous: &Value) {
|
||||
#[cfg(test)]
|
||||
mod reference_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn mapping_schema_is_available_without_a_comp_or_workspace() {
|
||||
let schema: Value = serde_json::from_str(include_str!("region-map.schema.json")).unwrap();
|
||||
assert_eq!(schema["properties"]["regions"]["items"]["required"], json!(["id","kind","note"]));
|
||||
assert_eq!(schema["properties"]["regions"]["items"]["oneOf"].as_array().unwrap().len(),3);
|
||||
let mut io = Io::stdio();
|
||||
io.cwd = std::path::PathBuf::from("/nonexistent/map-schema-test");
|
||||
io.stdout = Box::new(Vec::<u8>::new());
|
||||
assert_eq!(run(&["--schema".into()], &mut io),0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn automatic_snap_preserves_separated_navigation_and_multiline_copy() {
|
||||
|
||||
@@ -163,6 +163,14 @@ fn inspect(comp: &Image, input: &Value, comp_path: &str) -> Value {
|
||||
)
|
||||
.expect("individually validated regions");
|
||||
spec["regions"] = json!(measured);
|
||||
let group_issues = impeccable_comp::review_groups::issues(&measured);
|
||||
let invalid_groups: HashSet<String> = group_issues.iter().filter_map(|(id, _)| {
|
||||
measured.iter().find(|r| r["id"] == *id).and_then(|r| r["reviewGroup"].as_str()).map(String::from)
|
||||
}).collect();
|
||||
for (id, message) in group_issues {
|
||||
let code = if measured.iter().any(|r| r["id"] == id && r["kind"].as_str().is_some_and(is_raster_kind)) { "raster-group" } else { "invalid-group" };
|
||||
issue(&mut issues, "error", code, Some(&id), message);
|
||||
}
|
||||
let mut groups: serde_json::Map<String, Value> = serde_json::Map::new();
|
||||
for region in &mut measured {
|
||||
let id = region["id"].as_str().unwrap().to_string();
|
||||
@@ -195,23 +203,13 @@ fn inspect(comp: &Image, input: &Value, comp_path: &str) -> Value {
|
||||
}
|
||||
}
|
||||
if let Some(group) = region.get("reviewGroup") {
|
||||
if is_raster_kind(region["kind"].as_str().unwrap()) {
|
||||
issue(&mut issues,"warning","raster-group",Some(&id),"Raster assets require individual review; this group does not combine their decisions.".into());
|
||||
} else if let Some(name) = group.as_str().filter(|n| !n.trim().is_empty()) {
|
||||
if let Some(name) = group.as_str().filter(|n| !n.trim().is_empty() && !invalid_groups.contains(*n)) {
|
||||
groups
|
||||
.entry(name)
|
||||
.or_insert(json!([]))
|
||||
.as_array_mut()
|
||||
.unwrap()
|
||||
.push(json!(id));
|
||||
} else {
|
||||
issue(
|
||||
&mut issues,
|
||||
"error",
|
||||
"invalid-group",
|
||||
Some(&id),
|
||||
"reviewGroup must be a nonempty name.".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if is_raster_kind(region["kind"].as_str().unwrap()) {
|
||||
@@ -421,6 +419,21 @@ mod tests {
|
||||
use super::*;
|
||||
use impeccable_comp::raster::create_image;
|
||||
|
||||
#[test]
|
||||
fn mixed_review_groups_are_reported_without_discarding_geometry() {
|
||||
let comp = Image { width: 100, height: 100, data: vec![255; 100*100*4] };
|
||||
let input = json!({"regions":[
|
||||
{"id":"frame","kind":"chrome","container":true,"reviewGroup":"cards","pixelBox":{"x":0,"y":0,"w":20,"h":20},"note":"Card border"},
|
||||
{"id":"label","kind":"text","reviewGroup":"cards","pixelBox":{"x":2,"y":2,"w":10,"h":5},"note":"Card label"}
|
||||
]});
|
||||
let report = inspect(&comp, &input, "comp.png");
|
||||
assert_eq!(report["regions"].as_array().unwrap().len(),2);
|
||||
assert!(report["issues"].as_array().unwrap().iter().any(|i| i["code"]=="invalid-group"));
|
||||
assert!(report["reviewGroups"].get("cards").is_none());
|
||||
let measured = comp_spec::measure_regions(&comp, &input, "comp.png").unwrap();
|
||||
assert_eq!(measured["regions"][1]["reviewGroup"], "cards");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_inspection_writes_only_new_reference_artifacts_and_escapes_labels() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Impeccable region map",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"regions"
|
||||
],
|
||||
"properties": {
|
||||
"regions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"kind",
|
||||
"note"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Stable unique identity retained through asset production and review."
|
||||
},
|
||||
"kind": {
|
||||
"enum": [
|
||||
"plate",
|
||||
"image",
|
||||
"texture",
|
||||
"text",
|
||||
"control",
|
||||
"chrome"
|
||||
]
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"minLength": 8,
|
||||
"description": "The actual subject, material and role visible at these bounds."
|
||||
},
|
||||
"pixelBox": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"w",
|
||||
"h"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"y": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"w": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"h": {
|
||||
"type": "integer",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"description": "Whole pixels in the original comp. Must fit within its dimensions; inspect the crop."
|
||||
},
|
||||
"box": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"x",
|
||||
"y",
|
||||
"w",
|
||||
"h"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"x": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"y": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"w": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
},
|
||||
"h": {
|
||||
"type": "number",
|
||||
"exclusiveMinimum": 0
|
||||
}
|
||||
},
|
||||
"description": "Fractions of the full comp in 0..1; x+w and y+h must not exceed 1."
|
||||
},
|
||||
"grid": {
|
||||
"type": "string",
|
||||
"description": "Inclusive grid cells A0 through J9, e.g. A0:B1. Coarse; replace with exact bounds when needed."
|
||||
},
|
||||
"snap": {
|
||||
"type": "boolean",
|
||||
"description": "Grid text/control ink snapping; false retains the entire grid span. Exact boxes do not snap."
|
||||
},
|
||||
"container": {
|
||||
"type": "boolean",
|
||||
"description": "A code region enclosing separate children. Does not replace their inventory."
|
||||
},
|
||||
"parentId": {
|
||||
"type": "string",
|
||||
"description": "Existing distinct enclosing container ID. Containment only, never approval inheritance."
|
||||
},
|
||||
"reviewGroup": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 120,
|
||||
"description": "Repeated instances of the same code component and role. Same kind and container status; peers only. Never group raster assets, ancestors with children, or different parts of one component."
|
||||
},
|
||||
"bleed": {
|
||||
"type": "boolean",
|
||||
"description": "Only when the comp intentionally clips this artwork."
|
||||
},
|
||||
"codeDrawn": {
|
||||
"type": "boolean",
|
||||
"description": "Only when code truly draws this region; never a workaround for painted artwork."
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": [
|
||||
"pixelBox"
|
||||
],
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"box"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"grid"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"box"
|
||||
],
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"pixelBox"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"grid"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"grid"
|
||||
],
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{
|
||||
"required": [
|
||||
"pixelBox"
|
||||
]
|
||||
},
|
||||
{
|
||||
"required": [
|
||||
"box"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"draft": {
|
||||
"const": false,
|
||||
"description": "Automatic band drafts are not authored element maps."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,3 +52,5 @@ pub fn crc32_f32(data: &[f32]) -> u32 {
|
||||
}
|
||||
crc32(&bytes)
|
||||
}
|
||||
|
||||
pub mod review_groups;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Structural constraints shared by map inspection and component review.
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
pub fn issues(regions: &[Value]) -> Vec<(String, String)> {
|
||||
let mut issues = Vec::new();
|
||||
let mut groups: BTreeMap<&str, Vec<&Value>> = BTreeMap::new();
|
||||
for region in regions {
|
||||
let Some(group) = region.get("reviewGroup") else { continue; };
|
||||
let id = region["id"].as_str().unwrap_or("");
|
||||
let Some(name) = group.as_str().filter(|s| !s.trim().is_empty() && s.len() <= 120) else {
|
||||
issues.push((id.into(), "reviewGroup needs a nonempty name of at most 120 bytes".into()));
|
||||
continue;
|
||||
};
|
||||
groups.entry(name).or_default().push(region);
|
||||
}
|
||||
for (name, members) in groups {
|
||||
let signature = |r: &Value| (r["kind"].as_str().unwrap_or("").to_string(), r["container"] == true);
|
||||
let first = signature(members[0]);
|
||||
let mixed = members.iter().any(|r| signature(r) != first);
|
||||
for region in &members {
|
||||
let id = region["id"].as_str().unwrap_or("");
|
||||
let message = if !matches!(region["kind"].as_str(), Some("text" | "control" | "chrome")) {
|
||||
Some("Review groups are for repeated code components; raster assets remain individual".to_string())
|
||||
} else if mixed {
|
||||
Some(format!("Review group {name} mixes region kinds or containers with their contents; group only repeated instances of the same code component"))
|
||||
} else {
|
||||
let mut parent = region["parentId"].as_str();
|
||||
let mut visited = HashSet::new();
|
||||
let mut nested = false;
|
||||
while let Some(p) = parent {
|
||||
if !visited.insert(p) { break; }
|
||||
if members.iter().any(|r| r["id"] == p) { nested = true; break; }
|
||||
parent = regions.iter().find(|r| r["id"] == p).and_then(|r| r["parentId"].as_str());
|
||||
}
|
||||
nested.then(|| format!("Review group {name} includes an ancestor and its child; group peers, not nested components"))
|
||||
};
|
||||
if let Some(message) = message { issues.push((id.into(), message)); }
|
||||
}
|
||||
}
|
||||
issues
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
#[test]
|
||||
fn grouped_peers_keep_identity_but_mixed_or_nested_components_are_refused() {
|
||||
let peers = json!([
|
||||
{"id":"a","kind":"text","reviewGroup":"labels"},
|
||||
{"id":"b","kind":"text","reviewGroup":"labels"}
|
||||
]);
|
||||
assert!(issues(peers.as_array().unwrap()).is_empty());
|
||||
for change in [json!({"kind":"image"}),json!({"kind":"chrome","container":true}),json!({"parentId":"a"})] {
|
||||
let mut bad = peers.clone();
|
||||
for (key, value) in change.as_object().unwrap() { bad[1][key] = value.clone(); }
|
||||
assert!(!issues(bad.as_array().unwrap()).is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,13 +113,15 @@ pub fn freeze(project: &Path, input: &Value) -> Result<(Value, BTreeMap<String,
|
||||
}
|
||||
let mut files = BTreeMap::new();
|
||||
let mut comp_files = BTreeMap::new();
|
||||
let mut measured_regions = Vec::new();
|
||||
// The component inventory is measured against this spec. Bind it centrally
|
||||
// rather than requiring every component author to repeat this dependency.
|
||||
if input["stage"] == "components" {
|
||||
let spec_path = ".impeccable/build/spec.json";
|
||||
comp_files.insert(spec_path.into(), pin(project, spec_path, &mut files)?);
|
||||
let spec: Value = serde_json::from_slice(&files[spec_path]).map_err(|e| e.to_string())?;
|
||||
for region in spec["regions"].as_array().ok_or("measured spec needs regions")? {
|
||||
measured_regions = spec["regions"].as_array().ok_or("measured spec needs regions")?.clone();
|
||||
for region in &measured_regions {
|
||||
let component = input["components"].as_array().and_then(|items| items.iter().find(|c| c["id"] == region["id"]))
|
||||
.ok_or_else(|| format!("component review omitted measured region {}", region["id"]))?;
|
||||
if matches!(region["kind"].as_str(), Some("text" | "control")) && component["preview"]["kind"] != "page" {
|
||||
@@ -161,6 +163,16 @@ pub fn freeze(project: &Path, input: &Value) -> Result<(Value, BTreeMap<String,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate the actual review request against measured region roles, including
|
||||
// groups introduced or renamed after map authoring. Do not infer roles from labels.
|
||||
for region in &mut measured_regions {
|
||||
region.as_object_mut().ok_or("measured region must be an object")?.remove("reviewGroup");
|
||||
if let Some(group) = input["components"].as_array().and_then(|cs| cs.iter().find(|c| c["id"] == region["id"]))
|
||||
.and_then(|c| c.get("reviewGroup")) { region["reviewGroup"] = group.clone(); }
|
||||
}
|
||||
if let Some((id, message)) = impeccable_comp::review_groups::issues(&measured_regions).first() {
|
||||
return Err(format!("component {id}: {message}"));
|
||||
}
|
||||
let mut ids = BTreeSet::new();
|
||||
let components = packet["components"]
|
||||
.as_array_mut()
|
||||
|
||||
@@ -691,3 +691,21 @@ fn invalid_component_geometry_names_the_component_and_bounds() {
|
||||
let error = manifest::freeze(&f.project, &input).unwrap_err();
|
||||
assert!(error.contains("duplicate component id") && error.contains("art"), "{error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_review_refuses_groups_that_mix_measured_roles() {
|
||||
let f = Fixture::new();
|
||||
fs::create_dir_all(f.project.join(".impeccable/build")).unwrap();
|
||||
let mut input = f.manifest();
|
||||
input["stage"] = json!("components");
|
||||
input["components"][1]["reviewGroup"] = json!("peers");
|
||||
let mut peer = input["components"][1].clone(); peer["id"] = json!("peer");
|
||||
input["components"].as_array_mut().unwrap().push(peer);
|
||||
let mut spec = json!({"regions":[{"id":"control","kind":"control"},{"id":"peer","kind":"text"}]});
|
||||
let path = f.project.join(".impeccable/build/spec.json");
|
||||
fs::write(&path, spec.to_string()).unwrap();
|
||||
assert!(manifest::freeze(&f.project,&input).unwrap_err().contains("mixes region kinds"));
|
||||
spec["regions"][1]["kind"] = json!("control");
|
||||
fs::write(&path, spec.to_string()).unwrap();
|
||||
assert_eq!(manifest::freeze(&f.project,&input).unwrap().0["components"].as_array().unwrap().len(),3);
|
||||
}
|
||||
|
||||
@@ -449,7 +449,7 @@ 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 [--out <draft.json>]` writes approximate bands to a new draft file (default `.impeccable/build/regions.draft.json`), without modifying the measured spec or build state; `--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.
|
||||
- **`comp-spec`** — turns an approved comp into a measured build spec. `--schema` prints the region-map JSON schema without reading or writing a project. `--comp <png> --regions <json> --inspect-map` writes reference-only crops, overlay and report in a new directory; exit 2 means diagnosed hard errors, exit 1 means command/input failure. Mapping metadata (`parentId`, `reviewGroup`) survives measurement. Review groups must contain peer code regions of one kind and container status; mixed groups and grouped raster assets are refused both at measurement and at component-review preparation. `--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 [--out <draft.json>]` writes approximate bands to a new draft file (default `.impeccable/build/regions.draft.json`), without modifying the measured spec or build state; `--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.
|
||||
Automatic drafts require decomposition into actual visible elements before measurement; a draft flag blocks accidental submission, and existing draft files are never overwritten. Successful `--regions` measurements record the source path and SHA-256. Spec and plate gates reject changed or missing source files, so a failed region edit cannot silently reuse the previous measurements. Legacy specs without source metadata remain readable.
|
||||
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.
|
||||
|
||||
@@ -6,7 +6,7 @@ Use this checkpoint on comp-led builds after producing the initial component kit
|
||||
|
||||
Keep the measured spec's region IDs. Include every visible region: produced raster assets and working HTML/CSS/SVG for text, controls, patterns, decoration and layout elements. A region rendered in code needs an actual review document, not a promise to implement it later. Use semantic HTML for content and controls. Do not flatten the page or combine unrelated regions to avoid review. Report omitted regions so the user can mark what is missing.
|
||||
|
||||
For a repeated code pattern, give its instances the same `reviewGroup` name. Keep every instance and its region ID in the manifest, in the same kit document. The user can inspect instances and explicitly apply one decision to the unreviewed group. Unique raster assets still require their own review; grouping never removes inventory or gate checks.
|
||||
For a repeated code pattern, give instances of the same component and role the same `reviewGroup` name. Group peers of the same kind, not a container with its contents or unrelated text roles. Keep every instance and its region ID in the manifest, in the same kit document. The user can inspect instances and explicitly apply one decision to the unreviewed group. Unique raster assets still require their own review; grouping never removes inventory or gate checks.
|
||||
|
||||
Before producing assets, inspect each reference crop against its named subject. Coarse grid cells and automatic ink snapping can include neighbors or omit parts of a compound element. Correct the measured region with an explicit normalized `box`; do not build to a known bad crop. Check the code preview contains the complete component before presenting it. The capture tool refuses content cut off by the review crop.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Use this flow for a new surface or a replacement visual identity. PRODUCT.md owns product truth. DESIGN.md owns durable visual decisions. A surface brief keeps strategy that belongs to one route or artifact. Complete [init.md](init.md) first when PRODUCT.md is missing; a missing DESIGN.md does not route back to init.
|
||||
|
||||
For a chosen comp’s region map, see [region-map.md](region-map.md) for the command sequence and schema.
|
||||
|
||||
## 1. Decide what is already true
|
||||
|
||||
Read DESIGN.md, representative code, tokens, components, and assets.
|
||||
@@ -108,7 +110,7 @@ Then, in order, each closed by `{{scripts_path}}/impeccable build-phase advance`
|
||||
The comp-led path is a frontier-tier job: it asks the builder to hold a measured layout, place plates at their boxes, and act on numeric readings across a dozen attempts. Smaller or faster models produce a recognisable page and stall under the hero gate; if the model in hand is one of those, say so before the direction round and take the code-led path, or expect the run to end at the hero with its readings unmet.
|
||||
|
||||
1. **spec.** Measure the comp: `impeccable comp-spec --comp <comp> --grid` writes a coordinate grid over the comp; open it, name every salient region by grid span in a regions file (text and control regions narrow to an ink cluster only when it retains at least 95% of the span’s contrasting pixels; inspect the resulting crop, especially for compound controls and multiline text; `snap: false` keeps the span, and an explicit `box` is taken as drawn) (kind `plate` / `image` / `texture` for anything painted: every illustration, photograph, figure, product object, and material texture; `text` / `control` / `chrome` for what code draws; every region carries a `note` saying what the comp shows there, which the plate prompt and the gate messages read), and run `impeccable comp-spec --comp <comp> --regions <file>`. The spec carries each region's box, sampled palette, and medium; `impeccable comp-spec --print` is the build's reference from here on. Type is measured, not guessed: `impeccable font-match --measure <text region>` reads the comp's cap height, width class, and weight off the pixels, and `impeccable font-match --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 drawn is a plate: an inline SVG past an icon's budget (a diagram, notation, leader lines with arrows, a "quick approximation" of the artwork) is refused at the hero; icon-sized SVG (under 64px, a few paths) is fine, and a chart the page draws from data at runtime is a chart, not an illustration. Callout lines and arrows that annotate a drawing belong to that drawing's plate, with only their labels set as text. A crop of the comp is never a plate (the plates gate refuses a file that is a resample of the comp region: the comp's grain, its neighbours' edges, and its resolution would ship as the artwork); the crop is the reference the plate is generated from. A plate region's box has to hold its whole artwork with a margin: the spec measures the artwork's contact with the box edges and refuses a box that cuts through it (`bleed: true` only when the page really crops it there), because a plate placed with `object-fit: cover` on such a box shows the artwork minus the side the box lost. 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), icon glyphs (close enough, exact if the user chose an icon library; this covers the pictogram only, never a control's chrome, so a chevron, an arrow, a dropdown's border and fill, a button's shape are the comp's), and genuine defects in the comp such as spelling errors. <!-- rule:skill-comp-spec -->
|
||||
Inspect the proposed map before producing assets: `impeccable comp-spec --comp <comp> --regions <file> --inspect-map` writes a numbered overlay, exact crops, foreground-mask previews, and a consolidated report without changing the spec or build state. Inspect boundaries and excluded pixels together; an empty reference needs corrected geometry. Optional `parentId` names an enclosing `container` and `reviewGroup` identifies repeated code components for inspection; neither removes regions or approves assets. The report diagnoses geometry, not semantic completeness.
|
||||
Inspect the proposed map before producing assets: `impeccable comp-spec --comp <comp> --regions <file> --inspect-map` writes a numbered overlay, exact crops, foreground-mask previews, and a consolidated report without changing the spec or build state. Inspect boundaries and excluded pixels together; an empty reference needs corrected geometry. `parentId` names an enclosing `container`; `reviewGroup` names repeated instances of the same code component and role, not all parts of one component. Neither removes regions or approves assets. The report diagnoses geometry, not semantic completeness.
|
||||
|
||||
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 (isolated ink, figures, or objects use native transparent PNG so they sit on the page's own ground; photos and textures stay opaque); a texture (paper, cloth, grain) is a clean patch of the comp region mirror-tiled to size, generated only when no clean patch exists. `impeccable comp-spec --crop <id>` writes the reference; save `impeccable comp-spec --plate-prompt <id> --background transparent` to a prompt file for a cutout, or use `--background opaque` otherwise. Prefer the harness-native image tool with that crop and prompt, then `impeccable embed-prompt <plate> --prompt-file <prompt.txt>`. The API fallback is `impeccable generate-image --ref <crop.png> --prompt-file <prompt.txt> --out <plate.png> --size <WxH> --quality high --background transparent` (use `--background opaque` for full-frame assets); create the output directory first. Verify actual alpha, white foregrounds, fine edges, and clear holes on light and dark grounds; do not chroma-key native output. After the initial assets exist, prepare the isolated code component previews and complete [component-review.md](component-review.md) before further gate-driven repair: the user reviews the whole component kit, including code, before page assembly. This checkpoint keeps the existing gates; it does not require passing them first. After the full page and responsive checks, use the assembled-hero checkpoint before the final response. <!-- rule:skill-human-component-review --> The plates gate scores the assets against the comp; also inspect placement and scale visually. 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 -->
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Region map
|
||||
|
||||
A region map names what is actually visible in the approved comp before asset production. It is not a page build or an asset approval.
|
||||
|
||||
1. Run `{{scripts_path}}/impeccable comp-spec --comp <comp.png> --grid` and open the original and gridded images.
|
||||
2. Run `{{scripts_path}}/impeccable comp-spec --schema` for the JSON fields. Write `regions.json` with a `regions` array. Each region needs a stable `id`, `kind`, `note`, and exactly one of `pixelBox`, normalized `box`, or `grid`. Use the original comp’s dimensions.
|
||||
3. Run `{{scripts_path}}/impeccable comp-spec --comp <comp.png> --regions regions.json --inspect-map`. The output points to a report, overlay and exact crops. The default prints findings; `--json` prints the entire report.
|
||||
4. Open the crops and compare their bounds with the original. Inspect excluded foreground pixels as well as geometry errors. Correct the map and inspect again; use a new output directory each time. Coverage warnings are hints, not proof of completeness.
|
||||
|
||||
If the request ends at mapping, stop with the map, inspection report and unresolved findings. To continue a build, measure the inspected map with `comp-spec --comp <comp.png> --regions regions.json` and follow [new-work.md](new-work.md).
|
||||
|
||||
`--auto` produces horizontal band scaffolding, not element identification. It is optional and does not replace authoring a map.
|
||||
|
||||
## Containment and repetition
|
||||
|
||||
`parentId` identifies an enclosing `container: true` region. Parent and children keep separate IDs and crops. Containment never transfers approval.
|
||||
|
||||
`reviewGroup` identifies repeated instances of the same code component and role. Members must have the same kind and container status and be peers, not ancestors and children. Sharing a card or section is not a reason to group its different parts. Keep every instance in the map and component kit. Raster assets remain individually reviewable. Grouping does not waive crop checks or approve anything; applying a decision to peers remains the user’s choice.
|
||||
|
||||
Comp crops are reference evidence only, never production assets. The map inspector marks its PNGs as comp-derived.
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"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 [--out f] write a band draft; refine into elements before --regions\n comp-spec.mjs --comp <png> --regions <json> --inspect-map [--out-dir dir] [--json] inspect all crops and masks without writing a spec\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": "MAP WORKFLOW: open --grid, author regions.json, run --regions regions.json --inspect-map, inspect its crops, then correct the map. Stop here for a mapping-only task.\nSCHEMA: comp-spec --schema lists required fields, coordinates, parentId and reviewGroup. Default inspection output is concise; --json prints the full report.\nREGION 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 [--out f] write a band draft; refine into elements before --regions\n comp-spec.mjs --comp <png> --regions <json> --inspect-map [--out-dir dir] [--json] inspect all crops and masks without writing a spec\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,
|
||||
|
||||
Reference in New Issue
Block a user