Review closed semantic constructor groups with retained typed drafts
This commit is contained in:
+155
-16
@@ -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(¤t, &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(¤t));
|
||||||
|
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(¤t),
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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") {
|
||||||
|
|||||||
@@ -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, ¤t)
|
.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(¤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 {
|
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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user