Files
ARCADIA-Studio/crates/oa-desktop/src/semantic_review/view.rs
T

1117 lines
50 KiB
Rust

use super::*;
use crate::{style, views::short};
use iced::{
Element, Fill,
widget::{
Space, button, checkbox, column, container, pick_list, row, scrollable, text, text_input,
},
};
fn event(event: Event) -> Message {
Message::Workflow(crate::workflow::Event::Semantic(event))
}
fn action<'a>(label: impl Into<String>, event_value: Event) -> 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()));
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::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<Id>, 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::<Vec<_>>()
.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<Id>) -> 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::<Id>()
&& 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::<Vec<_>>()
.join("\n"),
Value::Object(values) => values
.iter()
.map(|(key, value)| {
format!("{}: {}", words(key), formatted(project, value, references))
})
.collect::<Vec<_>>()
.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::<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 {
"Absent".into()
}
} else {
formatted(project, &value["Value"], &scalar::references(fact))
}
}
impl State {
pub fn view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
let trace = self.trace.and_then(|id| project.traces.get(&id));
let mut traces: Vec<_> = project
.traces
.values()
.filter(|t| t.oa == self.anchor || t.sa == self.anchor)
.map(|t| Choice {
id: t.id,
label: format!(
"{} · {:?} · {} ↔ {} · @{}",
t.name,
t.kind,
project.named(t.oa),
project.named(t.sa),
t.alias
),
})
.collect();
traces.sort_by(|a, b| a.label.cmp(&b.label));
let selected_trace = traces.iter().find(|c| Some(c.id) == self.trace).cloned();
let direction = if self.direction == TransitionDirection::OaToSa {
"Use Operational changes in System"
} else {
"Use System changes in Operational"
};
let mut content = column![
text("Review semantics").size(22),
row![
pick_list(traces, selected_trace, |c| event(Event::Trace(c))).placeholder("Choose a trace / review history").text_size(12).padding(7).width(Fill),
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 / 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),
Tab::References => self.references_view(project),
Tab::Members => self.members_view(project),
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 · 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));
}
content
.push(
row![
text(
"Apply updates the selected history and model together; Undo restores both."
)
.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(
self.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(Event::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(&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(Event::Select(fact.selector.clone()))),
);
}
}
row![
scrollable(list).height(305).width(260),
scrollable(self.detail(project)).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!(
"{}: {}",
title(project, &f.selector()),
observed(project, Some(f), layer)
)
})
.collect::<Vec<_>>()
.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(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(Event::Reset(facet.clone(), v))),
);
}
if keyed::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…", Event::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: {}",
title(project, &prior)
))
.text_size(11)
.on_toggle(move |v| event(Event::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}"),
Event::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget))
),
action(
format!("Take {source}"),
Event::Decide(fact.selector.clone(), Some(SemanticDecision::TakeSource))
)
]
.spacing(5);
buttons = buttons.push(action(
if is_group {
"Set final kind and payload…"
} else {
"Set final value…"
},
Event::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)),
));
detail = detail.push(buttons.wrap()).push(action(
"Leave unselected",
Event::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(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)
&& 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", Event::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget))))
.push(action("Leave unselected", Event::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",
Event::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", title(project, other)),
Event::Decide(other.clone(), None),
));
}
detail = detail.push(action(
"Leave this decision unselected; use the related decisions",
Event::Decide(fact.selector.clone(), None),
));
}
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)
}))
&& let Some(review) = self.plan.as_ref().and_then(|p| p.reviews.first())
{
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| title(project, s))
.collect::<Vec<_>>()
.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));
}
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(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))
.push(row![checkbox(self.literal_oa).label("Operational identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Oa,v))), checkbox(self.literal_sa).label("System identity").text_size(12).on_toggle(|v| event(Event::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(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 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));
for facet in &self.staged {
content = content.push(
row![
text(title(project, facet)).size(11).width(Fill),
action(
"Remove address / decision",
Event::DropStaged(facet.clone())
)
]
.spacing(7),
);
}
}
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
.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));
}
if let Some(baseline) = baseline {
for fact in &baseline.facets {
let facet = fact.selector();
let mut item = column![
text(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(Event::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: {}",
title(project, &facet)
))
.text_size(11)
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
);
}
}
scrollable(content).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(Event::ReferenceSearch(v))).padding(7).size(12),
row![
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).width(Fill),
].spacing(9).width(Fill);
for (oa, sa) in &self.references {
content = content.push(
row![
text(format!(
"{}\n↔ {}",
choice(project, *oa).label,
choice(project, *sa).label
))
.size(12)
.width(Fill),
action("Remove pair", Event::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| choice(project, id).label)
.unwrap_or_else(|| "Absent".into()),
impact
.objects
.sa
.map(|id| 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)),
}
}
scrollable(content).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(Event::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).height(305).into()
}
}