diff --git a/crates/comp-verbs/src/build_phase.rs b/crates/comp-verbs/src/build_phase.rs index dbca89226..dac03c5d9 100644 --- a/crates/comp-verbs/src/build_phase.rs +++ b/crates/comp-verbs/src/build_phase.rs @@ -336,6 +336,8 @@ fn gate_spec(io: &Io, state: &Value) -> Gate { "no spec at {SPEC_PATH}: run comp-spec.mjs --comp {comp} --grid, name the regions, then --regions regions.json" )]); }; + if spec["draft"] == true { return Gate::fail(vec!["automatic region draft is not a measured element map; refine it with comp-spec --regions".into()]); } + if let Some(issue) = crate::comp_spec::region_source_issue(io,&spec) { return Gate::fail(vec![issue]); } let regions = spec_regions(&spec); if regions.is_empty() { return Gate::fail(vec!["spec has no regions".into()]); @@ -496,6 +498,7 @@ fn gate_plates(io: &Io) -> Gate { fn gate_plates_for(io: &Io, spec: &Value, only_id: Option<&str>) -> Gate { let s = self_cmd(io); + if let Some(issue) = crate::comp_spec::region_source_issue(io,spec) { return Gate::fail(vec![issue]); } let regions = spec_regions(&spec); let raster_regions: Vec = 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(); diff --git a/crates/comp-verbs/src/build_phase/integrity_tests.rs b/crates/comp-verbs/src/build_phase/integrity_tests.rs index 0709c761b..6307e248c 100644 --- a/crates/comp-verbs/src/build_phase/integrity_tests.rs +++ b/crates/comp-verbs/src/build_phase/integrity_tests.rs @@ -754,3 +754,51 @@ fn stripped_blurred_shifted_comp_pixels_cannot_pass_with_a_generation_prompt() { let texture_gate = gate_plates(&ws.io()); assert!(!texture_gate.reasons.iter().any(|r|r.contains("is the comp crop")),"{:?}",texture_gate.reasons); } + +#[test] +fn rejected_region_revision_cannot_advance_using_an_older_spec() { + let ws = Workspace::new(); + let image = r::create_image(64,64,[80,100,120,255]); + ws.write("comp.png", &png_io::encode_png(&image, &[]).unwrap()); + let input = json!({"regions":[{"id":"photo","kind":"image","bleed":true, + "pixelBox":{"x":0,"y":0,"w":64,"h":64},"note":"Full frame photograph"}]}); + ws.write("regions.json",input.to_string().as_bytes()); + let mut io=ws.io(); + let args=["--comp","comp.png","--regions","regions.json"].map(String::from); + assert_eq!(crate::comp_spec::run(&args,&mut io),0); + let state=json!({"comp":"comp.png"}); + assert!(gate_spec(&io,&state).ok); + let measured=std::fs::read(ws.path.join(SPEC_PATH)).unwrap(); + ws.write("regions.json",b"{bad json"); + assert_eq!(crate::comp_spec::run(&args,&mut io),1); + assert_eq!(std::fs::read(ws.path.join(SPEC_PATH)).unwrap(),measured); + let gate=gate_spec(&io,&state); + assert!(!gate.ok,"a rejected edit must not silently reuse the last measured map"); + assert!(gate.reasons.join(" ").contains("regions.json")); + assert!(gate_plates(&io).reasons.join(" ").contains("regions.json")); + ws.write("regions.json",input.to_string().as_bytes()); + assert!(gate_spec(&io,&state).ok); + std::fs::remove_file(ws.path.join("regions.json")).unwrap(); + assert!(!gate_spec(&io,&state).ok); +} + +#[test] +fn automatic_bands_are_a_draft_not_a_build_spec() { + let ws=Workspace::new(); + let mut image=r::create_image(128,128,[255,255,255,255]); + for y in (5..100).step_by(6) { r::fill_rect(&mut image,10.,y as f64,100.,3.,[0.,0.,0.,255.]); } + ws.write("comp.png",&png_io::encode_png(&image,&[]).unwrap()); + let mut io=ws.io(); + let args=["--comp","comp.png","--auto"].map(String::from); + assert_eq!(crate::comp_spec::run(&args,&mut io),0,"auto must produce a usable draft even on a busy comp"); + let draft=ws.path.join(".impeccable/build/regions.draft.json"); + assert!(draft.exists()); + assert_eq!(crate::comp_spec::run(&["--comp","comp.png","--regions",".impeccable/build/regions.draft.json"].map(String::from),&mut io),1); + assert!(!ws.path.join(SPEC_PATH).exists()); + assert!(!gate_spec(&io,&json!({"comp":"comp.png"})).ok); + ws.write(SPEC_PATH,&std::fs::read(&draft).unwrap()); + assert!(!gate_spec(&io,&json!({"comp":"comp.png"})).ok,"copying a draft to the spec path cannot validate it"); + ws.write(SPEC_PATH,b"previous accepted spec"); + assert_eq!(crate::comp_spec::run(&args,&mut io),1,"do not overwrite an edited draft"); + assert_eq!(std::fs::read(ws.path.join(SPEC_PATH)).unwrap(),b"previous accepted spec"); +} diff --git a/crates/comp-verbs/src/comp_spec.rs b/crates/comp-verbs/src/comp_spec.rs index eace115a4..028e158a0 100644 --- a/crates/comp-verbs/src/comp_spec.rs +++ b/crates/comp-verbs/src/comp_spec.rs @@ -893,11 +893,24 @@ fn resolve(io: &Io, p: &str) -> PathBuf { // ---- CLI ------------------------------------------------------------------- /// `impeccable comp-spec ...` +/// A failed edit leaves the last valid measurements intact, but they cannot +/// authorize a build against different source geometry. +pub fn region_source_issue(io: &Io, spec: &Value) -> Option { + let source = spec.get("regionsSource")?; + let Some(path) = source["path"].as_str() else { + return Some("spec has invalid region source evidence; re-run comp-spec --regions".into()); + }; + let current = std::fs::read(resolve(io,path)).ok() + .map(|bytes| format!("{:x}",Sha256::digest(&bytes))); + if current.as_deref().is_some_and(|hash| Some(hash) == source["sha256"].as_str()) { return None; } + Some(format!("region source {path} changed or is missing since measurement; fix it and re-run comp-spec --regions {path} before continuing")) +} + 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 --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp --regions 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 --auto band regions when you have no regions file\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt [--background transparent|opaque|auto] the regeneration prompt for a raster region\n"); + io.out("usage: comp-spec.mjs --comp --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp --regions 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 --auto [--out f] write a band draft; refine into elements before --regions\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt [--background transparent|opaque|auto] the regeneration prompt for a raster region\n"); return 0; } if flag(argv, "print") { @@ -1037,10 +1050,39 @@ pub fn run(argv: &[String], io: &mut Io) -> i32 { return 0; } + if flag(argv,"auto") && arg(argv,"regions").is_none() { + if flag(argv,"spec") { + io.err("comp-spec: --auto writes a draft, not a measured spec; use --out instead of --spec\n"); + return 1; + } + let draft_path = arg_or(argv,"out",".impeccable/build/regions.draft.json"); + let dest = resolve(io,draft_path); + if dest == resolve(io,SPEC_PATH) { + io.err("comp-spec: an automatic draft cannot replace the measured spec\n"); + return 1; + } + let mut draft = auto_regions(&comp); + draft["draft"] = json!(true); + draft["comp"] = json!(comp_path); + if let Some(parent) = dest.parent() { let _ = std::fs::create_dir_all(parent); } + use std::io::Write; + let written = std::fs::OpenOptions::new().write(true).create_new(true).open(&dest) + .and_then(|mut file| file.write_all(util::json_pretty(&draft).as_bytes())); + if let Err(error) = written { + io.err(&format!("comp-spec: cannot write draft {draft_path}: {error}; use a new --out path to preserve existing work\n")); + return 1; + } + io.out(&format!("DRAFT {draft_path}: {} approximate horizontal bands, not an element map.\nRefine the bands into the visible elements, remove the draft flag, then run comp-spec --comp {comp_path} --regions {draft_path}. The measured spec and build state are unchanged.\n",draft["regions"].as_array().map_or(0,Vec::len))); + return 0; + } + let regions_source; let regions_input: Value = if let Some(rf) = arg(argv, "regions") { match std::fs::read_to_string(resolve(io, rf)) { Ok(raw) => match serde_json::from_str(&raw) { - Ok(v) => v, + Ok(v) => { + regions_source = json!({"path":rf,"sha256":format!("{:x}",Sha256::digest(raw.as_bytes()))}); + v + }, Err(e) => { io.err(&format!("comp-spec: cannot read regions {rf}: {e}\n")); return 1; @@ -1051,12 +1093,14 @@ pub fn run(argv: &[String], io: &mut Io) -> i32 { return 1; } } - } else if flag(argv, "auto") { - auto_regions(&comp) } else { io.err("comp-spec: pass --grid to get the coordinate grid, then --regions (or --auto for band regions)\n"); return 1; }; + if regions_input["draft"] == true { + 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 mut spec = match measure_regions(&comp, ®ions_input, comp_path) { Ok(s) => s, Err(e) => { @@ -1072,6 +1116,7 @@ pub fn run(argv: &[String], io: &mut Io) -> i32 { hasher.update(comp.height.to_le_bytes()); hasher.update(&comp.data); spec["compSha256"] = json!(format!("{:x}", hasher.finalize())); + spec["regionsSource"] = regions_source; if let Some(previous) = std::fs::read(&spec_out).ok() .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) { preserve_typography(&mut spec, &previous); diff --git a/docs/CLI-CONTRACT.md b/docs/CLI-CONTRACT.md index 62db5ede8..38811ce66 100644 --- a/docs/CLI-CONTRACT.md +++ b/docs/CLI-CONTRACT.md @@ -449,7 +449,8 @@ 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 `. All four resolve paths against the process cwd. Printed commands spell the launcher via `IMPECCABLE_SELF` (default `impeccable`), so they name `{{scripts_path}}/impeccable `, 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 --grid` writes `.impeccable/build/comp-grid.png` (10x10 labeled grid) and prints PALETTE/BANDS/NEXT; `--comp --regions ` 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 --auto` derives band regions; `--print` prints the compact spec; `--crop [--out f] [--scale n] [--raw]` writes a reference crop; `--plate-prompt ` prints the regeneration prompt. `--spec ` 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 `:`, 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. `--comp --grid` writes `.impeccable/build/comp-grid.png` (10x10 labeled grid) and prints PALETTE/BANDS/NEXT; `--comp --regions ` 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 --auto [--out ]` 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 [--out f] [--scale n] [--raw]` writes a reference crop; `--plate-prompt ` prints the regeneration prompt. `--spec ` 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 `:`, 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 --build [--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/.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 ` 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 [--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. diff --git a/tests/oracle/DELTAS.md b/tests/oracle/DELTAS.md index 5c6ff829c..69d646eba 100644 --- a/tests/oracle/DELTAS.md +++ b/tests/oracle/DELTAS.md @@ -191,3 +191,13 @@ 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. + +## Recorded 2026-09-18: draft region authoring and source binding + +`comp-spec-usage` now describes --auto as a draft writer. In +`comp-spec-regions`, the only file-content addition is regionsSource with the +input path and fixture-derived SHA-256; all existing fields and measurements +were compared unchanged. No frozen function vectors changed. New Rust +regressions verify automatic drafts do not overwrite specs or existing drafts, +cannot be submitted unchanged, and a rejected/missing region-source revision +cannot advance the build using the last successful measurements. diff --git a/tests/oracle/golden/comp-spec-regions.json b/tests/oracle/golden/comp-spec-regions.json index 75a047fe9..f70cb2f69 100644 --- a/tests/oracle/golden/comp-spec-regions.json +++ b/tests/oracle/golden/comp-spec-regions.json @@ -4,6 +4,6 @@ "exit": 0, "signal": null, "files": { - ".impeccable/build/spec.json": "{\n \"tool\": \"comp-spec\",\n \"version\": 1,\n \"createdAt\": \"\",\n \"comp\": \"comp.png\",\n \"warnings\": [],\n \"uncoveredInkCells\": [\n \"A2\",\n \"B2\",\n \"C2\",\n \"E5\",\n \"F5\",\n \"G5\",\n \"H5\",\n \"I5\",\n \"J5\",\n \"E6\",\n \"A9\",\n \"B9\",\n \"C9\"\n ],\n \"compSize\": {\n \"width\": 768,\n \"height\": 512\n },\n \"aspect\": 1.5,\n \"orientation\": \"landscape\",\n \"palette\": [\n {\n \"hex\": \"#f7e8e8\",\n \"coverage\": 0.6868\n },\n {\n \"hex\": \"#182838\",\n \"coverage\": 0.1403\n },\n {\n \"hex\": \"#9a9aa5\",\n \"coverage\": 0.0683\n },\n {\n \"hex\": \"#c5c5d1\",\n \"coverage\": 0.0672\n },\n {\n \"hex\": \"#7c7c82\",\n \"coverage\": 0.0374\n }\n ],\n \"bands\": [\n {\n \"y\": 0.0588,\n \"strength\": 1\n },\n {\n \"y\": 0.0941,\n \"strength\": 0.4087\n },\n {\n \"y\": 0.1412,\n \"strength\": 0.7036\n },\n {\n \"y\": 0.1765,\n \"strength\": 0.6963\n },\n {\n \"y\": 0.2,\n \"strength\": 0.6026\n },\n {\n \"y\": 0.2353,\n \"strength\": 0.6125\n },\n {\n \"y\": 0.4706,\n \"strength\": 0.3982\n },\n {\n \"y\": 0.5059,\n \"strength\": 0.9754\n },\n {\n \"y\": 0.5412,\n \"strength\": 0.5699\n },\n {\n \"y\": 0.5765,\n \"strength\": 0.8782\n },\n {\n \"y\": 0.6,\n \"strength\": 0.3576\n },\n {\n \"y\": 0.6353,\n \"strength\": 0.4894\n },\n {\n \"y\": 0.6588,\n \"strength\": 0.5699\n },\n {\n \"y\": 0.6941,\n \"strength\": 0.8782\n },\n {\n \"y\": 0.7176,\n \"strength\": 0.3576\n },\n {\n \"y\": 0.7529,\n \"strength\": 0.4877\n },\n {\n \"y\": 0.7765,\n \"strength\": 0.5699\n },\n {\n \"y\": 0.8118,\n \"strength\": 0.8782\n },\n {\n \"y\": 0.8353,\n \"strength\": 0.3576\n },\n {\n \"y\": 0.8824,\n \"strength\": 0.4877\n },\n {\n \"y\": 0.9059,\n \"strength\": 0.3028\n },\n {\n \"y\": 0.9765,\n \"strength\": 0.3039\n }\n ],\n \"regions\": [\n {\n \"id\": \"top\",\n \"kind\": \"chrome\",\n \"note\": \"top masthead band area\",\n \"grid\": \"A0:J1\",\n \"box\": {\n \"x\": 0,\n \"y\": 0,\n \"w\": 1,\n \"h\": 0.2\n },\n \"px\": {\n \"x\": 0,\n \"y\": 0,\n \"w\": 768,\n \"h\": 102\n },\n \"aspect\": 7.5294,\n \"palette\": [\n {\n \"hex\": \"#f5e7e8\",\n \"coverage\": 0.392\n },\n {\n \"hex\": \"#182838\",\n \"coverage\": 0.3825\n },\n {\n \"hex\": \"#bdbdc8\",\n \"coverage\": 0.1348\n },\n {\n \"hex\": \"#8b8b96\",\n \"coverage\": 0.0908\n }\n ],\n \"detail\": {\n \"energy\": 39.1367\n },\n \"medium\": \"semantic\",\n \"plate\": null,\n \"text\": null\n },\n {\n \"id\": \"art\",\n \"kind\": \"plate\",\n \"note\": \"an exploded illustration drawing\",\n \"grid\": \"E2:J4\",\n \"box\": {\n \"x\": 0.4,\n \"y\": 0.2,\n \"w\": 0.6,\n \"h\": 0.3\n },\n \"px\": {\n \"x\": 307,\n \"y\": 102,\n \"w\": 461,\n \"h\": 154\n },\n \"aspect\": 2.9935,\n \"palette\": [\n {\n \"hex\": \"#e6dfe5\",\n \"coverage\": 0.5326\n },\n {\n \"hex\": \"#a2a2ad\",\n \"coverage\": 0.383\n },\n {\n \"hex\": \"#7f7f88\",\n \"coverage\": 0.0844\n }\n ],\n \"detail\": {\n \"energy\": 38.5615\n },\n \"medium\": \"raster\",\n \"plate\": \"assets/plates/art.png\",\n \"text\": null\n },\n {\n \"id\": \"body\",\n \"kind\": \"text\",\n \"note\": \"a paragraph of body text content\",\n \"grid\": \"A5:D8\",\n \"box\": {\n \"x\": 0,\n \"y\": 0.5,\n \"w\": 0.4,\n \"h\": 0.4\n },\n \"px\": {\n \"x\": 0,\n \"y\": 256,\n \"w\": 307,\n \"h\": 205\n },\n \"aspect\": 1.4976,\n \"palette\": [\n {\n \"hex\": \"#f8e8e8\",\n \"coverage\": 0.6826\n },\n {\n \"hex\": \"#182838\",\n \"coverage\": 0.223\n },\n {\n \"hex\": \"#787878\",\n \"coverage\": 0.0848\n },\n {\n \"hex\": \"#b82828\",\n \"coverage\": 0.0096\n }\n ],\n \"detail\": {\n \"energy\": 32.0304\n },\n \"medium\": \"semantic\",\n \"plate\": null,\n \"text\": null\n }\n ],\n \"compSha256\": \"4c640a415d36339b4895de01ef99f1ab7726d8e4cf70029652485137c24d914e\"\n}" + ".impeccable/build/spec.json": "{\n \"tool\": \"comp-spec\",\n \"version\": 1,\n \"createdAt\": \"\",\n \"comp\": \"comp.png\",\n \"warnings\": [],\n \"uncoveredInkCells\": [\n \"A2\",\n \"B2\",\n \"C2\",\n \"E5\",\n \"F5\",\n \"G5\",\n \"H5\",\n \"I5\",\n \"J5\",\n \"E6\",\n \"A9\",\n \"B9\",\n \"C9\"\n ],\n \"compSize\": {\n \"width\": 768,\n \"height\": 512\n },\n \"aspect\": 1.5,\n \"orientation\": \"landscape\",\n \"palette\": [\n {\n \"hex\": \"#f7e8e8\",\n \"coverage\": 0.6868\n },\n {\n \"hex\": \"#182838\",\n \"coverage\": 0.1403\n },\n {\n \"hex\": \"#9a9aa5\",\n \"coverage\": 0.0683\n },\n {\n \"hex\": \"#c5c5d1\",\n \"coverage\": 0.0672\n },\n {\n \"hex\": \"#7c7c82\",\n \"coverage\": 0.0374\n }\n ],\n \"bands\": [\n {\n \"y\": 0.0588,\n \"strength\": 1\n },\n {\n \"y\": 0.0941,\n \"strength\": 0.4087\n },\n {\n \"y\": 0.1412,\n \"strength\": 0.7036\n },\n {\n \"y\": 0.1765,\n \"strength\": 0.6963\n },\n {\n \"y\": 0.2,\n \"strength\": 0.6026\n },\n {\n \"y\": 0.2353,\n \"strength\": 0.6125\n },\n {\n \"y\": 0.4706,\n \"strength\": 0.3982\n },\n {\n \"y\": 0.5059,\n \"strength\": 0.9754\n },\n {\n \"y\": 0.5412,\n \"strength\": 0.5699\n },\n {\n \"y\": 0.5765,\n \"strength\": 0.8782\n },\n {\n \"y\": 0.6,\n \"strength\": 0.3576\n },\n {\n \"y\": 0.6353,\n \"strength\": 0.4894\n },\n {\n \"y\": 0.6588,\n \"strength\": 0.5699\n },\n {\n \"y\": 0.6941,\n \"strength\": 0.8782\n },\n {\n \"y\": 0.7176,\n \"strength\": 0.3576\n },\n {\n \"y\": 0.7529,\n \"strength\": 0.4877\n },\n {\n \"y\": 0.7765,\n \"strength\": 0.5699\n },\n {\n \"y\": 0.8118,\n \"strength\": 0.8782\n },\n {\n \"y\": 0.8353,\n \"strength\": 0.3576\n },\n {\n \"y\": 0.8824,\n \"strength\": 0.4877\n },\n {\n \"y\": 0.9059,\n \"strength\": 0.3028\n },\n {\n \"y\": 0.9765,\n \"strength\": 0.3039\n }\n ],\n \"regions\": [\n {\n \"id\": \"top\",\n \"kind\": \"chrome\",\n \"note\": \"top masthead band area\",\n \"grid\": \"A0:J1\",\n \"box\": {\n \"x\": 0,\n \"y\": 0,\n \"w\": 1,\n \"h\": 0.2\n },\n \"px\": {\n \"x\": 0,\n \"y\": 0,\n \"w\": 768,\n \"h\": 102\n },\n \"aspect\": 7.5294,\n \"palette\": [\n {\n \"hex\": \"#f5e7e8\",\n \"coverage\": 0.392\n },\n {\n \"hex\": \"#182838\",\n \"coverage\": 0.3825\n },\n {\n \"hex\": \"#bdbdc8\",\n \"coverage\": 0.1348\n },\n {\n \"hex\": \"#8b8b96\",\n \"coverage\": 0.0908\n }\n ],\n \"detail\": {\n \"energy\": 39.1367\n },\n \"medium\": \"semantic\",\n \"plate\": null,\n \"text\": null\n },\n {\n \"id\": \"art\",\n \"kind\": \"plate\",\n \"note\": \"an exploded illustration drawing\",\n \"grid\": \"E2:J4\",\n \"box\": {\n \"x\": 0.4,\n \"y\": 0.2,\n \"w\": 0.6,\n \"h\": 0.3\n },\n \"px\": {\n \"x\": 307,\n \"y\": 102,\n \"w\": 461,\n \"h\": 154\n },\n \"aspect\": 2.9935,\n \"palette\": [\n {\n \"hex\": \"#e6dfe5\",\n \"coverage\": 0.5326\n },\n {\n \"hex\": \"#a2a2ad\",\n \"coverage\": 0.383\n },\n {\n \"hex\": \"#7f7f88\",\n \"coverage\": 0.0844\n }\n ],\n \"detail\": {\n \"energy\": 38.5615\n },\n \"medium\": \"raster\",\n \"plate\": \"assets/plates/art.png\",\n \"text\": null\n },\n {\n \"id\": \"body\",\n \"kind\": \"text\",\n \"note\": \"a paragraph of body text content\",\n \"grid\": \"A5:D8\",\n \"box\": {\n \"x\": 0,\n \"y\": 0.5,\n \"w\": 0.4,\n \"h\": 0.4\n },\n \"px\": {\n \"x\": 0,\n \"y\": 256,\n \"w\": 307,\n \"h\": 205\n },\n \"aspect\": 1.4976,\n \"palette\": [\n {\n \"hex\": \"#f8e8e8\",\n \"coverage\": 0.6826\n },\n {\n \"hex\": \"#182838\",\n \"coverage\": 0.223\n },\n {\n \"hex\": \"#787878\",\n \"coverage\": 0.0848\n },\n {\n \"hex\": \"#b82828\",\n \"coverage\": 0.0096\n }\n ],\n \"detail\": {\n \"energy\": 32.0304\n },\n \"medium\": \"semantic\",\n \"plate\": null,\n \"text\": null\n }\n ],\n \"compSha256\": \"4c640a415d36339b4895de01ef99f1ab7726d8e4cf70029652485137c24d914e\",\n \"regionsSource\": {\n \"path\": \"regions.json\",\n \"sha256\": \"868d2c385a36694d057f07d9dc0c5b7bf28d8151ba80b1c95a9809fd7943126c\"\n }\n}" } } diff --git a/tests/oracle/golden/comp-spec-usage.json b/tests/oracle/golden/comp-spec-usage.json index f32cd8388..a3b119aa7 100644 --- a/tests/oracle/golden/comp-spec-usage.json +++ b/tests/oracle/golden/comp-spec-usage.json @@ -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 --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp --regions 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 --auto band regions when you have no regions file\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt [--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 --grid write .impeccable/build/comp-grid.png (10x10 labeled grid) + palette + bands\n comp-spec.mjs --comp --regions 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 --auto [--out f] write a band draft; refine into elements before --regions\n comp-spec.mjs --print the compact spec\n comp-spec.mjs --crop [--out f] [--scale n] reference crop of a region (never a shipping asset)\n comp-spec.mjs --plate-prompt [--background transparent|opaque|auto] the regeneration prompt for a raster region\n", "stderr": "", "exit": 0, "signal": null,