use super::row::form_event; use super::*; use crate::{style, views::short}; use iced::{ Element, Fill, widget::{ Space, button, checkbox, column, container, pick_list, row, scrollable, text, text_input, }, }; #[cfg(test)] #[path = "view_reserved_artifact_tests.rs"] mod reserved_artifact_tests; fn event(event: RowEvent) -> Message { Message::Workflow(crate::workflow::Event::semantic(Event::Editor(event))) } fn action<'a>( label: impl Into, event_value: RowEvent, ) -> iced::widget::Button<'a, Message> { button(text(label.into()).size(12)) .padding([7, 9]) .style(style::nav) .on_press(event(event_value)) } pub(super) fn choice(project: &Project, id: Id) -> Choice { let alias = project .elements .get(&id) .map(|v| v.alias.as_str()) .or_else(|| project.relations.get(&id).map(|v| v.alias.as_str())) .or_else(|| project.artifact_links.get(&id).map(|v| v.alias.as_str())) .or_else(|| project.traces.get(&id).map(|v| v.alias.as_str())); Choice { id, label: alias.map_or_else( || format!("Deleted reference · {id}"), |alias| format!("{} · @{alias}", project.named(id)), ), } } fn words(value: &str) -> String { let mut out = String::new(); for (index, c) in value.chars().enumerate() { if c == '_' { out.push(' '); } else { if index > 0 && c.is_uppercase() { out.push(' '); } out.push(c); } } out } pub(super) fn group(facet: &FacetSelector) -> &'static str { match facet { FacetSelector::Header(_) => "Name and ownership", FacetSelector::Data(_) | FacetSelector::Field(_) | FacetSelector::Literal(_) => { "Data and defaults" } FacetSelector::Category(_) => "Exchange categories", FacetSelector::Port(_) | FacetSelector::Interface(_) | FacetSelector::ExchangeItem(_) => { "Contracts and interfaces" } FacetSelector::Scenario(_) | FacetSelector::Lifeline(_) | FacetSelector::Occurrence(_) | FacetSelector::Event(_) | FacetSelector::State(_) | FacetSelector::Annotation(_) | FacetSelector::Binding(_) => "Behavior and scenarios", FacetSelector::Relation(_) | FacetSelector::Constraint(_) => { "Relationships and constraints" } FacetSelector::Member { .. } | FacetSelector::Order(_) => "Membership and order", FacetSelector::Child(_) | FacetSelector::Incident { .. } | FacetSelector::Artifact { .. } => "Attached records", } } pub(super) fn title(project: &Project, facet: &FacetSelector) -> String { let keys = |keys: &KeyPair, missing: &str| { format!( "{} ↔ {}", keys.oa .map(|id| choice(project, id).label) .unwrap_or_else(|| missing.into()), keys.sa .map(|id| choice(project, id).label) .unwrap_or_else(|| missing.into()) ) }; match facet { FacetSelector::Member { collection, keys: pair, } => format!( "{} · {}", words(&format!("{collection:?}")), keys(pair, "No identity") ), FacetSelector::Order(collection) => format!("{} order", words(&format!("{collection:?}"))), FacetSelector::Literal(pair) => format!( "Literal · {} ↔ {}", pair.oa .as_ref() .map(|s| format!("{s:?}")) .unwrap_or_else(|| "No identity".into()), pair.sa .as_ref() .map(|s| format!("{s:?}")) .unwrap_or_else(|| "No identity".into()) ), FacetSelector::Annotation(pair) => format!( "Message value · {} ↔ {}", pair.oa .as_ref() .map(|key| annotation_title(project, key)) .unwrap_or_else(|| "Absent".into()), pair.sa .as_ref() .map(|key| annotation_title(project, key)) .unwrap_or_else(|| "Absent".into()) ), FacetSelector::Binding(pair) => format!("Lifeline binding · {}", keys(pair, "Absent")), FacetSelector::Child(pair) => format!("Owned child · {}", keys(pair, "Absent")), FacetSelector::Incident { keys: pair, end } => { format!("Incident {end:?} · {}", keys(pair, "Absent")) } FacetSelector::Artifact { keys: pair, facet } => { format!( "Artifact {} · {}", words(&format!("{facet:?}")), keys(pair, "Absent") ) } _ => { let json = serde_json::to_value(facet).expect("Typed selector"); let value = json .as_object() .expect("Family") .values() .next() .expect("Leaf"); words(value.as_str().unwrap_or("Property")) } } } fn annotation_title(project: &Project, key: &AnnotationKey) -> String { key.path .iter() .chain(std::iter::once(&key.field)) .map(|id| choice(project, *id).label) .collect::>() .join(" / ") } fn drift(drift: Drift) -> &'static str { match drift { Drift::Unreviewed => "Unreviewed", Drift::Unchanged => "Unchanged since review", Drift::OaChanged => "Operational changed", Drift::SaChanged => "System changed", Drift::BothChanged => "Both changed", } } pub(crate) fn summary(project: &Project, trace: &Trace) -> String { let references = BTreeMap::from([(trace.oa, trace.sa)]); match trace_semantic_drift(project, trace, &references) { Ok(value) => format!( "Semantics: {}{} · Linked: {}{}", drift(value.own), if value.own_unreviewed && value.own != Drift::Unreviewed { " · incomplete review" } else { "" }, drift(value.linked), if value.linked_unreviewed && value.linked != Drift::Unreviewed { " · incomplete review" } else { "" } ), Err(error) => format!("Semantic review unavailable: {error}"), } } fn formatted(project: &Project, value: &Value, references: &BTreeSet) -> String { match value { Value::Null => "Not set".into(), Value::Bool(value) => if *value { "Yes" } else { "No" }.into(), Value::Number(value) => value.to_string(), Value::String(value) => { if let Ok(id) = value.parse::() && references.contains(&id) { choice(project, id).label } else if value.is_empty() { "Empty text".into() } else { format!("“{value}”") } } Value::Array(values) if values.is_empty() => "Empty list (0 members)".into(), Value::Array(values) => values .iter() .enumerate() .map(|(i, v)| format!("{}. {}", i + 1, formatted(project, v, references))) .collect::>() .join("\n"), Value::Object(values) => values .iter() .map(|(key, value)| { format!("{}: {}", words(key), formatted(project, value, references)) }) .collect::>() .join("\n"), } } 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 matches!(fact, FacetReview::Annotation { .. }) { if let Ok(row) = serde_json::from_value::(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 { "Absent".into() } } else { formatted(project, &value["Value"], &scalar::references(fact)) } } impl ReviewRow { pub(super) fn review_title(&self, project: &Project, facet: &FacetSelector) -> String { if let FacetSelector::Artifact { keys, facet } = facet { let label = |id: Option| { id.map(|id| self.review_choice(project, id).label) .unwrap_or_else(|| "Absent".into()) }; format!( "Artifact {} · {} ↔ {}", words(&format!("{facet:?}")), label(keys.oa), label(keys.sa) ) } else { title(project, facet) } } pub(super) fn review_choice(&self, project: &Project, id: Id) -> Choice { self.artifacts.choice(project, id) } pub(super) fn view<'a>( &'a self, project: &'a Project, header: Element<'a, Message>, can_apply: bool, ) -> Element<'a, Message> { let trace = self.trace.and_then(|id| project.traces.get(&id)); let mut content = column![ header, row![ text("Review area").size(12), pick_list( vec![ Tab::Facts, Tab::Members, Tab::Attachments, Tab::Artifacts, Tab::History, Tab::References, Tab::Impacts, ], Some(self.tab), |tab| event(RowEvent::Tab(tab)), ) .text_size(12) .padding(7), Space::new().width(Fill), action("Refresh preview", RowEvent::Preview) ] .spacing(6) .wrap(), ] .spacing(8); let impacts = self.plan.as_ref().map_or(0, |plan| plan.impacts.len()); if impacts > 0 && self.tab != Tab::Impacts { content = content.push(action( format!("View {impacts} additional changes"), RowEvent::Tab(Tab::Impacts), )); } if self.direction == TransitionDirection::SaToOa && trace.is_some_and(|trace| { matches!( project.elements.get(&trace.sa).and_then(|e| e.shared()), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { operation: Some(_), message_kind: MessageKind::Call | MessageKind::Asynchronous, .. }, .. }) ) }) { content = content.push( checkbox(self.operation_omission.is_some()) .label("Explicitly omit the technical operation in the OA counterpart") .on_toggle(|enabled| event(RowEvent::OperationOmission(enabled))), ); if let Some(raw) = &self.operation_omission { content = content.push( text_input("Required engineering rationale", raw) .on_input(|raw| event(RowEvent::OperationRationale(raw))) .padding(8) .size(12), ); } } let body = match self.tab { Tab::Facts => self.facts_view(project), Tab::References => self.references_view(project), Tab::Members => self.members_view(project), Tab::Artifacts => { if let Some(trace) = trace { scrollable( artifacts_view::pairs(&self.artifacts, project, trace) .map(|e| event(RowEvent::Artifact(e))), ) .height(Fill) .into() } else { text("Choose a trace first.").into() } } Tab::Attachments => { if let Some(trace) = trace { scrollable( attachments_view::pairs(&self.attachments, project, trace) .map(|e| event(RowEvent::Attachment(e))), ) .height(Fill) .into() } else { text("Choose a trace first.").into() } } Tab::History => self.history_view(project), Tab::Impacts => self.impacts_view(project), }; content = content.push(body); if let Some(error) = &self.error { content = content.push( text(error) .size(12) .color(iced::Color::from_rgb8(178, 54, 58)), ); } if let Some(plan) = &self.plan { for error in &plan.conflicts { content = content.push( text(error) .size(12) .color(iced::Color::from_rgb8(178, 54, 58)), ); } } let unreviewed = self .facts .iter() .filter(|f| f.drift == Drift::Unreviewed) .count(); content = content.push( text(format!( "{} decisions · {} exact resets · {unreviewed} unreviewed facts", 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)); } content .push( row![ text( "Apply updates all included histories and the model once; Undo restores the whole batch." ) .size(10) .width(Fill), button(text("Cancel").size(12)) .padding(8) .style(style::nav) .on_press(Message::Workflow(crate::workflow::Event::Cancel)), button(text("Apply reviewed changes").size(12)) .padding(8) .on_press_maybe( can_apply .then_some(Message::Workflow(crate::workflow::Event::Apply)) ), ] .spacing(8) .align_y(iced::Center), ) .into() } fn facts_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { let mut list = column![ checkbox(self.show_unchanged) .label("Include unchanged facts") .text_size(11) .on_toggle(|v| event(RowEvent::ShowUnchanged(v))) ] .spacing(5); let mut groups: BTreeMap<&str, Vec<&FacetDrift>> = BTreeMap::new(); for fact in &self.facts { if self.show_unchanged || fact.drift != Drift::Unchanged || self.decisions.contains_key(&fact.selector) || self.selected.as_ref() == Some(&fact.selector) { groups.entry(group(&fact.selector)).or_default().push(fact); } } for (group, facts) in groups { list = list.push(text(group).size(12).color(style::TEAL)); for fact in facts { let mark = match self.decisions.get(&fact.selector) { Some(SemanticDecision::KeepTarget) => "Keep · ", Some(SemanticDecision::TakeSource) => "Take · ", Some(SemanticDecision::Candidate) => "Final value · ", None => "", }; list = list.push( button( column![ text(short(&self.review_title(project, &fact.selector), 52)).size(12), text(format!("{mark}{}", drift(fact.drift))).size(10) ] .spacing(3), ) .padding(7) .width(Fill) .style(if self.selected.as_ref() == Some(&fact.selector) { style::selected } else { style::nav }) .on_press(event(RowEvent::Select(fact.selector.clone()))), ); } } row![ scrollable(list.padding(iced::Padding::default().right(18))) .height(305) .width(260), scrollable(container(self.detail(project)).padding(iced::Padding::default().right(18))) .height(305) .width(Fill) ] .spacing(12) .into() } fn detail<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { let Some(fact) = self .selected .as_ref() .and_then(|s| self.facts.iter().find(|f| &f.selector == s)) else { return text("Choose a trace and property to compare.") .size(13) .into(); }; let trace = self.trace.and_then(|id| project.traces.get(&id)); let previous = trace .and_then(|t| t.baseline.as_ref()) .and_then(|b| b.semantic.as_ref()) .and_then(|b| b.facets.iter().find(|f| f.selector() == fact.selector)); let is_group = scalar::grouped(&fact.selector); let group_observations = |project: &Project, facts: Vec<&FacetReview>, layer| { facts .into_iter() .filter(|f| constructors::contains(&fact.selector, &f.selector())) .map(|f| { format!( "{}: {}", self.review_title(project, &f.selector()), observed(project, Some(f), layer) ) }) .collect::>() .join("\n") }; let side = |layer, label: &str| { let mut before = observed(project, previous, layer); let mut current = observed(project, Some(&fact.current), layer); if is_group { before = group_observations( project, trace .and_then(|t| t.baseline.as_ref()) .and_then(|b| b.semantic.as_ref()) .map(|b| b.facets.iter().collect()) .unwrap_or_default(), layer, ); if before.is_empty() { before = "No previous group observations".into(); } current = group_observations( project, self.facts.iter().map(|f| &f.current).collect(), layer, ); } column![ text(format!( "{label}{}", if layer == self.direction.target_layer() { " · destination" } else { " · source" } )) .size(12) .color(style::TEAL), text("Previously reviewed").size(10).color(style::MUTED), text(before).size(12), text("Current").size(10).color(style::MUTED), text(current).size(12), ] .spacing(5) .width(Fill) }; let mut detail = column![ text(self.review_title(project, &fact.selector)).size(17), text(drift(fact.drift)).size(11), row![side(Layer::Oa, "Operational"), side(Layer::Sa, "System")].spacing(14) ] .spacing(10); if previous.is_some() && history::resettable(&fact.selector) { let facet = fact.selector.clone(); detail = detail.push( checkbox(self.resets.contains(&facet)) .label("Reset this exact previous observation before review") .text_size(11) .on_toggle(move |v| event(RowEvent::Reset(facet.clone(), v))), ); } if keyed::supported(&fact.selector) || attachments::supported(&fact.selector) || artifacts::supported(&fact.selector) { 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…", RowEvent::EditPair(fact.selector.clone()))); if let Some(baseline) = trace .and_then(|t| t.baseline.as_ref()) .and_then(|b| b.semantic.as_ref()) { for prior in &baseline.facets { let prior = prior.selector(); if prior != fact.selector && keyed::overlap(&prior, &fact.selector) { detail = detail.push( checkbox(self.resets.contains(&prior)) .label(format!( "Reset overlapping history: {}", self.review_title(project, &prior) )) .text_size(11) .on_toggle(move |v| event(RowEvent::Reset(prior.clone(), v))), ); } } } } 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) { detail = detail .push( text("Proposed destination value · validated preview") .size(11) .color(style::TEAL), ) .push( text(if is_group { semantic_facets(proposed, &proposed.traces[&trace.id], &self.references) .ok() .map(|facts| { group_observations( proposed, facts.iter().map(|f| &f.current).collect(), self.direction.target_layer(), ) }) .unwrap_or_else(|| "Group payload is unavailable.".into()) } else { observed(proposed, Some(&after), self.direction.target_layer()) }) .size(12), ); } else { detail = detail.push( text("No validated final preview. Resolve the messages below before applying.") .size(11) .color(style::MUTED), ); } if self.editable(project, &fact.selector) { if is_group { detail = detail.push(text(constructors::description(&fact.selector)).size(12)); detail = detail.push(text("Replacing a kind reviews its complete payload. Review every removed reference, row and prior observation in the exact scope below; changing kind retains your draft for switching back.").size(11)); if self.can_acknowledge_constructor(project, &fact.selector) { detail = detail.push(text("These initial kinds are equal. Keep or Take acknowledges only the kind; an unchanged Candidate kind needs independent descendant decisions for payload edits.").size(11).color(style::MUTED)); } } let destination = if self.direction.target_layer() == Layer::Sa { "System" } else { "Operational" }; let source = if self.direction.source_layer() == Layer::Oa { "Operational" } else { "System" }; let mut buttons = row![ action( format!("Keep {destination}"), RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget)) ), action( format!("Take {source}"), RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::TakeSource)) ) ] .spacing(5); if !matches!( fact.selector, FacetSelector::Artifact { facet: ArtifactFacet::Presence, .. } ) { buttons = buttons.push(action( if is_group { "Set final kind and payload…" } else { "Set final value…" }, RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)), )); } detail = detail.push(buttons.wrap()).push(action( "Leave unselected", RowEvent::Decide(fact.selector.clone(), None), )); if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) && keyed::supported(&fact.selector) { let facet = fact.selector.clone(); detail = detail.push( checkbox(self.presence.get(&facet).copied().unwrap_or(false)) .label( "Include in destination collection (unchecked removes only this entry)", ) .text_size(12) .on_toggle(move |v| event(RowEvent::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(RowEvent::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(RowEvent::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(editor) = attachments_view::candidate( &self.attachments, project, &fact.selector, self.direction.target_layer(), ) { detail = detail.push(editor.map(form_event)); } if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) && let Some(editor) = artifacts_view::candidate(&self.artifacts, project, &fact.selector) { detail = detail.push(editor.map(|e| event(RowEvent::Artifact(e)))); } if artifacts::supported(&fact.selector) { detail = detail.push(text("Metadata is reviewed from this initially attached owner. Owner moves and source-absence deletion must agree with kept Presence. Creating a reserved counterpart requires Presence plus all seven metadata decisions; each raw draft remains independent.").size(11)); } if !attachments::supported(&fact.selector) && !artifacts::supported(&fact.selector) && self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) && let Some(draft) = self.candidate_draft(&fact.selector) && let Some(paths) = self .target(project) .and_then(|id| constructors::roots(project, id, &fact.selector)) { detail = detail.push(text("Final destination value").size(12)); for path in paths { let label = if path.ends_with("/default") { "Default" } else if path.ends_with("/legacy") { "Generic association" } else { "Kind and payload" }; detail = detail.push( crate::forms::scoped( draft, project, &path, if is_group { label } else { "Final value" }, ) .map(form_event), ); } } } else if self.can_acknowledge_constructor(project, &fact.selector) { detail = detail.push(text("Both constructor kinds are equal and have no prior observation. Acknowledge only the kind; its properties remain independent.").size(12)) .push(action("Acknowledge equal kind only", RowEvent::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget)))) .push(action("Leave unselected", RowEvent::Decide(fact.selector.clone(), None))); } else { detail = detail.push(text(if scalar::grouped(&fact.selector) { "This constructor is unavailable on one of the current counterparts. Review the outer constructor before its nested kind." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED)); } if self.staged.contains(&fact.selector) { detail = detail.push(action( "Remove staged address and its decision", RowEvent::DropStaged(fact.selector.clone()), )); } let overlaps: Vec<_> = self .decisions .keys() .filter(|other| { **other != fact.selector && (constructors::contains(&fact.selector, other) || constructors::contains(other, &fact.selector) || keyed::overlap(other, &fact.selector)) }) .collect(); if self.decisions.contains_key(&fact.selector) && !overlaps.is_empty() { detail = detail.push(text("Related decisions are selected. Paired rows cannot review the same OA or SA identity twice. Replacing groups require one combined decision; first equal-kind acknowledgments may coexist with descendants. Resolve overlaps explicitly; all drafts are retained.").size(11)); for other in overlaps { detail = detail.push(action( format!("Leave {} unselected", self.review_title(project, other)), RowEvent::Decide(other.clone(), None), )); } detail = detail.push(action( "Leave this decision unselected; use the related decisions", RowEvent::Decide(fact.selector.clone(), None), )); } if (is_group || keyed::supported(&fact.selector) || artifacts::supported(&fact.selector) || matches!(fact.selector, FacetSelector::Order(_)) || constructors::selectors().iter().any(|group| { self.decisions.contains_key(group) && constructors::contains(group, &fact.selector) })) && let Some(review) = self.plan.as_ref().and_then(|plan| { let trace = project.traces.get(&self.trace?)?; let source = if self.direction == TransitionDirection::OaToSa { trace.oa } else { trace.sa }; plan.reviews.iter().find(|review| review.source == source) }) { detail = detail.push(text("Exact review scope from preview").size(13)); for (label, selectors) in [ ("Acknowledge", &review.acknowledged), ("Retire previous observation", &review.retired), ] { detail = detail.push( text(format!( "{label}: {}", if selectors.is_empty() { "None".into() } else { selectors .iter() .map(|s| self.review_title(project, s)) .collect::>() .join(", ") } )) .size(11), ); } } if matches!( fact.selector, FacetSelector::Child(_) | FacetSelector::Incident { .. } | FacetSelector::Artifact { .. } ) { detail = detail.push(text("Attached facts are separate from referenced definitions. Removing membership never implies deleting a semantic object.").size(11).color(style::MUTED)); } if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) && let Some(draft) = self.candidate_draft(&fact.selector) { crate::forms::reference_chooser::trigger::scope(detail, draft.reference_session()) } else { detail.into() } } fn members_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { let families = self.member_families(project); let available = families.contains(&self.member_family); let mut content = column![ 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(RowEvent::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…", RowEvent::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(RowEvent::Literal(v))).size(12).padding(7)) .push(row![checkbox(self.literal_oa).label("Operational identity").text_size(12).on_toggle(|v| event(RowEvent::LiteralSide(Layer::Oa,v))), checkbox(self.literal_sa).label("System identity").text_size(12).on_toggle(|v| event(RowEvent::LiteralSide(Layer::Sa,v)))].spacing(12)); } else { content = content.push(text("Identities may exist outside the collection. Choosing both explicitly sets their OA → SA correspondence. No identity means an unpaired side, distinct from an absent member.").size(11)) .push(row![pick_list(self.member_choices(project, Layer::Oa), self.member_oa.clone(), |v| event(RowEvent::MemberOa(Some(v)))).placeholder("Operational identity").text_size(12).padding(7).width(Fill), action("No OA identity", RowEvent::MemberOa(None))].spacing(7)) .push(row![pick_list(self.member_choices(project, Layer::Sa), self.member_sa.clone(), |v| event(RowEvent::MemberSa(Some(v)))).placeholder("System identity").text_size(12).padding(7).width(Fill), action("No SA identity", RowEvent::MemberSa(None))].spacing(7)); } 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(RowEvent::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)); for facet in &self.staged { content = content.push( row![ text(self.review_title(project, facet)).size(11).width(Fill), action( "Remove address / decision", RowEvent::DropStaged(facet.clone()) ) ] .spacing(7), ); } } scrollable(content.padding(iced::Padding::default().right(18))) .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", RowEvent::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(RowEvent::OrderMove(which, index, -1))), ); let down = button(text("Down").size(11)).padding(5).on_press_maybe( (index + 1 < ids.len()).then_some(event(RowEvent::OrderMove(which, index, 1))), ); content = content.push( row![ text(format!( "{}. {}", index + 1, self.review_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| self.review_choice(project, *id).label) .collect(); let excluded: Vec<_> = ids .iter() .filter(|id| !members.contains(id)) .map(|id| self.review_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", RowEvent::AnnotationBrowse(layer, None, 0)), action("No identity", RowEvent::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", RowEvent::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::>(); let selected = visible .iter() .find(|choice| Some(&choice.key) == address.selected.as_ref()) .cloned(); content = content.push( pick_list(visible, selected, move |choice| { event(RowEvent::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", RowEvent::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", RowEvent::AnnotationBrowse(layer, address.scope.clone(), page - 1), )); } if page + 1 < pages { navigation = navigation.push(action( "Next", RowEvent::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 .trace .and_then(|id| project.traces.get(&id)) .and_then(|t| t.baseline.as_ref()) .and_then(|b| b.semantic.as_ref()); if baseline.is_none_or(|b| b.facets.is_empty()) { content = content.push(text("No previous semantic observations on this trace.").size(12)); } content = content.push(action("Restore reviewed reference pairs", RowEvent::RestoreReviewedReferences)).push(text("Restores exact historical key pairs only. Ordered lists do not identify counterpart pairs. Pending resets are excluded.").size(11)); if let Some(baseline) = baseline { for fact in &baseline.facets { let facet = fact.selector(); let mut item = column![ text(self.review_title(project, &facet)).size(13), text(format!( "Operational: {}\nSystem: {}", observed(project, Some(fact), Layer::Oa), observed(project, Some(fact), Layer::Sa) )) .size(11) ] .spacing(4); if history::resettable(&facet) { item = item.push( checkbox(self.resets.contains(&facet)) .label("Reset this exact observation") .text_size(11) .on_toggle(move |v| event(RowEvent::Reset(facet.clone(), v))), ); } else { item = item.push( text("Artifact history reset remains pending with artifact review.") .size(11), ); } content = content.push(container(item).padding(7)); } } for pending in &self.resets { if baseline.is_none_or(|b| !b.facets.iter().any(|f| f.selector() == *pending)) { let facet = pending.clone(); content = content.push( checkbox(true) .label(format!( "Pending reset no longer in current history: {}", self.review_title(project, &facet) )) .text_size(11) .on_toggle(move |v| event(RowEvent::Reset(facet.clone(), v))), ); } } scrollable(content.padding(iced::Padding::default().right(18))) .height(305) .into() } fn references_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { let mut content = column![ text("Reference correspondence · always Operational → System").size(15), text("Choose explicit counterpart identities before taking reference values. Names do not establish correspondence. Changing an already reviewed keyed pair requires an exact reset in History. Existing decisions are retained; resolve their overlapping row identities explicitly.").size(12), text_input("Filter reference choices by name or alias", &self.reference_search).on_input(|v| event(RowEvent::ReferenceSearch(v))).padding(7).size(12), row![ pick_list(self.all_references(project, Layer::Oa), self.reference_oa.as_ref().map(|c| self.review_choice(project, c.id)), |c| event(RowEvent::ReferenceOa(c))).placeholder("Operational reference").padding(7).text_size(12).width(Fill), pick_list(self.all_references(project, Layer::Sa), self.reference_sa.as_ref().map(|c| self.review_choice(project, c.id)), |c| event(RowEvent::ReferenceSa(c))).placeholder("System reference").padding(7).text_size(12).width(Fill), action("Use pair", RowEvent::AddReference), ].spacing(7).width(Fill), ].spacing(9).width(Fill); for (oa, sa) in &self.references { content = content.push( row![ text(format!( "{}\n↔ {}", self.review_choice(project, *oa).label, self.review_choice(project, *sa).label )) .size(12) .width(Fill), action("Remove pair", RowEvent::RemoveReference(*oa)) ] .spacing(6) .width(Fill), ); } if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { content = content .push(text("Referenced definitions · reviewed on their own traces").size(14)); match linked_semantic_impacts(project, trace, &self.references) { Ok(impacts) => { for impact in impacts { content = content.push( text(format!( "{} ↔ {} · {}{} · {} trace(s)", impact .objects .oa .map(|id| self.review_choice(project, id).label) .unwrap_or_else(|| "Absent".into()), impact .objects .sa .map(|id| self.review_choice(project, id).label) .unwrap_or_else(|| "Absent".into()), drift(impact.drift), if impact.unreviewed { " · incomplete review" } else { "" }, impact.traces.len() )) .size(12), ); } } Err(error) => content = content.push(text(error).size(12)), } } // The vertical scrollbar overlays the content's right edge. scrollable(content.padding(iced::Padding::default().right(18))) .width(Fill) .height(305) .into() } fn impacts_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> { let mut content = column![text("Additional prepared changes").size(16), text("Accept each exact change required by model preparation. Acceptance never overrides a selected Keep or Take. Editing a decision clears these approvals.").size(12)].spacing(10); if let Some(plan) = &self.plan { if plan.impacts.is_empty() { content = content.push(text("No additional prepared changes in this preview.").size(12)); } for (index, impact) in plan.impacts.iter().enumerate() { let before = impact .before .as_ref() .map(|r| serde_json::to_value(r).expect("Typed impact")); let after = impact .after .as_ref() .map(|r| serde_json::to_value(r).expect("Typed impact")); let ids = project.identities().into_iter().map(|(id, _)| id).collect(); content = content.push( container( column![ checkbox(self.accepted_impacts.contains(impact)) .label(format!("Accept · {}", project.named(impact.object))) .text_size(12) .on_toggle(move |accepted| event(RowEvent::AcceptImpact( index, accepted ))), text("Before").size(11).color(style::MUTED), text( before .as_ref() .map(|v| formatted(project, v, &ids)) .unwrap_or_else(|| "Absent · record creation".into()) ) .size(12), text("After").size(11).color(style::MUTED), text( after .as_ref() .map(|v| formatted(project, v, &ids)) .unwrap_or_else(|| "Absent · record deletion".into()) ) .size(12), ] .spacing(6), ) .padding(10) .style(style::subtle), ); } } scrollable(content.padding(iced::Padding::default().right(18))) .height(305) .into() } }