Review closed semantic constructor groups with retained typed drafts

This commit is contained in:
2026-09-06 07:49:01 +03:00
parent 0e47384532
commit 1a68ae0795
10 changed files with 2298 additions and 193 deletions
+119 -68
View File
@@ -266,7 +266,7 @@ impl State {
.iter()
.filter(|f| f.drift == Drift::Unreviewed)
.count();
content = content.push(text(format!("{} selected decisions · {unreviewed} currently unreviewed facts · Other constructor groups and structured collections require further review.", self.decisions.len())).size(10).color(style::MUTED));
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));
if let Some(trace) = trace {
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED));
}
@@ -364,31 +364,42 @@ impl State {
.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_message_kind = message_kind::handles(&fact.selector);
let timer_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() == message_kind::timer_selector())
});
let timer_current = self
.facts
.iter()
.find(|f| f.selector == message_kind::timer_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_message_kind {
before.push_str(&format!(
"\nTimer expression: {}",
observed(project, timer_previous, layer)
));
current.push_str(&format!(
"\nTimer expression: {}",
observed(project, timer_current.map(|f| &f.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!(
@@ -426,11 +437,17 @@ impl State {
.color(style::TEAL),
)
.push(
text(if is_message_kind {
self.target(project)
.and_then(|id| message_kind::source_value(proposed, id).ok())
.map(|value| formatted(proposed, &value, &BTreeSet::new()))
.unwrap_or_else(|| "Message kind is not applicable.".into())
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())
})
@@ -444,10 +461,11 @@ impl State {
);
}
if self.editable(project, &fact.selector) {
if is_message_kind {
detail = detail.push(text("A replacing message-kind decision reviews its Timer Expression as a group. The exact acknowledgment and retirement appear below.").size(12));
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 final kind also needs a separate Timer Expression decision for expression edits.").size(11).color(style::MUTED));
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 {
@@ -472,8 +490,8 @@ impl State {
]
.spacing(5);
buttons = buttons.push(action(
if is_message_kind {
"Set final message kind…"
if is_group {
"Set final kind and payload…"
} else {
"Set final value…"
},
@@ -485,52 +503,85 @@ impl State {
));
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& let Some(draft) = self.candidate_draft(&fact.selector)
&& let Some(path) = self
&& let Some(paths) = self
.target(project)
.and_then(|id| scalar::binding(project, id, &fact.selector))
.and_then(|id| constructors::roots(project, id, &fact.selector))
{
detail = detail.push(text("Final destination value").size(12)).push(
crate::forms::scoped(draft, project, &path, &title(project, &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) { "Constructor groups are shown for comparison. Group replacement and its complete descendant review are a later milestone." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED));
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 is_message_kind || fact.selector == message_kind::timer_selector() {
if self.decisions.contains_key(&message_kind::selector())
&& self.decisions.contains_key(&message_kind::timer_selector())
{
detail = detail.push(text("Both kind and timer decisions are selected. An initial equal-kind acknowledgment can coexist with a timer decision; a replacing group requires one combined decision. Resolve any overlap explicitly; both drafts are retained.").size(11))
.push(action("Replace timer decision with message-kind group", Event::Decide(message_kind::timer_selector(), None)))
.push(action("Leave message kind unselected; use timer decision", Event::Decide(message_kind::selector(), None)));
let overlaps: Vec<_> = self
.decisions
.keys()
.filter(|other| {
**other != fact.selector
&& (constructors::contains(&fact.selector, other)
|| constructors::contains(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));
for other in overlaps {
detail = detail.push(action(
format!("Leave {} unselected", title(project, other)),
Event::Decide(other.clone(), None),
));
}
if 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),
);
}
detail = detail.push(action(
"Leave this decision unselected; use the related decisions",
Event::Decide(fact.selector.clone(), None),
));
}
if (is_group
|| 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!(