use oa_core::{transitions::*, *}; use std::collections::BTreeMap; fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str) -> Id { p.add_element(kind, layer, name, None).unwrap() } fn trace(p: &mut Project, oa: Id, sa: Id) -> Id { let id = Id::new_v4(); p.traces.insert( id, Trace { id, alias: format!("trace_{}", id.simple()), name: "Review".into(), description: String::new(), oa, sa, kind: TraceKind::Manual, baseline: Some(TraceBaseline { oa: p.properties(oa).unwrap(), sa: p.properties(sa).unwrap(), semantic: None, }), }, ); id } fn request( oa: Id, sa: Id, facets: Vec<(FacetSelector, SemanticDecision)>, ) -> SemanticTransitionRequest { let mut transition = TransitionRequest::new(TransitionDirection::OaToSa, vec![oa]); transition.mappings.push(TransitionMapping { source: oa, target: Some(sa), target_kind: None, }); let mut request = SemanticTransitionRequest::new(transition); request.reviews.push(SemanticReviewRequest { source: oa, resolutions: facets .into_iter() .map(|(facet, decision)| FacetResolution { facet, decision }) .collect(), reset: vec![], }); request } fn applied(p: &Project, r: SemanticTransitionRequest) -> Project { let plan = plan_semantic_transition(p, r).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); apply_semantic_transition(p, &plan).unwrap() } fn data(p: &mut Project, id: Id, min: i64, max: i64, unit: &str) { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum: Some(min), maximum: Some(max), }, default: None, unit: unit.into(), }; } #[test] fn independent_bounds_keep_take_and_stale_prepare() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "bounded"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "bounded"); data(&mut p, oa, 0, 10, "m"); data(&mut p, sa, 0, 10, "m"); let t = trace(&mut p, oa, sa); let min = FacetSelector::Data(DataFacet::IntegerMinimum); let max = FacetSelector::Data(DataFacet::IntegerMaximum); p = applied( &p, request( oa, sa, vec![ (min.clone(), SemanticDecision::KeepTarget), (max.clone(), SemanticDecision::KeepTarget), ], ), ); data(&mut p, oa, 2, 12, "m"); data(&mut p, sa, 0, 10, "cm"); let plan = plan_semantic_transition( &p, request(oa, sa, vec![(min.clone(), SemanticDecision::KeepTarget)]), ) .unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let q = apply_semantic_transition(&p, &plan).unwrap(); let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap(); assert_eq!( facts.iter().find(|f| f.selector == min).unwrap().drift, Drift::Unchanged ); assert_eq!( facts.iter().find(|f| f.selector == max).unwrap().drift, Drift::OaChanged ); assert_eq!( facts .iter() .find(|f| f.selector == FacetSelector::Data(DataFacet::Unit)) .unwrap() .drift, Drift::Unreviewed ); let taken = applied( &q, request(oa, sa, vec![(max.clone(), SemanticDecision::TakeSource)]), ); assert!( matches!(taken.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Integer{minimum:Some(0),maximum:Some(12)},unit,..}) if unit=="cm") ); let mut stale = p.clone(); stale.name.push('x'); assert!(prepare_semantic_transition(&stale, &plan).is_err()); assert_eq!(apply_semantic_transition(&p, &plan).unwrap(), q); } #[test] fn constructors_expand_into_independent_facts_without_outer_prose() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "definition"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "definition"); data(&mut p, oa, 1, 9, "m"); *p.elements.get_mut(&sa).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Real { minimum: None, maximum: Some(20.), }, default: Some(DataValue::Real(1.)), unit: "cm".into(), }; let t = trace(&mut p, oa, sa); let group = FacetSelector::Data(DataFacet::Constructor); p = applied( &p, request(oa, sa, vec![(group.clone(), SemanticDecision::TakeSource)]), ); let base = p.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert!( base.facets .iter() .any(|f| f.selector() == FacetSelector::Data(DataFacet::IntegerMaximum)) ); assert!( base.facets .iter() .any(|f| f.selector() == FacetSelector::Data(DataFacet::Default)) ); assert!( !base .facets .iter() .any(|f| f.selector() == FacetSelector::Data(DataFacet::Unit)) ); assert!(matches!(p.elements[&sa].shared(),Some(SharedBody::DataType{unit,..}) if unit=="cm")); let bad = plan_semantic_transition( &p, request(oa, sa, vec![(group, SemanticDecision::KeepTarget)]), ) .unwrap(); assert!(!bad.conflicts.is_empty()); } #[test] fn enum_members_preserve_target_only_and_no_whole_list_overwrite() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "statuses"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "statuses"); for (id, literals) in [ (oa, vec!["Ready", "Paused"]), (sa, vec!["Ready", "Maintenance"]), ] { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals: literals.into_iter().map(str::to_string).collect(), }, default: None, unit: String::new(), }; } trace(&mut p, oa, sa); let r = request( oa, sa, vec![ ( FacetSelector::Literal(KeyPair { oa: Some("Paused".into()), sa: Some("Paused".into()), }), SemanticDecision::TakeSource, ), ( FacetSelector::Literal(KeyPair { oa: None, sa: Some("Maintenance".into()), }), SemanticDecision::TakeSource, ), ], ); let q = applied(&p, r); assert!( matches!(q.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Enumeration{literals},..}) if literals==&vec!["Ready".to_string(),"Maintenance".into(),"Paused".into()]) ); } #[test] fn wrong_family_both_na_and_duplicate_resolutions_rejected_atomically() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "boolean"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "boolean"); trace(&mut p, oa, sa); for facet in [ FacetSelector::State(StateFacet::Entry), FacetSelector::Data(DataFacet::IntegerMinimum), ] { let r = request(oa, sa, vec![(facet, SemanticDecision::KeepTarget)]); let plan = plan_semantic_transition(&p, r).unwrap(); assert!(!plan.conflicts.is_empty()); assert!(apply_semantic_transition(&p, &plan).is_err()); } let facet = FacetSelector::Data(DataFacet::Unit); let plan = plan_semantic_transition( &p, request( oa, sa, vec![ (facet.clone(), SemanticDecision::KeepTarget), (facet, SemanticDecision::TakeSource), ], ), ) .unwrap(); assert!(!plan.conflicts.is_empty()); } #[test] fn historical_reference_remap_preserves_text_and_deleted_reference_observations() { let old = Id::new_v4(); let new = Id::new_v4(); let mut base = SemanticBaseline { facets: vec![ FacetReview::Field(FieldReview::Type(Reviewed { oa: Observed::Value(TypeRef::Named(old)), sa: Observed::Value(TypeRef::Boolean), })), FacetReview::Data(DataReview::Unit(Reviewed { oa: Observed::Value(old.to_string()), sa: Observed::Value("m".into()), })), ], }; assert!(validate_semantic_baseline(&base).is_ok()); visit_semantic_references(&mut base, |id| { if *id == old { *id = new; } }); assert_eq!( base.facets[0], FacetReview::Field(FieldReview::Type(Reviewed { oa: Observed::Value(TypeRef::Named(new)), sa: Observed::Value(TypeRef::Boolean) })) ); assert_eq!( base.facets[1], FacetReview::Data(DataReview::Unit(Reviewed { oa: Observed::Value(old.to_string()), sa: Observed::Value("m".into()) })) ); } #[test] fn same_target_member_keys_cannot_last_write_win() { let a = Id::new_v4(); let b = Id::new_v4(); let c = Id::new_v4(); let review = Reviewed { oa: Observed::Value(true), sa: Observed::Value(false), }; let base = SemanticBaseline { facets: vec![ FacetReview::Member { collection: ReferenceCollection::TransitionTriggers, keys: KeyPair { oa: Some(a), sa: Some(c), }, review: review.clone(), }, FacetReview::Member { collection: ReferenceCollection::TransitionTriggers, keys: KeyPair { oa: Some(b), sa: Some(c), }, review, }, ], }; assert!(validate_semantic_baseline(&base).is_err()); } #[test] fn source_and_target_semantic_headers_can_be_reviewed_in_reverse() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "OA"); let sa = add(&mut p, Kind::Function, Layer::Sa, "SA"); trace(&mut p, oa, sa); let mut r = request( oa, sa, vec![( FacetSelector::Header(HeaderFacet::Name), SemanticDecision::TakeSource, )], ); r.transition.direction = TransitionDirection::SaToOa; r.transition.sources = vec![sa]; r.transition.mappings = vec![TransitionMapping { source: sa, target: Some(oa), target_kind: None, }]; r.reviews[0].source = sa; let q = applied(&p, r); assert_eq!(q.elements[&oa].name, "SA"); } fn owned(p: &mut Project, kind: Kind, layer: Layer, name: &str, parent: Option) -> Id { p.add_element(kind, layer, name, parent).unwrap() } fn set(p: &mut Project, id: Id, body: SharedBody) { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body; } fn field(p: &mut Project, layer: Layer, owner: Id, name: &str, type_ref: TypeRef, max: u32) -> Id { let id = owned(p, Kind::DataField, layer, name, Some(owner)); set( p, id, SharedBody::DataField { type_ref, minimum: 0, maximum: Some(max), unit: String::new(), direction: ParameterDirection::Unspecified, storage: FieldStorage::Value, default: None, }, ); id } fn record(p: &mut Project, layer: Layer, name: &str, supertype: Option) -> Id { let id = owned(p, Kind::DataType, layer, name, None); set( p, id, SharedBody::DataType { definition: DataTypeDefinition::Record { supertype }, default: None, unit: String::new(), }, ); id } fn occurrence(p: &mut Project, layer: Layer, owner: Id, name: &str, body: OccurrenceBody) -> Id { let id = owned(p, Kind::Occurrence, layer, name, Some(owner)); set( p, id, SharedBody::Occurrence { body, precondition: String::new(), postcondition: String::new(), }, ); p.elements .get_mut(&owner) .unwrap() .steps_mut() .unwrap() .push(id); id } struct Payload { message: Id, record: Id, leaf: Id, first: AnnotationKey, second: AnnotationKey, } fn payload(p: &mut Project, layer: Layer, prefix: &str) -> Payload { let coord = record(p, layer, &format!("{prefix} Coordinate"), None); let leaf = field( p, layer, coord, &format!("{prefix} latitude"), TypeRef::Real, 1, ); let item = owned( p, Kind::ExchangeItem, layer, &format!("{prefix} Item"), None, ); let origin = field( p, layer, item, &format!("{prefix} origin"), TypeRef::Named(coord), 1, ); let destination = field( p, layer, item, &format!("{prefix} destination"), TypeRef::Named(coord), 1, ); let a = owned( p, if layer == Layer::Oa { Kind::Activity } else { Kind::Function }, layer, &format!("{prefix} A"), None, ); let b = owned( p, if layer == Layer::Oa { Kind::Activity } else { Kind::Function }, layer, &format!("{prefix} B"), None, ); let exchange = p.relate( if layer == Layer::Oa { RelationKind::Interaction } else { RelationKind::FunctionalExchange }, &format!("{prefix} exchange"), a, b, ); match &mut p.relations.get_mut(&exchange).unwrap().body { RelationBody::Interaction { items, .. } | RelationBody::FunctionalExchange { items, .. } => items.push(item), _ => unreachable!(), } if let RelationBody::FunctionalExchange { source_port, target_port, .. } = p.relations[&exchange].body { for id in [source_port, target_port] { match p.elements.get_mut(&id).unwrap().shared_mut().unwrap() { SharedBody::Port { contract: PortContract::FunctionInput { incoming_items }, .. } => incoming_items.push(item), SharedBody::Port { contract: PortContract::FunctionOutput { outgoing_items }, .. } => outgoing_items.push(item), _ => unreachable!(), } } } let scenario = owned( p, Kind::Scenario, layer, &format!("{prefix} scenario"), None, ); let first = AnnotationKey { path: vec![origin], field: leaf, }; let second = AnnotationKey { path: vec![destination], field: leaf, }; let message = occurrence( p, layer, scenario, &format!("{prefix} message"), OccurrenceBody::Exchange { operation: None, exchange: Some(exchange), sender: None, receiver: None, message_kind: MessageKind::Asynchronous, reply_to: None, values: vec![ MessageValue { path: first.path.clone(), field: leaf, expression: "first old".into(), }, MessageValue { path: second.path.clone(), field: leaf, expression: "second independent".into(), }, ], }, ); Payload { message, record: coord, leaf, first, second, } } fn values_mut(p: &mut Project, id: Id) -> &mut Vec { let Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { values, .. }, .. }) = p.elements.get_mut(&id).unwrap().shared_mut() else { panic!() }; values } struct Use { use_id: Id, referenced: Id, alternate: Id, extra_referenced: Id, } fn scenario_use(p: &mut Project, layer: Layer, prefix: &str) -> Use { let caller = owned(p, Kind::Scenario, layer, &format!("{prefix} caller"), None); let callee = owned(p, Kind::Scenario, layer, &format!("{prefix} callee"), None); let mut line = |owner, name: &str| { owned( p, Kind::Lifeline, layer, &format!("{prefix} {name}"), Some(owner), ) }; let referenced = line(callee, "referenced"); let extra_referenced = line(callee, "extra referenced"); let old_local = line(caller, "local"); let alternate = line(caller, "alternate"); let extra_local = line(caller, "extra local"); let use_id = occurrence( p, layer, caller, &format!("{prefix} use"), OccurrenceBody::ScenarioUse { scenario: Some(callee), bindings: vec![ LifelineBinding { referenced, local: old_local, }, LifelineBinding { referenced: extra_referenced, local: extra_local, }, ], }, ); Use { use_id, referenced, alternate, extra_referenced, } } fn bindings_mut(p: &mut Project, id: Id) -> &mut Vec { let Some(SharedBody::Occurrence { body: OccurrenceBody::ScenarioUse { bindings, .. }, .. }) = p.elements.get_mut(&id).unwrap().shared_mut() else { panic!() }; bindings } #[test] fn atomic_annotation_full_path_updates_only_selected_row() { let mut p = Project::empty(); let a = payload(&mut p, Layer::Oa, "oa"); let b = payload(&mut p, Layer::Sa, "sa"); trace(&mut p, a.message, b.message); values_mut(&mut p, a.message)[0].expression = "new first".into(); let retained = values_mut(&mut p, b.message)[1].clone(); let selector = FacetSelector::Annotation(KeyPair { oa: Some(a.first.clone()), sa: Some(b.first.clone()), }); let mut r = request( a.message, b.message, vec![(selector, SemanticDecision::TakeSource)], ); r.references.extend( a.first .path .iter() .copied() .zip(b.first.path.iter().copied()), ); r.references.insert(a.leaf, b.leaf); let mut q = applied(&p, r); let rows = values_mut(&mut q, b.message); assert_eq!(rows.len(), 2); assert!(rows.contains(&retained)); assert_eq!(AnnotationKey::from(&rows[0]), b.first); assert_eq!( rows[1], retained, "Unselected rows retain their authored position" ); assert!( rows.iter() .any(|v| AnnotationKey::from(v) == b.first && v.expression == "new first") ); assert!( rows.iter() .any(|v| AnnotationKey::from(v) == b.second && v.expression == retained.expression) ); } #[test] fn keeping_keyed_rows_with_an_unrelated_header_candidate_preserves_exact_authored_order() { for binding in [false, true] { for reverse in [false, true] { let mut p = Project::empty(); let (oa, sa, selector, references) = if binding { let a = scenario_use(&mut p, Layer::Oa, "oa"); let b = scenario_use(&mut p, Layer::Sa, "sa"); ( a.use_id, b.use_id, FacetSelector::Binding(KeyPair { oa: Some(a.referenced), sa: Some(b.referenced), }), BTreeMap::from([(a.referenced, b.referenced)]), ) } else { let a = payload(&mut p, Layer::Oa, "oa"); let b = payload(&mut p, Layer::Sa, "sa"); let mut references: BTreeMap<_, _> = a .first .path .iter() .copied() .zip(b.first.path.iter().copied()) .collect(); references.insert(a.leaf, b.leaf); ( a.message, b.message, FacetSelector::Annotation(KeyPair { oa: Some(a.first), sa: Some(b.first), }), references, ) }; trace(&mut p, oa, sa); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; let mut candidate = p.elements[&target].clone(); candidate.description = "Review the header while retaining authored rows".into(); let mut review = request( source, target, vec![ (selector, SemanticDecision::KeepTarget), ( FacetSelector::Header(HeaderFacet::Description), SemanticDecision::Candidate, ), ], ); if reverse { review.transition.direction = TransitionDirection::SaToOa; } review.references = references; review .candidates .push(SemanticRecord::Element(candidate.clone())); let accepted = applied(&p, review); assert_eq!(accepted.elements[&target], candidate); assert_eq!(accepted.elements[&source], p.elements[&source]); } } } #[test] fn atomic_binding_updates_local_and_rejects_pair_collision_and_remapping_without_reset() { let mut p = Project::empty(); let a = scenario_use(&mut p, Layer::Oa, "oa"); let b = scenario_use(&mut p, Layer::Sa, "sa"); trace(&mut p, a.use_id, b.use_id); bindings_mut(&mut p, a.use_id)[0].local = a.alternate; let retained = bindings_mut(&mut p, b.use_id)[1].clone(); let selector = FacetSelector::Binding(KeyPair { oa: Some(a.referenced), sa: Some(b.referenced), }); let mut r = request( a.use_id, b.use_id, vec![(selector.clone(), SemanticDecision::TakeSource)], ); r.references = BTreeMap::from([(a.referenced, b.referenced), (a.alternate, b.alternate)]); let mut q = applied(&p, r.clone()); assert!(bindings_mut(&mut q, b.use_id).contains(&retained)); assert_eq!(bindings_mut(&mut q, b.use_id)[0].referenced, b.referenced); assert_eq!(bindings_mut(&mut q, b.use_id)[1], retained); assert!( bindings_mut(&mut q, b.use_id) .iter() .any(|v| v.referenced == b.referenced && v.local == b.alternate) ); let collided = FacetSelector::Binding(KeyPair { oa: Some(a.extra_referenced), sa: Some(b.referenced), }); let mut bad = r.clone(); bad.references.insert(a.extra_referenced, b.referenced); bad.reviews[0].resolutions.push(FacetResolution { facet: collided, decision: SemanticDecision::KeepTarget, }); assert!( !plan_semantic_transition(&p, bad) .unwrap() .conflicts .is_empty() ); let mapped = FacetSelector::Binding(KeyPair { oa: Some(a.referenced), sa: Some(b.extra_referenced), }); r.references.insert(a.referenced, b.extra_referenced); r.reviews[0].resolutions = vec![FacetResolution { facet: mapped.clone(), decision: SemanticDecision::KeepTarget, }]; assert!( !plan_semantic_transition(&q, r.clone()) .unwrap() .conflicts .is_empty() ); r.reviews[0].reset = vec![selector]; q = applied(&q, r); let t = q.traces.values().find(|t| t.oa == a.use_id).unwrap(); let base = t.baseline.as_ref().unwrap().semantic.as_ref().unwrap(); assert_eq!(base.facets.len(), 1); assert_eq!(base.facets[0].selector(), mapped); } #[test] fn ordered_member_requires_anchor_or_explicit_order_and_preserves_target_only() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Process, Layer::Oa, "process"); let sa = add(&mut p, Kind::FunctionalChain, Layer::Sa, "chain"); let a = occurrence( &mut p, Layer::Oa, oa, "a", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); let c = occurrence( &mut p, Layer::Oa, oa, "c", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); let b = occurrence( &mut p, Layer::Oa, oa, "b", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); let ta = occurrence( &mut p, Layer::Sa, sa, "a", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); let tc = occurrence( &mut p, Layer::Sa, sa, "c", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); let tb = occurrence( &mut p, Layer::Sa, sa, "b", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); let extra = occurrence( &mut p, Layer::Sa, sa, "local", OccurrenceBody::Control { kind: ControlKind::Choice, }, ); *p.elements.get_mut(&oa).unwrap().steps_mut().unwrap() = vec![a, c, b]; *p.elements.get_mut(&sa).unwrap().steps_mut().unwrap() = vec![ta, extra, tb]; let t = trace(&mut p, oa, sa); let member = FacetSelector::Member { collection: ReferenceCollection::Steps, keys: KeyPair { oa: Some(c), sa: Some(tc), }, }; let mut r = request(oa, sa, vec![(member.clone(), SemanticDecision::TakeSource)]); r.references = BTreeMap::from([(a, ta), (b, tb), (c, tc)]); assert!( !plan_semantic_transition(&p, r.clone()) .unwrap() .conflicts .is_empty() ); r.reviews[0].resolutions.push(FacetResolution { facet: FacetSelector::Order(OrderedCollection::Steps), decision: SemanticDecision::Candidate, }); let mut candidate = p.elements[&sa].clone(); *candidate.steps_mut().unwrap() = vec![ta, extra, tc, tb]; r.candidates.push(SemanticRecord::Element(candidate)); let q = applied(&p, r); assert_eq!(q.elements[&sa].steps().unwrap(), &vec![ta, extra, tc, tb]); let facts = q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert_eq!(facts.facets.len(), 2); } #[test] fn prepared_inherited_annotation_cleanup_must_be_explicitly_accepted() { let mut p = Project::empty(); let a = payload(&mut p, Layer::Oa, "oa"); let b = payload(&mut p, Layer::Sa, "sa"); for (layer, part) in [(Layer::Oa, &a), (Layer::Sa, &b)] { let parent = record(&mut p, layer, "base", None); p.elements.get_mut(&part.leaf).unwrap().parent = Some(parent); set( &mut p, part.record, SharedBody::DataType { definition: DataTypeDefinition::Record { supertype: Some(parent), }, default: None, unit: String::new(), }, ); } trace(&mut p, a.record, b.record); let mut changed = p.elements[&a.record].clone(); set_record_supertype(&mut changed, None); p = prepare_command(&p, &Command::EditElement(a.record, changed)) .unwrap() .project; assert!(values_mut(&mut p, a.message).is_empty()); assert_eq!(values_mut(&mut p, b.message).len(), 2); let mut r = request( a.record, b.record, vec![( FacetSelector::Data(DataFacet::RecordSupertype), SemanticDecision::TakeSource, )], ); let plan = plan_semantic_transition(&p, r.clone()).unwrap(); assert!(!plan.conflicts.is_empty()); assert_eq!(plan.impacts.len(), 1); assert_eq!(plan.impacts[0].object, b.message); assert!(apply_semantic_transition(&p, &plan).is_err()); r.accepted_impacts = plan.impacts; let mut q = applied(&p, r); assert!(values_mut(&mut q, b.message).is_empty()); } fn set_record_supertype(e: &mut Element, supertype: Option) { let Some(SharedBody::DataType { definition: DataTypeDefinition::Record { supertype: s }, .. }) = e.shared_mut() else { panic!() }; *s = supertype; } #[test] fn repeated_keep_of_unequal_constructor_pair_cannot_swallow_bound_drift() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "real"); data(&mut p, oa, 0, 10, "m"); set( &mut p, sa, SharedBody::DataType { definition: DataTypeDefinition::Real { minimum: Some(0.), maximum: Some(20.), }, default: None, unit: "m".into(), }, ); let t = trace(&mut p, oa, sa); let selector = FacetSelector::Data(DataFacet::Constructor); p = applied( &p, request( oa, sa, vec![(selector.clone(), SemanticDecision::KeepTarget)], ), ); data(&mut p, oa, 0, 12, "m"); let plan = plan_semantic_transition( &p, request( oa, sa, vec![(selector.clone(), SemanticDecision::KeepTarget)], ), ) .unwrap(); assert!( plan.conflicts .iter() .any(|c| c.contains("No constructor switch")) ); let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap(); assert_eq!( facts .iter() .find(|f| f.selector == FacetSelector::Data(DataFacet::IntegerMaximum)) .unwrap() .drift, Drift::OaChanged ); let converted = applied( &p, request(oa, sa, vec![(selector, SemanticDecision::TakeSource)]), ); assert!(matches!( converted.elements[&sa].shared(), Some(SharedBody::DataType { definition: DataTypeDefinition::Integer { maximum: Some(12), .. }, .. }) )); } fn state_pair(p: &mut Project) -> (Id, Id) { let mut pair = vec![]; for layer in [Layer::Oa, Layer::Sa] { let machine = add(p, Kind::StateMachine, layer, "machine"); let region = owned(p, Kind::Region, layer, "region", Some(machine)); pair.push(owned(p, Kind::State, layer, "ready", Some(region))); } (pair[0], pair[1]) } #[test] fn changed_state_kind_remains_reviewable_in_both_directions() { let mut p = Project::empty(); let (oa, sa) = state_pair(&mut p); trace(&mut p, oa, sa); let Some(SharedBody::State { kind, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut() else { panic!() }; *kind = StateKind::Final; let selector = FacetSelector::State(StateFacet::Kind); let kept = applied( &p, request( oa, sa, vec![(selector.clone(), SemanticDecision::KeepTarget)], ), ); assert_eq!(kept.elements[&sa].kind(), Kind::State); let taken = applied( &p, request( oa, sa, vec![(selector.clone(), SemanticDecision::TakeSource)], ), ); assert_eq!(taken.elements[&sa].kind(), Kind::Final); let mut reverse = request(oa, sa, vec![(selector, SemanticDecision::TakeSource)]); reverse.transition.direction = TransitionDirection::SaToOa; reverse.transition.sources = vec![sa]; reverse.transition.mappings = vec![TransitionMapping { source: sa, target: Some(oa), target_kind: None, }]; reverse.reviews[0].source = sa; let back = applied(&p, reverse); assert_eq!(back.elements[&oa].kind(), Kind::State); } #[test] fn constructor_retirement_cannot_override_other_sources_kept_bound_in_either_order() { let mut p = Project::empty(); let a = add(&mut p, Kind::DataType, Layer::Oa, "real"); let b = add(&mut p, Kind::DataType, Layer::Oa, "integer"); let target = add(&mut p, Kind::DataType, Layer::Sa, "real"); for id in [a, target] { set( &mut p, id, SharedBody::DataType { definition: DataTypeDefinition::Real { minimum: Some(0.), maximum: Some(20.), }, default: None, unit: String::new(), }, ); } data(&mut p, b, 0, 10, ""); trace(&mut p, a, target); trace(&mut p, b, target); let mut r = request( a, target, vec![( FacetSelector::Data(DataFacet::RealMaximum), SemanticDecision::KeepTarget, )], ); r.transition.sources.push(b); r.transition.mappings.push(TransitionMapping { source: b, target: Some(target), target_kind: None, }); r.reviews.push(SemanticReviewRequest { source: b, resolutions: vec![FacetResolution { facet: FacetSelector::Data(DataFacet::Constructor), decision: SemanticDecision::TakeSource, }], reset: vec![], }); for reverse in [false, true] { let mut r = r.clone(); if reverse { r.reviews.reverse(); } let plan = plan_semantic_transition(&p, r).unwrap(); assert!( plan.conflicts.iter().any(|c| c.contains("Contradictory")), "{:?}", plan.conflicts ); assert!(apply_semantic_transition(&p, &plan).is_err()); } } #[test] fn semantic_rollup_does_not_claim_unchanged_when_only_unit_is_reviewed() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "integer"); data(&mut p, oa, 0, 10, "m"); data(&mut p, sa, 0, 10, "m"); let t = trace(&mut p, oa, sa); p = applied( &p, request( oa, sa, vec![( FacetSelector::Data(DataFacet::Unit), SemanticDecision::KeepTarget, )], ), ); data(&mut p, oa, 0, 12, "m"); let status = trace_semantic_drift(&p, &p.traces[&t], &BTreeMap::new()).unwrap(); assert_eq!(status.own, Drift::Unreviewed); assert!(status.own_unreviewed); } fn artifact(p: &mut Project, owner: Id, name: &str) -> Id { let id = Id::new_v4(); p.artifact_links.insert( id, ArtifactLink { id, alias: format!("artifact_{}", id.simple()), name: name.into(), description: String::new(), owner, relation: ArtifactRelation::References, uri: "https://example.test/requirement".into(), revision: None, fragment: None, }, ); id } #[test] fn artifact_metadata_is_attached_and_cannot_last_write_through_two_owner_traces() { let mut p = Project::empty(); let a = add(&mut p, Kind::Activity, Layer::Oa, "a"); let b = add(&mut p, Kind::Activity, Layer::Oa, "b"); let ta = add(&mut p, Kind::Function, Layer::Sa, "a"); let tb = add(&mut p, Kind::Function, Layer::Sa, "b"); trace(&mut p, a, ta); trace(&mut p, b, tb); let aa = artifact(&mut p, a, "A"); let ab = artifact(&mut p, b, "B"); let at = artifact(&mut p, ta, "retained"); let mut r = request( a, ta, vec![( FacetSelector::Artifact { keys: KeyPair { oa: Some(aa), sa: Some(at), }, facet: ArtifactFacet::Name, }, SemanticDecision::TakeSource, )], ); r.transition.sources.push(b); r.transition.mappings.push(TransitionMapping { source: b, target: Some(tb), target_kind: None, }); r.references = BTreeMap::from([(aa, at), (ab, at)]); r.reviews.push(SemanticReviewRequest { source: b, resolutions: vec![FacetResolution { facet: FacetSelector::Artifact { keys: KeyPair { oa: Some(ab), sa: Some(at), }, facet: ArtifactFacet::Name, }, decision: SemanticDecision::TakeSource, }], reset: vec![], }); for reverse in [false, true] { let mut r = r.clone(); if reverse { r.reviews.reverse(); } let plan = plan_semantic_transition(&p, r).unwrap(); assert!(!plan.conflicts.is_empty()); assert!(apply_semantic_transition(&p, &plan).is_err()); } let mut same = request( a, ta, vec![( FacetSelector::Artifact { keys: KeyPair { oa: Some(aa), sa: Some(at), }, facet: ArtifactFacet::Name, }, SemanticDecision::TakeSource, )], ); same.references.insert(aa, at); let q = applied(&p, same); assert_eq!(q.artifact_links[&at].name, "A"); } #[test] fn child_membership_and_parent_resolutions_agree_by_actual_child_slot() { let mut p = Project::empty(); let owner = add(&mut p, Kind::Activity, Layer::Oa, "owner"); let child = owned(&mut p, Kind::Activity, Layer::Oa, "child", Some(owner)); let target = add(&mut p, Kind::Function, Layer::Sa, "owner"); let target_child = add(&mut p, Kind::Function, Layer::Sa, "child"); let other = add(&mut p, Kind::Function, Layer::Sa, "other"); trace(&mut p, owner, target); trace(&mut p, child, target_child); let mut r = request( owner, target, vec![( FacetSelector::Child(KeyPair { oa: Some(child), sa: Some(target_child), }), SemanticDecision::TakeSource, )], ); r.transition.sources.push(child); r.transition.mappings.push(TransitionMapping { source: child, target: Some(target_child), target_kind: None, }); r.reviews.push(SemanticReviewRequest { source: child, resolutions: vec![FacetResolution { facet: FacetSelector::Header(HeaderFacet::Parent), decision: SemanticDecision::TakeSource, }], reset: vec![], }); let q = applied(&p, r.clone()); assert_eq!(q.elements[&target_child].parent, Some(target)); r.reviews[1].resolutions[0].decision = SemanticDecision::Candidate; let mut candidate = p.elements[&target_child].clone(); candidate.parent = Some(other); r.candidates.push(SemanticRecord::Element(candidate)); for reverse in [false, true] { let mut r = r.clone(); if reverse { r.reviews.reverse(); } let plan = plan_semantic_transition(&p, r).unwrap(); assert!( plan.conflicts.iter().any(|c| c.contains("Contradictory")), "{:?}", plan.conflicts ); } } #[test] fn state_guard_and_deleted_trigger_membership_preserve_target_only_trigger() { let mut p = Project::empty(); let (oa, sa) = state_pair(&mut p); let a = p.relate(RelationKind::StateTransition, "self", oa, oa); let b = p.relate(RelationKind::StateTransition, "self", sa, sa); let old = add(&mut p, Kind::Event, Layer::Oa, "old"); let retained = add(&mut p, Kind::Event, Layer::Sa, "old"); let local = add(&mut p, Kind::Event, Layer::Sa, "local"); for (id, triggers) in [(a, vec![old]), (b, vec![retained, local])] { let RelationBody::StateTransition { triggers: t, guard, .. } = &mut p.relations.get_mut(&id).unwrap().body else { panic!() }; *t = triggers; *guard = "old".into(); } let t = trace(&mut p, a, b); let selector = FacetSelector::Member { collection: ReferenceCollection::TransitionTriggers, keys: KeyPair { oa: Some(old), sa: Some(retained), }, }; let mut r = request(a, b, vec![(selector.clone(), SemanticDecision::KeepTarget)]); r.references.insert(old, retained); p = applied(&p, r.clone()); p = prepare_command(&p, &Command::Delete(old)).unwrap().project; let RelationBody::StateTransition { guard, .. } = &mut p.relations.get_mut(&a).unwrap().body else { panic!() }; *guard = "ready".into(); r.reviews[0].resolutions = vec![ FacetResolution { facet: selector, decision: SemanticDecision::TakeSource, }, FacetResolution { facet: FacetSelector::Relation(RelationFacet::Guard), decision: SemanticDecision::TakeSource, }, ]; let q = applied(&p, r); assert!( matches!(&q.relations[&b].body,RelationBody::StateTransition{guard,triggers,..} if guard=="ready"&&triggers==&vec![local]) ); assert!(q.elements.contains_key(&retained)); assert!(q.traces[&t].baseline.as_ref().unwrap().semantic.is_some()); } #[test] fn artifact_creation_requires_reviewed_metadata_and_deletion_retires_exact_facts() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa"); let sa = add(&mut p, Kind::Function, Layer::Sa, "sa"); let t = trace(&mut p, oa, sa); let a = artifact(&mut p, oa, "requirement"); let b = Id::new_v4(); let keys = KeyPair { oa: Some(a), sa: Some(b), }; let mut candidate = p.artifact_links[&a].clone(); candidate.id = b; candidate.alias = format!("artifact_{}", b.simple()); candidate.owner = sa; let mut r = request( oa, sa, vec![( FacetSelector::Artifact { keys: keys.clone(), facet: ArtifactFacet::Presence, }, SemanticDecision::TakeSource, )], ); r.references.insert(a, b); r.candidates .push(SemanticRecord::Artifact(candidate.clone())); assert!( !plan_semantic_transition(&p, r.clone()) .unwrap() .conflicts .is_empty() ); for facet in [ ArtifactFacet::Name, ArtifactFacet::Description, ArtifactFacet::Owner, ArtifactFacet::Relation, ArtifactFacet::Uri, ArtifactFacet::Revision, ArtifactFacet::Fragment, ] { r.reviews[0].resolutions.push(FacetResolution { facet: FacetSelector::Artifact { keys: keys.clone(), facet, }, decision: SemanticDecision::Candidate, }); } p = applied(&p, r); assert_eq!(p.artifact_links[&b], candidate); assert_eq!( p.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .len(), 8 ); p = prepare_command(&p, &Command::Delete(a)).unwrap().project; let mut removal = request( oa, sa, vec![( FacetSelector::Artifact { keys, facet: ArtifactFacet::Presence, }, SemanticDecision::TakeSource, )], ); removal.references.insert(a, b); let plan = plan_semantic_transition(&p, removal).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); assert_eq!(plan.reviews[0].retired.len(), 7); let q = apply_semantic_transition(&p, &plan).unwrap(); assert!(!q.artifact_links.contains_key(&b)); assert_eq!( q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .len(), 1 ); } #[test] fn semantic_batch_is_one_undo_and_replanning_acknowledged_values_is_noop() { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "source"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "target"); data(&mut p, oa, 0, 12, "m"); data(&mut p, sa, 0, 10, "ft"); trace(&mut p, oa, sa); let r = request( oa, sa, vec![ ( FacetSelector::Header(HeaderFacet::Name), SemanticDecision::TakeSource, ), ( FacetSelector::Data(DataFacet::IntegerMaximum), SemanticDecision::TakeSource, ), ( FacetSelector::Data(DataFacet::Unit), SemanticDecision::KeepTarget, ), ], ); let plan = plan_semantic_transition(&p, r.clone()).unwrap(); assert!(plan.conflicts.is_empty()); let mut session = Session::new(p.clone()); session.execute(Command::Batch(plan.commands)).unwrap(); let accepted = session.project().clone(); assert_eq!(session.revision(), 1); session.undo(); assert_eq!(session.project(), &p); session.redo(); assert_eq!(session.project(), &accepted); let repeated = plan_semantic_transition(&accepted, r).unwrap(); assert!(repeated.conflicts.is_empty()); assert_eq!( apply_semantic_transition(&accepted, &repeated).unwrap(), accepted ); let mut invalid = request( oa, sa, vec![( FacetSelector::Data(DataFacet::IntegerMinimum), SemanticDecision::Candidate, )], ); let mut candidate = accepted.elements[&sa].clone(); let Some(SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum, .. }, .. }) = candidate.shared_mut() else { panic!() }; *minimum = Some(100); invalid.candidates.push(SemanticRecord::Element(candidate)); let rejected = plan_semantic_transition(&accepted, invalid).unwrap(); assert!(!rejected.conflicts.is_empty()); assert!(apply_semantic_transition(&accepted, &rejected).is_err()); assert_eq!(session.project(), &accepted); } #[test] fn explicit_trace_selection_keeps_independent_trace_baselines() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "source"); let sa = add(&mut p, Kind::Function, Layer::Sa, "target"); let first = trace(&mut p, oa, sa); let second = Id::new_v4(); let mut extra = p.traces[&first].clone(); extra.id = second; extra.alias = format!("trace_{}", second.simple()); extra.kind = TraceKind::Refinement; extra.baseline = None; p.traces.insert(second, extra); let mut r = request( oa, sa, vec![( FacetSelector::Header(HeaderFacet::Name), SemanticDecision::KeepTarget, )], ); assert!( !plan_semantic_transition(&p, r.clone()) .unwrap() .conflicts .is_empty() ); r.traces.insert(oa, second); let q = applied(&p, r); assert!( q.traces[&first] .baseline .as_ref() .unwrap() .semantic .is_none() ); assert_eq!( q.traces[&second] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap() .facets .len(), 1 ); } fn enumeration(p: &mut Project, id: Id, literals: &[&str]) { set( p, id, SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals: literals.iter().map(|s| s.to_string()).collect(), }, default: None, unit: String::new(), }, ); } #[test] fn constructor_union_conflicts_with_another_sources_new_member_in_either_order() { let mut p = Project::empty(); let a = add(&mut p, Kind::DataType, Layer::Oa, "enumeration"); let b = add(&mut p, Kind::DataType, Layer::Oa, "boolean"); let target = add(&mut p, Kind::DataType, Layer::Sa, "enumeration"); enumeration(&mut p, a, &["New"]); enumeration(&mut p, target, &[]); trace(&mut p, a, target); trace(&mut p, b, target); let mut r = request( a, target, vec![( FacetSelector::Literal(KeyPair { oa: Some("New".into()), sa: Some("New".into()), }), SemanticDecision::TakeSource, )], ); r.transition.sources.push(b); r.transition.mappings.push(TransitionMapping { source: b, target: Some(target), target_kind: None, }); r.reviews.push(SemanticReviewRequest { source: b, resolutions: vec![FacetResolution { facet: FacetSelector::Data(DataFacet::Constructor), decision: SemanticDecision::TakeSource, }], reset: vec![], }); for reverse in [false, true] { let mut r = r.clone(); if reverse { r.reviews.reverse(); } let plan = plan_semantic_transition(&p, r).unwrap(); assert!( plan.conflicts.iter().any(|c| c.contains("Contradictory")), "{:?}", plan.conflicts ); } } #[test] fn same_target_constructor_preserves_local_members_and_reset_precedes_expansion() { for ready in [false, true] { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "integer"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "enumeration"); data(&mut p, oa, 0, 10, ""); enumeration( &mut p, sa, if ready { &["Ready", "Local"] } else { &["Local"] }, ); let t = trace(&mut p, oa, sa); let constructor = FacetSelector::Data(DataFacet::Constructor); p = applied( &p, request( oa, sa, vec![(constructor.clone(), SemanticDecision::KeepTarget)], ), ); enumeration(&mut p, oa, if ready { &["Ready"] } else { &["New"] }); let mut r = request(oa, sa, vec![(constructor, SemanticDecision::TakeSource)]); if ready { r.reviews[0].reset.push(FacetSelector::Literal(KeyPair { oa: None, sa: Some("Ready".into()), })); } let q = applied(&p, r); assert!( matches!(q.elements[&sa].shared(),Some(SharedBody::DataType{definition:DataTypeDefinition::Enumeration{literals},..}) if literals.contains(&"Local".to_string())) ); if ready { let base = q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert_eq!(base.facets.iter().filter(|f|matches!(f.selector(),FacetSelector::Literal(KeyPair{sa:Some(ref value),..})if value=="Ready")).count(),1); } } } #[test] fn artifact_owner_candidate_moves_link_and_conflicts_with_kept_attachment() { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa"); let sa = add(&mut p, Kind::Function, Layer::Sa, "sa"); let other = add(&mut p, Kind::Function, Layer::Sa, "other"); trace(&mut p, oa, sa); let a = artifact(&mut p, oa, "source"); let b = artifact(&mut p, sa, "target"); let keys = KeyPair { oa: Some(a), sa: Some(b), }; let mut r = request( oa, sa, vec![( FacetSelector::Artifact { keys: keys.clone(), facet: ArtifactFacet::Owner, }, SemanticDecision::Candidate, )], ); r.references.insert(a, b); let mut candidate = p.artifact_links[&b].clone(); candidate.owner = other; r.candidates.push(SemanticRecord::Artifact(candidate)); let q = applied(&p, r.clone()); assert_eq!(q.artifact_links[&b].owner, other); r.reviews[0].resolutions.push(FacetResolution { facet: FacetSelector::Artifact { keys, facet: ArtifactFacet::Presence, }, decision: SemanticDecision::KeepTarget, }); let plan = plan_semantic_transition(&p, r).unwrap(); assert!( plan.conflicts.iter().any(|c| c.contains("Contradictory")), "{:?}", plan.conflicts ); } #[test] fn shared_definition_drift_is_linked_without_copying_definition_into_field_baseline() { let mut p = Project::empty(); let a = add(&mut p, Kind::DataType, Layer::Oa, "type"); let b = add(&mut p, Kind::DataType, Layer::Sa, "type"); data(&mut p, a, 0, 10, "m"); data(&mut p, b, 0, 10, "m"); trace(&mut p, a, b); let oa_owner = record(&mut p, Layer::Oa, "record", None); let sa_owner = record(&mut p, Layer::Sa, "record", None); let oa = field(&mut p, Layer::Oa, oa_owner, "value", TypeRef::Named(a), 1); let sa = field(&mut p, Layer::Sa, sa_owner, "value", TypeRef::Named(b), 1); let t = trace(&mut p, oa, sa); p = applied( &p, request( a, b, vec![( FacetSelector::Data(DataFacet::IntegerMaximum), SemanticDecision::KeepTarget, )], ), ); let mut r = request( oa, sa, vec![( FacetSelector::Field(FieldFacet::Type), SemanticDecision::KeepTarget, )], ); r.references.insert(a, b); p = applied(&p, r); data(&mut p, a, 0, 12, "m"); let impacts = linked_semantic_impacts(&p, &p.traces[&t], &BTreeMap::from([(a, b)])).unwrap(); assert!(impacts.iter().any(|impact| impact.objects == KeyPair { oa: Some(a), sa: Some(b) } && impact.drift == Drift::OaChanged)); let base = p.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert_eq!(base.facets.len(), 1); assert_eq!( base.facets[0].selector(), FacetSelector::Field(FieldFacet::Type) ); } #[test] fn combined_ordered_membership_commutes_or_requires_agreed_anchors() { for insertion in [false, true] { let mut p = Project::empty(); let a = add(&mut p, Kind::Process, Layer::Oa, "first"); let b = add(&mut p, Kind::Process, Layer::Oa, "second"); let target = add(&mut p, Kind::FunctionalChain, Layer::Sa, "target"); let mut rows = Vec::new(); for (owner, layer) in [(a, Layer::Oa), (b, Layer::Oa), (target, Layer::Sa)] { rows.push(["left", "right", "middle"].map(|name| { occurrence( &mut p, layer, owner, name, OccurrenceBody::Control { kind: ControlKind::Choice, }, ) })); } let [left, right, middle] = rows[2]; *p.elements.get_mut(&a).unwrap().steps_mut().unwrap() = if insertion { vec![rows[0][0], rows[0][2], rows[0][1]] } else { vec![] }; *p.elements.get_mut(&b).unwrap().steps_mut().unwrap() = if insertion { vec![rows[1][2], rows[1][0], rows[1][1]] } else { vec![] }; *p.elements.get_mut(&target).unwrap().steps_mut().unwrap() = if insertion { vec![left, right] } else { vec![left, middle, right] }; trace(&mut p, a, target); trace(&mut p, b, target); let facet = |index: usize| FacetSelector::Member { collection: ReferenceCollection::Steps, keys: KeyPair { oa: Some(rows[index][if insertion { 2 } else { index }]), sa: Some(rows[2][if insertion { 2 } else { index }]), }, }; let mut r = request(a, target, vec![(facet(0), SemanticDecision::TakeSource)]); r.transition.sources.push(b); r.transition.mappings.push(TransitionMapping { source: b, target: Some(target), target_kind: None, }); r.reviews.push(SemanticReviewRequest { source: b, resolutions: vec![FacetResolution { facet: facet(1), decision: SemanticDecision::TakeSource, }], reset: vec![], }); for row in &rows[..2] { for index in 0..3 { r.references.insert(row[index], rows[2][index]); } } for reversed in [false, true] { let mut r = r.clone(); if reversed { r.reviews.reverse(); } let plan = plan_semantic_transition(&p, r).unwrap(); if insertion { assert!( plan.conflicts .iter() .any(|c| c.contains("Contradictory anchored orders")), "{:?}", plan.conflicts ); } else { assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); let q = apply_semantic_transition(&p, &plan).unwrap(); assert_eq!(q.elements[&target].steps().unwrap(), &vec![middle]); } } } } #[test] fn target_only_artifact_move_records_owner_and_metadata_in_either_order() { for rename in [false, true] { for reversed in [false, true] { let mut p = Project::empty(); let oa = add(&mut p, Kind::Activity, Layer::Oa, "oa"); let sa = add(&mut p, Kind::Function, Layer::Sa, "sa"); let other = add(&mut p, Kind::Function, Layer::Sa, "other"); let t = trace(&mut p, oa, sa); let b = artifact(&mut p, sa, "local"); let keys = KeyPair { oa: None, sa: Some(b), }; let owner = FacetSelector::Artifact { keys: keys.clone(), facet: ArtifactFacet::Owner, }; let name = FacetSelector::Artifact { keys: keys.clone(), facet: ArtifactFacet::Name, }; let mut r = request(oa, sa, vec![(owner.clone(), SemanticDecision::Candidate)]); let mut candidate = p.artifact_links[&b].clone(); candidate.owner = other; if rename { candidate.name = "moved local".into(); r.reviews[0].resolutions.push(FacetResolution { facet: name.clone(), decision: SemanticDecision::Candidate, }); } if reversed { r.reviews[0].resolutions.reverse(); } r.candidates .push(SemanticRecord::Artifact(candidate.clone())); let q = applied(&p, r); assert_eq!(q.artifact_links[&b], candidate); let baseline = q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); assert!(baseline.facets.iter().any(|f| matches!(f, FacetReview::Artifact { review: ArtifactReview::Owner(Reviewed { oa: Observed::NotApplicable, sa: Observed::Value(id) }), .. } if *id == other))); assert_eq!(baseline.facets.len(), if rename { 2 } else { 1 }); let invalid = plan_semantic_transition( &q, request(oa, sa, vec![(name, SemanticDecision::KeepTarget)]), ) .unwrap(); assert!( invalid .conflicts .iter() .any(|c| c.contains("initially attached owner")), "{:?}", invalid.conflicts ); assert!( validation::validate(&q) .iter() .all(|issue| issue.severity != validation::Severity::Error) ); } } } #[test] fn retired_source_branch_retains_historical_member_pair_in_both_directions() { for reverse in [false, true] { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa"); enumeration(&mut p, oa, &["X"]); enumeration(&mut p, sa, &["X"]); let t = trace(&mut p, oa, sa); let literal = FacetSelector::Literal(KeyPair { oa: Some("X".into()), sa: Some("X".into()), }); p = applied( &p, request( oa, sa, vec![(literal.clone(), SemanticDecision::KeepTarget)], ), ); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; data(&mut p, source, 0, 10, ""); let mut r = request( source, target, vec![( FacetSelector::Data(DataFacet::Constructor), SemanticDecision::KeepTarget, )], ); if reverse { r.transition.direction = TransitionDirection::SaToOa; } let q = applied(&p, r); let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap(); let literals = facts .iter() .filter(|fact| matches!(fact.selector, FacetSelector::Literal(_))) .collect::>(); assert_eq!(literals.len(), 1); assert_eq!(literals[0].selector, literal); assert_eq!(literals[0].drift, Drift::Unchanged); let FacetReview::Literal { review, .. } = &literals[0].current else { panic!() }; assert_eq!( if reverse { &review.sa } else { &review.oa }, &Observed::NotApplicable ); assert_eq!( if reverse { &review.oa } else { &review.sa }, &Observed::Value(true) ); } } #[test] fn constructor_closure_preserves_target_only_literals_in_both_directions() { for reverse in [false, true] { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa"); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; enumeration(&mut p, target, &["local"]); let t = trace(&mut p, oa, sa); let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap(); p.traces .get_mut(&t) .unwrap() .baseline .as_mut() .unwrap() .semantic = Some(SemanticBaseline { facets: facts.into_iter().map(|f| f.current).collect(), }); enumeration(&mut p, source, &["incoming"]); let mut r = request( source, target, vec![( FacetSelector::Data(DataFacet::Constructor), SemanticDecision::TakeSource, )], ); if reverse { r.transition.direction = TransitionDirection::SaToOa; } let q = applied(&p, r); assert!( matches!(q.elements[&target].shared(), Some(SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, .. }) if literals == &["incoming", "local"]), "reverse={reverse}" ); let base = q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); validate_semantic_baseline(base).unwrap(); let incoming = FacetSelector::Literal(KeyPair { oa: Some("incoming".into()), sa: Some("incoming".into()), }); assert_eq!( base.facets .iter() .filter(|f| f.selector() == incoming) .count(), 1 ); let facts = semantic_facets(&q, &q.traces[&t], &BTreeMap::new()).unwrap(); assert!( facts .iter() .filter(|f| matches!(f.selector, FacetSelector::Literal(_))) .all(|f| f.drift == Drift::Unchanged) ); } } #[test] fn constructor_closure_pairs_signal_members_and_requires_history_reset_in_both_directions() { for reverse in [false, true] { for historical in [false, true] { let mut p = Project::empty(); let oa = add(&mut p, Kind::Event, Layer::Oa, "oa"); let sa = add(&mut p, Kind::Event, Layer::Sa, "sa"); let oa_item = add(&mut p, Kind::ExchangeItem, Layer::Oa, "oa_item"); let sa_item = add(&mut p, Kind::ExchangeItem, Layer::Sa, "sa_item"); let (source, target, source_item) = if reverse { (sa, oa, sa_item) } else { (oa, sa, oa_item) }; set( &mut p, source, SharedBody::Event { body: EventBody::Signal { items: vec![source_item], }, }, ); set( &mut p, target, SharedBody::Event { body: EventBody::Change { expression: "old".into(), }, }, ); let t = trace(&mut p, oa, sa); let previous = FacetSelector::Member { collection: ReferenceCollection::SignalItems, keys: if reverse { KeyPair { oa: None, sa: Some(sa_item), } } else { KeyPair { oa: Some(oa_item), sa: None, } }, }; if historical { let facts = semantic_facets(&p, &p.traces[&t], &BTreeMap::new()).unwrap(); let old = facts .into_iter() .find(|f| f.selector == previous) .unwrap() .current; p.traces .get_mut(&t) .unwrap() .baseline .as_mut() .unwrap() .semantic = Some(SemanticBaseline { facets: vec![old] }); } let mut r = request( source, target, vec![( FacetSelector::Event(EventFacet::Constructor), SemanticDecision::TakeSource, )], ); if reverse { r.transition.direction = TransitionDirection::SaToOa; } r.references.insert(oa_item, sa_item); if historical { let invalid = plan_semantic_transition(&p, r.clone()).unwrap(); assert!( !invalid.conflicts.is_empty(), "Established one-sided history requires reset: reverse={reverse}" ); r.reviews[0].reset.push(previous); } let q = applied(&p, r); let target_item = if reverse { oa_item } else { sa_item }; assert!( matches!(q.elements[&target].shared(), Some(SharedBody::Event { body: EventBody::Signal { items } }) if items == &[target_item]) ); let base = q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); validate_semantic_baseline(base).unwrap(); let members: Vec<_> = base .facets .iter() .filter(|f| { matches!( f.selector(), FacetSelector::Member { collection: ReferenceCollection::SignalItems, .. } ) }) .collect(); assert_eq!( members.len(), 1, "reverse={reverse}, historical={historical}" ); assert_eq!( members[0].selector(), FacetSelector::Member { collection: ReferenceCollection::SignalItems, keys: KeyPair { oa: Some(oa_item), sa: Some(sa_item) } } ); assert!( validation::validate(&q) .iter() .all(|issue| issue.severity != validation::Severity::Error) ); } } } #[test] fn constructor_closure_does_not_restore_previously_corresponding_removed_members() { for reverse in [false, true] { let mut p = Project::empty(); let oa = add(&mut p, Kind::DataType, Layer::Oa, "oa"); let sa = add(&mut p, Kind::DataType, Layer::Sa, "sa"); let (source, target) = if reverse { (sa, oa) } else { (oa, sa) }; enumeration(&mut p, source, &["paired"]); let t = trace(&mut p, oa, sa); let constructor = FacetSelector::Data(DataFacet::Constructor); let directed = |facets| { let mut r = request(source, target, facets); if reverse { r.transition.direction = TransitionDirection::SaToOa; } r }; p = applied( &p, directed(vec![(constructor.clone(), SemanticDecision::KeepTarget)]), ); if reverse { // Explicitly establish the counterpart key while its target branch // is absent, rather than silently changing the one-sided history. let mut r = directed(vec![( FacetSelector::Literal(KeyPair { oa: Some("paired".into()), sa: Some("paired".into()), }), SemanticDecision::KeepTarget, )]); r.reviews[0].reset.push(FacetSelector::Literal(KeyPair { oa: None, sa: Some("paired".into()), })); p = applied(&p, r); } enumeration(&mut p, source, &["incoming"]); enumeration(&mut p, target, &["paired", "local"]); let q = applied( &p, directed(vec![(constructor, SemanticDecision::TakeSource)]), ); assert!( matches!(q.elements[&target].shared(), Some(SharedBody::DataType { definition: DataTypeDefinition::Enumeration { literals }, .. }) if literals == &["incoming", "local"]), "reverse={reverse}" ); validate_semantic_baseline( q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(), ) .unwrap(); } } fn reverse_legacy_port_fixture() -> (Project, Id, Id, Id) { let mut p = Project::empty(); let oa_owner = add(&mut p, Kind::Actor, Layer::Oa, "oa owner"); let sa_owner = add(&mut p, Kind::SystemActor, Layer::Sa, "sa owner"); let oa = Id::new_v4(); let sa = Id::new_v4(); let mut legacy = make_port(&p, oa, oa_owner, "oa port", PortContract::Legacy).unwrap(); *legacy.shared_mut().unwrap() = SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction: PortDirection::InOut, interface: None, }), }; p.elements.insert(oa, legacy); let typed = make_port( &p, sa, sa_owner, "sa port", PortContract::Component { kind: ComponentPortKind::Flow, orientation: ComponentPortOrientation::Out, }, ) .unwrap(); p.elements.insert(sa, typed); let t = trace(&mut p, oa, sa); (p, oa, sa, t) } #[test] fn reverse_existing_port_review_retains_legacy_and_roundtrips_typed_history() { for decision in [SemanticDecision::KeepTarget, SemanticDecision::Candidate] { let (p, oa, sa, t) = reverse_legacy_port_fixture(); let mut r = request( sa, oa, vec![(FacetSelector::Port(PortFacet::Constructor), decision)], ); r.transition.direction = TransitionDirection::SaToOa; r.traces.insert(sa, t); let expected_direction = if decision == SemanticDecision::Candidate { let mut candidate = p.elements[&oa].clone(); let Some(SharedBody::Port { legacy: Some(old), .. }) = candidate.shared_mut() else { panic!() }; old.direction = PortDirection::In; r.candidates.push(SemanticRecord::Element(candidate)); PortDirection::In } else { PortDirection::InOut }; let q = applied(&p, r); assert_eq!(q.elements[&sa], p.elements[&sa]); assert_eq!(q.elements.len(), p.elements.len()); assert!(matches!(q.elements[&oa].shared(), Some(SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction, interface: None }), }) if *direction == expected_direction)); let baseline = q.traces[&t] .baseline .as_ref() .unwrap() .semantic .as_ref() .unwrap(); validate_semantic_baseline(baseline).unwrap(); assert!(baseline.facets.iter().any(|f| matches!( f, FacetReview::Port(PortReview::Constructor(Reviewed { oa: Observed::Value(PortConstructor::Legacy), sa: Observed::Value(PortConstructor::Component), })) ))); assert_eq!(language::parse(&language::format(&q)).unwrap(), q); assert_eq!( persistence::decode(&serde_json::to_vec(&q).unwrap()).unwrap(), q ); } } #[test] fn reverse_existing_port_review_rejects_typed_oa_proposals_without_mutation() { for decision in [SemanticDecision::TakeSource, SemanticDecision::Candidate] { let (p, oa, sa, _) = reverse_legacy_port_fixture(); let snapshot = p.clone(); let mut r = request( sa, oa, vec![(FacetSelector::Port(PortFacet::Constructor), decision)], ); r.transition.direction = TransitionDirection::SaToOa; if decision == SemanticDecision::Candidate { let mut candidate = p.elements[&oa].clone(); *candidate.shared_mut().unwrap() = p.elements[&sa].shared().unwrap().clone(); r.candidates.push(SemanticRecord::Element(candidate)); } let plan = plan_semantic_transition(&p, r).unwrap(); assert!(!plan.conflicts.is_empty()); assert!( plan.conflicts .iter() .all(|c| !c.contains("outside selected transition")), "Must reach structural validation: {:?}", plan.conflicts ); assert!(prepare_semantic_transition(&p, &plan).is_err()); assert_eq!(p, snapshot); } } #[test] fn reverse_new_port_counterpart_remains_an_explicit_sa_only_omission() { let (p, _, sa, _) = reverse_legacy_port_fixture(); let mut r = TransitionRequest::new(TransitionDirection::SaToOa, vec![sa]); r.mappings.push(TransitionMapping { source: sa, target: None, target_kind: Some(Kind::Port), }); let plan = plan_transition(&p, &r).unwrap(); assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts); assert!(plan.changes.is_empty()); assert!(plan.warnings.iter().any(|w| w.contains("SA-only omission"))); assert_eq!(apply_transition(&p, &plan).unwrap(), p); }