Files
ARCADIA-Studio/experiments/r3-facet-carrier/src/lib.rs
T
sorokin.oleg.edu b2ab2b1feb
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Prove R3 self-tagged facet reviews against carrier baseline
2026-09-06 04:19:13 +03:00

935 lines
32 KiB
Rust

//! Throwaway R3 experiment. Nothing in oa-core depends on this crate.
//! A carrier stores independently reviewed facets; it is NOT a past valid model.
use oa_core::*;
use serde::{Deserialize, Serialize};
pub mod typed_reviews;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum OwnRecord {
Element {
parent: Option<Id>,
body: ElementBody,
},
Relation {
body: RelationBody,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MemberPair {
pub oa: Option<Id>,
pub sa: Option<Id>,
}
/// Deliberately finite proof subset, not the final exhaustive R3 catalog.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Facet {
IntegerMinimum,
IntegerMaximum,
DataUnit,
FieldMinimum,
StateGuard,
Trigger(MemberPair),
Step(MemberPair),
StepsOrder,
/// Definition AND default form one explicit constructor replacement group.
DataConstructor,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SemanticBaseline {
pub oa: OwnRecord,
pub sa: OwnRecord,
pub reviewed: Vec<Facet>,
pub orders: Vec<OrderCheckpoint>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum OrderedCollection {
Steps,
}
/// Order overlaps membership in the live Vec, so it needs a separate review value.
/// No observation IDs, history versions, or generic value representation.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OrderCheckpoint {
pub collection: OrderedCollection,
pub oa: Vec<Id>,
pub sa: Vec<Id>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Decision {
TakeSource,
KeepTarget,
Candidate,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Resolution {
pub facet: Facet,
pub decision: Decision,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Review {
pub source: Id,
pub target: Id,
pub candidate: Option<OwnRecord>,
pub resolutions: Vec<Resolution>,
}
pub fn own(p: &Project, id: Id) -> OwnRecord {
if let Some(e) = p.elements.get(&id) {
OwnRecord::Element {
parent: e.parent,
body: e.body.clone(),
}
} else {
OwnRecord::Relation {
body: p.relations[&id].body.clone(),
}
}
}
fn shared(record: &OwnRecord) -> Result<&SharedBody, String> {
match record {
OwnRecord::Element { body, .. } => body.shared().ok_or("Not a shared body".into()),
_ => Err("Not an element".into()),
}
}
fn shared_mut(record: &mut OwnRecord) -> Result<&mut SharedBody, String> {
match record {
OwnRecord::Element { body, .. } => body.shared_mut().ok_or("Not a shared body".into()),
_ => Err("Not an element".into()),
}
}
fn list(record: &OwnRecord, trigger: bool) -> Result<&Vec<Id>, String> {
match (record, trigger) {
(
OwnRecord::Relation {
body: RelationBody::StateTransition { triggers, .. },
},
true,
) => Ok(triggers),
(OwnRecord::Element { body, .. }, false) => {
body.steps().ok_or("Not an ordered sequence".into())
}
_ => Err("Wrong member selector".into()),
}
}
fn list_mut(record: &mut OwnRecord, trigger: bool) -> Result<&mut Vec<Id>, String> {
match (record, trigger) {
(
OwnRecord::Relation {
body: RelationBody::StateTransition { triggers, .. },
},
true,
) => Ok(triggers),
(OwnRecord::Element { body, .. }, false) => {
body.steps_mut().ok_or("Not an ordered sequence".into())
}
_ => Err("Wrong member selector".into()),
}
}
/// Copy same-coordinate facet; mapped reference proposals use copy_member below.
fn copy(dst: &mut OwnRecord, src: &OwnRecord, facet: &Facet, oa: bool) -> Result<(), String> {
match facet {
Facet::IntegerMinimum | Facet::IntegerMaximum => {
let SharedBody::DataType {
definition: DataTypeDefinition::Integer { minimum, maximum },
..
} = shared(src)?
else {
return Err("Explicit constructor decision required".into());
};
let (minimum, maximum) = (*minimum, *maximum);
let SharedBody::DataType {
definition:
DataTypeDefinition::Integer {
minimum: dst_min,
maximum: dst_max,
},
..
} = shared_mut(dst)?
else {
return Err("Explicit constructor decision required".into());
};
if *facet == Facet::IntegerMinimum {
*dst_min = minimum;
} else {
*dst_max = maximum;
}
}
Facet::DataUnit => {
let SharedBody::DataType { unit, .. } = shared(src)? else {
return Err("Wrong facet".into());
};
let unit = unit.clone();
let SharedBody::DataType { unit: dst_unit, .. } = shared_mut(dst)? else {
return Err("Wrong facet".into());
};
*dst_unit = unit;
}
Facet::FieldMinimum => {
let SharedBody::DataField { minimum, .. } = shared(src)? else {
return Err("Wrong facet".into());
};
let minimum = *minimum;
let SharedBody::DataField {
minimum: dst_min, ..
} = shared_mut(dst)?
else {
return Err("Wrong facet".into());
};
*dst_min = minimum;
}
Facet::StateGuard => {
let OwnRecord::Relation {
body: RelationBody::StateTransition { guard, .. },
} = src
else {
return Err("Wrong facet".into());
};
let OwnRecord::Relation {
body:
RelationBody::StateTransition {
guard: dst_guard, ..
},
} = dst
else {
return Err("Wrong facet".into());
};
*dst_guard = guard.clone();
}
Facet::Trigger(pair) | Facet::Step(pair) => {
let id = if oa { pair.oa } else { pair.sa };
copy_member(dst, src, matches!(facet, Facet::Trigger(_)), id, id)?;
}
Facet::StepsOrder => *list_mut(dst, false)? = list(src, false)?.clone(),
Facet::DataConstructor => {
let SharedBody::DataType {
definition,
default,
..
} = shared(src)?
else {
return Err("Wrong constructor scope".into());
};
let (definition, default) = (definition.clone(), default.clone());
let SharedBody::DataType {
definition: dst_definition,
default: dst_default,
..
} = shared_mut(dst)?
else {
return Err("Wrong constructor scope".into());
};
*dst_definition = definition;
*dst_default = default;
}
}
Ok(())
}
fn copy_member(
dst: &mut OwnRecord,
src: &OwnRecord,
trigger: bool,
src_id: Option<Id>,
dst_id: Option<Id>,
) -> Result<(), String> {
if src_id.is_none() && dst_id.is_none() {
return Err("Empty member identity".into());
}
let present =
src_id.is_some_and(|id| list(src, trigger).is_ok_and(|items| items.contains(&id)));
let Some(dst_id) = dst_id else {
return if present {
Err("Missing counterpart mapping".into())
} else {
Ok(())
};
};
let items = list_mut(dst, trigger)?;
if present && !items.contains(&dst_id) {
items.push(dst_id);
}
if !present {
items.retain(|id| *id != dst_id);
}
Ok(())
}
fn equal(a: &OwnRecord, b: &OwnRecord, facet: &Facet, oa: bool) -> Result<bool, String> {
let mut projected = a.clone();
copy(&mut projected, b, facet, oa)?;
Ok(projected == *a)
}
pub fn reviewed_changed(
base: &SemanticBaseline,
now: &OwnRecord,
facet: &Facet,
oa: bool,
) -> Result<Option<bool>, String> {
if !base.reviewed.contains(facet) {
return Ok(None);
}
if *facet == Facet::StepsOrder {
let checkpoint = base
.orders
.iter()
.find(|o| o.collection == OrderedCollection::Steps)
.ok_or("Missing order checkpoint")?;
return Ok(Some(
list(now, false)? != if oa { &checkpoint.oa } else { &checkpoint.sa },
));
}
equal(if oa { &base.oa } else { &base.sa }, now, facet, oa).map(|same| Some(!same))
}
/// One validated core candidate, then baseline assignment. No partial commit.
/// OA→SA only in this bounded experiment.
pub fn apply(
p: &Project,
base: &SemanticBaseline,
review: &Review,
) -> Result<(Project, SemanticBaseline), String> {
let source = own(p, review.source);
let current = own(p, review.target);
let mut target = current.clone();
let mut acknowledged = base.clone();
let mut seen = Vec::new();
if review
.resolutions
.iter()
.any(|r| r.facet == Facet::DataConstructor)
&& review
.resolutions
.iter()
.any(|r| matches!(r.facet, Facet::IntegerMinimum | Facet::IntegerMaximum))
{
return Err("Constructor and descendant facets overlap".into());
}
for resolution in &review.resolutions {
if seen.contains(&resolution.facet) {
return Err("Duplicate facet".into());
}
seen.push(resolution.facet.clone());
match resolution.decision {
Decision::KeepTarget => {}
Decision::TakeSource => match &resolution.facet {
Facet::Trigger(pair) | Facet::Step(pair) => copy_member(
&mut target,
&source,
matches!(resolution.facet, Facet::Trigger(_)),
pair.oa,
pair.sa,
)?,
Facet::StepsOrder => {
return Err("This probe requires an explicit final order".into());
}
facet => copy(&mut target, &source, facet, false)?,
},
Decision::Candidate => {
let candidate = review.candidate.as_ref().ok_or("Missing candidate")?;
if resolution.facet == Facet::StepsOrder {
let mut expected = list(&target, false)?.clone();
let mut proposed = list(candidate, false)?.clone();
expected.sort();
proposed.sort();
if expected != proposed || proposed.windows(2).any(|p| p[0] == p[1]) {
return Err(
"Final order must contain every surviving target member exactly once"
.into(),
);
}
}
copy(&mut target, candidate, &resolution.facet, false)?;
}
}
if resolution.facet == Facet::DataConstructor {
acknowledged.reviewed.retain(|f| {
!matches!(
f,
Facet::IntegerMinimum | Facet::IntegerMaximum | Facet::DataConstructor
)
});
}
if resolution.facet == Facet::StepsOrder {
acknowledged
.orders
.retain(|o| o.collection != OrderedCollection::Steps);
acknowledged.orders.push(OrderCheckpoint {
collection: OrderedCollection::Steps,
oa: list(&source, false)?.clone(),
sa: list(&target, false)?.clone(),
});
} else {
copy(&mut acknowledged.oa, &source, &resolution.facet, true)?;
copy(&mut acknowledged.sa, &target, &resolution.facet, false)?;
}
if !acknowledged.reviewed.contains(&resolution.facet) {
acknowledged.reviewed.push(resolution.facet.clone());
}
}
// Explicit candidate cannot smuggle changes to any unreviewed facet.
if let Some(candidate) = &review.candidate {
if *candidate != target {
return Err("Candidate changes an unselected facet".into());
}
}
let command = match target {
OwnRecord::Element { parent, body } => {
let mut e = p.elements[&review.target].clone();
e.parent = parent;
e.body = body;
Command::EditElement(review.target, e)
}
OwnRecord::Relation { body } => {
let mut r = p.relations[&review.target].clone();
r.body = body;
Command::EditRelation(review.target, r)
}
};
let prepared = prepare_command(p, &Command::Batch(vec![command])).map_err(|e| e.to_string())?;
Ok((prepared.project, acknowledged))
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn add(
p: &mut Project,
kind: Kind,
layer: Layer,
name: &str,
parent: Option<Id>,
) -> Id {
p.add_element(kind, layer, name, parent).unwrap()
}
pub(crate) fn set(p: &mut Project, id: Id, body: SharedBody) {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body;
}
pub(crate) fn valid(p: &Project) {
let errors: Vec<_> = validation::validate(p)
.into_iter()
.filter(|i| i.severity == validation::Severity::Error)
.collect();
assert!(errors.is_empty(), "{errors:?}");
}
fn baseline(p: &Project, oa: Id, sa: Id, reviewed: Vec<Facet>) -> SemanticBaseline {
let orders = if reviewed.contains(&Facet::StepsOrder) {
vec![OrderCheckpoint {
collection: OrderedCollection::Steps,
oa: list(&own(p, oa), false).unwrap().clone(),
sa: list(&own(p, sa), false).unwrap().clone(),
}]
} else {
vec![]
};
SemanticBaseline {
oa: own(p, oa),
sa: own(p, sa),
reviewed,
orders,
}
}
fn resolution(facet: Facet, decision: Decision) -> Resolution {
Resolution { facet, decision }
}
fn roundtrip(p: &Project, base: &SemanticBaseline) {
let json = serde_json::to_string(base).unwrap();
assert_eq!(
serde_json::from_str::<SemanticBaseline>(&json).unwrap(),
*base
);
assert_eq!(language::parse(&language::format(p)).unwrap(), *p);
assert_eq!(
serde_json::from_str::<Project>(&serde_json::to_string(p).unwrap()).unwrap(),
*p
);
}
fn evidence(name: &str, p: &Project, base: &SemanticBaseline) {
let folder =
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/r3-probe/evidence");
std::fs::create_dir_all(&folder).unwrap();
std::fs::write(
folder.join(format!("{name}-baseline.json")),
serde_json::to_string_pretty(base).unwrap(),
)
.unwrap();
std::fs::write(
folder.join(format!("{name}-candidate.arcadia")),
language::format(p),
)
.unwrap();
}
pub(crate) fn integer(minimum: i64, maximum: i64, unit: &str) -> SharedBody {
SharedBody::DataType {
definition: DataTypeDefinition::Integer {
minimum: Some(minimum),
maximum: Some(maximum),
},
default: None,
unit: unit.into(),
}
}
#[test]
fn bounded_data_partial_keep_take_and_constructor_atomicity() {
let mut p = Project::empty();
let oa = add(&mut p, Kind::DataType, Layer::Oa, "OA bounded", None);
let sa = add(&mut p, Kind::DataType, Layer::Sa, "SA bounded", None);
set(&mut p, oa, integer(0, 100, "m"));
set(&mut p, sa, integer(0, 100, "ft"));
let base = baseline(
&p,
oa,
sa,
vec![
Facet::IntegerMinimum,
Facet::IntegerMaximum,
Facet::DataUnit,
],
);
set(&mut p, oa, integer(120, 200, "cm"));
let review = Review {
source: oa,
target: sa,
candidate: None,
resolutions: vec![
resolution(Facet::IntegerMinimum, Decision::KeepTarget),
resolution(Facet::DataUnit, Decision::KeepTarget),
],
};
let (kept, mixed) = apply(&p, &base, &review).unwrap();
assert_eq!(kept, p);
// OA baseline now min=120,max=100. This is intentional: fields were reviewed independently.
assert!(matches!(
shared(&mixed.oa).unwrap(),
SharedBody::DataType {
definition: DataTypeDefinition::Integer {
minimum: Some(120),
maximum: Some(100)
},
..
}
));
assert_eq!(
reviewed_changed(&mixed, &own(&p, oa), &Facet::IntegerMinimum, true).unwrap(),
Some(false)
);
assert_eq!(
reviewed_changed(&mixed, &own(&p, oa), &Facet::IntegerMaximum, true).unwrap(),
Some(true)
);
let take = Review {
source: oa,
target: sa,
candidate: None,
resolutions: vec![resolution(Facet::IntegerMaximum, Decision::TakeSource)],
};
let (p, merged) = apply(&p, &mixed, &take).unwrap();
assert!(
matches!(shared(&own(&p, sa)).unwrap(), SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum: Some(0), maximum: Some(200) }, unit, .. } if unit == "ft")
);
let (again, same) = apply(&p, &merged, &take).unwrap();
assert_eq!(again, p);
assert_eq!(same, merged);
roundtrip(&p, &mixed);
evidence("mixed-range", &p, &mixed);
let mut fields = p;
let owner_oa = add(&mut fields, Kind::DataType, Layer::Oa, "OA record", None);
let owner_sa = add(&mut fields, Kind::DataType, Layer::Sa, "SA record", None);
for id in [owner_oa, owner_sa] {
set(
&mut fields,
id,
SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype: None },
default: None,
unit: String::new(),
},
);
}
let field_oa = add(
&mut fields,
Kind::DataField,
Layer::Oa,
"OA field",
Some(owner_oa),
);
let field_sa = add(
&mut fields,
Kind::DataField,
Layer::Sa,
"SA field",
Some(owner_sa),
);
for (id, max) in [(field_oa, 5), (field_sa, 9)] {
set(
&mut fields,
id,
SharedBody::DataField {
type_ref: TypeRef::Named(if id == field_oa { oa } else { sa }),
minimum: 0,
maximum: Some(max),
unit: String::new(),
direction: ParameterDirection::Unspecified,
storage: FieldStorage::Value,
default: None,
},
);
}
let field_base = baseline(&fields, field_oa, field_sa, vec![Facet::FieldMinimum]);
if let Some(SharedBody::DataField { minimum, .. }) =
fields.elements.get_mut(&field_oa).unwrap().shared_mut()
{
*minimum = 2;
}
let (fields, field_base) = apply(
&fields,
&field_base,
&Review {
source: field_oa,
target: field_sa,
candidate: None,
resolutions: vec![resolution(Facet::FieldMinimum, Decision::TakeSource)],
},
)
.unwrap();
assert!(matches!(
fields.elements[&field_sa].shared(),
Some(SharedBody::DataField {
minimum: 2,
maximum: Some(9),
..
})
));
roundtrip(&fields, &field_base);
let mut switched = fields.clone();
let constructor_base = baseline(&switched, owner_oa, owner_sa, vec![]);
// Remove only the source's child; the target-only field remains real independent work.
switched = prepare_command(&switched, &Command::Delete(field_oa))
.unwrap()
.project;
set(&mut switched, owner_oa, integer(0, 10, ""));
valid(&switched);
let before = switched.clone();
let before_base = constructor_base.clone();
let result = apply(
&switched,
&constructor_base,
&Review {
source: owner_oa,
target: owner_sa,
candidate: None,
resolutions: vec![resolution(Facet::DataConstructor, Decision::TakeSource)],
},
);
assert!(
result.is_err(),
"target Record child blocks primitive constructor switch"
);
assert_eq!(switched, before);
assert_eq!(constructor_base, before_base);
let forbidden = apply(
&switched,
&constructor_base,
&Review {
source: owner_oa,
target: owner_sa,
candidate: None,
resolutions: vec![resolution(Facet::IntegerMinimum, Decision::TakeSource)],
},
);
assert!(forbidden.unwrap_err().contains("constructor"));
println!(
"bounded data: mixed historical min/max permitted; unselected drift retained; target max/unit preserved; constructor candidate rejected atomically"
);
}
pub(crate) fn state_side(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) {
let owner = add(
p,
if layer == Layer::Oa {
Kind::Entity
} else {
Kind::System
},
layer,
&format!("{prefix} owner"),
None,
);
let machine = add(
p,
Kind::StateMachine,
layer,
&format!("{prefix} machine"),
Some(owner),
);
let region = add(
p,
Kind::Region,
layer,
&format!("{prefix} region"),
Some(machine),
);
let a = add(p, Kind::State, layer, &format!("{prefix} A"), Some(region));
let b = add(p, Kind::State, layer, &format!("{prefix} B"), Some(region));
let old = add(p, Kind::Event, layer, &format!("{prefix} old event"), None);
let new = add(p, Kind::Event, layer, &format!("{prefix} new event"), None);
let relation = p.relate(
RelationKind::StateTransition,
&format!("{prefix} transition"),
a,
b,
);
if let RelationBody::StateTransition {
triggers, guard, ..
} = &mut p.relations.get_mut(&relation).unwrap().body
{
*triggers = vec![old];
*guard = "ready".into();
}
(relation, old, new)
}
#[test]
fn state_guard_and_trigger_delta_retain_deleted_historical_identity() {
let mut p = Project::empty();
let (oa, old_oa, new_oa) = state_side(&mut p, Layer::Oa, "OA");
let (sa, old_sa, new_sa) = state_side(&mut p, Layer::Sa, "SA");
let extra = add(&mut p, Kind::Event, Layer::Sa, "Target only", None);
let old = MemberPair {
oa: Some(old_oa),
sa: Some(old_sa),
};
let new = MemberPair {
oa: Some(new_oa),
sa: Some(new_sa),
};
let base = baseline(
&p,
oa,
sa,
vec![Facet::StateGuard, Facet::Trigger(old), Facet::Trigger(new)],
);
p = prepare_command(&p, &Command::Delete(old_oa))
.unwrap()
.project;
if let RelationBody::StateTransition {
triggers, guard, ..
} = &mut p.relations.get_mut(&oa).unwrap().body
{
*triggers = vec![new_oa];
*guard = "armed".into();
}
if let RelationBody::StateTransition {
triggers, guard, ..
} = &mut p.relations.get_mut(&sa).unwrap().body
{
triggers.push(extra);
*guard = "local inhibit clear".into();
}
valid(&p);
assert!(!p.elements.contains_key(&old_oa));
assert!(list(&base.oa, true).unwrap().contains(&old_oa));
// Current source uses live @alias semantics. A historical body needs a
// distinct source context, not transplantation into a live declaration.
let mut historical_as_live = p.clone();
if let OwnRecord::Relation { body } = &base.oa {
historical_as_live.relations.get_mut(&oa).unwrap().body = body.clone();
}
let rejected = language::parse(&language::format(&historical_as_live));
assert!(
rejected.is_err(),
"deleted historical UUID is not a live alias"
);
let review = Review {
source: oa,
target: sa,
candidate: None,
resolutions: vec![
resolution(Facet::StateGuard, Decision::KeepTarget),
resolution(Facet::Trigger(old), Decision::TakeSource),
resolution(Facet::Trigger(new), Decision::TakeSource),
],
};
let (p, acknowledged) = apply(&p, &base, &review).unwrap();
assert_eq!(list(&own(&p, sa), true).unwrap(), &vec![extra, new_sa]);
assert!(p.elements.contains_key(&old_sa));
assert!(
matches!(&p.relations[&sa].body, RelationBody::StateTransition { guard, .. } if guard == "local inhibit clear")
);
roundtrip(&p, &base);
roundtrip(&p, &acknowledged);
evidence("deleted-trigger", &p, &base);
let (again, same) = apply(&p, &acknowledged, &review).unwrap();
assert_eq!(again, p);
assert_eq!(same, acknowledged);
println!(
"state: old source Event deleted; historical member pair still removes its target link, preserves target object and extra trigger, keeps guard"
);
}
pub(crate) fn sequence(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) {
let sequence = add(
p,
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
},
layer,
prefix,
None,
);
let a = add(
p,
Kind::Occurrence,
layer,
&format!("{prefix} A"),
Some(sequence),
);
let b = add(
p,
Kind::Occurrence,
layer,
&format!("{prefix} B"),
Some(sequence),
);
for id in [a, b] {
set(
p,
id,
SharedBody::Occurrence {
body: OccurrenceBody::Control {
kind: ControlKind::Choice,
},
precondition: String::new(),
postcondition: String::new(),
},
);
}
*p.elements.get_mut(&sequence).unwrap().steps_mut().unwrap() = vec![a, b];
(sequence, a, b)
}
#[test]
fn ordered_steps_preserve_target_only_members_and_reject_hidden_edits() {
let mut p = Project::empty();
let (oa, oa_a, oa_b) = sequence(&mut p, Layer::Oa, "OA flow");
let (sa, sa_a, sa_b) = sequence(&mut p, Layer::Sa, "SA flow");
let base = baseline(&p, oa, sa, vec![Facet::StepsOrder]);
// Counterexample: a bare carrier loses pending placement review after
// independently acknowledging append membership. The order checkpoint
// keeps the exact older order and therefore retains the drift.
let oa_c = Id::new_v4();
let sa_c = Id::new_v4();
let mut source_with_append = own(&p, oa);
list_mut(&mut source_with_append, false).unwrap().push(oa_c);
let mut membership_only = base.clone();
copy(
&mut membership_only.oa,
&source_with_append,
&Facet::Step(MemberPair {
oa: Some(oa_c),
sa: Some(sa_c),
}),
true,
)
.unwrap();
assert_eq!(
membership_only.oa, source_with_append,
"bare carrier would falsely report no order drift"
);
assert_eq!(
reviewed_changed(
&membership_only,
&source_with_append,
&Facet::StepsOrder,
true
)
.unwrap(),
Some(true)
);
let extra = add(
&mut p,
Kind::Occurrence,
Layer::Sa,
"Target only step",
Some(sa),
);
set(
&mut p,
extra,
SharedBody::Occurrence {
body: OccurrenceBody::Control {
kind: ControlKind::Merge,
},
precondition: String::new(),
postcondition: String::new(),
},
);
*p.elements.get_mut(&sa).unwrap().steps_mut().unwrap() = vec![sa_a, extra, sa_b];
*p.elements.get_mut(&oa).unwrap().steps_mut().unwrap() = vec![oa_b, oa_a];
valid(&p);
let take = Review {
source: oa,
target: sa,
candidate: None,
resolutions: vec![resolution(Facet::StepsOrder, Decision::TakeSource)],
};
assert!(
apply(&p, &base, &take)
.unwrap_err()
.contains("explicit final order")
);
let mut candidate = own(&p, sa);
*list_mut(&mut candidate, false).unwrap() = vec![sa_b, sa_a];
let mut review = Review {
candidate: Some(candidate),
resolutions: vec![resolution(Facet::StepsOrder, Decision::Candidate)],
..take
};
assert!(
apply(&p, &base, &review)
.unwrap_err()
.contains("every surviving")
);
*list_mut(review.candidate.as_mut().unwrap(), false).unwrap() = vec![sa_b, extra, sa_a];
let target_only = p.elements[&extra].clone();
let (p, acknowledged) = apply(&p, &base, &review).unwrap();
assert_eq!(list(&own(&p, sa), false).unwrap(), &vec![sa_b, extra, sa_a]);
assert_eq!(p.elements[&extra], target_only);
roundtrip(&p, &acknowledged);
evidence("target-only-step", &p, &acknowledged);
let mut duplicate = review.clone();
duplicate.resolutions.push(duplicate.resolutions[0].clone());
assert!(
apply(&p, &acknowledged, &duplicate)
.unwrap_err()
.contains("Duplicate")
);
let mut hidden = review;
if let Some(OwnRecord::Element { parent, .. }) = &mut hidden.candidate {
*parent = Some(extra);
}
assert!(
apply(&p, &acknowledged, &hidden)
.unwrap_err()
.contains("unselected")
);
println!(
"steps: ambiguous source reorder demands explicit candidate; missing target-only member and unselected parent edits reject"
);
}
}