Fix: isolate review components from assembled context

AI-assisted implementation with Codex under maintainer direction.
This commit is contained in:
Paul Bakaus
2026-09-14 17:36:22 -07:00
parent c65febd291
commit 2b7527e2cd
10 changed files with 267 additions and 46 deletions
+93 -4
View File
@@ -60,6 +60,7 @@ fn render_page(
width: u32,
height: u32,
box_: &Value,
isolation: Option<&Value>,
) -> Result<(Vec<u8>, Value), String> {
let server = snapshot.serve()?;
let url = server.entry_url();
@@ -80,6 +81,12 @@ fn render_page(
if(document.getAnimations().some(a=>a.playState==='running'))throw Error('Component is animated; provide its static review state.');
return {html:document.documentElement.outerHTML,svg:document.querySelectorAll('svg').length,images:document.images.length,controls:document.querySelectorAll('button,input,select,textarea,a[href]').length};
})()"#).map_err(|e|e.message)?;
let isolated = if let Some(targets) = isolation {
page.set_transparent_background().map_err(|e| e.message)?;
let script = format!("({})({})", include_str!("component_isolation.js"), targets);
Some(page.evaluate_value_in_world(&world, &script).map_err(|e| e.message)?)
} else { None };
let captured_dom = page.evaluate_value_in_world(&world, "document.documentElement.outerHTML").map_err(|e| e.message)?;
let coords = ["x", "y", "w", "h"].map(|k| box_[k].as_f64().unwrap());
let clip = [
coords[0] * width as f64,
@@ -87,6 +94,13 @@ fn render_page(
coords[2] * width as f64,
coords[3] * height as f64,
];
if let Some(isolated) = &isolated {
let b = &isolated["bounds"];
let (x, y, w, h) = (b["x"].as_f64().unwrap(), b["y"].as_f64().unwrap(), b["width"].as_f64().unwrap(), b["height"].as_f64().unwrap());
if x >= clip[0] + clip[2] || y >= clip[1] + clip[3] || x + w <= clip[0] || y + h <= clip[1] {
return Err("component target does not intersect its measured box".into());
}
}
let first = page
.screenshot_viewport()
.map_err(|e| e.message)?;
@@ -144,14 +158,18 @@ fn render_page(
let unchanged = page
.evaluate_value_in_world(&world, "document.documentElement.outerHTML")
.map_err(|e| e.message)?;
if unchanged != dom["html"] {
if unchanged != captured_dom {
return Err("component document changed during capture".into());
}
let viewport_png = base64::engine::general_purpose::STANDARD
.decode(first)
.map_err(|e| e.to_string())?;
let png = crop_viewport(&viewport_png, width, height, clip)?;
let proof = json!({"kind":"static-code","entry":snapshot.entry(),"inputSnapshot":snapshot.digest(),"inputs":snapshot.manifest(),"observedDependencies":responses,"domSha256":hash(dom["html"].as_str().unwrap().as_bytes()),"screenshotSha256":hash(&png),"viewportScreenshotSha256":hash(&viewport_png),"cropMethod":"verified-viewport-pixels","viewport":{"width":width,"height":height,"dpr":1},"box":box_,"reducedMotion":true,"svgElements":dom["svg"],"rasterElements":dom["images"],"semanticControls":dom["controls"]});
let mut proof = json!({"kind":"static-code","entry":snapshot.entry(),"inputSnapshot":snapshot.digest(),"inputs":snapshot.manifest(),"observedDependencies":responses,"domSha256":hash(dom["html"].as_str().unwrap().as_bytes()),"screenshotSha256":hash(&png),"viewportScreenshotSha256":hash(&viewport_png),"cropMethod":"verified-viewport-pixels","viewport":{"width":width,"height":height,"dpr":1},"box":box_,"reducedMotion":true,"svgElements":dom["svg"],"rasterElements":dom["images"],"semanticControls":dom["controls"]});
if let Some(isolated) = isolated {
proof["isolation"] = isolated;
proof["capturedDomSha256"] = json!(hash(captured_dom.as_str().unwrap().as_bytes()));
}
Ok((png, proof))
})();
page.close();
@@ -163,6 +181,20 @@ impl ComponentCapturer for NativeComponentCapturer {
packet: &mut Value,
inputs: &BTreeMap<String, Vec<u8>>,
) -> Result<CapturedPreviews, String> {
let isolated = packet["schemaVersion"] == 2 && packet["stage"] == "components";
if packet["stage"] == "components" && !isolated {
return Err("New component captures require schemaVersion 2 with preview.selector for each code component. Existing review records remain readable.".into());
}
let mut targets: BTreeMap<String, Vec<Value>> = BTreeMap::new();
if isolated {
for c in packet["components"].as_array().ok_or("missing components")? {
let target = if c["preview"]["kind"] == "page" { Some(&c["preview"]) }
else if c["context"]["kind"] == "page" { Some(&c["context"]) } else { None };
if let Some(target) = target {
targets.entry(source(target)?.into()).or_default().push(json!({"id":c["id"],"selector":target["selector"]}));
}
}
}
let width = packet["comp"]["width"]
.as_u64()
.ok_or("missing comp width")? as u32;
@@ -195,6 +227,13 @@ impl ComponentCapturer for NativeComponentCapturer {
{
let id = c["id"].as_str().ok_or("missing component id")?.to_string();
let mut views = serde_json::Map::new();
// Context comes from the same frozen document, not a separately
// authored approximation. It is never a second approval item.
if isolated && c["preview"]["kind"] == "page" {
c["context"] = c["preview"].clone();
c["context"].as_object_mut().unwrap().remove("selector");
c["context"]["layering"] = json!("Context only. This decision applies to the isolated component; the assembled page is reviewed separately.");
}
for key in ["preview", "context"] {
if c.get(key).is_none() {
continue;
@@ -223,12 +262,15 @@ impl ComponentCapturer for NativeComponentCapturer {
);
}
let snapshot = Arc::new(HtmlSnapshot::from_pinned(path.clone(), selected)?);
let cache_key = format!("{}:{}", snapshot.digest(), c["box"]);
let isolation = if isolated && key == "preview" {
Some(json!({"id":id,"targets":targets.get(&path).ok_or("missing component targets")?}))
} else { None };
let cache_key = format!("{}:{}:{}", snapshot.digest(), c["box"], isolation.as_ref().unwrap_or(&Value::Null));
let (png, proof) = if let Some(saved) = cache.get(&cache_key) {
saved.clone()
} else {
let captured =
render_page(&mut browser, snapshot, width, height, &c["box"])
render_page(&mut browser, snapshot, width, height, &c["box"], isolation.as_ref())
.map_err(|e| format!("{id} {key}: {e}"))?;
cache.insert(cache_key, captured.clone());
captured
@@ -237,6 +279,9 @@ impl ComponentCapturer for NativeComponentCapturer {
c[key]["url"] = json!(format!("/files/{output}"));
c[key]["kind"] = json!("image");
c[key]["sourceKind"] = json!("page");
if let Some(isolation) = proof.get("isolation") {
c[key]["isolation"] = isolation.clone();
}
if key == "preview" {
c["material"] = material(&png, "Captured HTML / CSS / SVG")?;
}
@@ -260,6 +305,50 @@ impl ComponentCapturer for NativeComponentCapturer {
#[cfg(test)]
mod tests {
use super::*;
// Real browser regression: shared-document crops used to duplicate the
// headline in its background card. Run explicitly on a browser-equipped host.
#[test]
#[ignore = "requires Chromium"]
fn isolated_component_capture_keeps_layout_excludes_children_and_preserves_context() {
let comp = impeccable_comp::raster::Image { width:100,height:100,data:vec![255;100*100*4] };
let reference = impeccable_comp::png_io::encode_png(&comp,&[]).unwrap();
let html = br#"<!doctype html><style>
html,body{margin:0;background:purple} #surface{position:absolute;inset:0;background:yellow}
#headline{position:absolute;left:10px;top:10px;width:40px;height:40px;background:red}
#headline::before{content:'';position:absolute;left:0;top:0;width:5px;height:5px;background:cyan;visibility:visible}
#headline::after{content:'';position:absolute;left:5px;top:0;width:5px;height:5px;background:black;visibility:hidden}
#child{position:absolute;left:10px;top:10px;width:10px;height:10px;background:lime}
#sibling{position:absolute;left:70px;top:70px;width:10px;height:10px;background:blue}
</style><div id="surface"><div id="headline"><span id="child"></span></div></div><div id="sibling"></div>"#;
let inputs = BTreeMap::from([("comp.png".into(),reference),("kit.html".into(),html.to_vec())]);
let make = |id:&str| json!({"id":id,"box":{"x":0,"y":0,"w":1,"h":1},"preview":{"kind":"page","url":"/files/kit.html","selector":format!("#{id}")},"dependencies":[]});
let mut packet = json!({"schemaVersion":2,"stage":"components","comp":{"url":"/files/comp.png","width":100,"height":100},"components":[make("surface"),make("headline"),make("child"),make("sibling")]});
let captured = NativeComponentCapturer.capture(&mut packet,&inputs).unwrap();
let pixels = |index:usize,view:&str,x:usize,y:usize| {
let path=packet["components"][index][view]["url"].as_str().unwrap().strip_prefix("/files/").unwrap();
let image=impeccable_comp::png_io::decode_png(&captured.files[path]).unwrap().image;
image.data[(y*100+x)*4..(y*100+x)*4+4].to_vec()
};
assert_eq!(pixels(0,"preview",12,12),[255,255,0,255]); // no foreground/pseudo
assert_eq!(pixels(0,"context",12,12),[0,255,255,255]); // real combined context
assert_eq!(pixels(1,"preview",16,12),[255,0,0,255]); // hidden pseudo stays hidden
assert_eq!(pixels(1,"preview",22,22),[255,0,0,255]); // independently owned child absent
assert_eq!(pixels(1,"preview",75,75)[3],0); // sibling and page canvas absent
assert_eq!(pixels(2,"preview",22,22),[0,255,0,255]);
assert_eq!(pixels(2,"preview",12,12)[3],0); // parent's paint absent
assert_eq!(packet["components"][1]["preview"]["isolation"]["excludedComponents"],json!(["child"]));
assert_eq!(packet["components"][1]["material"]["alpha"],"transparent");
let mut raster_child = json!({"schemaVersion":2,"stage":"components","comp":{"url":"/files/comp.png","width":100,"height":100},"components":[make("headline"),make("child")]});
raster_child["components"][1]["preview"] = json!({"kind":"image","url":"/files/comp.png"});
raster_child["components"][1]["context"] = json!({"kind":"page","url":"/files/kit.html","selector":"#child"});
NativeComponentCapturer.capture(&mut raster_child,&inputs).unwrap();
assert_eq!(raster_child["components"][0]["preview"]["isolation"]["excludedComponents"],json!(["child"]));
for selector in ["body", "#missing", "div", "#surface"] {
let mut broken = json!({"schemaVersion":2,"stage":"components","comp":{"url":"/files/comp.png","width":100,"height":100},"components":[make("surface"),make("headline")]});
broken["components"][1]["preview"]["selector"]=json!(selector);
assert!(NativeComponentCapturer.capture(&mut broken,&inputs).is_err(),"{selector}");
}
}
#[test]
fn component_edge_crops_round_endpoints_on_odd_viewports() {
let image = impeccable_comp::raster::Image {width:3,height:3,data:(0u8..36).collect()};
+59
View File
@@ -0,0 +1,59 @@
// Native capture adapter, evaluated in a CDP isolated world. Keep DOM/layout and
// authored styling; suppress paint belonging to other components. No page script
// or producer screenshot participates in this operation.
({ id, targets }) => {
const roots = targets.map(target => {
const matches = document.querySelectorAll(target.selector);
if (matches.length !== 1) throw Error(`${target.id}: selector must match exactly one element (got ${matches.length}).`);
const element = matches[0];
if (element === document.body || element === document.documentElement || !document.body.contains(element)) {
throw Error(`${target.id}: select a component element inside the body, not the whole document.`);
}
return { ...target, element };
});
if (new Set(roots.map(root => root.element)).size !== roots.length) throw Error('Components cannot own the same DOM element.');
const selected = roots.find(root => root.id === id);
if (!selected) throw Error('Missing component target.');
const rect = selected.element.getBoundingClientRect();
if (!rect.width || !rect.height || getComputedStyle(selected.element).visibility !== 'visible') throw Error(`${id}: component target is not visible.`);
const otherRoots = roots.filter(root => root !== selected && selected.element.contains(root.element));
const elements = [...document.querySelectorAll('body,body *')];
const visibility = elements.map(element => {
const owns = (element === selected.element || selected.element.contains(element))
&& !otherRoots.some(root => root.element === element || root.element.contains(element));
const computed = getComputedStyle(element);
return { owns, rect: element.getBoundingClientRect().toJSON(),
authored: owns ? Object.fromEntries([...computed].filter(key => key !== 'visibility').map(key => [key, computed.getPropertyValue(key)])) : {},
main: owns ? computed.visibility : 'hidden',
pseudo: ['::before','::after','::marker'].map(pseudo => owns ? getComputedStyle(element, pseudo).visibility : 'hidden') };
});
// Apply only after reading all original computed styles. Descendant components
// keep their layout space but cannot paint inside their parent's preview.
if (document.querySelector('[data-impeccable-capture]')) throw Error('Reserved capture attribute is already present.');
elements.forEach((element, index) => {
element.style.setProperty('visibility', visibility[index].main, 'important');
element.setAttribute('data-impeccable-capture', String(index));
});
const style = document.createElement('style');
// Explicit pseudo-element visibility can otherwise escape its hidden owner.
style.textContent = elements.map((_, index) => ['::before','::after','::marker'].map((pseudo, p) =>
`[data-impeccable-capture="${index}"]${pseudo}{visibility:${visibility[index].pseudo[p]}!important}`).join('')).join('');
document.head.append(style);
// A body's background can propagate to the viewport despite visibility:hidden.
// Neither document root is a component; leave the capture canvas transparent.
for (const root of [document.documentElement, document.body]) root.style.setProperty('background', 'transparent', 'important');
elements.forEach((element, index) => {
const before = visibility[index];
if (!before.owns) return;
const rect = element.getBoundingClientRect();
const computed = getComputedStyle(element);
if (['x','y','width','height'].some(key => Math.abs(before.rect[key] - rect[key]) > .01)
|| Object.entries(before.authored).some(([key, value]) => computed.getPropertyValue(key) !== value)) {
throw Error('Isolating the component changed its authored layout or styles. Use a dedicated static component document.');
}
});
const after = selected.element.getBoundingClientRect();
if (['x','y','width','height'].some(key => Math.abs(rect[key] - after[key]) > .01)) throw Error('Isolating the component changed its layout.');
return { method: 'dom-component-v1', selector: selected.selector, excludedComponents: otherRoots.map(root => root.id),
bounds: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
}