//! Independent membership and complete final order; the core validates every proposal. use super::*; pub(super) fn collection(which: OrderedCollection) -> ReferenceCollection { match which { OrderedCollection::Steps => ReferenceCollection::Steps, OrderedCollection::FragmentBranches => ReferenceCollection::FragmentBranches, } } pub(super) fn ordered(which: ReferenceCollection) -> Option { match which { ReferenceCollection::Steps => Some(OrderedCollection::Steps), ReferenceCollection::FragmentBranches => Some(OrderedCollection::FragmentBranches), _ => None, } } pub(super) fn read(value: &Value, which: OrderedCollection) -> Result, String> { let path = keyed::path(value, keyed::Family::Member(collection(which))) .ok_or("This ordered collection is unavailable on the destination.")?; serde_json::from_value(value.pointer(&path).unwrap().clone()).map_err(|e| e.to_string()) } fn write(value: &mut Value, which: OrderedCollection, ids: Vec) -> Result<(), String> { let path = keyed::path(value, keyed::Family::Member(collection(which))) .ok_or("This ordered collection is unavailable on the destination.")?; *value.pointer_mut(&path).unwrap() = serde_json::to_value(ids).expect("Ordered identities"); Ok(()) } impl ReviewRow { pub(super) fn ordered_members( &self, project: &Project, which: OrderedCollection, ) -> Result, String> { read(&self.combine(project, true, true)?, which) } pub(super) fn initialize_order( &mut self, project: &Project, which: OrderedCollection, ) -> Result<(), String> { if !self.order_drafts.contains_key(&which) { let ids = self.ordered_members(project, which)?; self.order_drafts.insert(which, ids); } Ok(()) } pub(super) fn reconcile_order( &mut self, project: &Project, which: OrderedCollection, ) -> Result<(), String> { self.require_order_candidate(which)?; let members = self.ordered_members(project, which)?; let draft = self .order_drafts .get_mut(&which) .ok_or("Choose a final order first.")?; // This is an explicit user action. Ordinary refresh never discards an authored order. let mut seen = BTreeSet::new(); draft.retain(|id| members.contains(id) && seen.insert(*id)); for id in members { if seen.insert(id) { draft.push(id); } } Ok(()) } fn require_order_candidate(&self, which: OrderedCollection) -> Result<(), String> { let facet = FacetSelector::Order(which); if self.selected.as_ref() != Some(&facet) || self.decisions.get(&facet) != Some(&SemanticDecision::Candidate) { return Err("Select Set final value for this order before changing it.".into()); } Ok(()) } pub(super) fn move_order( &mut self, which: OrderedCollection, index: usize, delta: i32, ) -> Result<(), String> { self.require_order_candidate(which)?; let ids = self .order_drafts .get_mut(&which) .ok_or("Choose a final order first.")?; let destination = match delta { -1 => index.checked_sub(1), 1 => index.checked_add(1), _ => None, }; let destination = destination .filter(|i| index < ids.len() && *i < ids.len()) .ok_or("Choose an existing adjacent position in the final order.")?; ids.swap(index, destination); Ok(()) } pub(super) fn project_order( &self, project: &Project, trace: &Trace, value: &mut Value, which: OrderedCollection, decision: SemanticDecision, ) -> Result<(), String> { let current = read(value, which)?; let supplied = match decision { SemanticDecision::KeepTarget => return Ok(()), SemanticDecision::Candidate => self .order_drafts .get(&which) .cloned() .ok_or("Choose a final order.")?, SemanticDecision::TakeSource => { let fact = capture_facet(project, trace.oa, trace.sa, &FacetSelector::Order(which))?; let source: Vec = serde_json::from_value( scalar::value(&fact, self.direction.source_layer()) .ok_or("The source has no applicable order.")?, ) .map_err(|e| e.to_string())?; let map = self.order_reference_map()?; source .into_iter() .map(|id| { map.get(&id).copied().ok_or_else(|| { format!( "Pair source order member {} explicitly in Reference pairs.", project.named(id) ) }) }) .collect::, _>>()? } }; let actual: BTreeSet<_> = current.iter().copied().collect(); let desired: BTreeSet<_> = supplied.iter().copied().collect(); if desired.len() != supplied.len() { return Err( "The final order contains a duplicate identity; reconcile its members explicitly." .into(), ); } if desired != actual { if decision == SemanticDecision::TakeSource && desired.is_subset(&actual) && current .iter() .filter(|id| desired.contains(id)) .copied() .collect::>() == supplied { return Ok(()); } return Err("Final order must contain every surviving destination member exactly once. Reconcile members, then choose their positions; membership is reviewed separately.".into()); } write(value, which, supplied) } fn order_reference_map(&self) -> Result, String> { let mut map = BTreeMap::new(); for (oa, sa) in &self.references { let (a, b) = if self.direction == TransitionDirection::OaToSa { (*oa, *sa) } else { (*sa, *oa) }; if map.insert(a, b).is_some_and(|old| old != b) { return Err("Reverse reference correspondence is ambiguous.".into()); } } Ok(map) } pub(super) fn project_ordered_member( &self, project: &Project, trace: &Trace, value: &mut Value, facet: &FacetSelector, decision: SemanticDecision, form_context: bool, ) -> Result<(), String> { let FacetSelector::Member { collection: member_collection, keys, } = facet else { unreachable!() }; let which = ordered(*member_collection).expect("Ordered member"); let before = read(value, which)?; self.keyed_value(project, trace, value, facet, decision)?; let after = read(value, which)?; let explicit = self .decisions .get(&FacetSelector::Order(which)) .is_some_and(|d| *d != SemanticDecision::KeepTarget); if decision != SemanticDecision::TakeSource || explicit || after.len() <= before.len() { return Ok(()); } let (source_key, target_key) = if self.direction == TransitionDirection::OaToSa { (keys.oa, keys.sa) } else { (keys.sa, keys.oa) }; let (Some(source_key), Some(target_key)) = (source_key, target_key) else { return Ok(()); }; let fact = capture_facet(project, trace.oa, trace.sa, &FacetSelector::Order(which))?; let source: Vec = serde_json::from_value( scalar::value(&fact, self.direction.source_layer()) .ok_or("Source order is unavailable.")?, ) .map_err(|e| e.to_string())?; let position = source .iter() .position(|id| *id == source_key) .ok_or("Source member is absent.")?; let map = self.order_reference_map()?; let anchor = |id: &Id| { map.get(id) .and_then(|mapped| before.iter().position(|id| id == mapped)) }; let left = source[..position].iter().rev().find_map(anchor); let right = source[position + 1..].iter().find_map(anchor); let at = match (left, right) { (Some(a), Some(b)) if a + 1 == b => Some(b), (Some(a), None) if a + 1 == before.len() => Some(a + 1), (None, Some(0)) => Some(0), (None, None) if before.is_empty() => Some(0), _ => None, }; if let Some(at) = at { let mut ordered = before; ordered.insert(at, target_key); write(value, which, ordered) } else if form_context { // An uncommitted membership list lets the user author a final order. // The actual request still rejects ambiguous placement until explicitly decided. Ok(()) } else { Err("Ordered member insertion has no unique unchanged anchor; select an explicit final Order in Properties.".into()) } } }