Add exact semantic history resets and unordered member review
This commit is contained in:
@@ -71,27 +71,37 @@ pub(super) fn group(facet: &FacetSelector) -> &'static str {
|
||||
}
|
||||
}
|
||||
pub(super) fn title(project: &Project, facet: &FacetSelector) -> String {
|
||||
let keys = |keys: &KeyPair<Id>| {
|
||||
let keys = |keys: &KeyPair<Id>, missing: &str| {
|
||||
format!(
|
||||
"{} ↔ {}",
|
||||
keys.oa
|
||||
.map(|id| choice(project, id).label)
|
||||
.unwrap_or_else(|| "Absent".into()),
|
||||
.unwrap_or_else(|| missing.into()),
|
||||
keys.sa
|
||||
.map(|id| choice(project, id).label)
|
||||
.unwrap_or_else(|| "Absent".into())
|
||||
.unwrap_or_else(|| missing.into())
|
||||
)
|
||||
};
|
||||
match facet {
|
||||
FacetSelector::Member {
|
||||
collection,
|
||||
keys: pair,
|
||||
} => format!("{} · {}", words(&format!("{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_deref().unwrap_or("Absent"),
|
||||
pair.sa.as_deref().unwrap_or("Absent")
|
||||
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 · {} ↔ {}",
|
||||
@@ -104,11 +114,17 @@ pub(super) fn title(project: &Project, facet: &FacetSelector) -> String {
|
||||
.map(|key| annotation_title(project, key))
|
||||
.unwrap_or_else(|| "Absent".into())
|
||||
),
|
||||
FacetSelector::Binding(pair) => format!("Lifeline binding · {}", keys(pair)),
|
||||
FacetSelector::Child(pair) => format!("Owned child · {}", keys(pair)),
|
||||
FacetSelector::Incident { keys: pair, end } => format!("Incident {end:?} · {}", keys(pair)),
|
||||
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))
|
||||
format!(
|
||||
"Artifact {} · {}",
|
||||
words(&format!("{facet:?}")),
|
||||
keys(pair, "Absent")
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
let json = serde_json::to_value(facet).expect("Typed selector");
|
||||
@@ -199,6 +215,12 @@ fn observed(project: &Project, fact: Option<&FacetReview>, layer: Layer) -> Stri
|
||||
let value = scalar::observation(fact, layer);
|
||||
if value.as_str() == Some("NotApplicable") {
|
||||
"Not applicable".into()
|
||||
} else if keyed::supported(&fact.selector()) {
|
||||
if value["Value"].as_bool() == Some(true) {
|
||||
"Present".into()
|
||||
} else {
|
||||
"Absent".into()
|
||||
}
|
||||
} else {
|
||||
formatted(project, &value["Value"], &scalar::references(fact))
|
||||
}
|
||||
@@ -237,11 +259,13 @@ 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(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),
|
||||
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(),
|
||||
].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);
|
||||
@@ -266,7 +290,7 @@ impl State {
|
||||
.iter()
|
||||
.filter(|f| f.drift == Drift::Unreviewed)
|
||||
.count();
|
||||
content = content.push(text(format!("{} selected decisions · {unreviewed} currently unreviewed facts · Independent keyed/ordered decisions and historical reset remain pending.", self.decisions.len())).size(10).color(style::MUTED));
|
||||
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));
|
||||
if let Some(trace) = trace {
|
||||
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED));
|
||||
}
|
||||
@@ -426,6 +450,38 @@ impl State {
|
||||
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("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))
|
||||
.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 Some(proposed) = &self.proposed
|
||||
&& let Some(trace) = trace
|
||||
&& let Ok(after) = capture_facet(proposed, trace.oa, trace.sa, &fact.selector)
|
||||
@@ -501,6 +557,19 @@ impl State {
|
||||
"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 self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
|
||||
&& let Some(draft) = self.candidate_draft(&fact.selector)
|
||||
&& let Some(paths) = self
|
||||
@@ -534,17 +603,24 @@ impl State {
|
||||
} 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))
|
||||
|| 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. A first equal-kind acknowledgment can coexist with descendants; a replacing group requires one combined decision. Resolve overlaps explicitly; all drafts are retained.").size(11));
|
||||
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)),
|
||||
@@ -557,6 +633,7 @@ impl State {
|
||||
));
|
||||
}
|
||||
if (is_group
|
||||
|| keyed::supported(&fact.selector)
|
||||
|| constructors::selectors().iter().any(|group| {
|
||||
self.decisions.contains_key(group) && constructors::contains(group, &fact.selector)
|
||||
}))
|
||||
@@ -595,10 +672,106 @@ impl State {
|
||||
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 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),
|
||||
pick_list(families, Some(self.member_family), |v| event(Event::MemberFamily(v))).text_size(12).padding(7),
|
||||
].spacing(9);
|
||||
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::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 identities and review presence").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 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 reset in the structured workflow.").size(12),
|
||||
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),
|
||||
|
||||
Reference in New Issue
Block a user