Integrate release recovery, semantic review, coverage and Windows packaging

This commit is contained in:
Codex
2026-09-06 16:31:55 -07:00
parent c091f80e6e
commit 2b2d2bc18b
174 changed files with 27635 additions and 127 deletions
+242 -12
View File
@@ -208,14 +208,24 @@ fn formatted(project: &Project, value: &Value, references: &BTreeSet<Id>) -> Str
.join("\n"),
}
}
fn observed(project: &Project, fact: Option<&FacetReview>, layer: Layer) -> String {
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 keyed::supported(&fact.selector()) {
} 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 {
@@ -259,7 +269,7 @@ 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("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(),
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),
@@ -290,7 +300,7 @@ impl State {
.iter()
.filter(|f| f.drift == Drift::Unreviewed)
.count();
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));
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));
}
@@ -460,7 +470,7 @@ impl State {
);
}
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))
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())
@@ -482,6 +492,12 @@ impl State {
}
}
}
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)
@@ -569,6 +585,44 @@ impl State {
.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)
@@ -634,6 +688,7 @@ impl State {
}
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)
}))
@@ -676,12 +731,22 @@ impl State {
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),
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))
@@ -691,7 +756,7 @@ impl State {
.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))))
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));
@@ -711,6 +776,170 @@ impl State {
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
@@ -777,8 +1006,8 @@ impl State {
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),
].spacing(9);
].spacing(7).width(Fill),
].spacing(9).width(Fill);
for (oa, sa) in &self.references {
content = content.push(
row![
@@ -791,7 +1020,8 @@ impl State {
.width(Fill),
action("Remove pair", Event::RemoveReference(*oa))
]
.spacing(6),
.spacing(6)
.width(Fill),
);
}
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
@@ -828,7 +1058,7 @@ impl State {
Err(error) => content = content.push(text(error).size(12)),
}
}
scrollable(content).height(305).into()
scrollable(content).width(Fill).height(305).into()
}
fn impacts_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {