//! Compare independent authored intent before building the core's unique target candidates. use super::*; #[cfg(test)] #[path = "compose_diagnostic_tests.rs"] mod diagnostic_tests; type Slot = (Id, FacetSelector); fn context_leaf_path(project: &Project, slot: &Slot) -> Option<&'static str> { if project.relations.get(&slot.0)?.kind() != RelationKind::ContextualLink { return None; } match slot.1 { FacetSelector::Relation(RelationFacet::Source) => { Some("/body/ContextualLink/source/occurrence") } FacetSelector::Relation(RelationFacet::Target) => { Some("/body/ContextualLink/target/occurrence") } _ => None, } } struct Intent { value: Value, row: RowAddress, origin: FacetSelector, } struct Proposal { address: RowAddress, value: Value, groups: Vec, copies: Vec, } fn canonical(facet: &FacetSelector, layer: Layer) -> Option { fn key(keys: &KeyPair, layer: Layer) -> Option> { Some(KeyPair { oa: None, sa: Some( if layer == Layer::Oa { keys.oa.as_ref()? } else { keys.sa.as_ref()? } .clone(), ), }) } Some(match facet { FacetSelector::Member { collection, keys } => FacetSelector::Member { collection: *collection, keys: key(keys, layer)?, }, FacetSelector::Literal(keys) => FacetSelector::Literal(key(keys, layer)?), FacetSelector::Annotation(keys) => FacetSelector::Annotation(key(keys, layer)?), FacetSelector::Binding(keys) => FacetSelector::Binding(key(keys, layer)?), FacetSelector::Child(_) | FacetSelector::Incident { .. } | FacetSelector::Artifact { .. } => return None, _ => facet.clone(), }) } fn observe(project: &Project, slot: &Slot) -> Result { if let Some(path) = context_leaf_path(project, slot) { let value = record_value(project, slot.0)? .pointer(path) .cloned() .ok_or("Qualified endpoint leaf is absent.")?; return Ok(serde_json::json!({"Value":value})); } if let Some((_, facet)) = artifacts::parts(&slot.1) { return Ok(artifacts::observation( project.artifact_links.get(&slot.0), facet, )); } // A retired destination slot can legitimately apply on neither side of this // synthetic target-only observation. Public capture_facet intentionally rejects // such historical facts; determine absence through the same closed UI binding. let applicable = if let Some(family) = keyed::family(&slot.1) { keyed::path(&record_value(project, slot.0)?, family).is_some() } else if let FacetSelector::Order(which) = slot.1 { ordered::read(&record_value(project, slot.0)?, which).is_ok() } else { scalar::binding(project, slot.0, &slot.1).is_some() }; if !applicable { return Ok(Value::String("NotApplicable".into())); } Ok(scalar::observation( &capture_facet(project, slot.0, slot.0, &slot.1)?, Layer::Sa, )) } fn record_value(project: &Project, id: Id) -> Result { Draft::from_project(project, id) .map(|draft| draft.value) .ok_or("Target has no editable semantic record.".into()) } fn install(project: &mut Project, id: Id, value: Value) -> Result<(), String> { if let std::collections::btree_map::Entry::Occupied(mut entry) = project.elements.entry(id) { let record: Element = serde_json::from_value(value) .map_err(|error| format!("Final target value: {error}"))?; if record.id != id { return Err("Candidate identity changed.".into()); } entry.insert(record); } else if let std::collections::btree_map::Entry::Occupied(mut entry) = project.relations.entry(id) { let record: Relation = serde_json::from_value(value) .map_err(|error| format!("Final target value: {error}"))?; if record.id != id { return Err("Candidate identity changed.".into()); } entry.insert(record); } else if let std::collections::btree_map::Entry::Occupied(mut entry) = project.artifact_links.entry(id) { let record: ArtifactLink = serde_json::from_value(value) .map_err(|error| format!("Final artifact value: {error}"))?; if record.id != id { return Err("Candidate artifact identity changed.".into()); } entry.insert(record); } else { return Err("The batch foundation requires an existing target record.".into()); } Ok(()) } fn identity(base: &Project, id: Id) -> String { base.identities() .into_iter() .find(|(key, _)| *key == id) .map_or_else( || format!("Missing identity {id}"), |(_, alias)| format!("{} · @{alias}", base.named(id)), ) } fn review_identity(state: &State, row: RowAddress, id: Id) -> String { if !state.base.contains(id) && let Some(review) = state.row(row) && review.artifacts.seeds.contains_key(&id) { return review.review_choice(&state.base, id).label; } identity(&state.base, id) } fn review_title(state: &State, row: RowAddress, facet: &FacetSelector) -> String { state.row(row).map_or_else( || view::title(&state.base, facet), |review| review.review_title(&state.base, facet), ) } #[cfg(test)] fn describe(base: &Project, slot: &Slot, value: &Value) -> String { describe_with(slot, value, |id| identity(base, id)) } fn describe_with(slot: &Slot, value: &Value, identify: impl Fn(Id) -> String) -> String { let text = if let Some((_, facet)) = artifacts::parts(&slot.1) { if value.as_str() == Some("NotApplicable") { "artifact absent".into() } else if facet == ArtifactFacet::Owner { value .get("Value") .and_then(Value::as_str) .and_then(|s| s.parse::().ok()) .map(|id| format!("owner {}", identify(id))) .unwrap_or_else(|| value.to_string()) } else { value.get("Value").unwrap_or(value).to_string() } } else { value.to_string() }; if text.chars().count() > 400 { format!( "{}… (full draft retained)", text.chars().take(400).collect::() ) } else { text } } fn agree( state: &State, slots: &mut BTreeMap, slot: Slot, value: Value, row: RowAddress, origin: &FacetSelector, ) -> Result<(), String> { let base = &state.base; if let Some(previous) = slots.get(&slot) { if previous.value != value { let same_row = previous.row == row; let heading = if same_row { "Decisions in this review disagree" } else { "Independent row proposals disagree" }; let scope = artifacts::parts(&slot.1).map_or_else( || view::title(base, &slot.1), |(_, facet)| format!("Artifact {facet:?}"), ); let first_row = format!( "{} → {} [trace {}]", identity(base, previous.row.source), identity(base, previous.row.target), identity(base, previous.row.trace) ); let second_row = if same_row { "the same review".into() } else { format!( "{} → {} [trace {}]", identity(base, row.source), identity(base, row.target), identity(base, row.trace) ) }; let action = if same_row { "Edit or unselect one of these decisions." } else { "Edit or unselect a decision, or exclude a row." }; return Err(format!( "{heading} for {} · {scope}: {} in {first_row} requires {}; {} in {second_row} requires {}. {action} Both drafts are retained.", review_identity(state, row, slot.0), review_title(state, previous.row, &previous.origin), describe_with(&slot, &previous.value, |id| review_identity( state, previous.row, id )), review_title(state, row, origin), describe_with(&slot, &value, |id| review_identity(state, row, id)) )); } } else { slots.insert( slot, Intent { value, row, origin: origin.clone(), }, ); } Ok(()) } fn write_slot(project: &mut Project, slot: &Slot, observation: &Value) -> Result<(), String> { if let Some((_, facet)) = artifacts::parts(&slot.1) { if observation.as_str() == Some("NotApplicable") { if facet == ArtifactFacet::Owner { project.artifact_links.remove(&slot.0); } return Ok(()); } let mut value = record_value(project, slot.0)?; let field = artifacts::field(facet).ok_or("Artifact actual slot must identify metadata.")?; value[field] = observation .get("Value") .cloned() .ok_or("Artifact observation lacks typed value.")?; return install(project, slot.0, value); } if scalar::grouped(&slot.1) || observation.as_str() == Some("NotApplicable") { return Ok(()); } let supplied = observation .get("Value") .ok_or("Missing typed destination observation.")? .clone(); let mut value = record_value(project, slot.0)?; if keyed_rows::supported(&slot.1) { let key = keyed::key(&slot.1, Layer::Sa).ok_or("Destination row key missing.")?; let path = keyed::path(&value, keyed::family(&slot.1).expect("Supported family")) .ok_or("Destination row collection is no longer applicable.")?; let rows = value .pointer_mut(&path) .and_then(Value::as_array_mut) .ok_or("Destination rows missing.")?; let index = rows.iter().position(|row| match &slot.1 { FacetSelector::Annotation(_) => { serde_json::json!({"path":row["path"],"field":row["field"]}) == key } FacetSelector::Binding(_) => row["referenced"] == key, _ => false, }); match (index, supplied.is_null()) { (Some(index), false) => rows[index] = supplied, (Some(index), true) => { rows.remove(index); } (None, false) => rows.push(supplied), (None, true) => {} } } else if keyed::supported(&slot.1) { keyed::project_presence( &mut value, &slot.1, Layer::Sa, supplied.as_bool().ok_or("Expected typed presence.")?, )?; } else if let FacetSelector::Order(which) = slot.1 { let order: Vec = serde_json::from_value(supplied).map_err(|error| error.to_string())?; let current = ordered::read(&value, which)?; if order.len() != order.iter().collect::>().len() || order.iter().collect::>() != current.iter().collect::>() { return Err("Combined ordered membership needs an explicit final target order containing each surviving member once. Review membership explicitly in the order's row.".into()); } let path = keyed::path(&value, keyed::Family::Member(ordered::collection(which))) .ok_or("Order collection missing.")?; *value.pointer_mut(&path).expect("Closed collection path") = serde_json::to_value(order).expect("Order"); } else { let path = scalar::binding(project, slot.0, &slot.1) .or_else(|| context_leaf_path(project, slot).map(str::to_owned)) .ok_or("A selected property was retired by another row's constructor proposal.")?; crate::forms::set_shape_value(&mut value, &path, supplied)?; } install(project, slot.0, value) } pub(super) fn request(state: &State) -> Result { let base = &state.base; let addresses = state.included_addresses(); if addresses.is_empty() { return Err("Include at least one existing trace row in the combined review.".into()); } let references = state.canonical_references()?; let mut transition = TransitionRequest::new( state.direction, addresses.iter().map(|address| address.source).collect(), ); transition.mappings = addresses .iter() .map(|address| TransitionMapping { source: address.source, target: Some(address.target), target_kind: None, }) .collect(); let mut request = SemanticTransitionRequest::new(transition); let mut omissions = BTreeMap::new(); for address in &addresses { if let Some(raw) = state .row(*address) .and_then(|row| row.operation_omission.as_ref()) && omissions .insert(address.source, raw.clone()) .is_some_and(|prior| prior != *raw) { return Err("Rows reviewing the same operation disagree on the explicit omission rationale. Retain one consistent decision before Apply.".into()); } } request.transition.operation_omissions = omissions .into_iter() .map(|(source, rationale)| OperationOmission { source, rationale }) .collect(); request.references = references.clone(); request.accepted_impacts = state.accepted_impacts.clone(); let mut slots = BTreeMap::new(); let mut proposals = vec![]; let mut candidate_targets = BTreeSet::new(); let mut implicit_orders = vec![]; let mut artifact_creations = BTreeMap::new(); for address in addresses { let row = state .row(address) .ok_or("Included row draft is unavailable.")?; let trace = row .effective_trace(base) .ok_or("Included trace is unavailable.")?; request.traces.insert(address.source, address.trace); request.reviews.push(SemanticReviewRequest { source: address.source, resolutions: row .decisions .iter() .map(|(facet, decision)| FacetResolution { facet: facet.clone(), decision: *decision, }) .collect(), reset: row.resets.iter().cloned().collect(), }); if row.decisions.iter().any(|(facet, decision)| { !attachments::supported(facet) && !artifacts::supported(facet) && *decision == SemanticDecision::Candidate }) { candidate_targets.insert(address.target); } let final_value = row.combined_value(base, false).map_err(|error| { format!( "{} [{}]: {error}", base.named(address.source), base.named(address.trace) ) })?; let mut proposed = (**base).clone(); install(&mut proposed, address.target, final_value.clone())?; let artifact = row.artifacts .propose(base, &trace, state.direction, &references, &row.decisions)?; for (slot, observation, origin) in artifact.slots { agree(state, &mut slots, slot, observation, address, &origin)?; } for seed in artifact.creations { if artifact_creations .insert(seed.id, seed.clone()) .is_some_and(|prior| prior != seed) { return Err( "Independent artifact creation records disagree. All drafts are retained." .into(), ); } } candidate_targets.extend(artifact.candidates); let mut groups = vec![]; let mut copies = vec![]; for (facet, decision) in &row.decisions { if artifacts::supported(facet) { continue; } if attachments::supported(facet) { let attachment = row.attachments.propose( base, &trace, state.direction, facet, *decision, false, )?; let mut record = record_value(&proposed, attachment.slot.0)?; *record .pointer_mut(&attachment.path) .ok_or("Attachment field was retired by another selected decision.")? = attachment .record .pointer(&attachment.path) .cloned() .ok_or("Attachment proposal is incomplete.")?; install(&mut proposed, attachment.slot.0, record)?; let observed = observe(&proposed, &attachment.slot)?; agree( state, &mut slots, attachment.slot.clone(), observed, address, facet, )?; if *decision == SemanticDecision::Candidate { candidate_targets.insert(attachment.slot.0); } continue; } let Some(selector) = canonical(facet, state.direction.target_layer()) else { continue; }; let slot = (address.target, selector); let observed = if *decision == SemanticDecision::KeepTarget { observe(base, &slot)? } else { observe(&proposed, &slot)? }; agree(state, &mut slots, slot.clone(), observed, address, facet)?; if matches!( facet, FacetSelector::Relation( RelationFacet::ContextSource | RelationFacet::ContextTarget ) ) { let leaf = ( address.target, FacetSelector::Relation( if matches!(facet, FacetSelector::Relation(RelationFacet::ContextSource)) { RelationFacet::Source } else { RelationFacet::Target }, ), ); let observed = observe( if *decision == SemanticDecision::KeepTarget { base } else { &proposed }, &leaf, )?; agree(state, &mut slots, leaf, observed, address, facet)?; } let first_equal = row.can_acknowledge_constructor(base, facet) && (*decision != SemanticDecision::Candidate || observe(base, &slot)? == observe(&proposed, &slot)?); if scalar::grouped(facet) && (*decision == SemanticDecision::Candidate || !first_equal) { copies.push(facet.clone()); } if scalar::grouped(facet) && first_equal && *decision == SemanticDecision::Candidate { // Preserve authored payload changes even when the first equal-kind // decision acknowledges only its discriminator. The core must see // these changes and require separate descendant decisions. let mut catalog = semantic_facets(base, &trace, &references)?; catalog.extend(semantic_facets(&proposed, &trace, &references)?); for fact in catalog { if constructors::contains(facet, &fact.selector) && let Some(selector) = canonical(&fact.selector, state.direction.target_layer()) { let slot = (address.target, selector); let supplied = observe(&proposed, &slot)?; if supplied != observe(base, &slot)? { agree(state, &mut slots, slot, supplied, address, facet)?; } } } } if scalar::grouped(facet) && !first_equal { groups.push(facet.clone()); let own = if *decision == SemanticDecision::KeepTarget { &**base } else { &proposed }; let mut catalog = semantic_facets(base, &trace, &references)?; catalog.extend(semantic_facets(own, &trace, &references)?); for fact in catalog { if constructors::contains(facet, &fact.selector) && let Some(selector) = canonical(&fact.selector, state.direction.target_layer()) { let slot = (address.target, selector); agree( state, &mut slots, slot.clone(), observe(own, &slot)?, address, facet, )?; } } } } for which in [ OrderedCollection::Steps, OrderedCollection::FragmentBranches, ] { if !row.decisions.contains_key(&FacetSelector::Order(which)) && let Ok(order) = ordered::read(&record_value(&proposed, address.target)?, which) && ordered::read(&record_value(base, address.target)?, which) .ok() .as_ref() != Some(&order) { implicit_orders.push((address.target, which, order)); } } proposals.push(Proposal { address, value: final_value, groups, copies, }); } // Recheck every selected slot against every complete constructor scope, including // new keys that were absent from that constructor row's own catalog. let selected: Vec<_> = slots.keys().cloned().collect(); for proposal in &proposals { if proposal.groups.is_empty() { continue; } let mut proposed = (**base).clone(); install( &mut proposed, proposal.address.target, proposal.value.clone(), )?; let row = state.row(proposal.address).expect("Included row"); for group in &proposal.groups { let own = if row.decisions[group] == SemanticDecision::KeepTarget { &**base } else { &proposed }; for slot in &selected { if slot.0 == proposal.address.target && constructors::contains(group, &slot.1) { agree( state, &mut slots, slot.clone(), observe(own, slot)?, proposal.address, group, )?; } } } } let mut merged = (**base).clone(); merged.artifact_links.extend(artifact_creations); let mut groups: Vec<_> = proposals .iter() .flat_map(|proposal| proposal.copies.iter().map(move |group| (proposal, group))) .collect(); groups.sort_by_key(|(proposal, group)| { ( proposal.address.target, matches!( group, FacetSelector::Occurrence(OccurrenceFacet::MessageKind) ), ) }); for (proposal, group) in groups { let row = state.row(proposal.address).expect("Included row"); if row.decisions[group] == SemanticDecision::KeepTarget { continue; } let mut value = record_value(&merged, proposal.address.target)?; let supplied = &proposal.value; for path in constructors::roots(base, proposal.address.target, group) .ok_or("Constructor scope is unavailable.")? { *value .pointer_mut(&path) .ok_or("Overlapping constructors retire an authored scope.")? = supplied .pointer(&path) .cloned() .ok_or("Constructor candidate is incomplete.")?; } install(&mut merged, proposal.address.target, value)?; } let mut ordered_slots: Vec<_> = slots.iter().collect(); ordered_slots.sort_by_key(|((_, facet), _)| matches!(facet, FacetSelector::Order(_))); for (slot, intention) in ordered_slots { write_slot(&mut merged, slot, &intention.value)?; } let mut final_orders = BTreeMap::new(); for (target, which, mut order) in implicit_orders { if slots.contains_key(&(target, FacetSelector::Order(which))) { continue; } let surviving: BTreeSet<_> = ordered::read(&record_value(&merged, target)?, which)? .into_iter() .collect(); order.retain(|id| surviving.contains(id)); if order.iter().copied().collect::>() != surviving { return Err("Combined ordered membership needs an explicit final target order. Retained row drafts are unchanged.".into()); } if final_orders .insert((target, which), order.clone()) .is_some_and(|previous| previous != order) { return Err("Contradictory anchored orders need an explicit final target order. Retained row drafts are unchanged.".into()); } } for ((target, which), order) in final_orders { write_slot( &mut merged, &(target, FacetSelector::Order(which)), &serde_json::json!({"Value":order}), )?; } for (slot, intention) in &slots { if observe(&merged, slot)? != intention.value { return Err(format!( "Combined candidate does not preserve the selected intent for {} · {}.", base.named(slot.0), view::title(base, &slot.1) )); } } for target in candidate_targets { request .candidates .push(semantic_record(&merged, target).ok_or("Merged candidate target disappeared.")?); } Ok(request) }