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
+155 -16
View File
@@ -46,7 +46,8 @@ pub struct Draft {
pub inputs: BTreeMap<String, Input>, pub inputs: BTreeMap<String, Input>,
pub exact_strings: bool, pub exact_strings: bool,
context: Option<Value>, context: Option<Value>,
message_timer_draft: Option<Input>, pub semantic_groups: bool,
constructor_drafts: BTreeMap<(String, String), (Value, BTreeMap<String, Input>)>,
port_contract_drafts: BTreeMap<String, Value>, port_contract_drafts: BTreeMap<String, Value>,
pub value_browsers: BTreeMap<String, ValueBrowser>, pub value_browsers: BTreeMap<String, ValueBrowser>,
} }
@@ -86,7 +87,8 @@ impl Draft {
inputs: BTreeMap::new(), inputs: BTreeMap::new(),
exact_strings: false, exact_strings: false,
context: None, context: None,
message_timer_draft: None, semantic_groups: false,
constructor_drafts: BTreeMap::new(),
port_contract_drafts: BTreeMap::new(), port_contract_drafts: BTreeMap::new(),
value_browsers: BTreeMap::new(), value_browsers: BTreeMap::new(),
}; };
@@ -250,27 +252,73 @@ impl Draft {
self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned())); self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned()));
Ok(()) Ok(())
} }
/// Message-kind selection preserves the timer's exact input/editor state /// Preserve each selected constructor branch, including incomplete raw input
/// while its payload is temporarily hidden by another kind. /// and editor state. No sibling fields are parsed or cleared by a switch.
pub fn message_kind_variant(&mut self, path: &str, value: Value) -> Result<(), String> { pub fn variant_scoped(&mut self, path: &str, proposed: Value) -> Result<(), String> {
serde_json::from_value::<MessageKind>(value.clone()).map_err(|e| e.to_string())?;
let current = self let current = self
.value .value
.pointer(path) .pointer(path)
.ok_or("Message kind no longer exists.")?; .ok_or("Property no longer exists.")?
if same_variant(current, &value) { .clone();
if same_variant(&current, &proposed) {
return Ok(()); return Ok(());
} }
let timer_path = format!("{path}/Timer/expression"); let constructor = path.ends_with("/DataType/definition")
if let Some(input) = self.inputs.remove(&timer_path) { || path.ends_with("/Port/contract")
self.message_timer_draft = Some(input); || path.ends_with("/Occurrence/body")
|| path.ends_with("/Event/body")
|| path.ends_with("/message_kind");
if !constructor {
return self.shape_scoped(path, proposed);
} }
self.shape_scoped(path, value)?; // Input/output direction changes carry the item declaration. Read those
if self.inputs.contains_key(&timer_path) // raw reference strings before stashing their editor buffers so an
&& let Some(input) = self.message_timer_draft.take() // incomplete reference cannot silently fall back to its stored UUID.
let port_current = if path.ends_with("/Port/contract") {
self.materialize_scopes(&[path.to_owned()])?
.pointer(path)
.cloned()
} else {
None
};
let tag = |value: &Value| {
value
.as_str()
.or_else(|| value.as_object()?.keys().next().map(String::as_str))
.unwrap_or("")
.to_owned()
};
let previous_key = (path.to_owned(), tag(&current));
let next_key = (path.to_owned(), tag(&proposed));
let mut inputs = BTreeMap::new();
for key in self
.inputs
.keys()
.filter(|key| within(key, path))
.cloned()
.collect::<Vec<_>>()
{ {
self.inputs.insert(timer_path, input); inputs.insert(key.clone(), self.inputs.remove(&key).unwrap());
} }
self.constructor_drafts
.insert(previous_key, (current.clone(), inputs));
let saved = self.constructor_drafts.remove(&next_key);
let (value, restored) = if let Some(saved) = saved {
saved
} else {
let value = if path.ends_with("/Port/contract") {
port_fields::switch_contract(
port_current.as_ref().unwrap_or(&current),
proposed,
&mut BTreeMap::new(),
)
} else {
proposed
};
(value, BTreeMap::new())
};
self.shape_scoped(path, value)?;
self.inputs.extend(restored);
Ok(()) Ok(())
} }
fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) { fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) {
@@ -1315,6 +1363,7 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
"layer" => &["Oa", "Sa"], "layer" => &["Oa", "Sa"],
"mechanism" => &["Unspecified", "Flow", "Event", "Operation", "SharedData"], "mechanism" => &["Unspecified", "Flow", "Event", "Operation", "SharedData"],
"storage" => &["Value", "Reference"], "storage" => &["Value", "Reference"],
"direction" if path.contains("/Port/legacy/") => &["In", "Out", "InOut"],
"direction" if path.contains("DataField") => { "direction" if path.contains("DataField") => {
&["Unspecified", "In", "Out", "InOut", "Return"] &["Unspecified", "In", "Out", "InOut", "Return"]
} }
@@ -1571,7 +1620,7 @@ fn property<'a>(
return view; return view;
} }
let label_view = || text(label.to_owned()).size(11).color(style::MUTED); let label_view = || text(label.to_owned()).size(11).color(style::MUTED);
if path.ends_with("/Port/legacy") { if path.ends_with("/Port/legacy") && !draft.semantic_groups {
let mut content = column![].spacing(4); let mut content = column![].spacing(4);
if !value.is_null() { if !value.is_null() {
content = content.push( content = content.push(
@@ -1585,6 +1634,29 @@ fn property<'a>(
} }
return content.into(); return content.into();
} }
if path.ends_with("/Port/legacy") && draft.semantic_groups && !skip_variant {
let mut content = column![
label_view(),
text(
"Retained generic association is part of this group. Change or clear it explicitly."
)
.size(11)
]
.spacing(5);
if value.is_null() {
content = content.push(button(text("Add generic association").size(11)).on_press(
Message::FieldShape(path.into(), json!({"direction":"InOut","interface":null})),
));
} else {
content = content
.push(property(draft, project, value, path, label, true))
.push(
button(text("Clear generic association").size(11))
.on_press(Message::FieldShape(path.into(), Value::Null)),
);
}
return content.into();
}
let enum_choices = if skip_variant { let enum_choices = if skip_variant {
vec![] vec![]
} else { } else {
@@ -1842,6 +1914,73 @@ fn property<'a>(
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn semantic_port_group_choices_use_combined_owner_and_retain_invalid_transferred_items() {
let mut p = Project::empty();
let function = p
.add_element(Kind::Function, Layer::Sa, "Function", None)
.unwrap();
let component = p
.add_element(Kind::System, Layer::Sa, "Component", None)
.unwrap();
let item = p
.add_element(Kind::ExchangeItem, Layer::Sa, "Item", None)
.unwrap();
let id = Id::new_v4();
p.elements.insert(
id,
ports::make_port(
&p,
id,
function,
"Port",
PortContract::FunctionInput {
incoming_items: vec![item],
},
)
.unwrap(),
);
let mut draft = Draft::from_project(&p, id).unwrap();
draft.semantic_groups = true;
let path = "/body/Sa/Shared/Port/contract";
let input = format!("{path}/FunctionInput/incoming_items/0");
draft.input(&input, "incomplete-id".into());
let cursor = draft.inputs[&input].editor.cursor();
draft
.variant_scoped(path, json!({"FunctionOutput":{"outgoing_items":[]}}))
.unwrap();
assert_eq!(
draft.inputs[&format!("{path}/FunctionOutput/outgoing_items/0")].raw,
"incomplete-id"
);
assert!(serde_json::from_value::<Element>(draft.materialize().unwrap()).is_err());
draft
.variant_scoped(path, json!({"FunctionInput":{"incoming_items":[]}}))
.unwrap();
assert_eq!(draft.inputs[&input].raw, "incomplete-id");
assert_eq!(draft.inputs[&input].editor.cursor(), cursor);
let choices =
port_fields::variants(&draft, &p, path, draft.value.pointer(path).unwrap()).unwrap();
assert!(
choices
.iter()
.any(|(_, v)| v.get("FunctionInput").is_some())
);
assert!(choices.iter().any(|(_, v)| *v == json!("Legacy")));
assert!(!choices.iter().any(|(_, v)| v.get("Component").is_some()));
let mut context = draft.value.clone();
context["parent"] = json!(component);
draft.set_context(Some(context));
let choices =
port_fields::variants(&draft, &p, path, draft.value.pointer(path).unwrap()).unwrap();
assert!(choices.iter().any(|(_, v)| v.get("Component").is_some()));
assert!(
!choices
.iter()
.any(|(_, v)| v.get("FunctionInput").is_some())
);
}
fn ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> { fn ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
reference_ids(draft, project, path) reference_ids(draft, project, path)
} }
+8 -1
View File
@@ -45,7 +45,11 @@ pub(super) fn variants(
let current = draft.context_value(); let current = draft.context_value();
let sa = current.pointer("/body/Sa").is_some(); let sa = current.pointer("/body/Sa").is_some();
if !sa { if !sa {
return Some(vec![]); return Some(if draft.semantic_groups {
vec![("Legacy generic port".into(), json!("Legacy"))]
} else {
vec![]
});
} }
let owner = current let owner = current
.get("parent") .get("parent")
@@ -69,6 +73,9 @@ pub(super) fn variants(
json!({"Component":{"kind":"Standard","orientation":"Unset"}}), json!({"Component":{"kind":"Standard","orientation":"Unset"}}),
)); ));
} }
if draft.semantic_groups {
choices.push(("Legacy generic port".into(), json!("Legacy")));
}
return Some(choices); return Some(choices);
} }
if path.ends_with("/Port/contract/Component/kind") { if path.ends_with("/Port/contract/Component/kind") {
+90 -75
View File
@@ -8,11 +8,14 @@ use oa_core::{transitions::*, *};
use serde_json::Value; use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
mod constructors;
mod message_kind; mod message_kind;
mod scalar; mod scalar;
mod view; mod view;
pub(crate) use view::summary; pub(crate) use view::summary;
#[cfg(test)] #[cfg(test)]
mod constructor_tests;
#[cfg(test)]
mod message_kind_tests; mod message_kind_tests;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@@ -57,9 +60,9 @@ pub struct State {
pub decisions: BTreeMap<FacetSelector, SemanticDecision>, pub decisions: BTreeMap<FacetSelector, SemanticDecision>,
pub references: BTreeMap<Id, Id>, pub references: BTreeMap<Id, Id>,
pub draft: Option<Draft>, pub draft: Option<Draft>,
// Group editing cannot reshape the independently authored TimerExpression // Each closed constructor has an independent draft, including nested kinds.
// scalar draft. Both drafts survive decision changes until this trace closes. // Scalar and sibling drafts survive decision and constructor changes.
pub message_draft: Option<Draft>, pub group_drafts: BTreeMap<FacetSelector, Draft>,
pub plan: Option<SemanticTransitionPlan>, pub plan: Option<SemanticTransitionPlan>,
pub proposed: Option<Project>, pub proposed: Option<Project>,
pub error: Option<String>, pub error: Option<String>,
@@ -102,7 +105,7 @@ impl State {
decisions: BTreeMap::new(), decisions: BTreeMap::new(),
references: BTreeMap::new(), references: BTreeMap::new(),
draft: None, draft: None,
message_draft: None, group_drafts: BTreeMap::new(),
plan: None, plan: None,
proposed: None, proposed: None,
error: None, error: None,
@@ -135,12 +138,19 @@ impl State {
self.draft = self self.draft = self
.target(project) .target(project)
.and_then(|id| Draft::from_project(project, id)); .and_then(|id| Draft::from_project(project, id));
self.message_draft = self self.group_drafts.clear();
.trace if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
.and_then(|id| project.traces.get(&id)) for facet in constructors::selectors() {
.filter(|trace| message_kind::available(project, trace)) if constructors::available(project, trace, &facet)
.and_then(|_| self.target(project)) && let Some(mut draft) = self
.and_then(|id| Draft::from_project(project, id)); .target(project)
.and_then(|id| Draft::from_project(project, id))
{
draft.semantic_groups = true;
self.group_drafts.insert(facet, draft);
}
}
}
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) { if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
self.references.insert(trace.oa, trace.sa); self.references.insert(trace.oa, trace.sa);
} }
@@ -200,19 +210,9 @@ impl State {
Event::Shape(path, value) => { Event::Shape(path, value) => {
self.edit(project, &path, |d| d.shape_scoped(&path, value)) self.edit(project, &path, |d| d.shape_scoped(&path, value))
} }
Event::Variant(path, value) => self.edit(project, &path, |d| { Event::Variant(path, value) => {
if path.ends_with("/message_kind") { self.edit(project, &path, |d| d.variant_scoped(&path, value))
d.message_kind_variant(&path, value) }
} else if d
.value
.pointer(&path)
.is_some_and(|v| crate::forms::same_variant(v, &value))
{
Ok(())
} else {
d.shape_scoped(&path, value)
}
}),
Event::Array(path, index, direction) => { Event::Array(path, index, direction) => {
self.edit(project, &path, |d| d.array_scoped(&path, index, direction)) self.edit(project, &path, |d| d.array_scoped(&path, index, direction))
} }
@@ -297,21 +297,23 @@ impl State {
if self.decisions.get(facet) != Some(&SemanticDecision::Candidate) { if self.decisions.get(facet) != Some(&SemanticDecision::Candidate) {
return Err("Choose Set final value before editing this property.".into()); return Err("Choose Set final value before editing this property.".into());
} }
let root = self let roots = self
.target(project) .target(project)
.and_then(|id| scalar::binding(project, id, facet)) .and_then(|id| constructors::roots(project, id, facet))
.ok_or("Selected scalar property is unavailable.")?; .ok_or("Selected property is unavailable.")?;
if path != root && !path.starts_with(&format!("{root}/")) { if !roots
.iter()
.any(|root| path == root || path.starts_with(&format!("{root}/")))
{
return Err("Only the selected review property can change in this draft.".into()); return Err("Only the selected review property can change in this draft.".into());
} }
let draft = if message_kind::handles(facet) { let draft = if scalar::grouped(facet) {
&mut self.message_draft self.group_drafts.get_mut(facet)
} else { } else {
&mut self.draft self.draft.as_mut()
} }
.as_mut()
.ok_or("Choose an existing target.")?; .ok_or("Choose an existing target.")?;
// The scoped view sends only descendants of the selected scalar property. // The scoped view sends only the selected property or closed group roots.
// Protected identity and top-level body/perspective controls are never exposed. // Protected identity and top-level body/perspective controls are never exposed.
if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" { if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" {
return Err("Semantic candidates retain identity, alias and perspective.".into()); return Err("Semantic candidates retain identity, alias and perspective.".into());
@@ -324,17 +326,16 @@ impl State {
.and_then(|id| scalar::binding(project, id, facet)) .and_then(|id| scalar::binding(project, id, facet))
.is_some() .is_some()
&& (!scalar::grouped(facet) && (!scalar::grouped(facet)
|| (message_kind::handles(facet) || (self.group_drafts.contains_key(facet)
&& self.message_draft.is_some()
&& self && self
.trace .trace
.and_then(|id| project.traces.get(&id)) .and_then(|id| project.traces.get(&id))
.is_some_and(|trace| message_kind::available(project, trace)))) .is_some_and(|trace| constructors::available(project, trace, facet))))
} }
fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> { fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> {
if message_kind::handles(facet) { if scalar::grouped(facet) {
self.message_draft.as_ref() self.group_drafts.get(facet)
} else { } else {
self.draft.as_ref() self.draft.as_ref()
} }
@@ -418,7 +419,7 @@ impl State {
Ok(request) Ok(request)
} }
/// Compose selected scalar values for both the candidate and dependent form /// Compose selected values for both the candidate and dependent form
/// controls. Incomplete inputs retain their stored shape only for controls; /// controls. Incomplete inputs retain their stored shape only for controls;
/// request construction always requires every selected input to parse. /// request construction always requires every selected input to parse.
fn combined_value(&self, project: &Project, form_context: bool) -> Result<Value, String> { fn combined_value(&self, project: &Project, form_context: bool) -> Result<Value, String> {
@@ -446,61 +447,75 @@ impl State {
} }
} }
map.insert(source, target); map.insert(source, target);
let mut message_candidate = None; let original = final_value.clone();
for (facet, decision) in &self.decisions { let mut group_candidates = vec![];
// Outer groups precede nested groups and leaves. The core rejects actual
// overlapping replacement scopes; this order also supports first-equal
// discriminator acknowledgment together with explicit descendant values.
let mut decisions: Vec<_> = self.decisions.iter().collect();
decisions.sort_by_key(|(facet, _)| match facet {
FacetSelector::Occurrence(OccurrenceFacet::Constructor) => 0,
facet if scalar::grouped(facet) => 1,
_ => 2,
});
for (facet, decision) in decisions {
if *decision == SemanticDecision::KeepTarget { if *decision == SemanticDecision::KeepTarget {
continue; continue;
} }
let path = scalar::binding(project, target, facet) let roots = constructors::roots(project, target, facet)
.ok_or("Selected property is not applicable to the target.")?; .ok_or("Selected property is not applicable to the target.")?;
let value = match decision { let supplied = match decision {
SemanticDecision::KeepTarget => continue, SemanticDecision::KeepTarget => continue,
SemanticDecision::Candidate => { SemanticDecision::Candidate => {
let draft = self let draft = self
.candidate_draft(facet) .candidate_draft(facet)
.ok_or("Target draft is unavailable.")?; .ok_or("Target draft is unavailable.")?;
let value = match draft.materialize_scopes(std::slice::from_ref(&path)) { match draft.materialize_scopes(&roots) {
Ok(value) => value, Ok(value) => value,
Err(_) if form_context => draft.value.clone(), Err(_) if form_context => draft.value.clone(),
Err(error) => return Err(error), Err(error) => return Err(error),
}; }
value
.pointer(&path)
.cloned()
.ok_or("Candidate property is no longer available.")?
} }
SemanticDecision::TakeSource if message_kind::handles(facet) => { SemanticDecision::TakeSource if scalar::grouped(facet) => {
// First equal kinds acknowledge only the discriminator. if self.can_acknowledge_constructor(project, facet) {
// An unselected Timer payload must stay at the destination. original.clone()
message_kind::source_value( } else {
project, constructors::take_value(project, trace, self.direction, facet, &map)?
if self.can_acknowledge_constructor(project, facet) { }
target
} else {
source
},
)?
} }
SemanticDecision::TakeSource => { SemanticDecision::TakeSource => {
let fact = capture_facet(project, trace.oa, trace.sa, facet)?; let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
scalar::source_value(&fact, self.direction.source_layer(), &map)? let value = scalar::source_value(&fact, self.direction.source_layer(), &map)?;
let mut supplied = original.clone();
*supplied
.pointer_mut(&roots[0])
.ok_or("Source property is unavailable.")? = value;
supplied
} }
}; };
if message_kind::handles(facet) && *decision == SemanticDecision::Candidate { for path in roots {
message_candidate = Some((path.clone(), value.clone())); let value = supplied
.pointer(&path)
.cloned()
.ok_or("Candidate property is no longer available.")?;
if scalar::grouped(facet) && *decision == SemanticDecision::Candidate {
group_candidates.push((path.clone(), value.clone()));
}
*final_value.pointer_mut(&path).ok_or("Final target property is unavailable; resolve the overlapping constructor decision.")? = value;
} }
*final_value
.pointer_mut(&path)
.ok_or("Final target property is unavailable.")? = value;
} }
if !form_context if !form_context {
&& let Some((path, candidate)) = message_candidate for (path, authored) in group_candidates {
&& let Ok(current) = message_kind::source_value(project, target) if !original
&& crate::forms::same_variant(&candidate, &current) .pointer(&path)
&& candidate != current .zip(final_value.pointer(&path))
&& final_value.pointer(&path) != Some(&candidate) .is_some_and(|(original, final_value)| {
{ constructors::authored_agrees(original, &authored, final_value)
return Err("The message-kind candidate and Timer Expression decision propose different timer values. Keep the kind or make their final values agree; both drafts are retained.".into()); })
{
return Err("The constructor candidate and descendant decision propose different final values. Keep the kind or make their final values agree; both drafts are retained.".into());
}
}
} }
Ok(final_value) Ok(final_value)
} }
@@ -513,8 +528,8 @@ impl State {
if let Some(draft) = &mut self.draft { if let Some(draft) = &mut self.draft {
draft.set_context(context.clone()); draft.set_context(context.clone());
} }
if let Some(draft) = &mut self.message_draft { for draft in self.group_drafts.values_mut() {
draft.set_context(context); draft.set_context(context.clone());
} }
let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else { let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else {
self.error = Some( self.error = Some(
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(&current, Layer::Oa) != scalar::observation(&current, 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 { pub(super) fn selector() -> FacetSelector {
FacetSelector::Occurrence(OccurrenceFacet::MessageKind) FacetSelector::Occurrence(OccurrenceFacet::MessageKind)
} }
#[cfg(test)]
pub(super) fn timer_selector() -> FacetSelector { pub(super) fn timer_selector() -> FacetSelector {
FacetSelector::Occurrence(OccurrenceFacet::TimerExpression) FacetSelector::Occurrence(OccurrenceFacet::TimerExpression)
} }
pub(super) fn handles(facet: &FacetSelector) -> bool { pub(super) fn handles(facet: &FacetSelector) -> bool {
*facet == selector() *facet == selector()
} }
#[cfg(test)]
pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> { pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> {
match project.elements.get(&id)?.shared()? { match project.elements.get(&id)?.shared()? {
SharedBody::Occurrence { SharedBody::Occurrence {
@@ -20,13 +22,3 @@ pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> {
_ => None, _ => 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, &mut app,
Event::Text(expression.clone(), text_editor::Action::SelectAll), 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 .editor
.cursor(); .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 .editor
.selection(); .selection();
send( send(
@@ -221,17 +229,30 @@ fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_ex
Event::Variant(kind, json!({"Timer":{"expression":""}})), Event::Variant(kind, json!({"Timer":{"expression":""}})),
); );
assert_eq!( 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 " " group β timer\n\t "
); );
assert_eq!( assert_eq!(
state(&app).message_draft.as_ref().unwrap().inputs[&expression] state(&app)
.group_drafts
.get(&message_kind::selector())
.unwrap()
.inputs[&expression]
.editor .editor
.cursor(), .cursor(),
cursor cursor
); );
assert_eq!( assert_eq!(
state(&app).message_draft.as_ref().unwrap().inputs[&expression] state(&app)
.group_drafts
.get(&message_kind::selector())
.unwrap()
.inputs[&expression]
.editor .editor
.selection(), .selection(),
selection selection
@@ -621,7 +642,7 @@ fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_t
.error .error
.as_ref() .as_ref()
.unwrap() .unwrap()
.contains("different timer values") .contains("different final values")
); );
send( send(
&mut app, &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!(state(&app).error.is_some());
assert_eq!(state(&app).decisions.len(), 1); assert_eq!(state(&app).decisions.len(), 1);
assert_eq!( 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 raw
); );
assert_eq!(app.document.bundle(), &before.bundle); assert_eq!(app.document.bundle(), &before.bundle);
@@ -568,7 +568,10 @@ fn read_only_structured_facts_and_partial_rollup_are_honest() {
send( send(
&mut app, &mut app,
Event::Decide( Event::Decide(
facet(DataFacet::Constructor), FacetSelector::Literal(KeyPair {
oa: Some("Unpaired".into()),
sa: None,
}),
Some(SemanticDecision::TakeSource), Some(SemanticDecision::TakeSource),
), ),
); );
+119 -68
View File
@@ -266,7 +266,7 @@ impl State {
.iter() .iter()
.filter(|f| f.drift == Drift::Unreviewed) .filter(|f| f.drift == Drift::Unreviewed)
.count(); .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 { if let Some(trace) = trace {
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED)); 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(|t| t.baseline.as_ref())
.and_then(|b| b.semantic.as_ref()) .and_then(|b| b.semantic.as_ref())
.and_then(|b| b.facets.iter().find(|f| f.selector() == fact.selector)); .and_then(|b| b.facets.iter().find(|f| f.selector() == fact.selector));
let is_message_kind = message_kind::handles(&fact.selector); let is_group = scalar::grouped(&fact.selector);
let timer_previous = trace let group_observations = |project: &Project, facts: Vec<&FacetReview>, layer| {
.and_then(|t| t.baseline.as_ref()) facts
.and_then(|b| b.semantic.as_ref()) .into_iter()
.and_then(|b| { .filter(|f| constructors::contains(&fact.selector, &f.selector()))
b.facets .map(|f| {
.iter() format!(
.find(|f| f.selector() == message_kind::timer_selector()) "{}: {}",
}); title(project, &f.selector()),
let timer_current = self observed(project, Some(f), layer)
.facts )
.iter() })
.find(|f| f.selector == message_kind::timer_selector()); .collect::<Vec<_>>()
.join("\n")
};
let side = |layer, label: &str| { let side = |layer, label: &str| {
let mut before = observed(project, previous, layer); let mut before = observed(project, previous, layer);
let mut current = observed(project, Some(&fact.current), layer); let mut current = observed(project, Some(&fact.current), layer);
if is_message_kind { if is_group {
before.push_str(&format!( before = group_observations(
"\nTimer expression: {}", project,
observed(project, timer_previous, layer) trace
)); .and_then(|t| t.baseline.as_ref())
current.push_str(&format!( .and_then(|b| b.semantic.as_ref())
"\nTimer expression: {}", .map(|b| b.facets.iter().collect())
observed(project, timer_current.map(|f| &f.current), layer) .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![ column![
text(format!( text(format!(
@@ -426,11 +437,17 @@ impl State {
.color(style::TEAL), .color(style::TEAL),
) )
.push( .push(
text(if is_message_kind { text(if is_group {
self.target(project) semantic_facets(proposed, &proposed.traces[&trace.id], &self.references)
.and_then(|id| message_kind::source_value(proposed, id).ok()) .ok()
.map(|value| formatted(proposed, &value, &BTreeSet::new())) .map(|facts| {
.unwrap_or_else(|| "Message kind is not applicable.".into()) group_observations(
proposed,
facts.iter().map(|f| &f.current).collect(),
self.direction.target_layer(),
)
})
.unwrap_or_else(|| "Group payload is unavailable.".into())
} else { } else {
observed(proposed, Some(&after), self.direction.target_layer()) observed(proposed, Some(&after), self.direction.target_layer())
}) })
@@ -444,10 +461,11 @@ impl State {
); );
} }
if self.editable(project, &fact.selector) { if self.editable(project, &fact.selector) {
if is_message_kind { if is_group {
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)); 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) { 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 { let destination = if self.direction.target_layer() == Layer::Sa {
@@ -472,8 +490,8 @@ impl State {
] ]
.spacing(5); .spacing(5);
buttons = buttons.push(action( buttons = buttons.push(action(
if is_message_kind { if is_group {
"Set final message kind…" "Set final kind and payload…"
} else { } else {
"Set final value…" "Set final value…"
}, },
@@ -485,52 +503,85 @@ impl State {
)); ));
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate) if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
&& let Some(draft) = self.candidate_draft(&fact.selector) && let Some(draft) = self.candidate_draft(&fact.selector)
&& let Some(path) = self && let Some(paths) = self
.target(project) .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( detail = detail.push(text("Final destination value").size(12));
crate::forms::scoped(draft, project, &path, &title(project, &fact.selector)) 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), .map(form_event),
); );
}
} }
} else if self.can_acknowledge_constructor(project, &fact.selector) { } 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)) 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("Acknowledge equal kind only", Event::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget))))
.push(action("Leave unselected", Event::Decide(fact.selector.clone(), None))); .push(action("Leave unselected", Event::Decide(fact.selector.clone(), None)));
} else { } 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() { let overlaps: Vec<_> = self
if self.decisions.contains_key(&message_kind::selector()) .decisions
&& self.decisions.contains_key(&message_kind::timer_selector()) .keys()
{ .filter(|other| {
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)) **other != fact.selector
.push(action("Replace timer decision with message-kind group", Event::Decide(message_kind::timer_selector(), None))) && (constructors::contains(&fact.selector, other)
.push(action("Leave message kind unselected; use timer decision", Event::Decide(message_kind::selector(), None))); || 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(action(
detail = detail.push(text("Exact review scope from preview").size(13)); "Leave this decision unselected; use the related decisions",
for (label, selectors) in [ Event::Decide(fact.selector.clone(), None),
("Acknowledge", &review.acknowledged), ));
("Retire previous observation", &review.retired), }
] { if (is_group
detail = detail.push( || constructors::selectors().iter().any(|group| {
text(format!( self.decisions.contains_key(group) && constructors::contains(group, &fact.selector)
"{label}: {}", }))
if selectors.is_empty() { && let Some(review) = self.plan.as_ref().and_then(|p| p.reviews.first())
"None".into() {
} else { detail = detail.push(text("Exact review scope from preview").size(13));
selectors for (label, selectors) in [
.iter() ("Acknowledge", &review.acknowledged),
.map(|s| title(project, s)) ("Retire previous observation", &review.retired),
.collect::<Vec<_>>() ] {
.join(", ") detail = detail.push(
} text(format!(
)) "{label}: {}",
.size(11), if selectors.is_empty() {
); "None".into()
} } else {
selectors
.iter()
.map(|s| title(project, s))
.collect::<Vec<_>>()
.join(", ")
}
))
.size(11),
);
} }
} }
if matches!( if matches!(
+98 -15
View File
@@ -1,8 +1,8 @@
# Semantic desktop review — scalars and nested message kind # Semantic desktop review — scalars and closed constructor groups
Status: milestones 1–2 implemented; the bounded MessageKind/TimerExpression Status: milestones 1–2 and the closed constructor group forms are implemented.
constructor slice is implemented. Full milestone 3, GUI parity, native acceptance Independent keyed/ordered review, historical pairing reset, multi-source review,
and release acceptance remain outstanding. full GUI parity, native acceptance and release acceptance remain outstanding.
Base: `d130013`. Dedicated build target: Base: `d130013`. Dedicated build target:
`D:/CodexBuilds/Capella2-semantic-desktop-target`. Native verification is root-owned. `D:/CodexBuilds/Capella2-semantic-desktop-target`. Native verification is root-owned.
@@ -17,10 +17,10 @@ Base: `d130013`. Dedicated build target:
- Support every applicable scalar leaf through Keep, Take and typed final-value - Support every applicable scalar leaf through Keep, Take and typed final-value
forms. Candidate records contain the complete combined selected result. Identity, forms. Candidate records contain the complete combined selected result. Identity,
alias and layer are protected. The closed core remains the validation boundary. alias and layer are protected. The closed core remains the validation boundary.
- State.Kind is a single structural scalar. Occurrence.MessageKind now supports - State.Kind is a single structural scalar. Data, Port, Occurrence, nested
its complete nested group, including TimerExpression. Other constructor switches, keyed MessageKind and Event support their closed constructor groups and complete
membership/order/annotation/binding/attachment decisions and pairing resets belong typed payloads. Independent keyed membership/order/annotation/binding/attachment
to milestone 3; show them read-only with an honest explanation. decisions and pairing resets remain visibly read-only outside a replacing group.
- Core `6bb2583` permits first equal constructor acknowledgment without descendant - Core `6bb2583` permits first equal constructor acknowledgment without descendant
expansion. The GUI exposes **Acknowledge equal kind only** when applicable; expansion. The GUI exposes **Acknowledge equal kind only** when applicable;
previously reviewed constructors remain governed by the core's switch rules. previously reviewed constructors remain governed by the core's switch rules.
@@ -38,9 +38,9 @@ Base: `d130013`. Dedicated build target:
2. Scalar decisions and typed candidates; immutable initial observations; combined 2. Scalar decisions and typed candidates; immutable initial observations; combined
final records; exact impact acceptance; atomic document/source commit. Focused final records; exact impact acceptance; atomic document/source commit. Focused
App event/source/history tests, strict Clippy and a dedicated desktop build. App event/source/history tests, strict Clippy and a dedicated desktop build.
3. Nested MessageKind/TimerExpression closure is implemented. Other constructor 3. All closed constructor forms are implemented, including nested MessageKind.
groups, keyed and ordered facts, reset, complete artifact lifecycle, multi-source Independent keyed and ordered facts, reset, complete artifact lifecycle,
review and remaining source/REST parity cases are outstanding. multi-source review and remaining source/REST parity cases are outstanding.
4. Root-owned native review, cross-surface fixtures, package/release acceptance. 4. Root-owned native review, cross-surface fixtures, package/release acceptance.
## Verification ## Verification
@@ -94,7 +94,8 @@ button availability, exact source retention before Apply, and one-step undo.
Occurrence.MessageKind is a complete closed nested family: Asynchronous, Call, Occurrence.MessageKind is a complete closed nested family: Asynchronous, Call,
Reply, Create, Delete and Timer. It is available only when both traced counterparts Reply, Create, Delete and Timer. It is available only when both traced counterparts
are Exchange occurrences. The only constructor-specific payload is the timer are Exchange occurrences. The only constructor-specific payload is the timer
expression. Outer occurrence replacement remains read-only. expression. This first slice left outer occurrence replacement read-only; the later closed
constructor slice below now covers it.
- Keep/Take/Candidate use the core's switch and first-equal acknowledgment policy. - Keep/Take/Candidate use the core's switch and first-equal acknowledgment policy.
Taking a replacing group copies the typed MessageKind, including its expression. Taking a replacing group copies the typed MessageKind, including its expression.
@@ -132,9 +133,8 @@ The build target is idle. No native application was launched by this task.
## Remaining work ## Remaining work
Milestone 3 must add the remaining constructor closure/replacement, keyed and ordered Milestone 3 must add independent keyed and ordered decisions, historical pairing
decisions, historical pairing reset, complete artifact lifecycle and multi-source reset, complete artifact lifecycle and multi-source review. These facts are currently inspectable, with unsupported writes visibly
review. These facts are currently inspectable, with unsupported writes visibly
identified. The reference table supplies explicit mappings for scalar reference identified. The reference table supplies explicit mappings for scalar reference
Take; it does not claim to complete a reset or establish keyed acknowledgments. Take; it does not claim to complete a reset or establish keyed acknowledgments.
@@ -142,3 +142,86 @@ Fresh independent requirements/correctness review and root-owned native checks o
the message-kind slice are required before release acceptance. Verify the compact the message-kind slice are required before release acceptance. Verify the compact
kind picker, timer text/focus retention, overlap choices, exact closure/retirement, kind picker, timer text/focus retention, overlap choices, exact closure/retirement,
mapped Reply, blocked Timer contract, source reopening and toolbar wrapping. mapped Reply, blocked Timer contract, source reopening and toolbar wrapping.
## Remaining closed constructor families (this slice)
Owned files: `semantic_review/**`, `semantic_review.rs`, the scoped Draft adapter
in `forms.rs`, semantic-only port choices in `forms/port_fields.rs`, and this ledger.
No shared traceability header, core, API, native state or source language edits.
Base: `f73551f`; same exclusive desktop target.
| Constructor | Complete group payload | Independent fields |
| --- | --- | --- |
| Data | Definition, bounds, literal members, record supertype, default | Unit, owner and headers |
| Port | Contract, function item members, retained legacy direction/interface | Owner and headers |
| Occurrence | Body, nested message kind/timer, references, qualified annotations, bindings, branch and step order | Pre/postconditions, owner and headers |
| Event | Body, signal item members, time kind/expression or change expression | Owner and headers |
| MessageKind | Complete nested kind and timer expression | Other occurrence body properties |
Each group owns an independent draft, including the nested MessageKind group.
Keep/Take/leave-unselected retain all drafts. Constructor picker changes save the
old branch value plus its raw inputs and editor state. Switching back restores them.
Port input/output changes also carry authored item reference strings, including
invalid strings, rather than falling back to stored UUIDs. A Data definition switch
leaves its existing default visible until the user deliberately changes or clears
it; Port's retained generic association has explicit add/edit/clear controls.
The same combined candidate projection supplies dependent form context. Only the
selected roots are materialized; unrelated invalid scalar/group drafts stay local.
Full Take payloads use the public core typed reference visitor, so UUID-shaped prose
is never remapped. For historical same-kind Take, the closed adapter retains only
current target-present/source-absent rows without a previous source-present
observation on that target slot; the core independently
checks the supplied final record. No GUI acknowledgment, retirement or impact list
is synthesized. The core remains authoritative for those lists and applicability.
Group details show every baseline/current/proposed group observation, including
qualified and historical row identities. Actual preview acknowledged/retired lists
remain explicit. A replacing group and selected descendants retain both decisions
and provide explicit leave-unselected actions. First-equal discriminator observation
can coexist with independent descendant decisions. Conflicting authored values
reject instead of silently discarding either draft's edits.
Array controls support the whole authorized payload, including order, existing
owned branches/steps, signal items, annotations and scenario bindings. They do not
create referenced semantic objects, change their ownership, delete dependencies,
or infer reference correspondences. Incompatible payload/owner combinations stay
visible as validation failures. Apply still sends exactly one prepared Document
batch, preserving comments, source/model/history agreement and one-step undo/redo.
New coverage exercises all six Data definitions and companion defaults in both
directions; all eight Occurrence and all three Event kinds; all four Port forms;
combined owner context; nested and scalar draft retention; explicit overlap;
protected fields; mapped record, state, event item and scenario binding payloads;
complete owned branch/step order; historical retirement and first-equal scope;
90 Data/Event kind-pair and direction comparisons against the core-only preview;
source comments/reopen and atomic undo/redo. Full checks and core dependency status
will be recorded at freeze. This task launches no native application.
The delegated keyed-closure fix `43370de` is integrated into SEM as `9c3184b`.
Reverse Literal/Member/Binding/Annotation closure and historical local-member
preservation now pass the desktop desired-success tests. The separately delegated existing-target Port fix `8a75294` is integrated into
SEM as `7512ac8`. Fresh reverse Keep and Candidate reviews can preserve/edit the
OA Legacy association; taking a typed contract into OA still rejects. Both fixes
are covered by desired-success desktop tests; no GUI workaround weakens validation.
Native acceptance remains root-owned: use the existing trace Review semantics
entry, inspect complete group before/after and retired identities, switch variants
away/back, resolve explicit nested overlaps, repair invalid defaults/references,
apply once, inspect source, undo/redo, save and reopen at 1100×700 and 1440×900.
## Closed constructor freeze verification
On integrated SEM base `7512ac8`, all **299 desktop tests pass**. This adds 13
constructor workflow tests and one scoped port form regression to the previous
285-test slice. The new suite includes 90 complete Data/Event kind-pair/direction
comparisons against core previews, all closed candidate kinds, mapped qualified
annotation addresses, bindings and references, actual reverse Legacy edits, owned
branch/step ordering, source/model/history agreement, retained drafts and strict
scope protection. Both delegated core defects are resolved and exercised.
Owned-file rustfmt check, strict Clippy (`--all-targets -- -D warnings`) and the
desktop build all pass using the dedicated target and commands above. Native checks remain root-owned. No native application
was launched, and full milestone 3 / GUI parity / release acceptance remain open.