Files
ARCADIA-Studio/crates/oa-core/src/semantic/review.rs
T

1201 lines
50 KiB
Rust

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<FacetResolution>,
/// Exact previous selectors deliberately invalidated after correspondence changes.
#[serde(default)]
pub reset: Vec<FacetSelector>,
}
#[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<ImpactRecord>,
pub after: Option<ImpactRecord>,
}
#[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<Id, Id>,
/// Canonical OA→SA reference correspondence, including historical deleted IDs.
#[serde(default)]
pub references: BTreeMap<Id, Id>,
#[serde(default)]
pub reviews: Vec<SemanticReviewRequest>,
/// Final typed candidate records. Identity and aliases cannot change here.
#[serde(default)]
pub candidates: Vec<SemanticRecord>,
#[serde(default)]
pub accepted_impacts: Vec<SemanticImpact>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SemanticReviewPreview {
pub source: Id,
pub target: Id,
pub facets: Vec<FacetDrift>,
pub acknowledged: Vec<FacetSelector>,
pub retired: Vec<FacetSelector>,
pub linked_impacts: Vec<LinkedSemanticImpact>,
}
#[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<SemanticReviewPreview>,
pub impacts: Vec<SemanticImpact>,
pub conflicts: Vec<String>,
pub commands: Vec<Command>,
pub prepared_fingerprint: Option<String>,
}
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<Id>) -> Result<Trace, String> {
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<Option<(Id, FacetSelector)>, 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<T: Clone>(key: &KeyPair<T>, layer: Layer) -> KeyPair<T> {
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<serde_json::Value, String> {
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<T>(review: &Reviewed<T>, layer: Layer) -> &Observed<T> {
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<Command> {
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<SemanticImpact> {
let mut result = vec![];
macro_rules! records {
($field:ident,$variant:ident) => {
for id in before
.$field
.keys()
.chain(after.$field.keys())
.copied()
.collect::<BTreeSet<_>>()
{
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<SemanticTransitionPlan, ModelError> {
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<Id>)> = vec![];
let mut seen_sources = BTreeSet::new();
let mut acknowledgements: Vec<(Id, Id, Id, SemanticBaseline, Vec<FacetSelector>)> = 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(&current, source_layer) == side(&current, target_layer)
&& (resolution.decision != SemanticDecision::Candidate
|| side(
&capture_facet(&candidates, oa, sa, &resolution.facet)?,
target_layer,
) == side(&current, target_layer));
Ok((
resolution,
is_constructor(&resolution.facet) && !first_equal_constructor,
))
})
.collect::<Result<Vec<_>, 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(&current)
{
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(&current, target_layer))?;
}
acknowledged.push(selected.clone());
} else if constructor_group {
let old = baseline.facets.iter().find(|v| v.selector() == *selected);
let current_source = side(&current, source_layer);
let current_target = side(&current, 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(&current));
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::<Vec<_>>();
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::<Vec<_>>();
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::<BTreeSet<_>>();
order.retain(|id| surviving.contains(id));
if surviving != order.iter().copied().collect::<BTreeSet<_>>() {
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<PreparedCommand, ModelError> {
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<Project, ModelError> {
prepare_semantic_transition(project, plan).map(|p| p.project)
}