//! Reviewed policy experiment using the production projection and translator. //! No production admission or atomic-refusal behavior is tested here. use super::*; struct Mixed { f: Fixture, an: Id, xn: Id, yn: Id, internal: Id, include_performer: bool, } fn mixed(layer: Layer, collapsed: bool, obstacle: bool, include_performer: bool) -> Mixed { let mut f = fixture(layer); let work = f.p.elements[&f.w].kind(); let exchange = match layer { Layer::Oa => RelationKind::Interaction, Layer::Sa => RelationKind::FunctionalExchange, }; let x = f.p.add_element(work, layer, "X", Some(f.w)).unwrap(); let y = f.p.add_element(work, layer, "Y", Some(f.w)).unwrap(); let outside = f.p.add_element(work, layer, "Outside", None).unwrap(); let xn = f.p.place(f.d, x, xy(140., 200.)); let yn = f.p.place(f.d, y, xy(940., 200.)); f.p.place(f.d, outside, xy(1800., 800.)); f.p.relate(RelationKind::Allocation, "A owns X", f.a, x); f.p.relate(RelationKind::Allocation, "B owns Y", f.b, y); // FunctionalExchange's actual builder creates valid function input/output // ports; do not manufacture Interaction records between SA Functions. let internal = f.p.relate(exchange, "X to Y", x, y); let external = f.p.relate(exchange, "Y to Outside", y, outside); let an = f.p.diagrams[&f.d] .nodes .values() .find(|n| n.element == f.a) .unwrap() .id; let d = f.p.diagrams.get_mut(&f.d).unwrap(); d.nodes.get_mut(&f.n).unwrap().position = xy(100., 800.); d.nodes.get_mut(&an).unwrap().collapsed = collapsed; d.routes = vec![route(internal), route(external)]; if obstacle { let id = f.p.add_element(work, layer, "Stationary obstacle", None) .unwrap(); f.p.place(f.d, id, xy(550., 230.)); } validate_candidate(&f.p).unwrap(); let captured = scope(&f); assert_eq!(captured.uniform, Uniform::MixedOrUnresolved); assert_eq!( captured.placements.keys().copied().collect::>(), BTreeSet::from([f.n, xn, yn]) ); // Explicit control: the real current API still refuses Arrange for this root. assert!(matches!( plan_semantics(&f.p, &captured, Destination::LayoutOnly, &BTreeMap::new()), Err(Refusal::MixedLayoutRequired) )); assert!(matches!( plan_semantics( &f.p, &captured, Destination::Performer(f.a), &BTreeMap::new() ), Err(Refusal::MixedLayoutRequired) )); Mixed { f, an, xn, yn, internal, include_performer, } } fn affected(m: &Mixed) -> BTreeSet { let mut ids: BTreeSet<_> = scope(&m.f).placements.keys().copied().collect(); let mut expected = BTreeSet::from([m.f.n, m.xn, m.yn]); if m.include_performer { ids.insert(m.an); ids.extend(projection::descendant_nodes(&m.f.p, m.f.d, m.an)); expected.insert(m.an); } assert_eq!(ids, expected); ids } // Diagnostic expansion changes only a temporary Diagram. Include affected // descendants and ancestors even when the normal projection hides them. // Unrelated folded interiors are intentionally OUTSIDE this diagnostic domain. fn diagnostic(p: &Project, diagram: Id, ids: &BTreeSet) -> ProjectedView { let mut d = p.diagrams[&diagram].clone(); for (id, node) in &mut d.nodes { if ids.contains(id) || projection::descendant_nodes(p, diagram, *id) .iter() .any(|n| ids.contains(n)) { node.collapsed = false; } } projection::project_diagram(p, &d) } fn rigid(before: &ProjectedView, after: &ProjectedView, ids: &BTreeSet) { for id in ids { let old = before.nodes.iter().find(|n| n.id == *id).unwrap(); let new = after.nodes.iter().find(|n| n.id == *id).unwrap(); assert_eq!(new.position, xy(old.position.x + 400., old.position.y)); assert_eq!(new.size, old.size, "no reflow of an affected footprint"); } } // The only experimental bypass: use the pure general geometry command instead // of plan_semantics(LayoutOnly), whose mixed-root pre-veto is asserted above. // This is not a fabricated Scope or a call to singleton check_geometry with a // falsely enlarged Scope. The combined scene uses the real overlap predicate. fn candidate(m: &Mixed) -> (Command, Project) { let ids = affected(m); let command = oa_core::diagram_edit::translate_placements(&m.f.p, m.f.d, &ids, xy(400., 0.)).unwrap(); let actual = prepare_command(&m.f.p, &command).unwrap().project; validate_candidate(&actual).unwrap(); // Independent literal expected Project: no planner or route helper oracle. let mut expected = m.f.p.clone(); let d = expected.diagrams.get_mut(&m.f.d).unwrap(); for (id, position) in [ (m.f.n, xy(500., 800.)), (m.xn, xy(540., 200.)), (m.yn, xy(1340., 200.)), ] { d.nodes.get_mut(&id).unwrap().position = position; } if m.include_performer { d.nodes.get_mut(&m.an).unwrap().position = xy(500., 100.); } let internal = d.routes.iter_mut().find(|r| r.edge == m.internal).unwrap(); internal.waypoints = vec![xy(820., 400.), xy(820., 550.)]; assert_eq!( actual, expected, "complete records, including folds, routes, other view and relations" ); (command, actual) } #[test] fn mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly() { for layer in [Layer::Oa, Layer::Sa] { let enclosure_height = match layer { Layer::Oa => 218., Layer::Sa => 256., }; for include_performer in [false, true] { for collapsed in [false, true] { let m = mixed(layer, collapsed, false, include_performer); let accepted = m.f.p.clone(); let (command, actual) = candidate(&m); let ids = affected(&m); let before = projection::project_view(&accepted, m.f.d); let after = projection::project_view(&actual, m.f.d); let before_expanded = diagnostic(&accepted, m.f.d, &ids); let after_expanded = diagnostic(&actual, m.f.d, &ids); assert!( !overlaps(&before, &after), "visible scene must admit the combined move" ); assert!( !overlaps(&before_expanded, &after_expanded), "expanded scene must admit the combined move" ); rigid(&before_expanded, &after_expanded, &ids); for node in after.nodes.iter().chain(&after_expanded.nodes) { assert!( (Rect { position: node.position, size: node.size }) .valid() ); } // Explicit selected headers remain visible. Native clipping/readability // and complete group gesture admission are intentionally not claimed. assert!(after.nodes.iter().any(|n| n.id == m.f.n)); if include_performer { assert!(after.nodes.iter().any(|n| n.id == m.an)); } else { assert_eq!( actual.diagrams[&m.f.d].nodes[&m.an], accepted.diagrams[&m.f.d].nodes[&m.an] ); assert_eq!(node(&before_expanded, m.an).position, xy(100., 100.)); assert_eq!( node(&before_expanded, m.an).size, Size { width: 288., height: enclosure_height } ); assert_eq!(node(&after_expanded, m.an).position, xy(100., 100.)); assert_eq!( node(&after_expanded, m.an).size, Size { width: 688., height: enclosure_height } ); } assert_eq!(roundtrip(&accepted, command), actual); assert_eq!(m.f.p, accepted); } } } } #[test] fn mixed_arrange_experiment_detects_moved_folded_child_collision() { for layer in [Layer::Oa, Layer::Sa] { let expected_overlap = match layer { Layer::Oa => 13696., Layer::Sa => 20116., }; let m = mixed(layer, true, true, true); let accepted = m.f.p.clone(); let (_, actual) = candidate(&m); let ids = affected(&m); let before = projection::project_view(&accepted, m.f.d); let after = projection::project_view(&actual, m.f.d); assert!(!before.nodes.iter().any(|n| n.id == m.xn)); assert!(!after.nodes.iter().any(|n| n.id == m.xn)); assert!( !overlaps(&before, &after), "must distinguish hidden collision from ordinary visible refusal" ); let before_expanded = diagnostic(&accepted, m.f.d, &ids); let after_expanded = diagnostic(&actual, m.f.d, &ids); rigid(&before_expanded, &after_expanded, &ids); let obstacle = before_expanded .nodes .iter() .find(|n| n.position == xy(550., 230.)) .unwrap() .id; assert_eq!( overlap( node(&before_expanded, m.xn), node(&before_expanded, obstacle) ), 0. ); assert_eq!( overlap(node(&after_expanded, m.xn), node(&after_expanded, obstacle)), expected_overlap ); assert!( overlaps(&before_expanded, &after_expanded), "combined diagnostic scene must refuse" ); // Detection only: there is no production group admission/commit API here. assert_eq!(m.f.p, accepted); } } fn node(view: &ProjectedView, id: Id) -> &ProjectedNode { view.nodes.iter().find(|n| n.id == id).unwrap() } #[test] fn mixed_arrange_experiment_detects_stationary_enclosure_growth_collision() { for layer in [Layer::Oa, Layer::Sa] { let enclosure_height = match layer { Layer::Oa => 218., Layer::Sa => 256., }; let mut m = mixed(layer, false, false, true); let kind = m.f.p.elements[&m.f.w].kind(); let z = m.f.p .add_element(kind, layer, "Stationary Z", None) .unwrap(); let zn = m.f.p.place(m.f.d, z, xy(1200., 100.)); validate_candidate(&m.f.p).unwrap(); let accepted = m.f.p.clone(); let (_, actual) = candidate(&m); let bn = accepted.diagrams[&m.f.d] .nodes .values() .find(|n| n.element == m.f.b) .unwrap() .id; let before = projection::project_view(&accepted, m.f.d); let after = projection::project_view(&actual, m.f.d); assert_eq!(node(&before, bn).position, xy(900., 100.)); assert_eq!( node(&before, bn).size, Size { width: 288., height: enclosure_height } ); assert_eq!(node(&after, bn).position, xy(900., 100.)); assert_eq!( node(&after, bn).size, Size { width: 688., height: enclosure_height } ); assert_eq!(node(&after, zn).position, xy(1200., 100.)); assert_eq!( node(&after, zn).size, Size { width: 224., height: 94. } ); assert_eq!(overlap(node(&before, bn), node(&before, zn)), 0.); assert_eq!(overlap(node(&after, bn), node(&after, zn)), 21056.); assert_eq!(node(&after, m.yn).position, xy(1340., 200.)); assert_eq!( node(&after, m.yn).position.y - (node(&after, zn).position.y + node(&after, zn).size.height), 6. ); for id in affected(&m) { assert_eq!(overlap(node(&before, id), node(&before, zn)), 0.); assert_eq!(overlap(node(&after, id), node(&after, zn)), 0.); } assert_eq!( actual.diagrams[&m.f.d].nodes[&bn], accepted.diagrams[&m.f.d].nodes[&bn] ); assert_eq!( actual.diagrams[&m.f.d].nodes[&zn], accepted.diagrams[&m.f.d].nodes[&zn] ); assert!( overlaps(&before, &after), "complete scene must detect stationary enclosure growth" ); let ids = affected(&m); assert!(overlaps( &diagnostic(&accepted, m.f.d, &ids), &diagnostic(&actual, m.f.d, &ids) )); assert_eq!(m.f.p, accepted); } } #[test] fn mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain() { for layer in [Layer::Oa, Layer::Sa] { // Independent source-derived literals: SA's auto-created function ports // raise projected X/Y height to 76 + 28 * (1 + 1), while Z is port-free. let (work_height, expected_overlap) = match layer { Layer::Oa => (94., 13696.), Layer::Sa => (132., 20116.), }; let mut m = mixed(layer, true, false, true); let performer = match layer { Layer::Oa => Kind::Actor, Layer::Sa => Kind::SystemActor, }; let kind = m.f.p.elements[&m.f.w].kind(); let d = m.f.p .add_element(performer, layer, "Unrelated D", None) .unwrap(); let z = m.f.p .add_element(kind, layer, "Unrelated hidden Z", None) .unwrap(); m.f.p.relate(RelationKind::Allocation, "D owns Z", d, z); let dn = m.f.p.place(m.f.d, d, xy(2500., 100.)); let zn = m.f.p.place(m.f.d, z, xy(550., 230.)); m.f.p .diagrams .get_mut(&m.f.d) .unwrap() .nodes .get_mut(&dn) .unwrap() .collapsed = true; validate_candidate(&m.f.p).unwrap(); let accepted = m.f.p.clone(); let (_, actual) = candidate(&m); let ids = affected(&m); let before = diagnostic(&accepted, m.f.d, &ids); let after = diagnostic(&actual, m.f.d, &ids); assert!(!before.nodes.iter().any(|n| n.id == zn)); assert!(!after.nodes.iter().any(|n| n.id == zn)); assert!( !overlaps(&before, &after), "chosen diagnostic domain excludes unrelated folded contents" ); assert!(!overlaps( &projection::project_view(&accepted, m.f.d), &projection::project_view(&actual, m.f.d) )); let all_unfolded = |p: &Project| { let mut diagram = p.diagrams[&m.f.d].clone(); for n in diagram.nodes.values_mut() { n.collapsed = false; } projection::project_diagram(p, &diagram) }; let broad_before = all_unfolded(&accepted); let broad_after = all_unfolded(&actual); assert_eq!(node(&broad_before, m.xn).position, xy(140., 200.)); assert_eq!(node(&broad_after, m.xn).position, xy(540., 200.)); assert_eq!( node(&broad_after, m.xn).size, Size { width: 224., height: work_height } ); assert_eq!(node(&broad_before, zn).position, xy(550., 230.)); assert_eq!(node(&broad_after, zn).position, xy(550., 230.)); assert_eq!( node(&broad_after, zn).size, Size { width: 224., height: 94. } ); assert_eq!( overlap(node(&broad_before, m.xn), node(&broad_before, zn)), 0. ); assert_eq!( overlap(node(&broad_after, m.xn), node(&broad_after, zn)), expected_overlap ); // Other newly exposed enclosure pairs may also overlap. X/Z is a // concrete discriminator, not a claim that it is the only cause. assert!(overlaps(&broad_before, &broad_after)); for id in [dn, zn] { assert_eq!( actual.diagrams[&m.f.d].nodes[&id], accepted.diagrams[&m.f.d].nodes[&id] ); } assert!(actual.diagrams[&m.f.d].nodes[&dn].collapsed); assert_eq!(m.f.p, accepted); } }