Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user