Add exact semantic history resets and unordered member review

This commit is contained in:
2026-09-06 08:09:44 +03:00
parent 2c0f93839f
commit 843635ed26
7 changed files with 1505 additions and 29 deletions
+188 -15
View File
@@ -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),