//! Direct core fixture/oracle helper; no native input or accepted-file mutation. use oa_core::{transitions::*, view_options::ViewOptions, *}; use std::{ collections::{BTreeMap, BTreeSet}, fs, path::Path, }; fn id(n: u32) -> Id { format!("c6060906-0000-4000-8000-{n:012x}").parse().unwrap() } fn element( p: &mut Project, n: u32, kind: Kind, layer: Layer, name: &str, parent: Option, ) -> Id { let key = id(n); p.elements.insert( key, Element { id: key, alias: format!("cm_{n}"), name: name.into(), description: String::new(), parent, body: if kind == Kind::Port { ElementBody::shared_in( layer, SharedBody::Port { contract: PortContract::Component { kind: ComponentPortKind::Standard, orientation: ComponentPortOrientation::Unset, }, legacy: None, }, ) } else { ElementBody::new(kind, layer).unwrap() }, }, ); key } fn relation(p: &mut Project, n: u32, kind: RelationKind, name: &str, source: Id, target: Id) -> Id { let key = id(n); p.relations.insert( key, Relation { id: key, alias: format!("cm_{n}"), name: name.into(), description: String::new(), body: RelationBody::new(kind, source, target), }, ); key } fn trace(p: &mut Project, n: u32, oa: Id, sa: Id, kind: TraceKind, name: &str) { p.traces.insert( id(n), Trace { id: id(n), alias: format!("cm_{n}"), name: name.into(), description: String::new(), oa, sa, kind, baseline: None, }, ); } fn diagram(p: &mut Project, n: u32, layer: Layer, name: &str, rows: &[(Id, f32, f32, f32, f32)]) { let nodes = rows .iter() .enumerate() .map(|(i, &(element, x, y, width, height))| { let key = id(n + 10 + i as u32); ( key, NodePlacement { id: key, alias: format!("cm_view_{}", n + i as u32), element, position: Position { x, y }, size: Size { width, height }, collapsed: false, port_anchors: vec![], }, ) }) .collect(); p.diagrams.insert( id(n), Diagram { id: id(n), alias: format!("cm_diagram_{n}"), name: name.into(), description: String::new(), layer, projection: ProjectionKind::Overview, view_options: ViewOptions::default(), nodes, routes: vec![], }, ); } fn port(p: &mut Project, n: u32, owner: Id, name: &str) -> Id { let key = element(p, n, Kind::Port, Layer::Sa, name, Some(owner)); if let Some(SharedBody::Port { contract, .. }) = p.elements.get_mut(&key).unwrap().shared_mut() { *contract = PortContract::Component { kind: ComponentPortKind::Standard, orientation: ComponentPortOrientation::Unset, }; } else { panic!("port"); } key } fn base() -> Project { let mut p = Project::empty(); p.id = id(1); p.alias = "communication_native".into(); p.name = "Rescue communication engineering".into(); let a = element(&mut p, 10, Kind::Entity, Layer::Oa, "Rescue team", None); let b = element(&mut p, 11, Kind::Actor, Layer::Oa, "Control centre", None); let x = element( &mut p, 12, Kind::Activity, Layer::Oa, "Report rescue status", None, ); let y = element( &mut p, 13, Kind::Activity, Layer::Oa, "Assess rescue status", None, ); relation( &mut p, 20, RelationKind::Allocation, "Team performs reporting", a, x, ); relation( &mut p, 21, RelationKind::Allocation, "Control performs assessment", b, y, ); relation(&mut p, 22, RelationKind::Interaction, "Rescue status", x, y); let sa = element(&mut p, 30, Kind::System, Layer::Sa, "Rescue radio", None); let sb = element( &mut p, 31, Kind::SystemActor, Layer::Sa, "Control radio", None, ); port(&mut p, 32, sa, "Existing team socket"); port(&mut p, 33, sb, "Existing control socket"); trace( &mut p, 40, a, sa, TraceKind::Participant, "Team radio correspondence", ); trace( &mut p, 41, b, sb, TraceKind::Participant, "Control radio correspondence", ); diagram( &mut p, 1000, Layer::Oa, "Draw operational communication", &[ (a, 80., 70., 250., 160.), (b, 550., 70., 250., 160.), (x, 80., 330., 250., 110.), (y, 550., 330., 250., 110.), ], ); diagram( &mut p, 1100, Layer::Sa, "Explicit system endpoints", &[(sa, 80., 140., 250., 170.), (sb, 550., 140., 250., 170.)], ); valid(&p); p } fn valid(p: &Project) { validate_candidate(p).unwrap(); assert_eq!(language::parse(&language::format(p)).unwrap(), *p); assert_eq!( persistence::decode(&serde_json::to_vec(p).unwrap()).unwrap(), *p ); } fn forward(p: &Project, mean: Id, create: Option) -> TransitionRequest { let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]); r.mappings = vec![ TransitionMapping { source: id(10), target: Some(id(30)), target_kind: None, }, TransitionMapping { source: id(11), target: Some(id(31)), target_kind: None, }, ]; r.resolutions = r .mappings .iter() .map(|m| FieldResolution { source: m.source, field: TransitionField::Name, decision: FieldDecision::KeepTarget, }) .collect(); let target_port = if let Some(key) = create { let name = format!("{} · target port", p.named(mean)); CommunicationPortChoice::Create { id: key, alias: p.fresh_alias(&name, "port", key), name, description: String::new(), kind: ComponentPortKind::Standard, orientation: ComponentPortOrientation::Unset, } } else { CommunicationPortChoice::Reuse { port: id(33) } }; r.communication_endpoints .push(CommunicationEndpointDecision { source: mean, source_port: CommunicationPortChoice::Reuse { port: id(32) }, target_port, }); r } fn apply(p: &Project, r: &TransitionRequest) -> Project { let plan = plan_transition(p, r).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let mut q = apply_transition(p, &plan).unwrap(); // Desktop Workflow::Transition::Apply adds presentation after the core // transition. Predict its policy from the plan and prior diagram, never // adopt placement coordinates/size/alias from the observed native result. let diagram = r .destination_diagram .or_else(|| { q.diagrams .values() .find(|d| d.layer == r.direction.target_layer()) .map(|d| d.id) }) .expect("These fixtures provide an existing destination-layer diagram"); for change in &plan.changes { if q.elements.contains_key(&change.target) { let count = q.diagrams[&diagram].nodes.len(); q.place( diagram, change.target, Position { x: 60. + (count % 3) as f32 * 280., y: 60. + (count / 3) as f32 * 150., }, ); } } valid(&q); q } // Separate policy oracle: the explicit scenario root opens in Behavior, so no // architecture placement is requested. Keep apply() above for the legacy binary. fn apply_reverse_behavior(p: &Project, r: &TransitionRequest) -> Project { assert_eq!(r.sources, vec![id(200)]); assert_eq!(p.elements[&id(200)].kind(), Kind::Scenario); assert!(r.destination_diagram.is_none()); let plan = plan_transition(p, r).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let q = apply_transition(p, &plan).unwrap(); assert_eq!( q.diagrams, p.diagrams, "Behavior must preserve every saved view" ); reverse_assertions(p, &q); valid(&q); q } fn generate_reverse_behavior(before: &Path, out: &Path) { let disk = source_files::load_project(before, source_project::ProjectLimits::default()).unwrap(); let compiled = source_project::compile_bundle( &disk.reachable_bundle(), source_project::ProjectLimits::default(), ) .unwrap(); let p = &compiled.decoded.project; let request = reverse(p); let q = deterministic(p, apply_reverse_behavior(p, &request)); let updated = source_project::update_bundle(&compiled, &q, source_project::ProjectLimits::default()) .unwrap(); fs::create_dir_all(out).unwrap(); write_bundle(out, &updated.bundle); fs::write( out.join("expected.json"), serde_json::to_vec_pretty(&q).unwrap(), ) .unwrap(); fs::write( out.join("request.json"), serde_json::to_vec_pretty(&request).unwrap(), ) .unwrap(); println!("PASS generate-reverse-behavior: core candidate, all saved views unchanged"); } // Only actual identity fields and references are remapped, never names/text/data. fn remap(mut p: Project, mappings: &BTreeMap) -> Project { for (&old, &new) in mappings { if old == new { continue; } assert!(!p.contains(new)); references::remap_references(&mut p, old, new); if let Some(mut e) = p.elements.remove(&old) { e.id = new; p.elements.insert(new, e); } if let Some(mut e) = p.relations.remove(&old) { e.id = new; p.relations.insert(new, e); } if let Some(mut e) = p.traces.remove(&old) { e.id = new; p.traces.insert(new, e); } if let Some(mut e) = p.artifact_links.remove(&old) { e.id = new; p.artifact_links.insert(new, e); } if let Some(mut e) = p.diagrams.remove(&old) { e.id = new; p.diagrams.insert(new, e); } for d in p.diagrams.values_mut() { if let Some(mut e) = d.nodes.remove(&old) { e.id = new; d.nodes.insert(new, e); } } } valid(&p); p } fn deterministic(base: &Project, q: Project) -> Project { let mut fresh: Vec<_> = q .identities() .into_iter() .filter(|(key, _)| !base.contains(*key)) .collect(); fresh.sort_by(|a, b| a.1.cmp(&b.1)); let mut next = 8000; let map = fresh .into_iter() .map(|(old, _)| { while base.contains(id(next)) { next += 1; } let new = id(next); next += 1; (old, new) }) .collect(); remap(q, &map) } fn emit(dir: &Path, name: &str, p: &Project) { valid(p); fs::write(dir.join(format!("{name}.arcadia")), language::format(p)).unwrap(); fs::write( dir.join(format!("{name}.json")), serde_json::to_vec_pretty(p).unwrap(), ) .unwrap(); } fn emit_after(dir: &Path, name: &str, base: &Project, after: &Project) { let entry = "entry.arcadia".to_string(); let bundle = source_project::SourceBundle { entry: entry.clone(), files: BTreeMap::from([(entry.clone(), language::format(base))]), }; let compiled = source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap(); let updated = source_project::update_bundle(&compiled, after, source_project::ProjectLimits::default()) .unwrap(); let output = dir.join(name); write_bundle(&output, &updated.bundle); fs::write( output.join("expected.json"), serde_json::to_vec_pretty(after).unwrap(), ) .unwrap(); } fn write_bundle(dir: &Path, bundle: &source_project::SourceBundle) { fs::create_dir_all(dir).unwrap(); for (name, text) in &bundle.files { let path = dir.join(name); fs::create_dir_all(path.parent().unwrap()).unwrap(); fs::write(path, text).unwrap(); } } fn scenario(p: &mut Project, boundary: Id) -> Id { let s = element( p, 200, Kind::Scenario, Layer::Sa, "Reverse rescue request", None, ); if let Some(SharedBody::Scenario { viewpoint, .. }) = p.elements.get_mut(&s).unwrap().shared_mut() { *viewpoint = ScenarioViewpoint::BoundaryInterface; } let a = element( p, 201, Kind::Lifeline, Layer::Sa, "Team radio lifeline", Some(s), ); let b = element( p, 202, Kind::Lifeline, Layer::Sa, "Control radio lifeline", Some(s), ); for (line, owner) in [(a, id(30)), (b, id(31))] { *p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline { represented: Some(owner), }; } for (n, name, sender, receiver, kind, reply_to) in [ (203, "Request rescue report", b, a, MessageKind::Call, None), ( 204, "Reply with rescue report", a, b, MessageKind::Reply, Some(id(203)), ), ] { let e = element(p, n, Kind::Occurrence, Layer::Sa, name, Some(s)); *p.elements.get_mut(&e).unwrap().shared_mut().unwrap() = SharedBody::Occurrence { body: OccurrenceBody::Exchange { exchange: Some(boundary), sender: Some(sender), receiver: Some(receiver), message_kind: kind, reply_to, values: vec![], }, precondition: String::new(), postcondition: String::new(), }; } *p.elements.get_mut(&s).unwrap().steps_mut().unwrap() = vec![id(203), id(204)]; valid(p); s } fn reverse(p: &Project) -> TransitionRequest { let mean = id(50); let boundary = p.traces.values().find(|t| t.oa == mean).unwrap().sa; let mut r = TransitionRequest::new(TransitionDirection::SaToOa, vec![id(200)]); r.scope = DependencyScope::Subtree; r.mappings = [(boundary, mean), (id(30), id(10)), (id(31), id(11))] .into_iter() .map(|(source, target)| TransitionMapping { source, target: Some(target), target_kind: None, }) .collect(); r.resolutions = r .mappings .iter() .map(|m| FieldResolution { source: m.source, field: TransitionField::Name, decision: FieldDecision::KeepTarget, }) .collect(); r } fn reverse_assertions(p: &Project, q: &Project) { use validation::MessageEndpointResolution as R; let mapped = |source| q.traces.values().find(|t| t.sa == source).unwrap().oa; let body = |id| match q.elements[&id].shared().unwrap() { SharedBody::Occurrence { body, .. } => body, _ => panic!(), }; let call = mapped(id(203)); let reply = mapped(id(204)); assert_eq!( validation::resolve_message_endpoints(q, body(call)), R::Resolved { source: id(11), target: id(10) } ); assert_eq!( validation::resolve_message_endpoints(q, body(reply)), R::Resolved { source: id(10), target: id(11) } ); assert!(matches!(body(reply),OccurrenceBody::Exchange{reply_to:Some(id),..} if *id==call)); for key in [id(200), id(201), id(202), id(203), id(204)] { assert_eq!(p.elements[&key], q.elements[&key]); } assert_eq!( q.elements .values() .filter(|e| e.layer() == Layer::Oa && e.kind() == Kind::Port) .count(), 0 ); } fn generate(dir: &Path) { fs::create_dir_all(dir).unwrap(); let p = base(); emit(dir, "cm-draw", &p); let mut ready = p.clone(); relation( &mut ready, 50, RelationKind::CommunicationMean, "Rescue radio channel", id(10), id(11), ); valid(&ready); emit(dir, "cm-transition-ready", &ready); for (label, created) in [("reuse", None), ("create", Some(id(60)))] { let r = forward(&ready, id(50), created); let q = apply(&ready, &r); let q = deterministic(&ready, q); emit_after(dir, &format!("cm-{label}.after-template"), &ready, &q); fs::write( dir.join(format!("cm-{label}.request.json")), serde_json::to_vec_pretty(&r).unwrap(), ) .unwrap(); } let mut p = deterministic(&ready, apply(&ready, &forward(&ready, id(50), None))); let boundary = p.traces.values().find(|t| t.oa == id(50)).unwrap().sa; scenario(&mut p, boundary); emit(dir, "cm-reverse-scenario", &p); let r = reverse(&p); let q = apply(&p, &r); reverse_assertions(&p, &q); let q = deterministic(&p, q); emit_after(dir, "cm-reverse-scenario.after-template", &p, &q); fs::write( dir.join("cm-reverse-scenario.request.json"), serde_json::to_vec_pretty(&r).unwrap(), ) .unwrap(); let manifest = serde_json::json!({"schema":6,"native_status":"pending","project":id(1),"oa_participants":[id(10),id(11)],"oa_activities":[id(12),id(13)],"interaction":id(22),"sa_participants":[id(30),id(31)],"existing_ports":[id(32),id(33)],"participant_traces":[id(40),id(41)],"draw_diagram":id(1000),"system_diagram":id(1100),"template_mean":id(50),"reverse_scenario":id(200),"reverse_call":id(203),"correlated_reply":id(204),"templates":"Generated target/trace UUIDs are reference examples, not native exact expectations. Use check with native before/after sources."}); fs::write( dir.join("manifest.json"), serde_json::to_vec_pretty(&manifest).unwrap(), ) .unwrap(); } fn check_draw( before: &source_project::CompiledSourceProject, actual: &source_project::CompiledSourceProject, out: &Path, ) { let p = &before.decoded.project; assert!( !p.relations .values() .any(|r| r.kind() == RelationKind::CommunicationMean) ); let means: Vec<_> = actual .decoded .project .relations .values() .filter(|r| r.kind() == RelationKind::CommunicationMean) .collect(); assert_eq!( means.len(), 1, "Drawing creates exactly one communication means" ); let identity = means[0].id; assert!(!p.contains(identity)); let name = format!( "Communication means: {} — {}", p.named(id(10)), p.named(id(11)) ); let drawn = Relation { id: identity, alias: p.fresh_alias(&name, "relation", identity), name, description: String::new(), body: RelationBody::CommunicationMean { source: id(10), target: id(11), oriented: false, items: vec![], }, }; let create = Command::CreateRelation(drawn.clone()); let after_draw = execute_batch(p, std::slice::from_ref(&create)).unwrap(); let draw_source = source_project::update_bundle( before, &after_draw, source_project::ProjectLimits::default(), ) .unwrap(); let mut renamed = drawn; renamed.name = "Rescue radio channel".into(); let rename = Command::EditRelation(identity, renamed); let expected = execute_batch(&after_draw, std::slice::from_ref(&rename)).unwrap(); let expected_source = source_project::update_bundle( &draw_source, &expected, source_project::ProjectLimits::default(), ) .unwrap(); assert_eq!( expected, actual.decoded.project, "Draw+rename full model differs; only actual new relation UUID was supplied" ); let mut actual_files = actual.bundle.files.clone(); let entry = actual_files.remove(&actual.bundle.entry).unwrap(); assert!( actual_files .insert(expected_source.bundle.entry.clone(), entry) .is_none() ); assert_eq!( expected_source.bundle.files, actual_files, "Draw+rename complete source bytes differ (entry filename only aligned)" ); write_bundle(out, &expected_source.bundle); fs::write( out.join("expected-draw.json"), serde_json::to_vec_pretty(&expected).unwrap(), ) .unwrap(); fs::write( out.join("draw-rename-commands.json"), serde_json::to_vec_pretty(&[create, rename]).unwrap(), ) .unwrap(); println!( "PASS draw: typed CreateRelation then EditRelation, exact full model/source, only new relation UUID {identity}" ); } fn check(mode: &str, before: &Path, after: &Path, out: &Path) { let before_disk = source_files::load_project(before, source_project::ProjectLimits::default()).unwrap(); let actual_disk = source_files::load_project(after, source_project::ProjectLimits::default()).unwrap(); let compiled = source_project::compile_bundle( &before_disk.reachable_bundle(), source_project::ProjectLimits::default(), ) .unwrap(); let actual_compiled = source_project::compile_bundle( &actual_disk.reachable_bundle(), source_project::ProjectLimits::default(), ) .unwrap(); let p = compiled.decoded.project.clone(); let actual = actual_compiled.decoded.project.clone(); valid(&p); valid(&actual); if mode == "draw" { return check_draw(&compiled, &actual_compiled, out); } let r = if matches!(mode, "reverse" | "reverse-behavior") { reverse(&p) } else { let means: Vec<_> = p .relations .values() .filter(|r| r.kind() == RelationKind::CommunicationMean) .collect(); assert_eq!(means.len(), 1); let mean = means[0].id; let created = if mode == "create" { let trace = actual.traces.values().find(|t| t.oa == mean).unwrap(); let key = actual.relations[&trace.sa].target(); assert!(!p.contains(key)); Some(key) } else { assert_eq!(mode, "reuse"); None }; forward(&p, mean, created) }; let mut expected = if mode == "reverse-behavior" { apply_reverse_behavior(&p, &r) } else { apply(&p, &r) }; let mut map = BTreeMap::new(); let mut actual_used = BTreeSet::new(); for t in expected .traces .values() .filter(|t| !p.traces.contains_key(&t.id)) { let source = if r.direction == TransitionDirection::OaToSa { t.oa } else { t.sa }; let counterparts: Vec<_> = actual .traces .values() .filter(|a| { if r.direction == TransitionDirection::OaToSa { a.oa == source } else { a.sa == source } }) .collect(); assert_eq!( counterparts.len(), 1, "each explicit source has one counterpart" ); let a = counterparts[0]; assert!(!p.contains(a.id)); assert!(actual_used.insert(a.id)); map.insert(t.id, a.id); let (old, new) = if r.direction == TransitionDirection::OaToSa { (t.sa, a.sa) } else { (t.oa, a.oa) }; if !p.contains(old) { assert!(!p.contains(new)); assert!(actual_used.insert(new)); map.insert(old, new); } } // Match fresh presentation identities by exact destination diagram + mapped // element. Every other placement property remains independently expected. let mut placement_count = 0; for (diagram, view) in &expected.diagrams { for node in view.nodes.values().filter(|node| !p.contains(node.id)) { let mapped_element = map.get(&node.element).copied().unwrap_or(node.element); let actual_nodes: Vec<_> = actual .diagrams .get(diagram) .expect("Expected destination diagram remains") .nodes .values() .filter(|n| n.element == mapped_element) .collect(); assert_eq!( actual_nodes.len(), 1, "Each generated placement must identify its exact diagram and mapped element" ); let actual_id = actual_nodes[0].id; assert!(!p.contains(actual_id)); assert!(actual_used.insert(actual_id)); map.insert(node.id, actual_id); placement_count += 1; } } expected = remap(expected, &map); if matches!(mode, "reverse" | "reverse-behavior") { reverse_assertions(&p, &expected); } assert_eq!( expected, actual, "full model oracle differs after UUID-only normalization" ); let updated = source_project::update_bundle( &compiled, &expected, source_project::ProjectLimits::default(), ) .unwrap(); let mut actual_files = actual_compiled.bundle.files.clone(); let actual_entry = actual_files.remove(&actual_compiled.bundle.entry).unwrap(); assert!( actual_files .insert(updated.bundle.entry.clone(), actual_entry) .is_none() ); assert_eq!( updated.bundle.files, actual_files, "complete exact source bundle differs (only entry filename normalized for Save As)" ); fs::create_dir_all(out).unwrap(); write_bundle(out, &updated.bundle); fs::write( out.join("expected-native.json"), serde_json::to_vec_pretty(&expected).unwrap(), ) .unwrap(); fs::write( out.join("native-request.json"), serde_json::to_vec_pretty(&r).unwrap(), ) .unwrap(); println!( "PASS {mode}: complete model and exact source; {} generated UUID substitutions only ({placement_count} placement identities)", map.len() ); } fn main() { let args: Vec<_> = std::env::args().collect(); match args.get(1).map(String::as_str) { Some("generate") if args.len() == 3 => generate(Path::new(&args[2])), Some("generate-reverse-behavior") if args.len() == 4 => { generate_reverse_behavior(Path::new(&args[2]), Path::new(&args[3])) } Some("check") if args.len() == 6 => check( &args[2], Path::new(&args[3]), Path::new(&args[4]), Path::new(&args[5]), ), _ => panic!( "generate DIRECTORY | generate-reverse-behavior BEFORE.arcadia OUTPUT_DIRECTORY | check draw|reuse|create|reverse|reverse-behavior BEFORE.arcadia AFTER.arcadia OUTPUT_DIRECTORY" ), } }