Review closed semantic constructor groups with retained typed drafts
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,327 @@
|
||||
//! Closed constructor payload bindings. The core owns decisions and review scope;
|
||||
//! this adapter supplies complete target records for its Candidate agreement check.
|
||||
use super::{scalar, *};
|
||||
|
||||
pub(super) fn selectors() -> [FacetSelector; 5] {
|
||||
[
|
||||
FacetSelector::Data(DataFacet::Constructor),
|
||||
FacetSelector::Port(PortFacet::Constructor),
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
||||
FacetSelector::Occurrence(OccurrenceFacet::MessageKind),
|
||||
FacetSelector::Event(EventFacet::Constructor),
|
||||
]
|
||||
}
|
||||
|
||||
pub(super) fn roots(project: &Project, target: Id, facet: &FacetSelector) -> Option<Vec<String>> {
|
||||
let root = scalar::binding(project, target, facet)?;
|
||||
let mut roots = vec![root.clone()];
|
||||
match facet {
|
||||
FacetSelector::Data(DataFacet::Constructor) => {
|
||||
roots.push(format!("{}/default", root.strip_suffix("/definition")?));
|
||||
}
|
||||
FacetSelector::Port(PortFacet::Constructor) => {
|
||||
roots.push(format!("{}/legacy", root.strip_suffix("/contract")?));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Some(roots)
|
||||
}
|
||||
|
||||
pub(super) fn available(project: &Project, trace: &Trace, facet: &FacetSelector) -> bool {
|
||||
roots(project, trace.oa, facet).is_some() && roots(project, trace.sa, facet).is_some()
|
||||
}
|
||||
|
||||
/// Used for explanation and explicit overlap controls, never for acknowledgment.
|
||||
pub(super) fn contains(group: &FacetSelector, facet: &FacetSelector) -> bool {
|
||||
use FacetSelector as F;
|
||||
match group {
|
||||
F::Data(DataFacet::Constructor) => {
|
||||
matches!(facet, F::Data(f) if *f != DataFacet::Unit) || matches!(facet, F::Literal(_))
|
||||
}
|
||||
F::Port(PortFacet::Constructor) => matches!(
|
||||
facet,
|
||||
F::Port(_)
|
||||
| F::Member {
|
||||
collection: ReferenceCollection::FunctionInputItems
|
||||
| ReferenceCollection::FunctionOutputItems,
|
||||
..
|
||||
}
|
||||
),
|
||||
F::Occurrence(OccurrenceFacet::Constructor) => {
|
||||
matches!(facet, F::Occurrence(f) if !matches!(f, OccurrenceFacet::Precondition | OccurrenceFacet::Postcondition))
|
||||
|| matches!(
|
||||
facet,
|
||||
F::Annotation(_)
|
||||
| F::Binding(_)
|
||||
| F::Member {
|
||||
collection: ReferenceCollection::Steps
|
||||
| ReferenceCollection::FragmentBranches,
|
||||
..
|
||||
}
|
||||
| F::Order(_)
|
||||
)
|
||||
}
|
||||
F::Occurrence(OccurrenceFacet::MessageKind) => matches!(
|
||||
facet,
|
||||
F::Occurrence(OccurrenceFacet::MessageKind | OccurrenceFacet::TimerExpression)
|
||||
),
|
||||
F::Event(EventFacet::Constructor) => matches!(
|
||||
facet,
|
||||
F::Event(_)
|
||||
| F::Member {
|
||||
collection: ReferenceCollection::SignalItems,
|
||||
..
|
||||
}
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn description(facet: &FacetSelector) -> &'static str {
|
||||
match facet {
|
||||
FacetSelector::Data(_) => {
|
||||
"Definition, bounds or literal members, record supertype and default. Unit stays independent."
|
||||
}
|
||||
FacetSelector::Port(_) => {
|
||||
"Contract, item members and retained generic association. Owner stays independent."
|
||||
}
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor) => {
|
||||
"Occurrence body, including references, message annotations, bindings and branch or step order. Pre/postconditions stay independent."
|
||||
}
|
||||
FacetSelector::Occurrence(_) => {
|
||||
"Message kind and Timer Expression. Exchange references and annotations stay independent."
|
||||
}
|
||||
FacetSelector::Event(_) => {
|
||||
"Event body, including signal items or its complete time/change expression."
|
||||
}
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
fn chosen<T>(keys: &KeyPair<T>, layer: Layer) -> Option<&T> {
|
||||
match layer {
|
||||
Layer::Oa => keys.oa.as_ref(),
|
||||
Layer::Sa => keys.sa.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
// The source payload is mapped by the core's typed visitor, never by replacing
|
||||
// UUID-looking strings. For a historical same-kind Take, retain only current
|
||||
// target-present/source-absent rows with no prior source-present observation. The final core planner
|
||||
// checks this assembled record against its own independent expected result.
|
||||
pub(super) fn take_value(
|
||||
project: &Project,
|
||||
trace: &Trace,
|
||||
direction: TransitionDirection,
|
||||
facet: &FacetSelector,
|
||||
map: &BTreeMap<Id, Id>,
|
||||
) -> Result<Value, String> {
|
||||
let (source, target) = if direction == TransitionDirection::OaToSa {
|
||||
(trace.oa, trace.sa)
|
||||
} else {
|
||||
(trace.sa, trace.oa)
|
||||
};
|
||||
let mut element = project
|
||||
.elements
|
||||
.get(&source)
|
||||
.cloned()
|
||||
.ok_or("Missing constructor source.")?;
|
||||
if !message_kind::handles(facet) {
|
||||
element.parent = None;
|
||||
let mut missing = BTreeSet::new();
|
||||
oa_core::references::visit_element_refs(&mut element, |id| {
|
||||
if let Some(mapped) = map.get(id) {
|
||||
*id = *mapped;
|
||||
} else {
|
||||
missing.insert(*id);
|
||||
}
|
||||
});
|
||||
if !missing.is_empty() {
|
||||
return Err(format!(
|
||||
"Missing explicit reference correspondence: {missing:?}"
|
||||
));
|
||||
}
|
||||
}
|
||||
let supplied = serde_json::to_value(element).map_err(|e| e.to_string())?;
|
||||
let mut value = Draft::from_project(project, target)
|
||||
.ok_or("Missing target draft.")?
|
||||
.value;
|
||||
for (from, to) in roots(project, source, facet)
|
||||
.ok_or("Source group is unavailable.")?
|
||||
.iter()
|
||||
.zip(roots(project, target, facet).ok_or("Target group is unavailable.")?)
|
||||
{
|
||||
*value
|
||||
.pointer_mut(&to)
|
||||
.ok_or("Target group payload is unavailable.")? = supplied
|
||||
.pointer(from)
|
||||
.cloned()
|
||||
.ok_or("Source group payload is unavailable.")?;
|
||||
}
|
||||
let current = capture_facet(project, trace.oa, trace.sa, facet)?;
|
||||
if scalar::observation(¤t, Layer::Oa) != scalar::observation(¤t, Layer::Sa) {
|
||||
return Ok(value);
|
||||
}
|
||||
let references = if direction == TransitionDirection::OaToSa {
|
||||
map.clone()
|
||||
} else {
|
||||
map.iter().map(|(sa, oa)| (*oa, *sa)).collect()
|
||||
};
|
||||
let old = trace.baseline.as_ref().and_then(|b| b.semantic.as_ref());
|
||||
for fact in semantic_facets(project, trace, &references)? {
|
||||
if !contains(facet, &fact.selector) {
|
||||
continue;
|
||||
}
|
||||
let Some((path, key)) =
|
||||
target_row(project, target, &fact.selector, direction.target_layer())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if row_present(&fact.current, direction.source_layer())
|
||||
|| !row_present(&fact.current, direction.target_layer())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let was_paired = old.is_some_and(|b| {
|
||||
b.facets.iter().any(|previous| {
|
||||
row_present(previous, direction.source_layer())
|
||||
&& target_row(
|
||||
project,
|
||||
target,
|
||||
&previous.selector(),
|
||||
direction.target_layer(),
|
||||
)
|
||||
.as_ref()
|
||||
== Some(&(path.clone(), key.clone()))
|
||||
})
|
||||
});
|
||||
if was_paired {
|
||||
continue;
|
||||
}
|
||||
let row = scalar::value(&fact.current, direction.target_layer())
|
||||
.ok_or("Target row is inapplicable.")?;
|
||||
let items = value
|
||||
.pointer_mut(&path)
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or("Target collection is unavailable.")?;
|
||||
match &fact.selector {
|
||||
FacetSelector::Member { .. } | FacetSelector::Literal(_) => {
|
||||
if row == Value::Bool(true) && !items.contains(&key) {
|
||||
items.push(key);
|
||||
} else if row == Value::Bool(false) {
|
||||
items.retain(|v| v != &key);
|
||||
}
|
||||
}
|
||||
FacetSelector::Annotation(_) => {
|
||||
items.retain(|v| {
|
||||
serde_json::from_value::<MessageValue>(v.clone())
|
||||
.ok()
|
||||
.map(|v| serde_json::to_value(AnnotationKey::from(&v)).unwrap())
|
||||
.as_ref()
|
||||
!= Some(&key)
|
||||
});
|
||||
if !row.is_null() {
|
||||
items.push(row);
|
||||
}
|
||||
}
|
||||
FacetSelector::Binding(_) => {
|
||||
items.retain(|v| v.get("referenced") != Some(&key));
|
||||
if !row.is_null() {
|
||||
items.push(row);
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn row_present(fact: &FacetReview, layer: Layer) -> bool {
|
||||
match fact {
|
||||
FacetReview::Member { .. } | FacetReview::Literal { .. } => {
|
||||
scalar::value(fact, layer) == Some(Value::Bool(true))
|
||||
}
|
||||
FacetReview::Annotation { .. } | FacetReview::Binding { .. } => {
|
||||
scalar::value(fact, layer).is_some_and(|value| !value.is_null())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn row_key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
|
||||
match facet {
|
||||
FacetSelector::Member { keys, .. } | FacetSelector::Binding(keys) => {
|
||||
chosen(keys, layer).map(|v| serde_json::json!(v))
|
||||
}
|
||||
FacetSelector::Literal(keys) => chosen(keys, layer).map(|v| serde_json::json!(v)),
|
||||
FacetSelector::Annotation(keys) => chosen(keys, layer).map(|v| serde_json::json!(v)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
fn target_row(
|
||||
project: &Project,
|
||||
target: Id,
|
||||
facet: &FacetSelector,
|
||||
layer: Layer,
|
||||
) -> Option<(String, Value)> {
|
||||
let key = row_key(facet, layer)?;
|
||||
let (group, suffix) = match facet {
|
||||
FacetSelector::Literal(_) => (
|
||||
FacetSelector::Data(DataFacet::Constructor),
|
||||
"/Enumeration/literals",
|
||||
),
|
||||
FacetSelector::Binding(_) => (
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
||||
"/ScenarioUse/bindings",
|
||||
),
|
||||
FacetSelector::Annotation(_) => (
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
||||
"/Exchange/values",
|
||||
),
|
||||
FacetSelector::Member { collection, .. } => match collection {
|
||||
ReferenceCollection::FunctionInputItems => (
|
||||
FacetSelector::Port(PortFacet::Constructor),
|
||||
"/FunctionInput/incoming_items",
|
||||
),
|
||||
ReferenceCollection::FunctionOutputItems => (
|
||||
FacetSelector::Port(PortFacet::Constructor),
|
||||
"/FunctionOutput/outgoing_items",
|
||||
),
|
||||
ReferenceCollection::SignalItems => (
|
||||
FacetSelector::Event(EventFacet::Constructor),
|
||||
"/Signal/items",
|
||||
),
|
||||
ReferenceCollection::Steps => (
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
||||
"/Branch/occurrences",
|
||||
),
|
||||
ReferenceCollection::FragmentBranches => (
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
|
||||
"/Fragment/branches",
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
};
|
||||
Some((
|
||||
format!("{}{suffix}", scalar::binding(project, target, &group)?),
|
||||
key,
|
||||
))
|
||||
}
|
||||
|
||||
/// A selected descendant may fill untouched group fields during first-equal
|
||||
/// acknowledgment, but cannot silently replace an authored group field.
|
||||
pub(super) fn authored_agrees(original: &Value, authored: &Value, final_value: &Value) -> bool {
|
||||
if original == authored || authored == final_value {
|
||||
return true;
|
||||
}
|
||||
match (original, authored, final_value) {
|
||||
(Value::Object(a), Value::Object(b), Value::Object(c)) if a.keys().eq(b.keys()) => {
|
||||
b.iter().all(|(key, v)| {
|
||||
c.get(key)
|
||||
.is_some_and(|final_value| authored_agrees(&a[key], v, final_value))
|
||||
})
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,14 @@ use super::*;
|
||||
pub(super) fn selector() -> FacetSelector {
|
||||
FacetSelector::Occurrence(OccurrenceFacet::MessageKind)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn timer_selector() -> FacetSelector {
|
||||
FacetSelector::Occurrence(OccurrenceFacet::TimerExpression)
|
||||
}
|
||||
pub(super) fn handles(facet: &FacetSelector) -> bool {
|
||||
*facet == selector()
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> {
|
||||
match project.elements.get(&id)?.shared()? {
|
||||
SharedBody::Occurrence {
|
||||
@@ -20,13 +22,3 @@ pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub(super) fn available(project: &Project, trace: &Trace) -> bool {
|
||||
value(project, trace.oa).is_some() && value(project, trace.sa).is_some()
|
||||
}
|
||||
/// MessageConstructor observations contain only the discriminator. Taking a
|
||||
/// replacing group needs the complete typed payload, including Timer.expression.
|
||||
pub(super) fn source_value(project: &Project, id: Id) -> Result<Value, String> {
|
||||
let value =
|
||||
value(project, id).ok_or("Message kind requires an existing Exchange occurrence.")?;
|
||||
serde_json::to_value(value).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -206,10 +206,18 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex
|
||||
&mut app,
|
||||
Event::Text(expression.clone(), text_editor::Action::SelectAll),
|
||||
);
|
||||
let cursor = state(&app).message_draft.as_ref().unwrap().inputs[&expression]
|
||||
let cursor = state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.cursor();
|
||||
let selection = state(&app).message_draft.as_ref().unwrap().inputs[&expression]
|
||||
let selection = state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.selection();
|
||||
send(
|
||||
@@ -221,17 +229,30 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex
|
||||
Event::Variant(kind, json!({"Timer":{"expression":""}})),
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app).message_draft.as_ref().unwrap().inputs[&expression].raw,
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.raw,
|
||||
" group β timer\n\t "
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app).message_draft.as_ref().unwrap().inputs[&expression]
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.cursor(),
|
||||
cursor
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app).message_draft.as_ref().unwrap().inputs[&expression]
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.selection(),
|
||||
selection
|
||||
@@ -621,7 +642,7 @@ fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_t
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("different timer values")
|
||||
.contains("different final values")
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
@@ -682,7 +703,12 @@ fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_l
|
||||
assert!(state(&app).error.is_some());
|
||||
assert_eq!(state(&app).decisions.len(), 1);
|
||||
assert_eq!(
|
||||
state(&app).message_draft.as_ref().unwrap().inputs[&expression].raw,
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.raw,
|
||||
raw
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
|
||||
@@ -568,7 +568,10 @@ fn read_only_structured_facts_and_partial_rollup_are_honest() {
|
||||
send(
|
||||
&mut app,
|
||||
Event::Decide(
|
||||
facet(DataFacet::Constructor),
|
||||
FacetSelector::Literal(KeyPair {
|
||||
oa: Some("Unpaired".into()),
|
||||
sa: None,
|
||||
}),
|
||||
Some(SemanticDecision::TakeSource),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user