Integrate release recovery, semantic review, coverage and Windows packaging

This commit is contained in:
Codex
2026-09-06 16:31:55 -07:00
parent c091f80e6e
commit 2b2d2bc18b
174 changed files with 27635 additions and 127 deletions
+85 -6
View File
@@ -1,10 +1,12 @@
//! Closed presence-only adapter. The core independently validates the full final record.
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Family {
Member(ReferenceCollection),
Literal,
Annotation,
Binding,
}
impl std::fmt::Display for Family {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -16,12 +18,17 @@ impl std::fmt::Display for Family {
Self::Member(ReferenceCollection::TransitionTriggers) => "Transition triggers",
Self::Member(ReferenceCollection::TransitionEffects) => "Transition effects",
Self::Member(ReferenceCollection::Constrained) => "Constrained identities",
Self::Member(ReferenceCollection::Steps) => "Ordered steps",
Self::Member(ReferenceCollection::FragmentBranches) => "Fragment branches",
Self::Literal => "Enumeration literals",
_ => "Ordered membership (read-only)",
Self::Annotation => "Message values",
Self::Binding => "Scenario lifeline bindings",
})
}
}
pub(super) const FAMILIES: [Family; 8] = [
pub(super) const FAMILIES: [Family; 12] = [
Family::Member(ReferenceCollection::Steps),
Family::Member(ReferenceCollection::FragmentBranches),
Family::Member(ReferenceCollection::FunctionInputItems),
Family::Member(ReferenceCollection::FunctionOutputItems),
Family::Member(ReferenceCollection::SignalItems),
@@ -30,11 +37,15 @@ pub(super) const FAMILIES: [Family; 8] = [
Family::Member(ReferenceCollection::TransitionEffects),
Family::Member(ReferenceCollection::Constrained),
Family::Literal,
Family::Annotation,
Family::Binding,
];
pub(super) fn family(facet: &FacetSelector) -> Option<Family> {
let family = match facet {
FacetSelector::Member { collection, .. } => Family::Member(*collection),
FacetSelector::Literal(_) => Family::Literal,
FacetSelector::Annotation(_) => Family::Annotation,
FacetSelector::Binding(_) => Family::Binding,
_ => return None,
};
FAMILIES.contains(&family).then_some(family)
@@ -43,11 +54,27 @@ pub(super) fn supported(facet: &FacetSelector) -> bool {
family(facet).is_some()
}
// Fixed paths for the seven unordered collections and the exact enum literal set.
// Fixed paths for the closed collections and the exact enum literal set.
// They are selected by the typed facet; callers cannot supply arbitrary paths.
pub(super) fn path(value: &Value, family: Family) -> Option<String> {
let prefixes = ["/body/Oa/Shared", "/body/Sa/Shared"];
let tails: &[&str] = match family {
Family::Member(ReferenceCollection::Steps) => {
return [
"/body/Oa/Process/steps",
"/body/Sa/FunctionalChain/steps",
"/body/Oa/Shared/Scenario/steps",
"/body/Sa/Shared/Scenario/steps",
"/body/Oa/Shared/Occurrence/body/Branch/occurrences",
"/body/Sa/Shared/Occurrence/body/Branch/occurrences",
]
.into_iter()
.find(|p| value.pointer(p).is_some_and(Value::is_array))
.map(str::to_owned);
}
Family::Member(ReferenceCollection::FragmentBranches) => {
&["Occurrence/body/Fragment/branches"]
}
Family::Member(ReferenceCollection::FunctionInputItems) => {
&["Port/contract/FunctionInput/incoming_items"]
}
@@ -57,6 +84,8 @@ pub(super) fn path(value: &Value, family: Family) -> Option<String> {
Family::Member(ReferenceCollection::SignalItems) => &["Event/body/Signal/items"],
Family::Member(ReferenceCollection::Constrained) => &["Constraint/constrained"],
Family::Literal => &["DataType/definition/Enumeration/literals"],
Family::Annotation => &["Occurrence/body/Exchange/values"],
Family::Binding => &["Occurrence/body/ScenarioUse/bindings"],
Family::Member(ReferenceCollection::ExchangeItems) => {
return ["Interaction", "FunctionalExchange", "BoundaryExchange"]
.iter()
@@ -75,14 +104,13 @@ pub(super) fn path(value: &Value, family: Family) -> Option<String> {
.is_some_and(Value::is_array)
.then(|| "/body/StateTransition/effects".into());
}
_ => return None,
};
prefixes
.iter()
.flat_map(|prefix| tails.iter().map(move |tail| format!("{prefix}/{tail}")))
.find(|p| value.pointer(p).is_some_and(Value::is_array))
}
fn key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
pub(super) fn key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
match facet {
FacetSelector::Member { keys, .. } => if layer == Layer::Oa { keys.oa } else { keys.sa }
.map(|id| serde_json::to_value(id).expect("Id")),
@@ -92,6 +120,14 @@ fn key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
keys.sa.as_ref()
}
.map(|v| Value::String(v.clone())),
FacetSelector::Annotation(keys) => if layer == Layer::Oa {
keys.oa.as_ref()
} else {
keys.sa.as_ref()
}
.map(|key| serde_json::to_value(key).expect("Annotation key")),
FacetSelector::Binding(keys) => if layer == Layer::Oa { keys.oa } else { keys.sa }
.map(|id| serde_json::to_value(id).expect("Binding key")),
_ => None,
}
}
@@ -137,6 +173,9 @@ impl State {
facet: &FacetSelector,
decision: SemanticDecision,
) -> Result<(), String> {
if keyed_rows::supported(facet) {
return self.project_row(project, trace, value, facet, decision);
}
let present = match decision {
SemanticDecision::KeepTarget => return Ok(()),
SemanticDecision::TakeSource => {
@@ -170,6 +209,9 @@ impl State {
}
pub(super) fn member_choices(&self, project: &Project, layer: Layer) -> Vec<Choice> {
if self.member_family == Family::Binding {
return self.binding_choices(project, layer, true);
}
let Some(trace) = self.effective_trace(project) else {
return vec![];
};
@@ -210,6 +252,20 @@ impl State {
) -> Result<(), String> {
self.member_family = family(&facet).ok_or("This keyed family remains read-only.")?;
match facet {
FacetSelector::Annotation(keys) => {
self.annotation_oa.selected = keys.oa.map(|key| crate::data_values::FieldPathKey {
path: key.path,
field: key.field,
});
self.annotation_sa.selected = keys.sa.map(|key| crate::data_values::FieldPathKey {
path: key.path,
field: key.field,
});
}
FacetSelector::Binding(keys) => {
self.member_oa = keys.oa.map(|id| view::choice(project, id));
self.member_sa = keys.sa.map(|id| view::choice(project, id));
}
FacetSelector::Member { keys, .. } => {
self.member_oa = keys.oa.map(|id| view::choice(project, id));
self.member_sa = keys.sa.map(|id| view::choice(project, id));
@@ -241,9 +297,32 @@ impl State {
oa: self.literal_oa.then(|| self.literal.clone()),
sa: self.literal_sa.then(|| self.literal.clone()),
}),
Family::Annotation => FacetSelector::Annotation(KeyPair {
oa: self
.annotation_oa
.selected
.as_ref()
.map(|key| AnnotationKey {
path: key.path.clone(),
field: key.field,
}),
sa: self
.annotation_sa
.selected
.as_ref()
.map(|key| AnnotationKey {
path: key.path.clone(),
field: key.field,
}),
}),
Family::Binding => FacetSelector::Binding(KeyPair {
oa: self.member_oa.as_ref().map(|c| c.id),
sa: self.member_sa.as_ref().map(|c| c.id),
}),
};
let trace = self.effective_trace(project).ok_or("Choose a trace.")?;
capture_facet(project, trace.oa, trace.sa, &facet)?;
self.stage_row_references(&facet)?;
if let FacetSelector::Member {
keys:
KeyPair {
@@ -0,0 +1,345 @@
//! Atomic message-value and lifeline-binding rows with exact, immutable keys.
use super::*;
use crate::data_values::{FieldPathChoice, FieldPathKey};
#[derive(Default)]
pub struct AddressDraft {
pub selected: Option<FieldPathKey>,
pub scope: Option<FieldPathKey>,
pub page: usize,
}
#[derive(Default)]
pub struct RowDraft {
pub expression: String,
pub editor: text_editor::Content,
pub local: Option<Choice>,
}
pub(super) fn supported(facet: &FacetSelector) -> bool {
matches!(
facet,
FacetSelector::Annotation(_) | FacetSelector::Binding(_)
)
}
pub(super) fn target_selector(facet: &FacetSelector, layer: Layer) -> FacetSelector {
match facet {
FacetSelector::Annotation(keys) => FacetSelector::Annotation(KeyPair {
oa: (layer == Layer::Oa).then(|| keys.oa.clone()).flatten(),
sa: (layer == Layer::Sa).then(|| keys.sa.clone()).flatten(),
}),
FacetSelector::Binding(keys) => FacetSelector::Binding(KeyPair {
oa: if layer == Layer::Oa { keys.oa } else { None },
sa: if layer == Layer::Sa { keys.sa } else { None },
}),
_ => facet.clone(),
}
}
impl State {
pub(super) fn annotation_address(&self, layer: Layer) -> &AddressDraft {
if layer == Layer::Oa {
&self.annotation_oa
} else {
&self.annotation_sa
}
}
pub(super) fn annotation_address_mut(&mut self, layer: Layer) -> &mut AddressDraft {
if layer == Layer::Oa {
&mut self.annotation_oa
} else {
&mut self.annotation_sa
}
}
fn row_owner(&self, project: &Project, layer: Layer) -> Option<Element> {
let trace = self.effective_trace(project)?;
let id = if layer == Layer::Oa {
trace.oa
} else {
trace.sa
};
if layer == self.direction.target_layer()
&& let Ok(value) = self.combined_value(project, true)
&& let Ok(element) = serde_json::from_value(value)
{
return Some(element);
}
project.elements.get(&id).cloned()
}
pub(super) fn annotation_choices(
&self,
project: &Project,
layer: Layer,
) -> Result<Vec<FieldPathChoice>, String> {
let element = self.row_owner(project, layer).ok_or("Choose a trace.")?;
let Some(SharedBody::Occurrence {
body:
OccurrenceBody::Exchange {
exchange: Some(exchange),
message_kind,
..
},
..
}) = element.shared()
else {
return Err(
"Choose an exchange on this message before selecting a carried field.".into(),
);
};
let exchange = project
.relations
.get(exchange)
.ok_or("The message exchange is unavailable.")?;
crate::data_values::at_scope(
project,
exchange,
message_kind,
self.annotation_address(layer).scope.as_ref(),
)
}
pub(super) fn binding_choices(
&self,
project: &Project,
layer: Layer,
referenced: bool,
) -> Vec<Choice> {
let Some(element) = self.row_owner(project, layer) else {
return vec![];
};
let scenario = if referenced {
match element.shared() {
Some(SharedBody::Occurrence {
body: OccurrenceBody::ScenarioUse { scenario, .. },
..
}) => *scenario,
_ => None,
}
} else {
element
.parent
.and_then(|parent| validation::ancestor_of_kind(project, parent, Kind::Scenario))
};
let mut choices: Vec<_> = project
.elements
.values()
.filter(|e| {
e.kind() == Kind::Lifeline
&& e.layer() == layer
&& scenario.is_some()
&& e.parent == scenario
})
.map(|e| view::choice(project, e.id))
.collect();
choices.sort_by_cached_key(|c| c.label.to_lowercase());
choices
}
pub(super) fn stage_row_references(&mut self, facet: &FacetSelector) -> Result<(), String> {
let pairs: Vec<_> = match facet {
FacetSelector::Annotation(KeyPair {
oa: Some(oa),
sa: Some(sa),
}) => {
if oa.path.len() != sa.path.len() {
return Err("Paired annotation paths must have the same depth; review distinct addresses separately.".into());
}
oa.path
.iter()
.chain(std::iter::once(&oa.field))
.copied()
.zip(sa.path.iter().chain(std::iter::once(&sa.field)).copied())
.collect()
}
FacetSelector::Binding(KeyPair {
oa: Some(oa),
sa: Some(sa),
}) => vec![(*oa, *sa)],
_ => vec![],
};
// A repeated recursive field may occur several times in one address.
// Never let the last occurrence silently replace a conflicting pair.
let mut supplied = BTreeMap::new();
for (oa, sa) in pairs {
if supplied.insert(oa, sa).is_some_and(|prior| prior != sa) {
return Err(
"This address pairs the same Operational field with different System fields."
.into(),
);
}
}
self.references.extend(supplied);
Ok(())
}
pub(super) fn initialize_row(&mut self, project: &Project, facet: &FacetSelector) {
if self.row_drafts.contains_key(facet) {
return;
}
let value = self
.effective_trace(project)
.and_then(|t| capture_facet(project, t.oa, t.sa, facet).ok())
.and_then(|f| scalar::value(&f, self.direction.target_layer()));
let present = value.as_ref().is_some_and(|v| !v.is_null());
let mut draft = RowDraft::default();
if let Some(value) = value {
draft.expression = value["expression"].as_str().unwrap_or_default().into();
draft.editor = text_editor::Content::with_text(&draft.expression);
draft.local = value["local"]
.as_str()
.and_then(|v| v.parse().ok())
.map(|id| view::choice(project, id));
}
self.presence.entry(facet.clone()).or_insert(present);
self.row_drafts.insert(facet.clone(), draft);
}
pub(super) fn edit_row(
&mut self,
facet: &FacetSelector,
action: impl FnOnce(&mut RowDraft),
) -> Result<(), String> {
if !supported(facet)
|| self.selected.as_ref() != Some(facet)
|| self.decisions.get(facet) != Some(&SemanticDecision::Candidate)
{
return Err("Select this row and choose Set final value before editing it.".into());
}
action(
self.row_drafts
.get_mut(facet)
.ok_or("The row draft is unavailable.")?,
);
Ok(())
}
pub(super) fn project_row(
&self,
project: &Project,
trace: &Trace,
value: &mut Value,
facet: &FacetSelector,
decision: SemanticDecision,
) -> Result<(), String> {
if decision == SemanticDecision::KeepTarget {
return Ok(());
}
let source_layer = self.direction.source_layer();
let target_layer = self.direction.target_layer();
if decision == SemanticDecision::TakeSource && keyed::key(facet, source_layer).is_none() {
return Ok(());
}
let target_key = keyed::key(facet, target_layer)
.ok_or("Choose the destination row address before setting or taking its value.")?;
let row = if decision == SemanticDecision::Candidate {
if !self.presence.get(facet).copied().unwrap_or(false) {
Value::Null
} else {
let draft = self
.row_drafts
.get(facet)
.ok_or("Set the final row value.")?;
match facet {
FacetSelector::Annotation(_) => {
let key: AnnotationKey = serde_json::from_value(target_key.clone())
.map_err(|e| e.to_string())?;
serde_json::to_value(MessageValue {
path: key.path,
field: key.field,
expression: draft.expression.clone(),
})
.expect("Message value")
}
FacetSelector::Binding(_) => {
serde_json::json!({"referenced": target_key, "local": draft.local.as_ref().ok_or("Choose the destination scenario's local lifeline.")?.id})
}
_ => return Err("Unsupported row.".into()),
}
}
} else {
let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
let row = scalar::value(&fact, source_layer)
.ok_or("Source row collection is not applicable; review its constructor first.")?;
if row.is_null() {
row
} else {
let map = |id: Id| -> Result<Id, String> {
if source_layer == Layer::Oa {
self.references.get(&id).copied().ok_or_else(|| {
format!("Choose a reference counterpart for {}.", project.named(id))
})
} else {
let matches: Vec<_> = self
.references
.iter()
.filter(|(_, sa)| **sa == id)
.map(|(oa, _)| *oa)
.collect();
if matches.len() == 1 {
Ok(matches[0])
} else {
Err(format!(
"Choose one unambiguous reference counterpart for {}.",
project.named(id)
))
}
}
};
match facet {
FacetSelector::Annotation(_) => {
let mut row: MessageValue =
serde_json::from_value(row).map_err(|e| e.to_string())?;
row.path = row.path.into_iter().map(map).collect::<Result<_, _>>()?;
row.field = map(row.field)?;
if serde_json::to_value(AnnotationKey::from(&row)).expect("key")
!= target_key
{
return Err("Mapped annotation address disagrees with the selected destination address.".into());
}
serde_json::to_value(row).expect("row")
}
FacetSelector::Binding(_) => {
let row: LifelineBinding =
serde_json::from_value(row).map_err(|e| e.to_string())?;
let mapped = LifelineBinding {
referenced: map(row.referenced)?,
local: map(row.local)?,
};
if serde_json::to_value(mapped.referenced).expect("Id") != target_key {
return Err("Mapped binding key disagrees with the selected destination lifeline.".into());
}
serde_json::to_value(mapped).expect("row")
}
_ => return Err("Unsupported row.".into()),
}
}
};
let family = keyed::family(facet).ok_or("Unsupported row.")?;
let path = keyed::path(value, family)
.ok_or("The destination constructor has no collection for this row.")?;
let rows = value
.pointer_mut(&path)
.and_then(Value::as_array_mut)
.ok_or("Destination rows are unavailable.")?;
let position = rows.iter().position(|row| match facet {
FacetSelector::Annotation(_) => {
serde_json::json!({"path": row["path"], "field": row["field"]}) == target_key
}
FacetSelector::Binding(_) => row["referenced"] == target_key,
_ => false,
});
match (position, row.is_null()) {
(Some(index), false) => rows[index] = row,
(Some(index), true) => {
rows.remove(index);
}
(None, false) => rows.push(row),
(None, true) => {}
}
Ok(())
}
}
@@ -0,0 +1,736 @@
use super::{tests::*, *};
use crate::{app::App, document::Document};
struct Fixture {
p: Project,
owners: [Id; 2],
trace: Id,
facet: FacetSelector,
sibling: FacetSelector,
locals: [Id; 2],
}
fn errors(s: &State) -> String {
s.error
.iter()
.chain(s.plan.iter().flat_map(|p| &p.conflicts))
.cloned()
.collect::<Vec<_>>()
.join("; ")
}
fn add(p: &mut Project, kind: Kind, layer: Layer, parent: Option<Id>) -> Id {
p.add_element(kind, layer, kind.label(), parent).unwrap()
}
fn fixture(binding: bool) -> Fixture {
let mut p = Project::empty();
let mut owners = vec![];
let mut keys = vec![];
let mut sibling_keys = vec![];
let mut locals = vec![];
for layer in [Layer::Oa, Layer::Sa] {
let scenario = add(&mut p, Kind::Scenario, layer, None);
let owner = add(&mut p, Kind::Occurrence, layer, Some(scenario));
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(owner);
let body = if binding {
let reused = add(&mut p, Kind::Scenario, layer, None);
let referenced = add(&mut p, Kind::Lifeline, layer, Some(reused));
let sibling = add(&mut p, Kind::Lifeline, layer, Some(reused));
let local = add(&mut p, Kind::Lifeline, layer, Some(scenario));
let sibling_local = add(&mut p, Kind::Lifeline, layer, Some(scenario));
keys.push(AnnotationKey {
path: vec![],
field: referenced,
});
sibling_keys.push(AnnotationKey {
path: vec![],
field: sibling,
});
locals.push(local);
OccurrenceBody::ScenarioUse {
scenario: Some(reused),
bindings: vec![
LifelineBinding { referenced, local },
LifelineBinding {
referenced: sibling,
local: sibling_local,
},
],
}
} else {
let work = if layer == Layer::Oa {
Kind::Activity
} else {
Kind::Function
};
let from = add(&mut p, work, layer, None);
let to = add(&mut p, work, layer, None);
let item = add(&mut p, Kind::ExchangeItem, layer, None);
let record = add(&mut p, Kind::DataType, layer, None);
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype: None },
default: None,
unit: String::new(),
};
let first = add(&mut p, Kind::DataField, layer, Some(item));
let second = add(&mut p, Kind::DataField, layer, Some(item));
for field in [first, second] {
if let Some(SharedBody::DataField { type_ref, .. }) =
p.elements.get_mut(&field).unwrap().shared_mut()
{
*type_ref = TypeRef::Named(record);
}
}
let leaf = add(&mut p, Kind::DataField, layer, Some(record));
let exchange = p.relate(
if layer == Layer::Oa {
RelationKind::Interaction
} else {
RelationKind::FunctionalExchange
},
"Carrier",
from,
to,
);
let ports = match &mut p.relations.get_mut(&exchange).unwrap().body {
RelationBody::Interaction { items, .. } => {
items.push(item);
None
}
RelationBody::FunctionalExchange {
items,
source_port,
target_port,
..
} => {
items.push(item);
Some((*source_port, *target_port))
}
_ => unreachable!(),
};
if let Some((output, input)) = ports {
if let Some(SharedBody::Port {
contract: PortContract::FunctionOutput { outgoing_items },
..
}) = p.elements.get_mut(&output).unwrap().shared_mut()
{
outgoing_items.push(item);
}
if let Some(SharedBody::Port {
contract: PortContract::FunctionInput { incoming_items },
..
}) = p.elements.get_mut(&input).unwrap().shared_mut()
{
incoming_items.push(item);
}
}
keys.push(AnnotationKey {
path: vec![first],
field: leaf,
});
sibling_keys.push(AnnotationKey {
path: vec![second],
field: leaf,
});
locals.push(leaf);
OccurrenceBody::Exchange {
exchange: Some(exchange),
sender: None,
receiver: None,
message_kind: MessageKind::Asynchronous,
reply_to: None,
values: vec![
MessageValue {
path: vec![first],
field: leaf,
expression: if layer == Layer::Oa {
"operational"
} else {
"system"
}
.into(),
},
MessageValue {
path: vec![second],
field: leaf,
expression: "sibling stays".into(),
},
],
}
};
*p.elements.get_mut(&owner).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body,
precondition: "Retain precondition".into(),
postcondition: "Retain postcondition".into(),
};
owners.push(owner);
}
let trace = add_trace(&mut p, owners[0], owners[1], "Individual row review");
let facet = |keys: &[AnnotationKey]| {
if binding {
FacetSelector::Binding(KeyPair {
oa: Some(keys[0].field),
sa: Some(keys[1].field),
})
} else {
FacetSelector::Annotation(KeyPair {
oa: Some(keys[0].clone()),
sa: Some(keys[1].clone()),
})
}
};
validate_candidate(&p).unwrap();
Fixture {
p,
owners: [owners[0], owners[1]],
trace,
facet: facet(&keys),
sibling: facet(&sibling_keys),
locals: [locals[0], locals[1]],
}
}
fn ready(f: &Fixture, reverse: bool) -> State {
let mut s = State::new(&f.p, f.owners[usize::from(reverse)]).unwrap();
s.stage_row_references(&f.facet).unwrap();
s.references.insert(f.locals[0], f.locals[1]);
s.update(&f.p, Event::Select(f.facet.clone()));
s
}
#[test]
fn independent_rows_keep_take_candidate_match_core_and_preserve_siblings_both_directions() {
for binding in [false, true] {
for reverse in [false, true] {
for decision in [
SemanticDecision::KeepTarget,
SemanticDecision::TakeSource,
SemanticDecision::Candidate,
] {
let f = fixture(binding);
let mut s = ready(&f, reverse);
let layer = s.direction.target_layer();
let before_sibling = scalar::value(
&capture_facet(&f.p, f.owners[0], f.owners[1], &f.sibling).unwrap(),
layer,
);
s.update(&f.p, Event::Decide(f.facet.clone(), Some(decision)));
if decision == SemanticDecision::Candidate {
s.update(&f.p, Event::Presence(f.facet.clone(), !binding));
if !binding {
s.update(
&f.p,
Event::RowExpression(f.facet.clone(), f.locals[0].to_string()),
);
}
}
// Force the GUI to compose the row alongside another candidate,
// so Take/Keep also exercise adapter scope and mapped references.
let header = FacetSelector::Header(HeaderFacet::Description);
s.update(&f.p, Event::Select(header.clone()));
s.update(
&f.p,
Event::Decide(header, Some(SemanticDecision::Candidate)),
);
s.update(
&f.p,
Event::Input("/description".into(), "Reviewed one row".into()),
);
assert!(
s.can_apply(),
"{binding}/{reverse}/{decision:?}: {}",
errors(&s)
);
let proposed = s.proposed.as_ref().unwrap().clone();
assert_eq!(
scalar::value(
&capture_facet(&proposed, f.owners[0], f.owners[1], &f.sibling).unwrap(),
layer
),
before_sibling
);
let current = scalar::value(
&capture_facet(&proposed, f.owners[0], f.owners[1], &f.facet).unwrap(),
layer,
)
.unwrap();
if decision == SemanticDecision::Candidate {
if binding {
assert!(current.is_null());
} else {
assert_eq!(current["expression"], f.locals[0].to_string());
}
}
assert_eq!(
prepare_command(&f.p, &s.command(&f.p).unwrap())
.unwrap()
.project,
proposed
);
let _ = s.view(&f.p);
}
}
}
}
#[test]
fn keyed_row_drafts_retain_expression_local_and_presence_across_decisions_and_staging() {
for binding in [false, true] {
let f = fixture(binding);
let mut s = ready(&f, false);
s.staged.insert(f.facet.clone());
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)),
);
if binding {
s.update(
&f.p,
Event::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])),
);
} else {
s.update(
&f.p,
Event::RowExpression(f.facet.clone(), "unfinished (\n literal UUID text".into()),
);
}
s.update(&f.p, Event::Presence(f.facet.clone(), false));
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)),
);
s.update(&f.p, Event::DropStaged(f.facet.clone()));
s.update(&f.p, Event::Select(f.facet.clone()));
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)),
);
assert!(!s.presence[&f.facet]);
if binding {
assert_eq!(
s.row_drafts[&f.facet].local.as_ref().unwrap().id,
f.locals[1]
);
} else {
assert_eq!(
s.row_drafts[&f.facet].expression,
"unfinished (\n literal UUID text"
);
}
assert!(s.can_apply(), "{}", errors(&s));
// A forged event for an unselected sibling cannot alter its row draft.
s.update(
&f.p,
Event::RowExpression(f.sibling.clone(), "wrong row".into()),
);
assert!(s.error.as_ref().unwrap().contains("Select this row"));
}
}
#[test]
fn annotation_address_browser_descends_qualified_fields_without_changing_rows() {
let f = fixture(false);
let mut s = ready(&f, false);
s.update(&f.p, Event::MemberFamily(keyed::Family::Annotation));
for layer in [Layer::Oa, Layer::Sa] {
let roots = s.annotation_choices(&f.p, layer).unwrap();
assert_eq!(roots.len(), 2);
let key = roots[0].key.clone();
s.update(&f.p, Event::AnnotationBrowse(layer, Some(key.clone()), 0));
let nested = s.annotation_choices(&f.p, layer).unwrap();
assert_eq!(nested.len(), 1);
assert_eq!(nested[0].key.path, vec![key.field]);
s.update(
&f.p,
Event::AnnotationAddress(layer, Some(nested[0].key.clone())),
);
}
s.update(&f.p, Event::StageMember);
assert!(s.error.is_none(), "{}", errors(&s));
assert_eq!(s.staged.len(), 1);
assert!(s.decisions.is_empty());
let _ = s.view(&f.p);
}
#[test]
fn binding_choices_separate_reused_keys_from_local_lifelines() {
let f = fixture(true);
let mut s = ready(&f, false);
s.update(&f.p, Event::MemberFamily(keyed::Family::Binding));
for (index, layer) in [Layer::Oa, Layer::Sa].into_iter().enumerate() {
assert_eq!(s.member_choices(&f.p, layer).len(), 2);
assert!(
!s.member_choices(&f.p, layer)
.iter()
.any(|c| c.id == f.locals[index])
);
let local_choices = s.binding_choices(&f.p, layer, false);
assert_eq!(local_choices.len(), 2);
assert!(local_choices.iter().any(|c| c.id == f.locals[index]));
}
}
#[test]
fn binding_local_picker_follows_a_candidate_parent_before_the_review_is_valid() {
let mut f = fixture(true);
let destination = add(&mut f.p, Kind::Scenario, Layer::Sa, None);
let local = add(&mut f.p, Kind::Lifeline, Layer::Sa, Some(destination));
let mut s = ready(&f, false);
let parent = FacetSelector::Header(HeaderFacet::Parent);
s.update(&f.p, Event::Select(parent.clone()));
s.update(
&f.p,
Event::Decide(parent, Some(SemanticDecision::Candidate)),
);
s.update(
&f.p,
Event::Shape("/parent".into(), serde_json::json!(destination)),
);
assert_eq!(
s.binding_choices(&f.p, Layer::Sa, false)
.iter()
.map(|c| c.id)
.collect::<Vec<_>>(),
vec![local]
);
assert!(
!s.binding_choices(&f.p, Layer::Sa, false)
.iter()
.any(|c| c.id == f.locals[1])
);
}
#[test]
fn row_review_apply_preserves_exact_source_and_semantic_history_through_undo_redo() {
for binding in [false, true] {
let f = fixture(binding);
let mut app = App::with_project(f.p);
let mut bundle = app.document.bundle().clone();
bundle
.files
.get_mut(&bundle.entry.clone())
.unwrap()
.insert_str(0, "// Exact row rationale\r\n");
app.document.apply_bundle(bundle).unwrap();
let before = app.document.snapshot();
start(&mut app, f.owners[0]);
send(&mut app, Event::EditPair(f.facet.clone()));
send(&mut app, Event::StageMember);
send(&mut app, Event::Select(f.facet.clone()));
send(
&mut app,
Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)),
);
send(&mut app, Event::Presence(f.facet.clone(), false));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
let expected = state(&app).proposed.as_ref().unwrap().clone();
apply(&mut app);
assert_eq!(app.project(), &expected);
assert!(
app.project().traces[&f.trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets
.iter()
.any(|r| r.selector() == f.facet)
);
let after = app.document.snapshot();
assert!(
app.document.bundle().files[&app.document.bundle().entry]
.starts_with("// Exact row rationale\r\n")
);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.project(), &before.decoded.project);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &after.bundle);
assert_eq!(app.project(), &after.decoded.project);
assert_eq!(
Document::from_bundle(
after.bundle.clone(),
source_project::ProjectLimits::default()
)
.unwrap()
.project(),
&expected
);
}
}
#[test]
fn keyed_rows_distinguish_unpaired_source_from_explicit_absence_and_require_exact_reset() {
for binding in [false, true] {
let mut f = fixture(binding);
let (source_only, target_only) = match &f.facet {
FacetSelector::Annotation(keys) => (
FacetSelector::Annotation(KeyPair {
oa: keys.oa.clone(),
sa: None,
}),
FacetSelector::Annotation(KeyPair {
oa: None,
sa: keys.sa.clone(),
}),
),
FacetSelector::Binding(keys) => (
FacetSelector::Binding(KeyPair {
oa: keys.oa,
sa: None,
}),
FacetSelector::Binding(KeyPair {
oa: None,
sa: keys.sa,
}),
),
_ => unreachable!(),
};
assert!(keyed::overlap(&source_only, &f.facet));
assert!(!keyed::overlap(&f.sibling, &f.facet));
let prior = capture_facet(&f.p, f.owners[0], f.owners[1], &source_only).unwrap();
f.p.traces.get_mut(&f.trace).unwrap().baseline = Some(TraceBaseline {
oa: f.p.properties(f.owners[0]).unwrap(),
sa: f.p.properties(f.owners[1]).unwrap(),
semantic: Some(SemanticBaseline {
facets: vec![prior],
}),
});
let mut s = ready(&f, false);
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::KeepTarget)),
);
assert!(
!s.can_apply(),
"An old one-sided address cannot silently acquire a paired history"
);
s.update(&f.p, Event::Reset(source_only.clone(), true));
assert!(s.can_apply(), "{}", errors(&s));
let next = s.proposed.as_ref().unwrap();
let facts = &next.traces[&f.trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap()
.facets;
assert!(
facts
.iter()
.any(|f| f.selector() == s.selected.clone().unwrap())
);
assert!(!facts.iter().any(|f| f.selector() == source_only));
f.p.traces.get_mut(&f.trace).unwrap().baseline = None;
if let Some(SharedBody::Occurrence { body, .. }) =
f.p.elements.get_mut(&f.owners[0]).unwrap().shared_mut()
{
match body {
OccurrenceBody::Exchange { values, .. } => values.clear(),
OccurrenceBody::ScenarioUse { bindings, .. } => bindings.clear(),
_ => unreachable!(),
}
}
for (facet, expect_absent) in [(target_only, false), (f.facet.clone(), true)] {
let mut s = ready(&f, false);
s.update(
&f.p,
Event::Decide(facet, Some(SemanticDecision::TakeSource)),
);
let header = FacetSelector::Header(HeaderFacet::Description);
s.update(
&f.p,
Event::Decide(header.clone(), Some(SemanticDecision::Candidate)),
);
s.update(&f.p, Event::Select(header));
s.update(
&f.p,
Event::Input("/description".into(), "Compose explicit absence".into()),
);
assert!(s.can_apply(), "{}", errors(&s));
let fact = capture_facet(
s.proposed.as_ref().unwrap(),
f.owners[0],
f.owners[1],
&f.facet,
)
.unwrap();
assert_eq!(
scalar::value(&fact, Layer::Sa).unwrap().is_null(),
expect_absent
);
}
}
}
#[test]
fn binding_take_requires_local_correspondence_and_candidate_rejects_wrong_scenario() {
let f = fixture(true);
let mut s = ready(&f, false);
s.references.remove(&f.locals[0]);
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::TakeSource)),
);
assert!(!s.can_apply());
s.references.insert(f.locals[0], f.locals[1]);
s.update(&f.p, Event::Preview);
assert!(s.can_apply(), "{}", errors(&s));
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)),
);
let wrong = s.binding_choices(&f.p, Layer::Sa, true)[0].clone();
s.update(&f.p, Event::RowLocal(f.facet.clone(), wrong));
assert!(!s.can_apply());
assert!(
s.row_drafts[&f.facet].local.is_some(),
"Invalid typed selection remains reviewable"
);
s.update(
&f.p,
Event::RowLocal(f.facet.clone(), view::choice(&f.p, f.locals[1])),
);
assert!(s.can_apply(), "{}", errors(&s));
}
#[test]
fn annotation_observations_keep_uuid_shaped_expression_text_in_both_sides_and_history() {
let mut f = fixture(false);
for owner in f.owners {
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange { values, .. },
..
}) = f.p.elements.get_mut(&owner).unwrap().shared_mut()
{
values[0].expression = values[0].field.to_string();
}
}
let historical = capture_facet(&f.p, f.owners[0], f.owners[1], &f.facet).unwrap();
for layer in [Layer::Oa, Layer::Sa] {
let expression = scalar::value(&historical, layer).unwrap()["expression"]
.as_str()
.unwrap()
.to_owned();
let display = view::observed(&f.p, Some(&historical), layer);
assert!(
display.contains(&format!("Expression: {expression:?}")),
"{display}"
);
}
}
#[test]
fn keyed_recovery_retains_raw_rows_and_staged_pairs_both_directions_without_applying() {
for binding in [false, true] {
for reverse in [false, true] {
let f = fixture(binding);
let mut s = ready(&f, reverse);
s.staged.insert(f.facet.clone());
s.update(
&f.p,
Event::Decide(f.facet.clone(), Some(SemanticDecision::Candidate)),
);
if !binding {
s.update(
&f.p,
Event::RowExpression(f.facet.clone(), "unfinished (\n exact raw draft".into()),
);
}
s.update(&f.p, Event::Presence(f.facet.clone(), false));
s.update(&f.p, Event::EditPair(f.facet.clone()));
let saved = s.recovery_snapshot().unwrap();
let restored = State::from_recovery(&f.p, saved).unwrap();
assert_eq!(restored.direction, s.direction);
assert_eq!(restored.decisions, s.decisions);
assert_eq!(restored.references, s.references);
assert_eq!(restored.presence, s.presence);
assert_eq!(restored.staged, s.staged);
assert_eq!(
restored.row_drafts[&f.facet].expression,
s.row_drafts[&f.facet].expression
);
assert_eq!(
restored.row_drafts[&f.facet].local,
s.row_drafts[&f.facet].local
);
assert_eq!(restored.proposed, s.proposed);
assert_eq!(restored.annotation_oa.selected, s.annotation_oa.selected);
assert_eq!(restored.annotation_sa.selected, s.annotation_sa.selected);
assert!(restored.can_apply(), "{}", errors(&restored));
}
}
}
#[test]
fn multiple_new_rows_compose_in_empty_collections_using_destination_key_order() {
for binding in [false, true] {
for reverse in [false, true] {
let mut f = fixture(binding);
let target_layer = if reverse { Layer::Oa } else { Layer::Sa };
let target = f.owners[usize::from(!reverse)];
let facets = [f.facet.clone(), f.sibling.clone()];
let original: Vec<_> = facets
.iter()
.map(|facet| {
scalar::value(
&capture_facet(&f.p, f.owners[0], f.owners[1], facet).unwrap(),
target_layer,
)
.unwrap()
})
.collect();
if let Some(SharedBody::Occurrence { body, .. }) =
f.p.elements.get_mut(&target).unwrap().shared_mut()
{
match body {
OccurrenceBody::Exchange { values, .. } => values.clear(),
OccurrenceBody::ScenarioUse { bindings, .. } => bindings.clear(),
_ => unreachable!(),
}
}
let object_count = f.p.elements.len();
let mut s = ready(&f, reverse);
for (facet, row) in facets.iter().zip(&original).rev() {
s.update(&f.p, Event::EditPair(facet.clone()));
s.update(&f.p, Event::StageMember);
s.update(
&f.p,
Event::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
);
s.update(&f.p, Event::Presence(facet.clone(), true));
if binding {
let local = row["local"].as_str().unwrap().parse().unwrap();
s.update(
&f.p,
Event::RowLocal(facet.clone(), view::choice(&f.p, local)),
);
} else {
s.update(
&f.p,
Event::RowExpression(facet.clone(), "new independent value".into()),
);
}
}
assert!(s.can_apply(), "{binding}/{reverse}: {}", errors(&s));
let proposed = s.proposed.as_ref().unwrap();
assert_eq!(proposed.elements.len(), object_count);
for facet in facets {
assert!(
!scalar::value(
&capture_facet(proposed, f.owners[0], f.owners[1], &facet).unwrap(),
target_layer
)
.unwrap()
.is_null()
);
}
}
}
}
@@ -0,0 +1,254 @@
//! Independent membership and complete final order; the core validates every proposal.
use super::*;
pub(super) fn collection(which: OrderedCollection) -> ReferenceCollection {
match which {
OrderedCollection::Steps => ReferenceCollection::Steps,
OrderedCollection::FragmentBranches => ReferenceCollection::FragmentBranches,
}
}
pub(super) fn ordered(which: ReferenceCollection) -> Option<OrderedCollection> {
match which {
ReferenceCollection::Steps => Some(OrderedCollection::Steps),
ReferenceCollection::FragmentBranches => Some(OrderedCollection::FragmentBranches),
_ => None,
}
}
pub(super) fn read(value: &Value, which: OrderedCollection) -> Result<Vec<Id>, String> {
let path = keyed::path(value, keyed::Family::Member(collection(which)))
.ok_or("This ordered collection is unavailable on the destination.")?;
serde_json::from_value(value.pointer(&path).unwrap().clone()).map_err(|e| e.to_string())
}
fn write(value: &mut Value, which: OrderedCollection, ids: Vec<Id>) -> Result<(), String> {
let path = keyed::path(value, keyed::Family::Member(collection(which)))
.ok_or("This ordered collection is unavailable on the destination.")?;
*value.pointer_mut(&path).unwrap() = serde_json::to_value(ids).expect("Ordered identities");
Ok(())
}
impl State {
pub(super) fn ordered_members(
&self,
project: &Project,
which: OrderedCollection,
) -> Result<Vec<Id>, String> {
read(&self.combine(project, true, true)?, which)
}
pub(super) fn initialize_order(
&mut self,
project: &Project,
which: OrderedCollection,
) -> Result<(), String> {
if !self.order_drafts.contains_key(&which) {
let ids = self.ordered_members(project, which)?;
self.order_drafts.insert(which, ids);
}
Ok(())
}
pub(super) fn reconcile_order(
&mut self,
project: &Project,
which: OrderedCollection,
) -> Result<(), String> {
self.require_order_candidate(which)?;
let members = self.ordered_members(project, which)?;
let draft = self
.order_drafts
.get_mut(&which)
.ok_or("Choose a final order first.")?;
// This is an explicit user action. Ordinary refresh never discards an authored order.
let mut seen = BTreeSet::new();
draft.retain(|id| members.contains(id) && seen.insert(*id));
for id in members {
if seen.insert(id) {
draft.push(id);
}
}
Ok(())
}
fn require_order_candidate(&self, which: OrderedCollection) -> Result<(), String> {
let facet = FacetSelector::Order(which);
if self.selected.as_ref() != Some(&facet)
|| self.decisions.get(&facet) != Some(&SemanticDecision::Candidate)
{
return Err("Select Set final value for this order before changing it.".into());
}
Ok(())
}
pub(super) fn move_order(
&mut self,
which: OrderedCollection,
index: usize,
delta: i32,
) -> Result<(), String> {
self.require_order_candidate(which)?;
let ids = self
.order_drafts
.get_mut(&which)
.ok_or("Choose a final order first.")?;
let destination = match delta {
-1 => index.checked_sub(1),
1 => index.checked_add(1),
_ => None,
};
let destination = destination
.filter(|i| index < ids.len() && *i < ids.len())
.ok_or("Choose an existing adjacent position in the final order.")?;
ids.swap(index, destination);
Ok(())
}
pub(super) fn project_order(
&self,
project: &Project,
trace: &Trace,
value: &mut Value,
which: OrderedCollection,
decision: SemanticDecision,
) -> Result<(), String> {
let current = read(value, which)?;
let supplied = match decision {
SemanticDecision::KeepTarget => return Ok(()),
SemanticDecision::Candidate => self
.order_drafts
.get(&which)
.cloned()
.ok_or("Choose a final order.")?,
SemanticDecision::TakeSource => {
let fact =
capture_facet(project, trace.oa, trace.sa, &FacetSelector::Order(which))?;
let source: Vec<Id> = serde_json::from_value(
scalar::value(&fact, self.direction.source_layer())
.ok_or("The source has no applicable order.")?,
)
.map_err(|e| e.to_string())?;
let map = self.order_reference_map()?;
source
.into_iter()
.map(|id| {
map.get(&id).copied().ok_or_else(|| {
format!(
"Pair source order member {} explicitly in Reference pairs.",
project.named(id)
)
})
})
.collect::<Result<Vec<_>, _>>()?
}
};
let actual: BTreeSet<_> = current.iter().copied().collect();
let desired: BTreeSet<_> = supplied.iter().copied().collect();
if desired.len() != supplied.len() {
return Err(
"The final order contains a duplicate identity; reconcile its members explicitly."
.into(),
);
}
if desired != actual {
if decision == SemanticDecision::TakeSource
&& desired.is_subset(&actual)
&& current
.iter()
.filter(|id| desired.contains(id))
.copied()
.collect::<Vec<_>>()
== supplied
{
return Ok(());
}
return Err("Final order must contain every surviving destination member exactly once. Reconcile members, then choose their positions; membership is reviewed separately.".into());
}
write(value, which, supplied)
}
fn order_reference_map(&self) -> Result<BTreeMap<Id, Id>, String> {
let mut map = BTreeMap::new();
for (oa, sa) in &self.references {
let (a, b) = if self.direction == TransitionDirection::OaToSa {
(*oa, *sa)
} else {
(*sa, *oa)
};
if map.insert(a, b).is_some_and(|old| old != b) {
return Err("Reverse reference correspondence is ambiguous.".into());
}
}
Ok(map)
}
pub(super) fn project_ordered_member(
&self,
project: &Project,
trace: &Trace,
value: &mut Value,
facet: &FacetSelector,
decision: SemanticDecision,
form_context: bool,
) -> Result<(), String> {
let FacetSelector::Member {
collection: member_collection,
keys,
} = facet
else {
unreachable!()
};
let which = ordered(*member_collection).expect("Ordered member");
let before = read(value, which)?;
self.keyed_value(project, trace, value, facet, decision)?;
let after = read(value, which)?;
let explicit = self
.decisions
.get(&FacetSelector::Order(which))
.is_some_and(|d| *d != SemanticDecision::KeepTarget);
if decision != SemanticDecision::TakeSource || explicit || after.len() <= before.len() {
return Ok(());
}
let (source_key, target_key) = if self.direction == TransitionDirection::OaToSa {
(keys.oa, keys.sa)
} else {
(keys.sa, keys.oa)
};
let (Some(source_key), Some(target_key)) = (source_key, target_key) else {
return Ok(());
};
let fact = capture_facet(project, trace.oa, trace.sa, &FacetSelector::Order(which))?;
let source: Vec<Id> = serde_json::from_value(
scalar::value(&fact, self.direction.source_layer())
.ok_or("Source order is unavailable.")?,
)
.map_err(|e| e.to_string())?;
let position = source
.iter()
.position(|id| *id == source_key)
.ok_or("Source member is absent.")?;
let map = self.order_reference_map()?;
let anchor = |id: &Id| {
map.get(id)
.and_then(|mapped| before.iter().position(|id| id == mapped))
};
let left = source[..position].iter().rev().find_map(anchor);
let right = source[position + 1..].iter().find_map(anchor);
let at = match (left, right) {
(Some(a), Some(b)) if a + 1 == b => Some(b),
(Some(a), None) if a + 1 == before.len() => Some(a + 1),
(None, Some(0)) => Some(0),
(None, None) if before.is_empty() => Some(0),
_ => None,
};
if let Some(at) = at {
let mut ordered = before;
ordered.insert(at, target_key);
write(value, which, ordered)
} else if form_context {
// An uncommitted membership list lets the user author a final order.
// The actual request still rejects ambiguous placement until explicitly decided.
Ok(())
} else {
Err("Ordered member insertion has no unique unchanged anchor; select an explicit final Order in Properties.".into())
}
}
}
@@ -0,0 +1,567 @@
use super::{tests::*, *};
use crate::{app::App, document::Document};
struct Fixture {
p: Project,
owners: [Id; 2],
rows: [[Id; 4]; 2],
trace: Id,
which: OrderedCollection,
}
fn fixture(shape: u8, reverse: bool, local: bool) -> Fixture {
let mut p = Project::empty();
let mut owners = vec![];
let mut rows = vec![];
let which = if shape == 3 {
OrderedCollection::FragmentBranches
} else {
OrderedCollection::Steps
};
for layer in [Layer::Oa, Layer::Sa] {
let owner = if shape == 0 {
p.add_element(
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
},
layer,
"Startup sequence",
None,
)
.unwrap()
} else {
let scenario = p
.add_element(Kind::Scenario, layer, "Startup scenario", None)
.unwrap();
if shape == 1 {
scenario
} else {
let fragment = p
.add_element(Kind::Occurrence, layer, "Alternatives", Some(scenario))
.unwrap();
*p.elements.get_mut(&fragment).unwrap().shared_mut().unwrap() =
SharedBody::Occurrence {
body: OccurrenceBody::Fragment {
operator: FragmentOperator::Alternative,
branches: vec![],
},
precondition: String::new(),
postcondition: String::new(),
};
p.elements
.get_mut(&scenario)
.unwrap()
.steps_mut()
.unwrap()
.push(fragment);
if shape == 3 {
fragment
} else {
let branch = p
.add_element(Kind::Occurrence, layer, "Start permitted", Some(fragment))
.unwrap();
*p.elements.get_mut(&branch).unwrap().shared_mut().unwrap() =
SharedBody::Occurrence {
body: OccurrenceBody::Branch {
guard: "permitted".into(),
occurrences: vec![],
},
precondition: String::new(),
postcondition: String::new(),
};
if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
{
branches.push(branch);
}
branch
}
}
};
let ids = [
"Request start",
"Energize",
"Verify interlock",
"Local self-test",
]
.map(|name| {
let id = p
.add_element(Kind::Occurrence, layer, name, Some(owner))
.unwrap();
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
body: if shape == 3 {
OccurrenceBody::Branch {
guard: name.into(),
occurrences: vec![],
}
} else if shape == 0 {
OccurrenceBody::Control {
kind: ControlKind::Choice,
}
} else {
OccurrenceBody::Action {
action: None,
lifeline: None,
}
},
precondition: String::new(),
postcondition: String::new(),
};
id
});
owners.push(owner);
rows.push(ids);
}
let source = usize::from(reverse);
let target = 1 - source;
set_order(
&mut p,
owners[source],
which,
vec![rows[source][0], rows[source][2], rows[source][1]],
);
set_order(
&mut p,
owners[target],
which,
if local {
vec![rows[target][0], rows[target][3], rows[target][1]]
} else {
vec![rows[target][0], rows[target][1]]
},
);
let trace = add_trace(&mut p, owners[0], owners[1], "Startup ordered review");
validate_candidate(&p).unwrap();
Fixture {
p,
owners: [owners[0], owners[1]],
rows: [rows[0], rows[1]],
trace,
which,
}
}
fn set_order(p: &mut Project, id: Id, which: OrderedCollection, rows: Vec<Id>) {
if which == OrderedCollection::Steps {
*p.elements.get_mut(&id).unwrap().steps_mut().unwrap() = rows;
} else if let Some(SharedBody::Occurrence {
body: OccurrenceBody::Fragment { branches, .. },
..
}) = p.elements.get_mut(&id).unwrap().shared_mut()
{
*branches = rows;
}
}
fn member(f: &Fixture, index: usize) -> FacetSelector {
FacetSelector::Member {
collection: ordered::collection(f.which),
keys: KeyPair {
oa: Some(f.rows[0][index]),
sa: Some(f.rows[1][index]),
},
}
}
fn ready(f: &Fixture, reverse: bool) -> State {
let mut state = State::new(&f.p, f.owners[usize::from(reverse)]).unwrap();
for i in 0..3 {
state.references.insert(f.rows[0][i], f.rows[1][i]);
}
state.refresh(&f.p);
state
}
fn decide(state: &mut State, p: &Project, facet: FacetSelector, decision: SemanticDecision) {
state.update(p, Event::Select(facet.clone()));
state.update(p, Event::Decide(facet, Some(decision)));
}
fn errors(s: &State) -> String {
s.error
.iter()
.chain(s.plan.iter().flat_map(|p| &p.conflicts))
.cloned()
.collect::<Vec<_>>()
.join("; ")
}
fn oracle(f: &Fixture, s: &State, expected: Option<Element>) -> Project {
let reverse = s.direction == TransitionDirection::SaToOa;
let source = f.owners[usize::from(reverse)];
let target = f.owners[1 - usize::from(reverse)];
let mut transition = TransitionRequest::new(s.direction, vec![source]);
transition.mappings.push(TransitionMapping {
source,
target: Some(target),
target_kind: None,
});
let mut request = SemanticTransitionRequest::new(transition);
request.traces.insert(source, f.trace);
request.references = s.references.clone();
request.reviews.push(SemanticReviewRequest {
source,
resolutions: s
.decisions
.iter()
.map(|(facet, decision)| FacetResolution {
facet: facet.clone(),
decision: *decision,
})
.collect(),
reset: s.resets.iter().cloned().collect(),
});
if let Some(expected) = expected {
request.candidates.push(SemanticRecord::Element(expected));
}
let plan = plan_semantic_transition(&f.p, request).unwrap();
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
let result = apply_semantic_transition(&f.p, &plan).unwrap();
assert!(s.can_apply(), "{}", errors(s));
assert_eq!(s.proposed.as_ref().unwrap(), &result);
assert_eq!(s.plan.as_ref().unwrap().reviews, plan.reviews);
result
}
#[test]
fn ordered_take_uses_unique_anchors_and_matches_core_with_unrelated_candidate_both_directions() {
for shape in 0..4 {
for reverse in [false, true] {
let f = fixture(shape, reverse, false);
let mut s = ready(&f, reverse);
decide(&mut s, &f.p, member(&f, 2), SemanticDecision::TakeSource);
let header = FacetSelector::Header(HeaderFacet::Description);
decide(&mut s, &f.p, header, SemanticDecision::Candidate);
s.update(
&f.p,
Event::Input("/description".into(), "Reviewed startup insertion".into()),
);
let ti = 1 - usize::from(reverse);
let mut expected = f.p.clone();
set_order(
&mut expected,
f.owners[ti],
f.which,
vec![f.rows[ti][0], f.rows[ti][2], f.rows[ti][1]],
);
expected
.elements
.get_mut(&f.owners[ti])
.unwrap()
.description = "Reviewed startup insertion".into();
oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone()));
}
}
}
#[test]
fn ambiguous_order_preserves_local_member_and_requires_explicit_candidate_positions() {
for shape in 0..4 {
for reverse in [false, true] {
let f = fixture(shape, reverse, true);
let mut s = ready(&f, reverse);
decide(&mut s, &f.p, member(&f, 2), SemanticDecision::TakeSource);
assert!(!s.can_apply());
assert!(errors(&s).contains("anchor"), "{}", errors(&s));
let order = FacetSelector::Order(f.which);
decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate);
let ti = 1 - usize::from(reverse);
assert_eq!(
s.order_drafts[&f.which],
vec![f.rows[ti][0], f.rows[ti][3], f.rows[ti][1], f.rows[ti][2]]
);
s.update(&f.p, Event::OrderMove(f.which, 3, -1));
let mut expected = f.p.clone();
set_order(
&mut expected,
f.owners[ti],
f.which,
vec![f.rows[ti][0], f.rows[ti][3], f.rows[ti][2], f.rows[ti][1]],
);
let result = oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone()));
let baseline = result.traces[&f.trace]
.baseline
.as_ref()
.unwrap()
.semantic
.as_ref()
.unwrap();
assert_eq!(baseline.facets.len(), 2);
assert!(baseline.facets.iter().any(|v| v.selector() == order));
}
}
}
#[test]
fn ordered_membership_choices_stage_exact_pairs_in_all_owner_shapes() {
for shape in 0..4 {
let f = fixture(shape, false, true);
let mut s = ready(&f, false);
s.update(
&f.p,
Event::MemberFamily(keyed::Family::Member(ordered::collection(f.which))),
);
for (i, layer) in [(0, Layer::Oa), (1, Layer::Sa)] {
let choices = s.member_choices(&f.p, layer);
for id in f.rows[i] {
assert!(
choices.iter().any(|c| c.id == id),
"shape {shape}: {choices:?}"
);
}
assert!(choices.iter().all(|c| !f.rows[1 - i].contains(&c.id)));
}
s.update(
&f.p,
Event::MemberOa(Some(view::choice(&f.p, f.rows[0][2]))),
);
s.update(
&f.p,
Event::MemberSa(Some(view::choice(&f.p, f.rows[1][2]))),
);
s.update(&f.p, Event::StageMember);
assert!(s.staged.contains(&member(&f, 2)));
assert_eq!(s.references[&f.rows[0][2]], f.rows[1][2]);
}
}
#[test]
fn raw_order_survives_membership_changes_decisions_and_recovery_until_explicit_reconcile() {
for reverse in [false, true] {
let f = fixture(0, reverse, true);
let mut s = ready(&f, reverse);
let order = FacetSelector::Order(f.which);
decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate);
s.update(&f.p, Event::OrderMove(f.which, 1, -1));
let authored = s.order_drafts[&f.which].clone();
decide(&mut s, &f.p, member(&f, 2), SemanticDecision::Candidate);
s.update(&f.p, Event::Presence(member(&f, 2), true));
assert!(!s.can_apply());
assert_eq!(s.order_drafts[&f.which], authored);
s.update(&f.p, Event::Decide(order.clone(), None));
decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate);
let saved = s.recovery_snapshot().unwrap();
let mut restored = State::from_recovery(&f.p, saved.clone()).unwrap();
assert_eq!(restored.order_drafts[&f.which], authored);
assert!(!restored.can_apply());
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
restored.update(&f.p, Event::OrderReconcile(f.which));
let ti = 1 - usize::from(reverse);
let mut expected = f.p.clone();
let mut final_order = authored;
final_order.push(f.rows[ti][2]);
set_order(&mut expected, f.owners[ti], f.which, final_order);
oracle(
&f,
&restored,
Some(expected.elements[&f.owners[ti]].clone()),
);
let mut stale = f.p.clone();
stale.name.push_str(" changed");
assert!(State::from_recovery(&stale, saved).is_err());
}
}
#[test]
fn order_take_preserves_destination_only_positions_and_order_alone_cannot_add_members() {
for reverse in [false, true] {
let mut f = fixture(0, reverse, true);
let si = usize::from(reverse);
let ti = 1 - si;
set_order(
&mut f.p,
f.owners[si],
f.which,
vec![f.rows[si][0], f.rows[si][1]],
);
let mut s = ready(&f, reverse);
let order = FacetSelector::Order(f.which);
decide(&mut s, &f.p, order.clone(), SemanticDecision::TakeSource);
let result = oracle(&f, &s, None);
assert_eq!(result.elements[&f.owners[ti]], f.p.elements[&f.owners[ti]]);
decide(&mut s, &f.p, order.clone(), SemanticDecision::Candidate);
s.order_drafts
.get_mut(&f.which)
.unwrap()
.push(f.rows[ti][2]);
s.refresh(&f.p);
assert!(!s.can_apply());
assert!(errors(&s).contains("surviving"), "{}", errors(&s));
s.update(&f.p, Event::OrderReconcile(f.which));
assert!(s.can_apply(), "{}", errors(&s));
}
}
#[test]
fn ordered_review_apply_is_exact_source_history_transaction_with_reopen_and_undo_redo() {
let f = fixture(0, false, true);
let mut app = App::with_project(f.p.clone());
let mut bundle = app.document.bundle().clone();
bundle
.files
.get_mut(&bundle.entry.clone())
.unwrap()
.insert_str(0, "// Keep startup explanation verbatim\r\n");
app.document.apply_bundle(bundle).unwrap();
let before = app.document.snapshot();
start(&mut app, f.owners[0]);
for i in 0..3 {
send(
&mut app,
Event::ReferenceOa(view::choice(&f.p, f.rows[0][i])),
);
send(
&mut app,
Event::ReferenceSa(view::choice(&f.p, f.rows[1][i])),
);
send(&mut app, Event::AddReference);
}
send(&mut app, Event::Select(member(&f, 2)));
send(
&mut app,
Event::Decide(member(&f, 2), Some(SemanticDecision::TakeSource)),
);
send(&mut app, Event::Select(FacetSelector::Order(f.which)));
send(
&mut app,
Event::Decide(
FacetSelector::Order(f.which),
Some(SemanticDecision::Candidate),
),
);
send(&mut app, Event::OrderMove(f.which, 3, -1));
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
let expected = state(&app).proposed.clone().unwrap();
apply(&mut app);
assert_eq!(app.project(), &expected);
let after = app.document.snapshot();
assert!(
app.document.bundle().files[&app.document.bundle().entry]
.starts_with("// Keep startup explanation verbatim\r\n")
);
let _ = app.update(Message::Undo);
assert_eq!(app.document.bundle(), &before.bundle);
assert_eq!(app.project(), &before.decoded.project);
let _ = app.update(Message::Redo);
assert_eq!(app.document.bundle(), &after.bundle);
assert_eq!(app.project(), &expected);
assert_eq!(
Document::from_bundle(
after.bundle.clone(),
source_project::ProjectLimits::default()
)
.unwrap()
.project(),
&expected
);
}
#[test]
fn several_incoming_members_follow_source_order_despite_independent_identity_sorting() {
for shape in 0..4 {
for reverse in [false, true] {
let mut f = fixture(shape, reverse, false);
let si = usize::from(reverse);
let ti = 1 - si;
set_order(
&mut f.p,
f.owners[si],
f.which,
vec![f.rows[si][0], f.rows[si][2], f.rows[si][3], f.rows[si][1]],
);
let mut s = ready(&f, reverse);
s.references.insert(f.rows[0][3], f.rows[1][3]);
decide(&mut s, &f.p, member(&f, 3), SemanticDecision::TakeSource);
decide(&mut s, &f.p, member(&f, 2), SemanticDecision::TakeSource);
decide(
&mut s,
&f.p,
FacetSelector::Header(HeaderFacet::Description),
SemanticDecision::Candidate,
);
s.update(
&f.p,
Event::Input("/description".into(), "Multiple incoming steps".into()),
);
let mut expected = f.p.clone();
set_order(
&mut expected,
f.owners[ti],
f.which,
vec![f.rows[ti][0], f.rows[ti][2], f.rows[ti][3], f.rows[ti][1]],
);
expected
.elements
.get_mut(&f.owners[ti])
.unwrap()
.description = "Multiple incoming steps".into();
oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone()));
}
}
}
#[test]
fn ordered_presence_keep_take_candidate_remove_only_selected_entry_and_order_take_reorders_only() {
for reverse in [false, true] {
for decision in [
SemanticDecision::KeepTarget,
SemanticDecision::TakeSource,
SemanticDecision::Candidate,
] {
let mut f = fixture(0, reverse, true);
let si = usize::from(reverse);
let ti = 1 - si;
set_order(&mut f.p, f.owners[si], f.which, vec![f.rows[si][1]]);
let mut s = ready(&f, reverse);
decide(&mut s, &f.p, member(&f, 0), decision);
if decision == SemanticDecision::Candidate {
s.update(&f.p, Event::Presence(member(&f, 0), false));
}
decide(
&mut s,
&f.p,
FacetSelector::Header(HeaderFacet::Description),
SemanticDecision::Candidate,
);
s.update(
&f.p,
Event::Input("/description".into(), "Only selected membership".into()),
);
let mut expected = f.p.clone();
if decision != SemanticDecision::KeepTarget {
set_order(
&mut expected,
f.owners[ti],
f.which,
vec![f.rows[ti][3], f.rows[ti][1]],
);
}
expected
.elements
.get_mut(&f.owners[ti])
.unwrap()
.description = "Only selected membership".into();
oracle(&f, &s, Some(expected.elements[&f.owners[ti]].clone()));
}
let mut f = fixture(0, reverse, false);
let si = usize::from(reverse);
let ti = 1 - si;
set_order(
&mut f.p,
f.owners[si],
f.which,
vec![f.rows[si][1], f.rows[si][0]],
);
for decision in [SemanticDecision::KeepTarget, SemanticDecision::TakeSource] {
let mut s = ready(&f, reverse);
decide(&mut s, &f.p, FacetSelector::Order(f.which), decision);
let result = oracle(&f, &s, None);
assert_eq!(
result.elements[&f.owners[ti]].steps().unwrap(),
&if decision == SemanticDecision::KeepTarget {
vec![f.rows[ti][0], f.rows[ti][1]]
} else {
vec![f.rows[ti][1], f.rows[ti][0]]
}
);
}
}
}
@@ -0,0 +1,347 @@
//! Recover authored intent against the exact accepted model; recompute every plan.
use super::*;
use crate::forms::RecoveryDraft;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Saved {
version: u32,
base_identity: Id,
base_fingerprint: String,
anchor: Id,
trace: Option<Id>,
direction: TransitionDirection,
selected: Option<FacetSelector>,
decisions: Vec<(FacetSelector, SemanticDecision)>,
references: BTreeMap<Id, Id>,
resets: BTreeSet<FacetSelector>,
staged: BTreeSet<FacetSelector>,
presence: Vec<(FacetSelector, bool)>,
member_family: keyed::Family,
member_oa: Option<Id>,
member_sa: Option<Id>,
literal: String,
literal_oa: bool,
literal_sa: bool,
annotation_oa: Option<AnnotationKey>,
annotation_sa: Option<AnnotationKey>,
draft: Option<RecoveryDraft>,
group_drafts: Vec<(FacetSelector, RecoveryDraft)>,
rows: Vec<(FacetSelector, String, Option<Id>)>,
#[serde(default)]
orders: Vec<(OrderedCollection, Vec<Id>)>,
reference_oa: Option<Id>,
reference_sa: Option<Id>,
reference_search: String,
accepted_impacts: Vec<SemanticImpact>,
}
fn unique<K: Ord, V>(values: Vec<(K, V)>) -> Result<BTreeMap<K, V>, String> {
let mut map = BTreeMap::new();
for (key, value) in values {
if map.insert(key, value).is_some() {
return Err("Recovery contains a duplicate semantic draft address.".into());
}
}
Ok(map)
}
impl State {
pub fn recovery_snapshot(&self) -> Result<Value, String> {
let key = |address: &keyed_rows::AddressDraft| {
address.selected.as_ref().map(|k| AnnotationKey {
path: k.path.clone(),
field: k.field,
})
};
serde_json::to_value(Saved {
version: 1,
base_identity: self.base_identity,
base_fingerprint: self.base_fingerprint.clone(),
anchor: self.anchor,
trace: self.trace,
direction: self.direction,
selected: self.selected.clone(),
decisions: self
.decisions
.iter()
.map(|(key, value)| (key.clone(), *value))
.collect(),
references: self.references.clone(),
resets: self.resets.clone(),
staged: self.staged.clone(),
presence: self
.presence
.iter()
.map(|(key, value)| (key.clone(), *value))
.collect(),
member_family: self.member_family,
member_oa: self.member_oa.as_ref().map(|c| c.id),
member_sa: self.member_sa.as_ref().map(|c| c.id),
literal: self.literal.clone(),
literal_oa: self.literal_oa,
literal_sa: self.literal_sa,
annotation_oa: key(&self.annotation_oa),
annotation_sa: key(&self.annotation_sa),
draft: self.draft.as_ref().map(Draft::recovery_snapshot),
group_drafts: self
.group_drafts
.iter()
.map(|(key, draft)| (key.clone(), draft.recovery_snapshot()))
.collect(),
rows: self
.row_drafts
.iter()
.map(|(key, draft)| {
(
key.clone(),
draft.expression.clone(),
draft.local.as_ref().map(|c| c.id),
)
})
.collect(),
reference_oa: self.reference_oa.as_ref().map(|c| c.id),
orders: self
.order_drafts
.iter()
.map(|(key, value)| (*key, value.clone()))
.collect(),
reference_sa: self.reference_sa.as_ref().map(|c| c.id),
reference_search: self.reference_search.clone(),
accepted_impacts: self.accepted_impacts.clone(),
})
.map_err(|e| e.to_string())
}
pub fn from_recovery(project: &Project, value: Value) -> Result<Self, String> {
let saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?;
if saved.version != 1
|| saved.base_identity != project.id
|| saved.base_fingerprint != project_fingerprint(project)
{
return Err(
"Semantic review recovery does not match the accepted model identity and revision."
.into(),
);
}
let count = [
saved.decisions.len(),
saved.references.len(),
saved.resets.len(),
saved.staged.len(),
saved.presence.len(),
saved.rows.len(),
saved
.orders
.iter()
.fold(0usize, |total, (_, rows)| total.saturating_add(rows.len())),
saved.accepted_impacts.len(),
]
.into_iter()
.fold(0usize, usize::saturating_add);
if count > crate::source_workspace::LIMITS.model.max_records
|| saved.group_drafts.len() > constructors::selectors().len()
{
return Err("Semantic recovery exceeds the supported draft limits.".into());
}
let mut state = Self::new(project, saved.anchor)?;
if saved.trace.is_some_and(|id| {
!project
.traces
.get(&id)
.is_some_and(|trace| trace.oa == saved.anchor || trace.sa == saved.anchor)
}) {
return Err("Semantic recovery trace and selected object disagree.".into());
}
state.trace = saved.trace;
state.direction = saved.direction;
state.load_trace(project);
state.selected = saved.selected;
state.decisions = unique(saved.decisions)?;
state.references = saved.references;
state.resets = saved.resets;
state.staged = saved.staged;
state.presence = unique(saved.presence)?;
state.member_family = saved.member_family;
state.member_oa = saved.member_oa.map(|id| view::choice(project, id));
state.member_sa = saved.member_sa.map(|id| view::choice(project, id));
state.literal = saved.literal;
state.literal_oa = saved.literal_oa;
state.literal_sa = saved.literal_sa;
let address = |key: Option<AnnotationKey>| keyed_rows::AddressDraft {
selected: key.map(|key| crate::data_values::FieldPathKey {
path: key.path,
field: key.field,
}),
..Default::default()
};
state.annotation_oa = address(saved.annotation_oa);
state.annotation_sa = address(saved.annotation_sa);
let target = state.target(project);
state.draft = saved
.draft
.map(|draft| Draft::from_recovery(project, draft))
.transpose()?;
if state
.draft
.as_ref()
.is_some_and(|draft| Some(draft.original) != target)
{
return Err("Recovered semantic draft belongs to another target.".into());
}
state.group_drafts = unique(
saved
.group_drafts
.into_iter()
.map(|(facet, draft)| {
if !constructors::selectors().contains(&facet) {
return Err("Recovered constructor draft has an unknown scope.".to_owned());
}
let mut draft = Draft::from_recovery(project, draft)?;
if Some(draft.original) != target {
return Err("Recovered constructor draft belongs to another target.".into());
}
draft.semantic_groups = true;
Ok((facet, draft))
})
.collect::<Result<Vec<_>, String>>()?,
)?;
state.row_drafts = unique(
saved
.rows
.into_iter()
.map(|(facet, expression, local)| {
if !keyed_rows::supported(&facet) {
return Err("Recovered row draft has an unknown scope.".to_owned());
}
Ok((
facet,
keyed_rows::RowDraft {
editor: text_editor::Content::with_text(&expression),
expression,
local: local.map(|id| view::choice(project, id)),
},
))
})
.collect::<Result<Vec<_>, String>>()?,
)?;
state.reference_oa = saved.reference_oa.map(|id| view::choice(project, id));
state.order_drafts = unique(saved.orders)?;
state.reference_sa = saved.reference_sa.map(|id| view::choice(project, id));
state.reference_search = saved.reference_search;
state.accepted_impacts = saved.accepted_impacts;
// Invalid numbers and incomplete mappings are recoverable authored input.
// Refresh reports their errors instead of dropping them or applying a plan.
state.refresh(project);
Ok(state)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_one_without_orders_restores_raw_intent_and_replans_without_execution() {
let mut p = Project::empty();
let oa = p
.add_element(Kind::DataType, Layer::Oa, "Operational", None)
.unwrap();
let sa = p
.add_element(Kind::DataType, Layer::Sa, "System", None)
.unwrap();
for id in [oa, sa] {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Integer {
minimum: Some(0),
maximum: Some(100),
},
default: None,
unit: String::new(),
};
}
super::super::tests::add_trace(&mut p, oa, sa, "Version one review");
let accepted = p.clone();
let facet = FacetSelector::Data(DataFacet::IntegerMinimum);
let path = "/body/Sa/Shared/DataType/definition/Integer/minimum";
for (raw, valid) in [("7", true), ("-", false)] {
let mut state = State::new(&p, oa).unwrap();
state.update(&p, Event::Select(facet.clone()));
state.update(
&p,
Event::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
);
state.update(&p, Event::Input(path.into(), raw.into()));
assert_eq!(state.can_apply(), valid);
let current = state.recovery_snapshot().unwrap();
let mut old = current.clone();
assert_eq!(old["version"], 1);
assert_eq!(
old.as_object_mut().unwrap().remove("orders"),
Some(serde_json::json!([]))
);
let restored = State::from_recovery(&p, old.clone()).unwrap();
assert!(restored.order_drafts.is_empty());
assert_eq!(restored.draft.as_ref().unwrap().inputs[path].raw, raw);
assert_eq!(restored.can_apply(), valid);
assert_eq!(restored.recovery_snapshot().unwrap(), current);
assert_eq!(
p, accepted,
"Recovery prepares a preview, never an accepted edit"
);
let mut changed = p.clone();
changed.name.push_str(" changed accepted model");
assert!(State::from_recovery(&changed, old).is_err());
}
}
#[test]
fn raw_scalar_and_closed_constructor_drafts_recover_against_exact_model_only() {
let mut p = Project::empty();
let oa = p
.add_element(Kind::DataType, Layer::Oa, "Operational", None)
.unwrap();
let sa = p
.add_element(Kind::DataType, Layer::Sa, "System", None)
.unwrap();
for id in [oa, sa] {
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Integer {
minimum: Some(0),
maximum: Some(100),
},
default: None,
unit: String::new(),
};
}
super::super::tests::add_trace(&mut p, oa, sa, "Recover review");
let mut state = State::new(&p, oa).unwrap();
let facet = FacetSelector::Data(DataFacet::IntegerMinimum);
state.update(&p, Event::Select(facet.clone()));
state.update(
&p,
Event::Decide(facet.clone(), Some(SemanticDecision::Candidate)),
);
let path = "/body/Sa/Shared/DataType/definition/Integer/minimum";
state.update(&p, Event::Input(path.into(), "-".into()));
assert!(!state.can_apply());
let saved = state.recovery_snapshot().unwrap();
let restored = State::from_recovery(&p, saved.clone()).unwrap();
assert_eq!(restored.draft.as_ref().unwrap().inputs[path].raw, "-");
assert_eq!(restored.decisions, state.decisions);
assert!(!restored.can_apply());
assert!(restored.plan.is_none());
assert!(
restored
.group_drafts
.values()
.all(|draft| draft.semantic_groups)
);
p.name.push_str(" changed");
assert!(
State::from_recovery(&p, saved)
.err()
.unwrap()
.contains("identity and revision")
);
}
}
+242 -12
View File
@@ -208,14 +208,24 @@ fn formatted(project: &Project, value: &Value, references: &BTreeSet<Id>) -> Str
.join("\n"),
}
}
fn observed(project: &Project, fact: Option<&FacetReview>, layer: Layer) -> String {
pub(super) fn observed(project: &Project, fact: Option<&FacetReview>, layer: Layer) -> String {
let Some(fact) = fact else {
return "Unreviewed — no previous observation".into();
};
let value = scalar::observation(fact, layer);
if value.as_str() == Some("NotApplicable") {
"Not applicable".into()
} else if keyed::supported(&fact.selector()) {
} else if matches!(fact, FacetReview::Annotation { .. }) {
if let Ok(row) = serde_json::from_value::<MessageValue>(value["Value"].clone()) {
format!(
"Field: {}\nExpression: {:?}",
annotation_title(project, &AnnotationKey::from(&row)),
row.expression
)
} else {
"Absent".into()
}
} else if keyed::supported(&fact.selector()) && !keyed_rows::supported(&fact.selector()) {
if value["Value"].as_bool() == Some(true) {
"Present".into()
} else {
@@ -259,7 +269,7 @@ impl State {
action(direction, Event::Direction(if self.direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa })),
].spacing(8),
text("Changing trace or direction starts a new review draft. Other pending edits remain separate.").size(10).color(style::MUTED),
row![action("Properties", Event::Tab(Tab::Facts)), action("Members / literals", Event::Tab(Tab::Members)), action(format!("History ({})", self.resets.len()), Event::Tab(Tab::History)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6).wrap(),
row![action("Properties", Event::Tab(Tab::Facts)), action("Members / rows", Event::Tab(Tab::Members)), action(format!("History ({})", self.resets.len()), Event::Tab(Tab::History)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6).wrap(),
].spacing(8);
let body = match self.tab {
Tab::Facts => self.facts_view(project),
@@ -290,7 +300,7 @@ impl State {
.iter()
.filter(|f| f.drift == Drift::Unreviewed)
.count();
content = content.push(text(format!("{} decisions · {} exact resets · {unreviewed} unreviewed facts · Ordered, annotation, binding and attached-record decisions remain pending.", self.decisions.len(), self.resets.len())).size(10).color(style::MUTED));
content = content.push(text(format!("{} decisions · {} exact resets · {unreviewed} unreviewed facts · Attached-record and multi-source decisions remain pending.", self.decisions.len(), self.resets.len())).size(10).color(style::MUTED));
if let Some(trace) = trace {
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED));
}
@@ -460,7 +470,7 @@ impl State {
);
}
if keyed::supported(&fact.selector) {
detail = detail.push(text("Presence changes only this collection entry; referenced objects stay intact. Take preserves a row with no source identity. An explicit source identity that is absent can remove its mapped destination entry.").size(11))
detail = detail.push(text("Review changes only this collection entry; referenced objects stay intact. Take preserves a row with no source identity. An explicit source identity that is absent can remove its mapped destination entry. Changing a row key requires separate removal and addition.").size(11))
.push(action("Choose / change exact row identities…", Event::EditPair(fact.selector.clone())));
if let Some(baseline) = trace
.and_then(|t| t.baseline.as_ref())
@@ -482,6 +492,12 @@ impl State {
}
}
}
if let FacetSelector::Order(which) = fact.selector {
detail = detail.push(text("Order changes positions only. Review member presence separately; the final order must include every surviving destination identity exactly once. Take preserves destination-only members when their surrounding order already agrees.").size(11));
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) {
detail = detail.push(self.order_view(project, which));
}
}
if let Some(proposed) = &self.proposed
&& let Some(trace) = trace
&& let Ok(after) = capture_facet(proposed, trace.oa, trace.sa, &fact.selector)
@@ -569,6 +585,44 @@ impl State {
.text_size(12)
.on_toggle(move |v| event(Event::Presence(facet.clone(), v))),
);
if keyed_rows::supported(&fact.selector)
&& let Some(draft) = self.row_drafts.get(&fact.selector)
{
let facet = fact.selector.clone();
detail = detail.push(text("The exact destination key stays fixed; its row value is independent of sibling rows.").size(11));
match &fact.selector {
FacetSelector::Annotation(_) => {
detail = detail.push(
iced::widget::text_editor(&draft.editor)
.placeholder("Expression")
.on_action(move |action| {
event(Event::RowText(facet.clone(), action))
})
.size(12)
.height(90)
.padding(7),
);
}
FacetSelector::Binding(_) => {
detail = detail.push(
pick_list(
self.binding_choices(
project,
self.direction.target_layer(),
false,
),
draft.local.clone(),
move |value| event(Event::RowLocal(facet.clone(), value)),
)
.placeholder("Local lifeline in destination scenario")
.text_size(12)
.padding(7)
.width(Fill),
);
}
_ => {}
}
}
}
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& let Some(draft) = self.candidate_draft(&fact.selector)
@@ -634,6 +688,7 @@ impl State {
}
if (is_group
|| keyed::supported(&fact.selector)
|| matches!(fact.selector, FacetSelector::Order(_))
|| constructors::selectors().iter().any(|group| {
self.decisions.contains_key(group) && constructors::contains(group, &fact.selector)
}))
@@ -676,12 +731,22 @@ impl State {
let families = self.member_families(project);
let available = families.contains(&self.member_family);
let mut content = column![
text("Review an individual member or literal").size(16),
text("Choose an address, then review its presence in Properties. This also stages a new row in an empty collection. No model object is created.").size(12),
text("Review an individual member or row").size(16),
text("Choose an exact address, then review its value and presence in Properties. This also stages a new row in an empty collection. No model object is created.").size(12),
pick_list(families, Some(self.member_family), |v| event(Event::MemberFamily(v))).text_size(12).padding(7),
].spacing(9);
if let keyed::Family::Member(collection) = self.member_family
&& let Some(which) = ordered::ordered(collection)
{
content = content.push(text("Membership and order are separate decisions. An incoming member with ambiguous placement requires an explicit final order; referenced objects are not created or reparented here.").size(11))
.push(action("Review final order…", Event::Select(FacetSelector::Order(which))));
}
if !available {
content = content.push(text("This collection is not available on the current counterparts. Choose an available collection above; existing staged addresses remain removable below.").size(12));
} else if self.member_family == keyed::Family::Annotation {
content = content.push(text("Pair the complete carried-field paths explicitly. Each path component establishes a reference pair. Repeated fields under different paths remain independent rows.").size(11))
.push(self.annotation_address_view(project, Layer::Oa))
.push(self.annotation_address_view(project, Layer::Sa));
} else if self.member_family == keyed::Family::Literal {
content = content.push(text("Literal identity is exact, including case and spaces. Both sides use the same string; a rename requires separate old/new rows.").size(11))
.push(text_input("Exact literal", &self.literal).on_input(|v| event(Event::Literal(v))).size(12).padding(7))
@@ -691,7 +756,7 @@ impl State {
.push(row![pick_list(self.member_choices(project, Layer::Oa), self.member_oa.clone(), |v| event(Event::MemberOa(Some(v)))).placeholder("Operational identity").text_size(12).padding(7).width(Fill), action("No OA identity", Event::MemberOa(None))].spacing(7))
.push(row![pick_list(self.member_choices(project, Layer::Sa), self.member_sa.clone(), |v| event(Event::MemberSa(Some(v)))).placeholder("System identity").text_size(12).padding(7).width(Fill), action("No SA identity", Event::MemberSa(None))].spacing(7));
}
content = content.push(button(text("Use these identities and review presence").size(12)).padding([7,9]).style(style::nav).on_press_maybe(available.then_some(event(Event::StageMember))))
content = content.push(button(text("Use these addresses and review row").size(12)).padding([7,9]).style(style::nav).on_press_maybe(available.then_some(event(Event::StageMember))))
.push(text("Changing a row address retains its earlier decisions and drafts. Reset conflicting historical observations and leave overlapping decisions unselected explicitly.").size(11).color(style::MUTED));
if !self.staged.is_empty() {
content = content.push(text("Staged addresses in this draft").size(13));
@@ -711,6 +776,170 @@ impl State {
scrollable(content).height(305).into()
}
fn order_view<'a>(
&'a self,
project: &'a Project,
which: OrderedCollection,
) -> Element<'a, Message> {
let mut content = column![text("Final destination order").size(13),
text("Up/Down changes only positions. Reconcile members explicitly after changing presence: retain surviving positions, remove excluded entries and append newly included entries.").size(11),
action("Reconcile with selected membership", Event::OrderReconcile(which))].spacing(6);
if let Some(ids) = self.order_drafts.get(&which) {
if ids.is_empty() {
content = content.push(text("Empty order").size(12));
}
for (index, id) in ids.iter().enumerate() {
let up = button(text("Up").size(11)).padding(5).on_press_maybe(
(index > 0).then_some(event(Event::OrderMove(which, index, -1))),
);
let down = button(text("Down").size(11)).padding(5).on_press_maybe(
(index + 1 < ids.len()).then_some(event(Event::OrderMove(which, index, 1))),
);
content = content.push(
row![
text(format!("{}. {}", index + 1, choice(project, *id).label))
.size(12)
.width(Fill),
up,
down
]
.spacing(5),
);
}
match self.ordered_members(project, which) {
Ok(members) => {
let missing: Vec<_> = members
.iter()
.filter(|id| !ids.contains(id))
.map(|id| choice(project, *id).label)
.collect();
let excluded: Vec<_> = ids
.iter()
.filter(|id| !members.contains(id))
.map(|id| choice(project, *id).label)
.collect();
if !missing.is_empty() {
content = content.push(
text(format!("Missing from this draft: {}", missing.join(", ")))
.size(11),
);
}
if !excluded.is_empty() {
content = content.push(
text(format!("No longer included: {}", excluded.join(", "))).size(11),
);
}
}
Err(error) => content = content.push(text(error).size(11)),
}
}
content.into()
}
fn annotation_address_view<'a>(
&'a self,
project: &'a Project,
layer: Layer,
) -> Element<'a, Message> {
let address = self.annotation_address(layer);
let side = if layer == Layer::Oa {
"Operational"
} else {
"System"
};
let mut content =
column![text(format!("{side} carried-field address")).size(13)].spacing(6);
if let Some(key) = &address.selected {
content = content.push(
text(annotation_title(
project,
&AnnotationKey {
path: key.path.clone(),
field: key.field,
},
))
.size(11),
);
} else {
content = content.push(text("No identity on this side").size(11));
}
content = content.push(
row![
action("Root fields", Event::AnnotationBrowse(layer, None, 0)),
action("No identity", Event::AnnotationAddress(layer, None)),
]
.spacing(6),
);
if let Some(scope) = &address.scope {
content = content
.push(
text(format!(
"Browsing: {}",
annotation_title(
project,
&AnnotationKey {
path: scope.path.clone(),
field: scope.field
}
)
))
.size(11),
)
.push(action(
"Up",
Event::AnnotationBrowse(layer, crate::data_values::parent(scope), 0),
));
}
match self.annotation_choices(project, layer) {
Err(error) => content = content.push(text(error).size(11).color(style::MUTED)),
Ok(choices) => {
const PAGE: usize = 24;
let pages = choices.len().div_ceil(PAGE).max(1);
let page = address.page.min(pages - 1);
let visible = choices
.into_iter()
.skip(page * PAGE)
.take(PAGE)
.collect::<Vec<_>>();
let selected = visible
.iter()
.find(|choice| Some(&choice.key) == address.selected.as_ref())
.cloned();
content = content.push(
pick_list(visible, selected, move |choice| {
event(Event::AnnotationAddress(layer, Some(choice.key)))
})
.placeholder("Choose a field")
.text_size(12)
.padding(7)
.width(Fill),
);
if let Some(selected) = &address.selected {
content = content.push(action(
"Browse fields within selected record",
Event::AnnotationBrowse(layer, Some(selected.clone()), 0),
));
}
let mut navigation =
row![text(format!("Page {} of {pages}", page + 1)).size(11)].spacing(7);
if page > 0 {
navigation = navigation.push(action(
"Previous",
Event::AnnotationBrowse(layer, address.scope.clone(), page - 1),
));
}
if page + 1 < pages {
navigation = navigation.push(action(
"Next",
Event::AnnotationBrowse(layer, address.scope.clone(), page + 1),
));
}
content = content.push(navigation);
}
}
content.into()
}
fn history_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
let mut content = column![text("Exact previous observations").size(16), text("Check only the observations to forget. Resets run before this review's decisions; reset alone changes history, not model values. Uncheck to restore the pending observation. Prose history remains separate.").size(12)].spacing(9);
let baseline = self
@@ -777,8 +1006,8 @@ impl State {
pick_list(self.all_references(project, Layer::Oa), self.reference_oa.clone(), |c| event(Event::ReferenceOa(c))).placeholder("Operational reference").padding(7).text_size(12).width(Fill),
pick_list(self.all_references(project, Layer::Sa), self.reference_sa.clone(), |c| event(Event::ReferenceSa(c))).placeholder("System reference").padding(7).text_size(12).width(Fill),
action("Use pair", Event::AddReference),
].spacing(7),
].spacing(9);
].spacing(7).width(Fill),
].spacing(9).width(Fill);
for (oa, sa) in &self.references {
content = content.push(
row![
@@ -791,7 +1020,8 @@ impl State {
.width(Fill),
action("Remove pair", Event::RemoveReference(*oa))
]
.spacing(6),
.spacing(6)
.width(Fill),
);
}
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
@@ -828,7 +1058,7 @@ impl State {
Err(error) => content = content.push(text(error).size(12)),
}
}
scrollable(content).height(305).into()
scrollable(content).width(Fill).height(305).into()
}
fn impacts_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {