mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-21 18:47:02 +03:00
Capture original review images and bind the measured inventory
Support static WebP and JPEG without conversion-cache evidence. Bind the measured spec centrally, require complete semantic component previews, and present the initial kit before automatic gate-driven repairs. AI assistance: implemented and validated with OpenAI Codex.
This commit is contained in:
@@ -23,15 +23,8 @@ fn source(view: &Value) -> Result<&str, String> {
|
||||
.ok_or_else(|| "preview needs a pinned source".into())
|
||||
}
|
||||
fn material(png: &[u8], format: &str) -> Result<Value, String> {
|
||||
if !impeccable_comp::png_io::is_png(png) || png.len() < 24 {
|
||||
return Err("native review currently requires PNG raster assets".into());
|
||||
}
|
||||
let width = u32::from_be_bytes(png[16..20].try_into().unwrap());
|
||||
let height = u32::from_be_bytes(png[20..24].try_into().unwrap());
|
||||
if u64::from(width) * u64::from(height) > 32_000_000 {
|
||||
return Err("preview exceeds 32 megapixels".into());
|
||||
}
|
||||
let image = impeccable_comp::png_io::decode_png(png)?.image;
|
||||
let (image, source_format) = impeccable_comp::png_io::decode_review_image(png)?;
|
||||
let format = if format == "Captured HTML / CSS / SVG" { format } else { source_format };
|
||||
Ok(
|
||||
json!({"format":format,"width":image.width,"height":image.height,"alpha":if image.data.chunks_exact(4).any(|p|p[3]<255){"transparent"}else{"opaque"}}),
|
||||
)
|
||||
@@ -158,7 +151,7 @@ impl ComponentCapturer for NativeComponentCapturer {
|
||||
.ok_or("missing approved reference")?;
|
||||
let reference_size = material(reference, "PNG")?;
|
||||
if reference_size["width"] != width || reference_size["height"] != height {
|
||||
return Err("comp dimensions do not match its PNG".into());
|
||||
return Err("comp dimensions do not match its image".into());
|
||||
}
|
||||
let env = std::env::vars().collect();
|
||||
let exe =
|
||||
|
||||
@@ -154,3 +154,48 @@ pub fn load_raster(file: &std::path::Path) -> Result<(Decoded, std::path::PathBu
|
||||
let _ = std::fs::write(&cache, &bytes);
|
||||
Ok((Decoded { image: img, text: HashMap::new() }, cache))
|
||||
}
|
||||
|
||||
/// Decode a static review image from its pinned bytes, without consulting or
|
||||
/// writing sibling conversion caches. Keep the original file as review evidence.
|
||||
pub fn decode_review_image(bytes: &[u8]) -> Result<(Image, &'static str), String> {
|
||||
if is_png(bytes) {
|
||||
if bytes.len() < 24 { return Err("truncated PNG".into()); }
|
||||
let width = u32::from_be_bytes(bytes[16..20].try_into().unwrap());
|
||||
let height = u32::from_be_bytes(bytes[20..24].try_into().unwrap());
|
||||
if u64::from(width) * u64::from(height) > 32_000_000 { return Err("preview exceeds 32 megapixels".into()); }
|
||||
return Ok((decode_png(bytes)?.image, "PNG"));
|
||||
}
|
||||
let format = image::guess_format(bytes).map_err(|e| format!("unsupported review image: {e}"))?;
|
||||
let name = match format {
|
||||
image::ImageFormat::WebP => {
|
||||
let decoder = image::codecs::webp::WebPDecoder::new(std::io::Cursor::new(bytes)).map_err(|e| e.to_string())?;
|
||||
if decoder.has_animation() { return Err("animated WebP requires a static review state".into()); }
|
||||
"WebP"
|
||||
},
|
||||
image::ImageFormat::Jpeg => "JPEG",
|
||||
_ => return Err("review images must be static PNG, WebP or JPEG".into()),
|
||||
};
|
||||
let reader = image::ImageReader::with_format(std::io::Cursor::new(bytes), format);
|
||||
let (width, height) = reader.into_dimensions().map_err(|e| e.to_string())?;
|
||||
if u64::from(width) * u64::from(height) > 32_000_000 { return Err("preview exceeds 32 megapixels".into()); }
|
||||
let rgba = image::load_from_memory_with_format(bytes, format).map_err(|e| e.to_string())?.to_rgba8();
|
||||
Ok((Image {width: width as usize, height: height as usize, data: rgba.into_raw()}, name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod review_image_tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn review_webp_and_jpeg_decode_original_bytes_and_keep_alpha() {
|
||||
let pixels = [25, 90, 65, 120, 200, 40, 35, 255];
|
||||
let mut webp = Vec::new();
|
||||
image::codecs::webp::WebPEncoder::new_lossless(&mut webp).encode(&pixels, 2, 1, image::ExtendedColorType::Rgba8).unwrap();
|
||||
let (decoded, format) = decode_review_image(&webp).unwrap();
|
||||
assert_eq!(format, "WebP"); assert_eq!(decoded.width, 2); assert_eq!(decoded.data, pixels);
|
||||
let mut jpeg = Vec::new();
|
||||
image::codecs::jpeg::JpegEncoder::new(&mut jpeg).encode(&[25, 90, 65], 1, 1, image::ExtendedColorType::Rgb8).unwrap();
|
||||
let (decoded, format) = decode_review_image(&jpeg).unwrap();
|
||||
assert_eq!(format, "JPEG"); assert_eq!(decoded.data[3], 255);
|
||||
assert!(decode_review_image(b"not an image").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,21 @@ pub fn freeze(project: &Path, input: &Value) -> Result<(Value, BTreeMap<String,
|
||||
}
|
||||
let mut files = BTreeMap::new();
|
||||
let mut comp_files = BTreeMap::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")? {
|
||||
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" {
|
||||
return Err(format!("semantic region {} requires a rendered code preview", region["id"]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
view(&mut packet["comp"], project, &mut files, &mut comp_files)?;
|
||||
let mut ids = BTreeSet::new();
|
||||
let components = packet["components"]
|
||||
|
||||
@@ -471,3 +471,21 @@ fn verify_requires_native_approval_and_current_manifest_and_dependencies() {
|
||||
fs::write(f.project.join("shared.css"), b"changed after approval").unwrap();
|
||||
assert!(super::verify::approved(&f.store,&f.project,"review.json").unwrap_err().contains("changed"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measured_inventory_is_bound_without_repeated_author_dependencies() {
|
||||
let f = Fixture::new();
|
||||
fs::create_dir_all(f.project.join(".impeccable/build")).unwrap();
|
||||
let path = f.project.join(".impeccable/build/spec.json");
|
||||
fs::write(&path, br#"{"regions":[{"id":"art","kind":"plate"},{"id":"control","kind":"control"}]}"#).unwrap();
|
||||
let mut input = f.manifest(); input["stage"] = json!("components");
|
||||
let dir = store::prepare(&f.store, &f.project, &input).unwrap();
|
||||
let state = store::read(&dir.join("current.json")).unwrap();
|
||||
assert!(state["sources"][".impeccable/build/spec.json"].is_string());
|
||||
let mut missing = input.clone(); missing["components"].as_array_mut().unwrap().pop();
|
||||
assert!(store::prepare(&f.store, &f.project, &missing).unwrap_err().contains("omitted measured region"));
|
||||
let mut flattened = input; flattened["components"][1]["preview"] = json!({"kind":"image","path":"art.png"});
|
||||
assert!(store::prepare(&f.store, &f.project, &flattened).unwrap_err().contains("rendered code preview"));
|
||||
fs::write(path, br#"{"regions":[]}"#).unwrap();
|
||||
assert!(store::sources_current(&state).is_err());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user