use super::catalog::{capture_observation, collection, is_constructor, replace_observation, sides}; use super::historical::{keyed_slots_overlap, selector_slots}; use super::mutate::*; use super::*; use crate::model::*; use crate::transitions::{self, TransitionDirection, TransitionPlan, TransitionRequest}; use crate::{Command, ModelError, PreparedCommand, prepare_command}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SemanticDecision { KeepTarget, TakeSource, Candidate, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct FacetResolution { pub facet: FacetSelector, pub decision: SemanticDecision, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SemanticReviewRequest { pub source: Id, pub resolutions: Vec, /// Exact previous selectors deliberately invalidated after correspondence changes. #[serde(default)] pub reset: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub enum ImpactRecord { Element(Element), Relation(Relation), Artifact(ArtifactLink), Diagram(Diagram), Trace(Trace), } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SemanticImpact { pub object: Id, pub before: Option, pub after: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SemanticTransitionRequest { pub transition: TransitionRequest, /// Source object → chosen existing trace, required when the pair has several traces. #[serde(default)] pub traces: BTreeMap, /// Canonical OA→SA reference correspondence, including historical deleted IDs. #[serde(default)] pub references: BTreeMap, #[serde(default)] pub reviews: Vec, /// Final typed candidate records. Identity and aliases cannot change here. #[serde(default)] pub candidates: Vec, #[serde(default)] pub accepted_impacts: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SemanticReviewPreview { pub source: Id, pub target: Id, pub facets: Vec, pub acknowledged: Vec, pub retired: Vec, pub linked_impacts: Vec, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct SemanticTransitionPlan { pub base_fingerprint: String, pub request: SemanticTransitionRequest, pub transition: TransitionPlan, pub reviews: Vec, pub impacts: Vec, pub conflicts: Vec, pub commands: Vec, pub prepared_fingerprint: Option, } impl SemanticTransitionRequest { pub fn new(mut transition: TransitionRequest) -> Self { // Semantic headers have their own explicit facets. Existing prose clients // continue to use the original request and planner unchanged. transition.fields.clear(); transition.resolutions.clear(); Self { transition, traces: BTreeMap::new(), references: BTreeMap::new(), reviews: vec![], candidates: vec![], accepted_impacts: vec![], } } } fn canonical(direction: TransitionDirection, source: Id, target: Id) -> (Id, Id) { match direction { TransitionDirection::OaToSa => (source, target), TransitionDirection::SaToOa => (target, source), } } fn trace_for(p: &Project, oa: Id, sa: Id, selected: Option) -> Result { if let Some(id) = selected { return p .traces .get(&id) .filter(|t| t.oa == oa && t.sa == sa) .cloned() .ok_or( "Selected trace does not match the explicit source/target correspondence".into(), ); } let traces: Vec<_> = p .traces .values() .filter(|t| t.oa == oa && t.sa == sa) .collect(); if traces.len() != 1 { return Err( "Semantic review requires exactly one explicit trace for this correspondence".into(), ); } Ok(traces[0].clone()) } fn old_facts(t: &Trace) -> SemanticBaseline { t.baseline .as_ref() .and_then(|b| b.semantic.clone()) .unwrap_or_default() } fn side(fact: &FacetReview, layer: Layer) -> serde_json::Value { let (oa, sa) = sides(fact); match layer { Layer::Oa => oa, Layer::Sa => sa, } } fn target_slot( target: Id, selector: &FacetSelector, layer: Layer, ) -> Result, String> { let has_target = match selector { FacetSelector::Member { keys, .. } | FacetSelector::Binding(keys) | FacetSelector::Child(keys) | FacetSelector::Incident { keys, .. } | FacetSelector::Artifact { keys, .. } => chosen(keys, layer).is_some(), FacetSelector::Annotation(keys) => chosen(keys, layer).is_some(), FacetSelector::Literal(keys) => chosen(keys, layer).is_some(), _ => true, }; if !has_target { return Ok(None); } fn single(key: &KeyPair, layer: Layer) -> KeyPair { KeyPair { oa: None, sa: chosen(key, layer).cloned(), } } let selector = match selector { FacetSelector::Member { collection, keys } => FacetSelector::Member { collection: *collection, keys: single(keys, layer), }, FacetSelector::Literal(keys) => FacetSelector::Literal(single(keys, layer)), FacetSelector::Annotation(keys) => FacetSelector::Annotation(single(keys, layer)), FacetSelector::Binding(keys) => FacetSelector::Binding(single(keys, layer)), FacetSelector::Child(keys) => { return Ok(Some(( *chosen(keys, layer).ok_or("Target child mapping missing")?, FacetSelector::Header(HeaderFacet::Parent), ))); } FacetSelector::Incident { keys, end } => { return Ok(Some(( *chosen(keys, layer).ok_or("Target incident mapping missing")?, FacetSelector::Relation(match end { IncidentEnd::Source => RelationFacet::Source, IncidentEnd::Target => RelationFacet::Target, }), ))); } FacetSelector::Artifact { keys, facet } => { return Ok(Some(( *chosen(keys, layer).ok_or("Missing target artifact")?, FacetSelector::Artifact { keys: single(keys, layer), facet: if *facet == ArtifactFacet::Presence { ArtifactFacet::Owner } else { *facet }, }, ))); } v => v.clone(), }; Ok(Some((target, selector))) } fn agree( slots: &mut BTreeMap<(Id, FacetSelector), serde_json::Value>, slot: (Id, FacetSelector), value: serde_json::Value, ) -> Result<(), String> { if slots.get(&slot).is_some_and(|old| *old != value) { return Err(format!( "Contradictory resolutions for canonical target slot {slot:?}" )); } slots.insert(slot, value); Ok(()) } fn slot_observation( project: &Project, slot: &(Id, FacetSelector), ) -> Result { if let FacetSelector::Artifact { facet, .. } = &slot.1 { let artifact = project.artifact_links.get(&slot.0); if *facet == ArtifactFacet::Presence { return Ok(serde_json::json!({"Value":artifact.is_some()})); } let Some(artifact) = artifact else { return Ok(serde_json::Value::String("NotApplicable".into())); }; let record = serde_json::to_value(artifact).map_err(|e| e.to_string())?; let field = match facet { ArtifactFacet::Presence => unreachable!(), ArtifactFacet::Name => "name", ArtifactFacet::Description => "description", ArtifactFacet::Owner => "owner", ArtifactFacet::Relation => "relation", ArtifactFacet::Uri => "uri", ArtifactFacet::Revision => "revision", ArtifactFacet::Fragment => "fragment", }; return Ok(serde_json::json!({"Value":record[field]})); } Ok(side( &capture_observation(project, slot.0, slot.0, &slot.1)?, Layer::Sa, )) } fn audit_slots( project: &Project, slots: &BTreeMap<(Id, FacetSelector), serde_json::Value>, ) -> Result<(), String> { for (slot, desired) in slots { if slot_observation(project, slot)? != *desired { return Err(format!( "Contradictory final value for selected canonical slot {slot:?}" )); } } Ok(()) } fn apply_canonical_slot( project: &mut Project, slot: &(Id, FacetSelector), desired: &serde_json::Value, ) -> Result<(), String> { if is_constructor(&slot.1) { return Ok(()); } if desired.as_str() == Some("NotApplicable") { if matches!( slot.1, FacetSelector::Artifact { facet: ArtifactFacet::Owner, .. } ) { project.artifact_links.remove(&slot.0); } return Ok(()); } let owner = if matches!(slot.1, FacetSelector::Artifact { .. }) { project .artifact_links .get(&slot.0) .ok_or("Missing agreed artifact candidate")? .owner } else { slot.0 }; let fact = capture_observation(project, owner, owner, &slot.1)?; let fact = replace_observation(fact, Layer::Sa, desired.clone())?; write_fact(project, owner, &fact, Layer::Sa, Layer::Sa, false) } fn member_present(fact: &FacetReview, layer: Layer) -> bool { fn observed(review: &Reviewed, layer: Layer) -> &Observed { match layer { Layer::Oa => &review.oa, Layer::Sa => &review.sa, } } match fact { FacetReview::Member { review, .. } | FacetReview::Literal { review, .. } => { matches!(observed(review, layer), Observed::Value(true)) } FacetReview::Annotation { review, .. } => { matches!(observed(review, layer), Observed::Value(Some(_))) } FacetReview::Binding { review, .. } => { matches!(observed(review, layer), Observed::Value(Some(_))) } _ => false, } } fn preserve_constructor_members( project: &mut Project, target: Id, group: &FacetSelector, initial_facts: &[FacetDrift], baseline: &SemanticBaseline, source_layer: Layer, target_layer: Layer, ) -> Result<(), String> { // A correspondence key can exist without membership on that side (for // example an OA literal automatically projects the same SA literal key). // Preserve actual target-only values, except previously reviewed removals. for fact in initial_facts { if !constructor_contains(group, &fact.selector) || member_present(&fact.current, source_layer) || !member_present(&fact.current, target_layer) { continue; } let slot = target_slot(target, &fact.selector, target_layer)?; let was_corresponding = baseline.facets.iter().any(|old| { member_present(old, source_layer) && target_slot(target, &old.selector(), target_layer).ok() == Some(slot.clone()) }); if was_corresponding { continue; } write_fact( project, target, &fact.current, target_layer, target_layer, false, )?; } Ok(()) } fn install_candidate(p: &mut Project, record: &SemanticRecord) -> Result<(), String> { match record { SemanticRecord::Element(e) => { let old = p .elements .get(&e.id) .ok_or("Candidate element is not mapped")?; if e.alias != old.alias || e.layer() != old.layer() { return Err("Candidate cannot change alias or layer".into()); } p.elements.insert(e.id, e.clone()); } SemanticRecord::Relation(r) => { let old = p .relations .get(&r.id) .ok_or("Candidate relation is not mapped")?; if r.alias != old.alias || std::mem::discriminant(&r.body) != std::mem::discriminant(&old.body) { return Err("Relation identity/kind changes require explicit mapping".into()); } p.relations.insert(r.id, r.clone()); } SemanticRecord::Artifact(a) => { if p.artifact_links .get(&a.id) .is_some_and(|old| old.alias != a.alias) { return Err("Artifact candidate cannot change identity alias".into()); } p.artifact_links.insert(a.id, a.clone()); } } Ok(()) } fn edits(before: &Project, after: &Project) -> Vec { let mut commands = vec![]; for (id, e) in &after.elements { if before.elements.get(id) != Some(e) { commands.push(if before.elements.contains_key(id) { Command::EditElement(*id, e.clone()) } else { Command::CreateElement(e.clone()) }); } } for (id, r) in &after.relations { if before.relations.get(id) != Some(r) { commands.push(if before.relations.contains_key(id) { Command::EditRelation(*id, r.clone()) } else { Command::CreateRelation(r.clone()) }); } } for (id, a) in &after.artifact_links { if before.artifact_links.get(id) != Some(a) { commands.push(if before.artifact_links.contains_key(id) { Command::EditArtifact(*id, a.clone()) } else { Command::CreateArtifact(a.clone()) }); } } for id in before.artifact_links.keys() { if !after.artifact_links.contains_key(id) { commands.push(Command::Delete(*id)); } } for (id, t) in &after.traces { if before.traces.get(id) != Some(t) { commands.push(if before.traces.contains_key(id) { Command::EditTrace(*id, t.clone()) } else { Command::CreateTrace(t.clone()) }); } } commands } fn impacts(before: &Project, after: &Project) -> Vec { let mut result = vec![]; macro_rules! records { ($field:ident,$variant:ident) => { for id in before .$field .keys() .chain(after.$field.keys()) .copied() .collect::>() { let a = before.$field.get(&id); let b = after.$field.get(&id); if a != b { result.push(SemanticImpact { object: id, before: a.cloned().map(ImpactRecord::$variant), after: b.cloned().map(ImpactRecord::$variant), }); } } }; } records!(elements, Element); records!(relations, Relation); records!(artifact_links, Artifact); records!(diagrams, Diagram); records!(traces, Trace); result } /// Preview typed semantic decisions on top of the existing mapping/dependency /// planner. All mutation is eventually one ordinary Command::Batch preparation. pub fn plan_semantic_transition( project: &Project, request: SemanticTransitionRequest, ) -> Result { let transition = transitions::plan_transition(project, &request.transition)?; let mut plan = SemanticTransitionPlan { base_fingerprint: transitions::project_fingerprint(project), request, commands: transition.commands.clone(), transition, reviews: vec![], impacts: vec![], conflicts: vec![], prepared_fingerprint: None, }; plan.conflicts .extend(plan.transition.conflicts.iter().map(|c| c.message.clone())); if !plan.conflicts.is_empty() { return Ok(plan); } let initial = prepare_command(project, &Command::Batch(plan.commands.clone()))?.project; if let Err(error) = build_review(project, &initial, &mut plan) { plan.conflicts.push(error); } Ok(plan) } fn build_review( original: &Project, initial: &Project, plan: &mut SemanticTransitionPlan, ) -> Result<(), String> { let direction = plan.request.transition.direction; let source_layer = direction.source_layer(); let target_layer = direction.target_layer(); let mut references = plan.request.references.clone(); for c in &plan.transition.changes { let (oa, sa) = canonical(direction, c.source, c.target); if references.insert(oa, sa).is_some_and(|old| old != sa) { return Err("Transition mapping disagrees with frozen correspondence".into()); } } for (oa, sa) in &references { if initial.layer_of(*oa).is_some_and(|v| v != Layer::Oa) || initial.layer_of(*sa).is_some_and(|v| v != Layer::Sa) { return Err("Reference correspondence must use canonical OA/SA sides".into()); } } let map = directional_map(&references, direction)?; let mut candidates = initial.clone(); let mut candidate_ids = BTreeSet::new(); for candidate in &plan.request.candidates { if !candidate_ids.insert(candidate.id()) { return Err("Duplicate candidate record".into()); } install_candidate(&mut candidates, candidate)?; } let mut slots = BTreeMap::new(); let mut constructor_proposals: Vec<(Id, FacetSelector, SemanticRecord)> = vec![]; let mut constructor_actions: Vec<(Id, FacetSelector, SemanticRecord)> = vec![]; let mut created_artifacts = BTreeMap::new(); let mut implicit_orders: Vec<(Id, OrderedCollection, Vec)> = vec![]; let mut seen_sources = BTreeSet::new(); let mut acknowledgements: Vec<(Id, Id, Id, SemanticBaseline, Vec)> = vec![]; for request in &plan.request.reviews { // Every source's desired values are derived from the same frozen project. // No source observes writes proposed by an earlier source. let mut expected = initial.clone(); if !seen_sources.insert(request.source) { return Err("Duplicate semantic review source".into()); } let target = plan .transition .changes .iter() .find(|c| c.source == request.source) .map(|c| c.target) .ok_or("Review source is outside selected transition mappings")?; let (oa, sa) = canonical(direction, request.source, target); let mut trace = trace_for( initial, oa, sa, plan.request.traces.get(&request.source).copied(), )?; let mut baseline = old_facts(&trace); let mut reset_set = BTreeSet::new(); for reset in &request.reset { if !reset_set.insert(reset.clone()) || !baseline.facets.iter().any(|v| v.selector() == *reset) { return Err("Reset must name an existing historical selector exactly once".into()); } } baseline .facets .retain(|v| !reset_set.contains(&v.selector())); if let Some(outer) = trace.baseline.as_mut() { outer.semantic = Some(baseline.clone()); } let all = semantic_facets(initial, &trace, &references)?; let mut acknowledged = vec![]; let mut retired = vec![]; let mut resolutions = request .resolutions .iter() .map(|resolution| { let current = capture_facet(initial, oa, sa, &resolution.facet)?; // A first equal discriminator observation has only its scalar // scope. Existing history and actual proposals retain the full // constructor group, including its no-switch rejection. let first_equal_constructor = is_constructor(&resolution.facet) && !baseline .facets .iter() .any(|old| old.selector() == resolution.facet) && side(¤t, source_layer) == side(¤t, target_layer) && (resolution.decision != SemanticDecision::Candidate || side( &capture_facet(&candidates, oa, sa, &resolution.facet)?, target_layer, ) == side(¤t, target_layer)); Ok(( resolution, is_constructor(&resolution.facet) && !first_equal_constructor, )) }) .collect::, String>>()?; for (i, (resolution, constructor_group)) in resolutions.iter().enumerate() { check_keys(&resolution.facet, &references)?; if let FacetSelector::Artifact { keys, facet } = &resolution.facet && *facet != ArtifactFacet::Presence { for (owner, key) in [(oa, keys.oa), (sa, keys.sa)] { if key .and_then(|id| initial.artifact_links.get(&id)) .is_some_and(|artifact| artifact.owner != owner) { return Err("Artifact metadata can only be reviewed on its initially attached owner trace".into()); } } } if resolutions[..i].iter().any(|(r, previous_group)| { keyed_slots_overlap(&r.facet, &resolution.facet) || (*previous_group && constructor_contains(&r.facet, &resolution.facet)) || (*constructor_group && constructor_contains(&resolution.facet, &r.facet)) }) { return Err("Duplicate, colliding or overlapping resolution scopes".into()); } if baseline.facets.iter().any(|old| { old.selector() != resolution.facet && keyed_slots_overlap(&old.selector(), &resolution.facet) }) { return Err( "Reference pairing changed; explicitly reset the affected historical selector" .into(), ); } for group in [ FacetSelector::Data(DataFacet::Constructor), FacetSelector::Port(PortFacet::Constructor), FacetSelector::Occurrence(OccurrenceFacet::Constructor), FacetSelector::Occurrence(OccurrenceFacet::MessageKind), FacetSelector::Event(EventFacet::Constructor), ] { if group != resolution.facet && constructor_contains(&group, &resolution.facet) && let Some(old) = baseline.facets.iter().find(|f| f.selector() == group) && let Ok(current) = capture_facet(initial, oa, sa, &group) && sides(old) != sides(¤t) { return Err("Constructor changed since review; resolve its explicit group before descendant facets".into()); } } } resolutions.sort_by_key(|(r, constructor_group)| { if *constructor_group { 0 } else if matches!(r.facet, FacetSelector::Order(_)) { 2 } else { 1 } }); for (resolution, constructor_group) in resolutions { let selected = &resolution.facet; let current = capture_facet(initial, oa, sa, selected)?; if is_constructor(selected) && !constructor_group { if let Some(slot) = target_slot(target, selected, target_layer)? { agree(&mut slots, slot, side(¤t, target_layer))?; } acknowledged.push(selected.clone()); } else if constructor_group { let old = baseline.facets.iter().find(|v| v.selector() == *selected); let current_source = side(¤t, source_layer); let current_target = side(¤t, target_layer); let candidate_fact = capture_facet(&candidates, oa, sa, selected)?; let proposed = match resolution.decision { SemanticDecision::KeepTarget => current_target.clone(), SemanticDecision::TakeSource => current_source.clone(), SemanticDecision::Candidate => side(&candidate_fact, target_layer), }; let historical_switch = old.is_some_and(|old| sides(old) != sides(¤t)); if proposed == current_target && !historical_switch && (old.is_some() || current_source == current_target) { return Err( "No constructor switch; review descendant facets individually".into(), ); } if resolution.decision != SemanticDecision::KeepTarget { let mut supplied = semantic_record( if resolution.decision == SemanticDecision::TakeSource { initial } else { &candidates }, if resolution.decision == SemanticDecision::TakeSource { request.source } else { target }, ) .ok_or("Missing constructor candidate")?; if resolution.decision == SemanticDecision::TakeSource && !matches!( selected, FacetSelector::Occurrence(OccurrenceFacet::MessageKind) | FacetSelector::State(StateFacet::Kind) ) { if let SemanticRecord::Element(e) = &mut supplied { e.parent = None; } remap_record(&mut supplied, &map)?; } copy_constructor(&mut expected, target, &supplied, selected)?; if resolution.decision == SemanticDecision::TakeSource && proposed == current_target { preserve_constructor_members( &mut expected, target, selected, &all, &baseline, source_layer, target_layer, )?; } } let observed = if resolution.decision == SemanticDecision::KeepTarget { initial } else { &expected }; let local_record = semantic_record(observed, target).ok_or("Missing constructor proposal")?; constructor_proposals.push((target, selected.clone(), local_record.clone())); if resolution.decision != SemanticDecision::KeepTarget { constructor_actions.push((target, selected.clone(), local_record)); } let refreshed = semantic_facets(observed, &trace, &references)?; let mut group_selectors: BTreeSet<_> = refreshed .iter() .chain(all.iter()) .map(|f| f.selector.clone()) .filter(|f| constructor_contains(selected, f)) .collect(); // A constructor copy can turn a live one-sided member into a // pair. Its initial projection is not another reviewed fact. // Established one-sided history still requires an explicit reset. let paired_sides: BTreeSet<_> = group_selectors .iter() .map(selector_slots) .filter(|slots| slots.len() == 2) .flatten() .collect(); group_selectors.retain(|selector| { !paired_sides.contains(selector) || baseline .facets .iter() .any(|old| old.selector() == *selector) }); let mut group = vec![]; for selector in group_selectors { match capture_facet(observed, oa, sa, &selector) { Ok(current) => group.push(FacetDrift { selector, current, drift: transitions::Drift::Unreviewed, }), Err(_) => { if let Some(slot) = target_slot(target, &selector, target_layer)? { agree( &mut slots, slot, serde_json::Value::String("NotApplicable".into()), )?; } retired.push(selector); } } } for fact in &group { if let Some(slot) = target_slot(target, &fact.selector, target_layer)? { agree(&mut slots, slot, side(&fact.current, target_layer))?; } } baseline .facets .retain(|f| !constructor_contains(selected, &f.selector())); acknowledged.extend(group.into_iter().map(|f| f.selector)); } else { let from = match resolution.decision { SemanticDecision::TakeSource => source_layer, _ => target_layer, }; let mut fact = if resolution.decision == SemanticDecision::Candidate { capture_facet(&candidates, oa, sa, selected)? } else { current.clone() }; if resolution.decision == SemanticDecision::TakeSource { fact = if let Some(adapted) = transitions::review_endpoint_fact( initial, &plan.request.transition, &map, request.source, target, &fact, )? { adapted } else { remap_fact(&fact, &map, source_layer)? }; } // A Keep reads the frozen target, never a value written by an earlier source. if resolution.decision != SemanticDecision::KeepTarget { if let FacetSelector::Member { collection: which, keys, } = selected && matches!( which, ReferenceCollection::Steps | ReferenceCollection::FragmentBranches ) && resolution.decision == SemanticDecision::TakeSource && let (Some(source_key), Some(target_key)) = (chosen(keys, source_layer), chosen(keys, target_layer)) && let Some(source_order) = collection(initial, request.source, *which) && source_order.contains(source_key) { let mut target_order = collection(&expected, target, *which).ok_or("Missing target order")?; let order_facet = FacetSelector::Order(match which { ReferenceCollection::Steps => OrderedCollection::Steps, _ => OrderedCollection::FragmentBranches, }); if !target_order.contains(target_key) && !request.resolutions.iter().any(|r| { r.facet == order_facet && r.decision != SemanticDecision::KeepTarget }) { let position = source_order.iter().position(|id| id == source_key).unwrap(); let anchor = |id: &Id| { map.get(id).and_then(|mapped| { target_order.iter().position(|id| id == mapped) }) }; let left = source_order[..position].iter().rev().find_map(anchor); let right = source_order[position + 1..].iter().find_map(anchor); let at=match (left,right) { (Some(a),Some(b)) if a+1==b=>b, (Some(a),None) if a+1==target_order.len()=>a+1, (None,Some(0))=>0, (None,None) if target_order.is_empty()=>0, _=>return Err("Ordered member insertion has no unique unchanged anchor; review an explicit final target order".into()), }; target_order.insert(at, *target_key); set_collection(&mut expected, target, *which, target_order)?; } } if let FacetSelector::Artifact { keys, facet: ArtifactFacet::Presence, } = selected && let Some(id) = chosen(keys, target_layer) && !expected.artifact_links.contains_key(id) && let Some(candidate) = candidates.artifact_links.get(id) { for field in [ ArtifactFacet::Name, ArtifactFacet::Description, ArtifactFacet::Owner, ArtifactFacet::Relation, ArtifactFacet::Uri, ArtifactFacet::Revision, ArtifactFacet::Fragment, ] { if !request.resolutions.iter().any(|r| { r.facet == (FacetSelector::Artifact { keys: keys.clone(), facet: field, }) }) { return Err("Creating an artifact requires explicit review of each introduced metadata facet".into()); } } expected.artifact_links.insert(*id, candidate.clone()); if let Some(old) = created_artifacts.insert(*id, candidate.clone()) && old != *candidate { return Err("Conflicting complete artifact creation candidates".into()); } } if resolution.decision == SemanticDecision::Candidate && matches!( selected, FacetSelector::Child(_) | FacetSelector::Incident { .. } | FacetSelector::Artifact { facet: ArtifactFacet::Owner, .. } ) { match selected { FacetSelector::Child(keys) => { let key = chosen(keys, target_layer).ok_or("Missing target child")?; expected .elements .get_mut(key) .ok_or("Missing target child")? .parent = candidates .elements .get(key) .ok_or("Missing child candidate")? .parent; } FacetSelector::Incident { keys, end } => { let key = chosen(keys, target_layer).ok_or("Missing target relation")?; let proposed = candidates .relations .get(key) .ok_or("Missing endpoint candidate")? .body .endpoints(); let current = expected .relations .get_mut(key) .ok_or("Missing target relation")?; let (a, b) = current.body.endpoints(); match end { IncidentEnd::Source => { current.body.set_endpoints(proposed.0, b) } IncidentEnd::Target => { current.body.set_endpoints(a, proposed.1) } } } FacetSelector::Artifact { keys, facet: ArtifactFacet::Owner, } => { let key = chosen(keys, target_layer).ok_or("Missing target artifact")?; let current = expected .artifact_links .get_mut(key) .ok_or("Missing target artifact")?; if current.owner != target { return Err( "Move the artifact from its currently attached owner trace" .into(), ); } current.owner = candidates .artifact_links .get(key) .ok_or("Missing artifact owner candidate")? .owner; } _ => unreachable!(), } } else { // Attachment was authorized on the immutable initial // snapshot. Another selected facet may have moved this // same artifact within the local proposal. let write_target = match selected { FacetSelector::Artifact { keys, facet } if *facet != ArtifactFacet::Presence => { chosen(keys, target_layer) .and_then(|id| expected.artifact_links.get(id)) .map_or(target, |artifact| artifact.owner) } _ => target, }; write_fact( &mut expected, write_target, &fact, from, target_layer, resolution.decision == SemanticDecision::TakeSource, )?; } if let FacetSelector::Artifact { keys, facet: ArtifactFacet::Presence, } = selected && let Some(id) = chosen(keys, target_layer) && initial.artifact_links.contains_key(id) && !expected.artifact_links.contains_key(id) { for field in [ ArtifactFacet::Name, ArtifactFacet::Description, ArtifactFacet::Owner, ArtifactFacet::Relation, ArtifactFacet::Uri, ArtifactFacet::Revision, ArtifactFacet::Fragment, ] { let removed = FacetSelector::Artifact { keys: keys.clone(), facet: field, }; if let Some(slot) = target_slot(target, &removed, target_layer)? { agree( &mut slots, slot, serde_json::Value::String("NotApplicable".into()), )?; } if capture_facet(&expected, oa, sa, &removed).is_err() { baseline.facets.retain(|v| v.selector() != removed); retired.push(removed); } else { acknowledged.push(removed); } } } } if let Some(slot) = target_slot(target, selected, target_layer)? { let observed_project = if resolution.decision == SemanticDecision::KeepTarget { initial } else { &expected }; let observed = slot_observation(observed_project, &slot)?; agree(&mut slots, slot, observed)?; } acknowledged.push(selected.clone()); } } for ordered in [ OrderedCollection::Steps, OrderedCollection::FragmentBranches, ] { let which = super::catalog::order_collection(ordered); if let Some(order) = collection(&expected, target, which) && collection(initial, target, which).as_ref() != Some(&order) && !request .resolutions .iter() .any(|r| r.facet == FacetSelector::Order(ordered)) { implicit_orders.push((target, ordered, order)); } } plan.reviews.push(SemanticReviewPreview { source: request.source, target, facets: all, acknowledged: acknowledged.clone(), retired, linked_impacts: linked_semantic_impacts(initial, &trace, &references)?, }); acknowledgements.push((oa, sa, trace.id, baseline, acknowledged)); } // A constructor owns every canonical branch slot mentioned by any proposal, // including a member introduced by another source. Compare all of these // against its independently derived record before any mutation is applied. let selected_slots = slots.keys().cloned().collect::>(); for (target, group, record) in &constructor_proposals { let mut proposal = initial.clone(); install_candidate(&mut proposal, record)?; for slot in &selected_slots { if slot.0 == *target && constructor_contains(group, &slot.1) { agree(&mut slots, slot.clone(), slot_observation(&proposal, slot)?)?; } } } let mut expected = initial.clone(); constructor_actions.sort_by_key(|(_, group, _)| { matches!( group, FacetSelector::Occurrence(OccurrenceFacet::MessageKind) ) }); for (target, group, record) in &constructor_actions { copy_constructor(&mut expected, *target, record, group)?; } for (id, artifact) in created_artifacts { expected.artifact_links.insert(id, artifact); } let mut merged = slots.iter().collect::>(); merged.sort_by_key(|((_, facet), _)| matches!(facet, FacetSelector::Order(_))); for (slot, desired) in merged { apply_canonical_slot(&mut expected, slot, desired)?; } let mut merged_orders = BTreeMap::new(); for (target, ordered, mut order) in implicit_orders { if slots.contains_key(&(target, FacetSelector::Order(ordered))) { continue; } let which = super::catalog::order_collection(ordered); let actual = collection(&expected, target, which).ok_or("Missing merged ordered collection")?; let surviving = actual.iter().copied().collect::>(); order.retain(|id| surviving.contains(id)); if surviving != order.iter().copied().collect::>() { return Err("Combined ordered membership needs an explicit final target order".into()); } if let Some(previous) = merged_orders.insert((target, ordered), order.clone()) && previous != order { return Err("Contradictory anchored orders need an explicit final target order".into()); } } for ((target, ordered), order) in merged_orders { set_collection( &mut expected, target, super::catalog::order_collection(ordered), order, )?; } audit_slots(&expected, &slots)?; for candidate in &plan.request.candidates { if semantic_record(&expected, candidate.id()).as_ref() != Some(candidate) { return Err(format!( "Candidate {} changes an unselected facet", candidate.id() )); } } let mut commands = plan.commands.clone(); commands.extend(edits(initial, &expected)); let prepared = prepare_command(original, &Command::Batch(commands.clone())).map_err(|e| e.to_string())?; plan.impacts = impacts(&expected, &prepared.project); if plan.impacts.len() != plan.request.accepted_impacts.len() || plan .impacts .iter() .any(|impact| !plan.request.accepted_impacts.contains(impact)) { return Err("Prepared candidate induces additional changes; explicitly review every exact before/after impact".into()); } audit_slots(&prepared.project, &slots)?; let mut final_project = prepared.project.clone(); for (oa, sa, trace_id, mut baseline, acknowledged) in acknowledgements { for selector in acknowledged { let fact = capture_facet(&final_project, oa, sa, &selector)?; baseline.facets.retain(|v| v.selector() != selector); baseline.facets.push(fact); } baseline.facets.sort_by_key(FacetReview::selector); validate_semantic_baseline(&baseline).map_err(|e| e.join("; "))?; let mut trace = trace_for(&final_project, oa, sa, Some(trace_id))?; let mut outer = trace.baseline.clone().unwrap_or(TraceBaseline { oa: final_project .properties(oa) .ok_or("Missing OA properties")?, sa: final_project .properties(sa) .ok_or("Missing SA properties")?, semantic: None, }); outer.semantic = Some(baseline); trace.baseline = Some(outer); final_project.traces.insert(trace.id, trace); } commands.extend(edits(&prepared.project, &final_project)); let result = prepare_command(original, &Command::Batch(commands.clone())).map_err(|e| e.to_string())?; if result.project != final_project { return Err("Final preparation changed the reviewed candidate".into()); } plan.prepared_fingerprint = Some(transitions::project_fingerprint(&result.project)); plan.commands = commands; Ok(()) } pub fn prepare_semantic_transition( project: &Project, plan: &SemanticTransitionPlan, ) -> Result { if transitions::project_fingerprint(project) != plan.base_fingerprint { return Err(ModelError("Semantic transition preview is stale".into())); } if !plan.conflicts.is_empty() { return Err(ModelError( "Resolve semantic transition conflicts before applying".into(), )); } let result = prepare_command(project, &Command::Batch(plan.commands.clone()))?; if Some(transitions::project_fingerprint(&result.project)) != plan.prepared_fingerprint { return Err(ModelError( "Prepared semantic candidate differs from reviewed result".into(), )); } Ok(result) } pub fn apply_semantic_transition( project: &Project, plan: &SemanticTransitionPlan, ) -> Result { prepare_semantic_transition(project, plan).map(|p| p.project) }