Preserve approvals for unchanged isolated component previews

This commit is contained in:
Paul Bakaus
2026-09-17 14:30:26 -07:00
parent 957bcdd2be
commit dc33f54c89
2 changed files with 34 additions and 2 deletions
@@ -599,6 +599,24 @@ fn visual_approvals_survive_shared_source_edits_but_not_changed_scope_or_pixels(
before["packet"]["revision"].as_str().unwrap()
)))
.unwrap();
// Component decisions approve the isolated preview, not its contextual
// surroundings or the dependency closure of a shared document.
let mut component_previous = previous.clone();
component_previous["packet"]["stage"] = json!("components");
let mut component_current = after.clone();
component_current["packet"]["stage"] = json!("components");
component_current["packet"]["components"][1]["dependencies"] = json!(["shared.css", "unrelated.png"]);
component_current["packet"]["components"][1]["context"] = json!({"kind":"image","url":"/files/unrelated-context.png"});
component_current["capture"]["components"][1]["views"]["preview"]["rasterElements"] = json!(12);
assert_eq!(super::visual_approval::carry(&component_previous, &mut component_current, &dir.join("blobs")), 1);
assert_eq!(component_current["draft"]["decisions"]["control"]["action"], "approve");
assert!(component_current["receipt"].is_null());
for field in ["box", "medium", "note", "preview"] {
let mut changed = component_current.clone();
changed["draft"]["decisions"].as_object_mut().unwrap().remove("control");
changed["packet"]["components"][1][field] = json!("changed");
assert_eq!(super::visual_approval::carry(&component_previous, &mut changed, &dir.join("blobs")), 0, "component scope: {field}");
}
for field in ["box", "medium", "note", "context"] {
let mut changed = after.clone();
changed["packet"]["components"][1][field] = json!("changed");
@@ -14,9 +14,20 @@ fn identity(state: &Value, component: &Value, blobs: &Path) -> Option<Value> {
let prefix = format!("/files/{}/", state["packet"]["revision"].as_str()?);
let mut definition = component.clone();
definition.as_object_mut()?.remove("revision");
// Dependency paths remain part of scope; their bytes are checked by sources_current.
let component_stage = state["packet"]["stage"] == "components";
if component_stage {
// The checkpoint approves the isolated component. Context is explicitly
// reference-only; shared document dependencies and thumbnails are not
// additional things the user approves. Source integrity still binds the
// full frozen closure and is checked separately by sources_current.
for key in ["dependencies", "context", "thumbnail"] {
definition.as_object_mut()?.remove(key);
}
}
let mut views = serde_json::Map::new();
for key in ["preview", "context", "thumbnail", "comp"] {
let keys: &[&str] = if component_stage { &["preview", "comp"] }
else { &["preview", "context", "thumbnail", "comp"] };
for &key in keys {
let view = if key == "comp" {
&state["packet"]["comp"]
} else {
@@ -60,6 +71,9 @@ fn identity(state: &Value, component: &Value, blobs: &Path) -> Option<Value> {
"rasterElements",
"semanticControls",
] {
// These counts describe the entire shared document, including
// components outside this decision's isolated preview.
if component_stage && matches!(field, "svgElements" | "rasterElements" | "semanticControls") { continue; }
scoped[field] = proof[field].clone();
}
}