Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user