//! Native evidence fixture/checker; install as a temporary oa-core example. //! This invariant evaluator derives delta from saved A, NOT from pointer input. use oa_core::{source_project::*, *}; use std::{fs, io::Write, path::Path}; type Result = std::result::Result>; fn id(alias: &str) -> Id { // Make the intended OA view first for default UUID-ordered view selection. if alias == "oa_view" { return Id::from_u128(0x29_0001); } Id::new_v5( &Id::from_u128(0xc1192026_0923_5000_8000_000000000029), alias.as_bytes(), ) } fn point(x: f32, y: f32) -> Position { Position { x, y } } fn element(p: &mut Project, alias: &str, name: &str, kind: Kind, layer: Layer, parent: Option) { p.elements.insert( id(alias), Element { id: id(alias), alias: alias.into(), name: name.into(), description: format!("Preserve engineering record {alias}."), parent, body: ElementBody::new(kind, layer).unwrap(), }, ); } fn diagram(p: &mut Project, alias: &str, name: &str, layer: Layer) { p.diagrams.insert( id(alias), Diagram { id: id(alias), alias: alias.into(), name: name.into(), description: "Selection changes placement geometry only.".into(), layer, projection: ProjectionKind::Overview, view_options: Default::default(), layout: None, nodes: Default::default(), routes: vec![], }, ); } fn place(p: &mut Project, view: &str, alias: &str, object: &str, position: Position, size: Size) { p.diagrams.get_mut(&id(view)).unwrap().nodes.insert( id(alias), NodePlacement { id: id(alias), alias: alias.into(), element: id(object), position, size, collapsed: false, port_anchors: vec![], }, ); } fn route(edge: &str, pins: Vec) -> EdgeRoute { EdgeRoute { edge: id(edge), waypoints: pins, source_attachment: Some(EdgeAttachment { side: PortAnchorSide::Right, offset: 0.25, }), target_attachment: Some(EdgeAttachment { side: PortAnchorSide::Left, offset: 0.7, }), label: Some(EdgeLabel { fraction: 0.45, offset: point(7., -19.), }), } } fn fixture() -> Result { let mut p = Project::empty(); p.id = id("project"); p.alias = "multiselect_native".into(); p.name = "Multiselect native".into(); p.description = "Shift-select A and B; move together, retain C and engineering semantics.".into(); for (alias, name, parent) in [ ("activity_a", "A - receive request", None), ("activity_b", "B - assess request", None), ("activity_c", "C - dispatch response", None), ( "activity_child", "A.1 - classify request", Some(id("activity_a")), ), ] { element(&mut p, alias, name, Kind::Activity, Layer::Oa, parent); } element( &mut p, "sa_function", "Technical request processing", Kind::Function, Layer::Sa, None, ); for (alias, name, source, target) in [ ("internal", "Request assessment", "activity_a", "activity_b"), ("external", "Response decision", "activity_b", "activity_c"), ] { let old = p.relate(RelationKind::Interaction, name, id(source), id(target)); reidentify(&mut p, old, id(alias))?; p.relations.get_mut(&id(alias)).unwrap().alias = alias.into(); } diagram( &mut p, "oa_view", "Multiselect native - OA overview", Layer::Oa, ); diagram(&mut p, "oa_other", "Other OA view - preserve", Layer::Oa); diagram(&mut p, "sa_view", "SA engineering - preserve", Layer::Sa); let size = Size { width: 224., height: 94., }; place( &mut p, "oa_view", "node_a", "activity_a", point(80., 80.), Size { width: 310., height: 260., }, ); place( &mut p, "oa_view", "node_b", "activity_b", point(470., 120.), size, ); place( &mut p, "oa_view", "node_c", "activity_c", point(780., 270.), size, ); place( &mut p, "oa_view", "node_child", "activity_child", point(120., 210.), size, ); place( &mut p, "oa_other", "other_a", "activity_a", point(40., 60.), size, ); place( &mut p, "sa_view", "sa_node", "sa_function", point(100., 110.), size, ); p.diagrams.get_mut(&id("oa_view")).unwrap().routes = vec![ route("internal", vec![point(420., 145.), point(420., 186.)]), route("external", vec![point(725., 150.), point(725., 336.)]), ]; p.traces.insert( id("trace"), Trace { id: id("trace"), alias: "preserved_refinement".into(), name: "Reviewed request refinement".into(), description: "Group geometry and Hide preserve this trace and both endpoints.".into(), oa: id("activity_a"), sa: id("sa_function"), kind: TraceKind::Refinement, baseline: Some(TraceBaseline { oa: p.properties(id("activity_a")).unwrap(), sa: p.properties(id("sa_function")).unwrap(), semantic: None, }), }, ); validate_candidate(&p)?; Ok(p) } fn load(path: &Path) -> Result { if path .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia")) { let disk = source_files::load_project(path, ProjectLimits::default())?; // This deliberately partial engineering fixture has completeness // warnings (allocation/behavior context). They are not source errors. // Compilation, candidate validation and whole-Project equality still // reject malformed source or any unexpected model change. if disk .diagnostics .iter() .any(|d| d.diagnostic.severity == validation::Severity::Error) { return Err(format!("Source discovery diagnostics: {:?}", disk.diagnostics).into()); } let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default()) .map_err(|e| format!("Source compile diagnostics: {e:?}"))?; Ok(compiled.decoded.project) } else { Ok(persistence::load(path)?) } } fn equal(expected: &Project, actual: &Project) -> Result<()> { if expected == actual { return Ok(()); } let e = serde_json::to_value(expected)?; let a = serde_json::to_value(actual)?; let fields: Vec<_> = e .as_object() .unwrap() .keys() .filter(|key| e[*key] != a[*key]) .cloned() .collect(); Err(format!("Whole Project mismatch in {fields:?}; no partial PASS").into()) } fn roundtrip(p: &Project) -> Result { validate_candidate(p)?; equal(p, &persistence::decode(&serde_json::to_vec(p)?)?)?; let bundle = format_bundle(p); let compiled = compile_bundle(&bundle, ProjectLimits::default()) .map_err(|e| format!("Roundtrip: {e:?}"))?; equal(p, &compiled.decoded.project)?; Ok(bundle) } fn write_new(path: &Path, bytes: &[u8]) -> Result<()> { fs::create_dir_all(path.parent().ok_or("Missing parent directory")?)?; fs::OpenOptions::new() .write(true) .create_new(true) .open(path)? .write_all(bytes)?; Ok(()) } fn generate(out: &Path) -> Result<()> { if out.exists() { return Err("Output directory already exists; choose a fresh path".into()); } let p = fixture()?; let bundle = roundtrip(&p)?; write_new(&out.join("baseline.json"), &serde_json::to_vec_pretty(&p)?)?; for (path, source) in &bundle.files { write_new(&out.join("source").join(path), source.as_bytes())?; } let names = [ "project", "oa_view", "oa_other", "sa_view", "activity_a", "activity_b", "activity_c", "activity_child", "node_a", "node_b", "node_c", "node_child", "internal", "external", "trace", "sa_function", "sa_node", "other_a", ]; let ids: std::collections::BTreeMap<_, _> = names.into_iter().map(|name| (name, id(name))).collect(); write_new( &out.join("manifest.json"), &serde_json::to_vec_pretty(&serde_json::json!({ "fixture": "multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry, "move_members": ["node_a", "node_b", "node_child"], "fixed_member": "node_c", "evaluation": "Whole Project invariant comparison; saved A supplies bounded shared delta, not an exact pointer oracle", "hide": "Remove node_a/node_b only; retain child, stored routes, all model records and other views", }))?, )?; equal(&p, &load(&out.join("baseline.json"))?)?; equal(&p, &load(&out.join("source/project.arcadia"))?)?; println!("PASS generated JSON and source bundle: {}", out.display()); Ok(()) } fn check(mode: &str, baseline: &Path, saved: &Path) -> Result<()> { let before = load(baseline)?; let actual = load(saved)?; validate_candidate(&before)?; validate_candidate(&actual)?; if before.id != id("project") { return Err("Not this fixture's baseline project".into()); } let mut expected = before.clone(); let mut delta = None; match mode { "unchanged" => {} "hide" => { let view = expected .diagrams .get_mut(&id("oa_view")) .ok_or("Missing OA view")?; for alias in ["node_a", "node_b"] { view.nodes .remove(&id(alias)) .ok_or("Baseline selected placement missing")?; } // references::remove_objects removes routes only when their semantic // edge is deleted. Hide deletes placements; both routes MUST survive. } "moved" => { let old = before .diagrams .get(&id("oa_view")) .ok_or("Missing baseline OA view")?; let new = actual .diagrams .get(&id("oa_view")) .ok_or("Missing saved OA view")?; let a = old .nodes .get(&id("node_a")) .ok_or("Missing baseline A")? .position; let moved_a = new .nodes .get(&id("node_a")) .ok_or("Missing saved A")? .position; let d = point(moved_a.x - a.x, moved_a.y - a.y); if !d.x.is_finite() || !d.y.is_finite() || d.x.abs().max(d.y.abs()) > 500. || d.x.hypot(d.y) < 10. { return Err(format!( "Move delta must have length >=10 and each axis <=500 world units: {d:?}" ) .into()); } if moved_a.x % 10. != 0. || moved_a.y % 10. != 0. { return Err("Saved anchor is not on 10-unit grid".into()); } let view = expected.diagrams.get_mut(&id("oa_view")).unwrap(); // Independent explicit fixture enumeration: no group/descendant/ // routing planner is called to predict the result. for alias in ["node_a", "node_b", "node_child"] { let node = view .nodes .get_mut(&id(alias)) .ok_or("Missing expected moved placement")?; node.position.x += d.x; node.position.y += d.y; } let internal = view .routes .iter_mut() .find(|route| route.edge == id("internal")) .ok_or("Missing internal pins")?; for pin in &mut internal.waypoints { pin.x += d.x; pin.y += d.y; } delta = Some(d); } _ => return Err("Mode must be moved, hide, or unchanged".into()), } equal(&expected, &actual)?; roundtrip(&actual)?; println!( "{}", serde_json::to_string_pretty(&serde_json::json!({ "result": "PASS", "mode": mode, "baseline": baseline, "saved": saved, "comparison": "complete Project equality", "inferred_common_delta": delta, "limits": "Invariant evaluator only; no pointer/action/focus/Undo/source-byte acceptance claim", }))? ); Ok(()) } fn main() -> Result<()> { let args: Vec<_> = std::env::args().skip(1).collect(); match args.as_slice() { [command, out] if command == "generate" => generate(Path::new(out)), [command, mode, baseline, saved] if command == "check" => check(mode, Path::new(baseline), Path::new(saved)), _ => Err("Usage: multiselect-native-producer generate DIRECTORY | check moved|hide|unchanged BASELINE SAVED".into()), } } #[cfg(test)] mod tests { use super::*; use std::collections::BTreeSet; // Produce evidence through real validated commands; the checker itself uses // explicit fixture enumeration and never calls this movement implementation. fn moved(before: &Project, delta: Position) -> Project { let command = diagram_edit::translate_placements( before, id("oa_view"), &BTreeSet::from([id("node_a"), id("node_b")]), delta, ) .unwrap(); let mut session = Session::new(before.clone()); session.execute(command).unwrap(); session.project().clone() } fn hidden(before: &Project) -> Project { let mut session = Session::new(before.clone()); session .execute(Command::Batch(vec![ Command::Delete(id("node_a")), Command::Delete(id("node_b")), ])) .unwrap(); session.project().clone() } fn through_saved_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> { let dir = tempfile::tempdir()?; let before = dir.path().join("before.json"); let saved = dir.path().join("saved.json"); // These assertions distinguish meaningful invariant corruptions from // malformed projects rejected before the evaluator even sees them. validate_candidate(baseline).expect("test baseline must be valid"); validate_candidate(actual).expect("corruption must remain a valid model"); persistence::save(&before, baseline)?; persistence::save(&saved, actual)?; check(mode, &before, &saved) } fn mismatch(mode: &str, before: &Project, actual: &Project, case: &str) { let error = through_saved_json(mode, before, actual) .expect_err(case) .to_string(); assert!( error.contains("Whole Project mismatch"), "{case}: unexpected rejection: {error}" ); } fn edge<'a>(p: &'a mut Project, alias: &str) -> &'a mut EdgeRoute { p.diagrams .get_mut(&id("oa_view")) .unwrap() .routes .iter_mut() .find(|route| route.edge == id(alias)) .unwrap() } #[test] fn generated_json_and_real_source_bundle_are_accepted_unchanged() { let dir = tempfile::tempdir().unwrap(); let out = dir.path().join("fixture"); generate(&out).unwrap(); let baseline = out.join("baseline.json"); let saved = out.join("saved.json"); persistence::save(&saved, &fixture().unwrap()).unwrap(); check("unchanged", &baseline, &saved).unwrap(); check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap(); let bytes = fs::read(&baseline).unwrap(); assert!( generate(&out).is_err(), "generation cannot overwrite evidence" ); assert_eq!(fs::read(&baseline).unwrap(), bytes); let invalid = dir.path().join("invalid.arcadia"); fs::write(&invalid, "project {").unwrap(); assert!(load(&invalid).is_err(), "source errors must still reject"); } #[test] fn real_group_command_outputs_pass_and_are_not_unchanged() { let before = fixture().unwrap(); for delta in [point(40., 60.), point(-20., 30.)] { let actual = moved(&before, delta); through_saved_json("moved", &before, &actual).unwrap(); mismatch( "unchanged", &before, &actual, "real movement cannot count as unchanged", ); } } #[test] fn real_hide_command_outputs_pass_before_and_after_movement() { let initial = fixture().unwrap(); for before in [initial.clone(), moved(&initial, point(40., 60.))] { let actual = hidden(&before); through_saved_json("hide", &before, &actual).unwrap(); mismatch( "unchanged", &before, &actual, "real Hide cannot count as unchanged", ); } } #[test] fn omitted_and_double_moved_child_are_rejected() { let before = fixture().unwrap(); let delta = point(40., 60.); let correct = moved(&before, delta); let initial = before.diagrams[&id("oa_view")].nodes[&id("node_child")].position; for position in [ initial, point(initial.x + 2. * delta.x, initial.y + 2. * delta.y), ] { let mut bad = correct.clone(); bad.diagrams .get_mut(&id("oa_view")) .unwrap() .nodes .get_mut(&id("node_child")) .unwrap() .position = position; mismatch("moved", &before, &bad, "child did not move exactly once"); } } #[test] fn internal_pins_left_behind_and_external_pins_moved_are_rejected() { let mut before = fixture().unwrap(); let original_pins = edge(&mut before, "internal").waypoints.clone(); let correct = moved(&before, point(40., 60.)); let mut bad = correct.clone(); edge(&mut bad, "internal").waypoints = original_pins; mismatch("moved", &before, &bad, "internal pins left behind"); let mut bad = correct; for pin in &mut edge(&mut bad, "external").waypoints { pin.x += 40.; pin.y += 60.; } mismatch("moved", &before, &bad, "one-ended pins moved"); } #[test] fn manual_attachments_and_label_data_must_remain_exact() { let before = fixture().unwrap(); let correct = moved(&before, point(40., 60.)); let mutations: [fn(&mut EdgeRoute); 4] = [ |r| r.source_attachment.as_mut().unwrap().offset = 0.65, |r| r.target_attachment.as_mut().unwrap().side = PortAnchorSide::Top, |r| r.label.as_mut().unwrap().offset.x += 40., |r| r.label.as_mut().unwrap().fraction = 0.75, ]; for mutate in mutations { let mut bad = correct.clone(); mutate(edge(&mut bad, "internal")); mismatch("moved", &before, &bad, "relative route metadata changed"); } } #[test] fn semantic_other_view_fixed_node_and_trace_drift_are_rejected() { let before = fixture().unwrap(); let correct = moved(&before, point(40., 60.)); let mutations: [fn(&mut Project); 5] = [ |p| { p.elements .get_mut(&id("activity_a")) .unwrap() .description .push_str(" drift") }, |p| { p.diagrams .get_mut(&id("oa_other")) .unwrap() .nodes .get_mut(&id("other_a")) .unwrap() .position .x += 40. }, |p| { p.diagrams .get_mut(&id("sa_view")) .unwrap() .nodes .get_mut(&id("sa_node")) .unwrap() .position .y += 60. }, |p| { p.diagrams .get_mut(&id("oa_view")) .unwrap() .nodes .get_mut(&id("node_c")) .unwrap() .position .x += 40. }, |p| { p.traces .get_mut(&id("trace")) .unwrap() .description .push_str(" drift") }, ]; for mutate in mutations { let mut bad = correct.clone(); mutate(&mut bad); mismatch( "moved", &before, &bad, "record outside permitted movement changed", ); } } #[test] fn zero_small_excessive_and_offgrid_moves_cannot_satisfy_move_evidence() { let before = fixture().unwrap(); for (delta, reason) in [ (point(0., 0.), "length >=10"), (point(3., 0.), "length >=10"), (point(510., 0.), "each axis <=500"), (point(20.5, 0.), "10-unit grid"), ] { let actual = moved(&before, delta); let error = through_saved_json("moved", &before, &actual) .unwrap_err() .to_string(); assert!(error.contains(reason), "{delta:?}: {error}"); } } #[test] fn hide_must_preserve_child_routes_and_semantic_objects() { let before = fixture().unwrap(); let correct = hidden(&before); let mut missing_child = correct.clone(); missing_child .diagrams .get_mut(&id("oa_view")) .unwrap() .nodes .remove(&id("node_child")); mismatch( "hide", &before, &missing_child, "Hide removed unselected child placement", ); let mut missing_routes = correct; missing_routes .diagrams .get_mut(&id("oa_view")) .unwrap() .routes .clear(); mismatch( "hide", &before, &missing_routes, "Hide removed persistent route records", ); let mut semantic_delete = Session::new(before.clone()); semantic_delete .execute(Command::Batch(vec![ Command::Delete(id("activity_a")), Command::Delete(id("activity_b")), ])) .unwrap(); mismatch( "hide", &before, semantic_delete.project(), "Hide became semantic deletion", ); } #[test] fn unchanged_rejects_valid_semantic_drift_without_geometry_changes() { let before = fixture().unwrap(); let mut actual = before.clone(); actual .elements .get_mut(&id("activity_b")) .unwrap() .name .push_str(" edited"); mismatch( "unchanged", &before, &actual, "geometry equality is not complete Project equality", ); } }