Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c091f80e6e | ||
|
|
14b8e187ea | ||
|
|
00ac6eb86c | ||
|
|
843635ed26 | ||
|
|
2c0f93839f | ||
|
|
09b7ebddc4 | ||
|
|
cf04ff1442 | ||
|
|
1a68ae0795 | ||
|
|
0e47384532 | ||
|
|
28ebbbbc4c | ||
|
|
4f5906b0ff | ||
|
|
f08507a789 | ||
|
|
691ef26a01 | ||
|
|
5930f91ed6 | ||
|
|
1b80671dd0 | ||
|
|
49a2446f85 | ||
|
|
67ed8e09be |
@@ -0,0 +1,138 @@
|
||||
use super::*;
|
||||
use iced::advanced::{Layout, layout, widget};
|
||||
use iced::{Rectangle, Vector};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Geometry {
|
||||
scrolls: Vec<(Option<widget::Id>, Rectangle, Vector)>,
|
||||
actions: Option<Rectangle>,
|
||||
}
|
||||
impl widget::Operation for Geometry {
|
||||
fn traverse(&mut self, operate: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
||||
operate(self);
|
||||
}
|
||||
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
||||
if id == Some(&widget::Id::new("test-process-actions")) {
|
||||
self.actions = Some(bounds);
|
||||
}
|
||||
}
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
_: Rectangle,
|
||||
translation: Vector,
|
||||
_: &mut dyn widget::operation::Scrollable,
|
||||
) {
|
||||
self.scrolls.push((id.cloned(), bounds, translation));
|
||||
}
|
||||
}
|
||||
fn workspace(pending: bool) -> iced::Element<'static, Message, iced::Theme, ()> {
|
||||
let diagram = scrollable(iced::widget::Space::new().width(500).height(1200))
|
||||
.id("test-process-page")
|
||||
.height(Fill)
|
||||
.into();
|
||||
let form = if pending {
|
||||
editor_panel(
|
||||
container(iced::widget::Space::new().height(32))
|
||||
.id("test-process-actions")
|
||||
.into(),
|
||||
iced::widget::Space::new().height(1400).into(),
|
||||
)
|
||||
} else {
|
||||
iced::widget::Space::new().height(0).into()
|
||||
};
|
||||
panes(diagram, form, pending)
|
||||
}
|
||||
fn geometry(
|
||||
view: &mut iced::Element<'_, Message, iced::Theme, ()>,
|
||||
tree: &mut widget::Tree,
|
||||
size: Size,
|
||||
) -> (layout::Node, Geometry) {
|
||||
let node = view
|
||||
.as_widget_mut()
|
||||
.layout(tree, &(), &layout::Limits::new(Size::ZERO, size));
|
||||
let mut geometry = Geometry::default();
|
||||
view.as_widget_mut()
|
||||
.operate(tree, Layout::new(&node), &(), &mut geometry);
|
||||
(node, geometry)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_process_pending_editor_uses_available_height_and_keeps_actions_visible() {
|
||||
// Actual available center-pane sizes at the minimum/default supported windows.
|
||||
// Space represents arbitrarily long diagram/form content; the production
|
||||
// pane/card builders and real Iced layout/scroll operations are exercised.
|
||||
for size in [Size::new(500., 570.), Size::new(840., 770.)] {
|
||||
let mut view = workspace(false);
|
||||
let mut tree = widget::Tree::new(view.as_widget());
|
||||
let (node, _) = geometry(&mut view, &mut tree, size);
|
||||
let mut scroll = widget::operation::scrollable::scroll_to::<()>(
|
||||
widget::Id::new("test-process-page"),
|
||||
widget::operation::scrollable::AbsoluteOffset {
|
||||
x: None,
|
||||
y: Some(180.),
|
||||
},
|
||||
);
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &(), &mut scroll);
|
||||
view = workspace(true);
|
||||
tree.diff(view.as_widget());
|
||||
let (node, before) = geometry(&mut view, &mut tree, size);
|
||||
let page = before
|
||||
.scrolls
|
||||
.iter()
|
||||
.find(|s| s.0 == Some(widget::Id::new("test-process-page")))
|
||||
.unwrap();
|
||||
let form = before
|
||||
.scrolls
|
||||
.iter()
|
||||
.find(|s| s.0 == Some(widget::Id::new("process-form")))
|
||||
.unwrap();
|
||||
assert_eq!(node.size(), size);
|
||||
assert!(
|
||||
form.1.height >= 200.,
|
||||
"usable form viewport: {size:?} {:?}",
|
||||
form.1
|
||||
);
|
||||
assert!(form.1.y >= page.1.y + page.1.height);
|
||||
assert!(form.1.y + form.1.height <= size.height);
|
||||
assert_eq!(page.2.y, 180., "opening a form retains the page scroll");
|
||||
let actions = before.actions.unwrap();
|
||||
assert!(actions.y >= page.1.y + page.1.height);
|
||||
assert!(actions.y + actions.height <= form.1.y);
|
||||
let mut scroll = widget::operation::scrollable::scroll_to::<()>(
|
||||
widget::Id::new("process-form"),
|
||||
widget::operation::scrollable::AbsoluteOffset {
|
||||
x: None,
|
||||
y: Some(600.),
|
||||
},
|
||||
);
|
||||
view.as_widget_mut()
|
||||
.operate(&mut tree, Layout::new(&node), &(), &mut scroll);
|
||||
let (_, after) = geometry(&mut view, &mut tree, size);
|
||||
assert_eq!(
|
||||
after.actions,
|
||||
Some(actions),
|
||||
"Create/Cancel stay pinned during form scrolling"
|
||||
);
|
||||
assert_eq!(
|
||||
after.scrolls[0].2.y, 180.,
|
||||
"form scrolling does not move the diagram page"
|
||||
);
|
||||
assert!(
|
||||
after
|
||||
.scrolls
|
||||
.iter()
|
||||
.any(|s| s.0 == Some(widget::Id::new("process-form")) && s.2.y == 600.)
|
||||
);
|
||||
view = workspace(false);
|
||||
tree.diff(view.as_widget());
|
||||
let (_, closed) = geometry(&mut view, &mut tree, size);
|
||||
assert_eq!(
|
||||
closed.scrolls[0].2.y, 180.,
|
||||
"cancel retains the page scroll"
|
||||
);
|
||||
assert_eq!(closed.scrolls[0].1.height, size.height);
|
||||
}
|
||||
}
|
||||
@@ -305,6 +305,7 @@ pub(super) fn view<'a>(
|
||||
button("Back to caller").on_press(Message::TopologyCanvas(CanvasEvent::Back(context))),
|
||||
);
|
||||
}
|
||||
let mut form_panel: Element<'_, Message> = iced::widget::Space::new().height(0).into();
|
||||
if let Some(d) = state.draft.as_ref().filter(|d| d.owner == owner) {
|
||||
let mut form = column![text(d.tool.title()).size(16)].spacing(8);
|
||||
if let Some(link) = d.insertion.link.and_then(|id| p.relations.get(&id)) {
|
||||
@@ -473,21 +474,65 @@ pub(super) fn view<'a>(
|
||||
button("Cancel").on_press(event(Event::Cancel))
|
||||
]
|
||||
.spacing(7);
|
||||
out = out.push(
|
||||
container(
|
||||
column![
|
||||
actions,
|
||||
scrollable(form).height((app.window_size.height - 520.).clamp(150., 260.))
|
||||
]
|
||||
.spacing(8),
|
||||
)
|
||||
.padding(12)
|
||||
.style(style::card),
|
||||
);
|
||||
form_panel = editor_panel(actions.into(), form.into());
|
||||
}
|
||||
super::super::scenario_scroll::observe_topology(
|
||||
context,
|
||||
scenario::ScrollArea::Page,
|
||||
scrollable(out.padding(18)),
|
||||
panes(
|
||||
super::super::scenario_scroll::observe_topology(
|
||||
context,
|
||||
scenario::ScrollArea::Page,
|
||||
scrollable(out.padding(18)),
|
||||
),
|
||||
form_panel,
|
||||
pending,
|
||||
)
|
||||
}
|
||||
|
||||
/// Keep the diagram mounted while an independent editor uses the remaining
|
||||
/// workspace height. The editor must never be clipped by the page scrollable.
|
||||
fn panes<'a, R: iced::advanced::Renderer + 'a>(
|
||||
diagram: iced::Element<'a, Message, iced::Theme, R>,
|
||||
form: iced::Element<'a, Message, iced::Theme, R>,
|
||||
pending: bool,
|
||||
) -> iced::Element<'a, Message, iced::Theme, R> {
|
||||
column![
|
||||
container(diagram).width(Fill).height(if pending {
|
||||
iced::Length::FillPortion(1)
|
||||
} else {
|
||||
Fill
|
||||
}),
|
||||
container(form).width(Fill).height(if pending {
|
||||
iced::Length::FillPortion(1)
|
||||
} else {
|
||||
iced::Length::Shrink
|
||||
}),
|
||||
]
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn editor_panel<'a, R: iced::advanced::text::Renderer + 'a>(
|
||||
actions: iced::Element<'a, Message, iced::Theme, R>,
|
||||
fields: iced::Element<'a, Message, iced::Theme, R>,
|
||||
) -> iced::Element<'a, Message, iced::Theme, R> {
|
||||
container(
|
||||
container(column![actions, scrollable(fields).id("process-form").height(Fill)].spacing(8))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.padding(12)
|
||||
.style(style::card),
|
||||
)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.padding(iced::Padding {
|
||||
top: 0.,
|
||||
right: 18.,
|
||||
bottom: 18.,
|
||||
left: 18.,
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
#[path = "layout_tests.rs"]
|
||||
mod layout_tests;
|
||||
|
||||
@@ -46,6 +46,8 @@ pub struct Draft {
|
||||
pub inputs: BTreeMap<String, Input>,
|
||||
pub exact_strings: bool,
|
||||
context: Option<Value>,
|
||||
pub semantic_groups: bool,
|
||||
constructor_drafts: BTreeMap<(String, String), (Value, BTreeMap<String, Input>)>,
|
||||
port_contract_drafts: BTreeMap<String, Value>,
|
||||
pub value_browsers: BTreeMap<String, ValueBrowser>,
|
||||
}
|
||||
@@ -85,6 +87,8 @@ impl Draft {
|
||||
inputs: BTreeMap::new(),
|
||||
exact_strings: false,
|
||||
context: None,
|
||||
semantic_groups: false,
|
||||
constructor_drafts: BTreeMap::new(),
|
||||
port_contract_drafts: BTreeMap::new(),
|
||||
value_browsers: BTreeMap::new(),
|
||||
};
|
||||
@@ -248,6 +252,75 @@ impl Draft {
|
||||
self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned()));
|
||||
Ok(())
|
||||
}
|
||||
/// Preserve each selected constructor branch, including incomplete raw input
|
||||
/// and editor state. No sibling fields are parsed or cleared by a switch.
|
||||
pub fn variant_scoped(&mut self, path: &str, proposed: Value) -> Result<(), String> {
|
||||
let current = self
|
||||
.value
|
||||
.pointer(path)
|
||||
.ok_or("Property no longer exists.")?
|
||||
.clone();
|
||||
if same_variant(¤t, &proposed) {
|
||||
return Ok(());
|
||||
}
|
||||
let constructor = path.ends_with("/DataType/definition")
|
||||
|| path.ends_with("/Port/contract")
|
||||
|| path.ends_with("/Occurrence/body")
|
||||
|| path.ends_with("/Event/body")
|
||||
|| path.ends_with("/message_kind");
|
||||
if !constructor {
|
||||
return self.shape_scoped(path, proposed);
|
||||
}
|
||||
// Input/output direction changes carry the item declaration. Read those
|
||||
// raw reference strings before stashing their editor buffers so an
|
||||
// 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<_>>()
|
||||
{
|
||||
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(())
|
||||
}
|
||||
fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) {
|
||||
let previous = std::mem::take(&mut self.inputs);
|
||||
self.rebuild();
|
||||
@@ -645,7 +718,12 @@ fn free_string(path: &str) -> bool {
|
||||
// These are choices for the closed model, not a second validation boundary.
|
||||
// A current reference is always retained, even while other draft fields make it
|
||||
// incompatible. Applying the complete draft remains the core validator's job.
|
||||
fn reference_choices(draft: &Draft, project: &Project, path: &str, current: Id) -> Vec<Choice> {
|
||||
pub(crate) fn reference_choices(
|
||||
draft: &Draft,
|
||||
project: &Project,
|
||||
path: &str,
|
||||
current: Id,
|
||||
) -> Vec<Choice> {
|
||||
let identities = project.identities().into_iter().collect::<BTreeMap<_, _>>();
|
||||
let compatible = reference_ids(draft, project, path);
|
||||
let mut objects = compatible
|
||||
@@ -1290,6 +1368,7 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
|
||||
"layer" => &["Oa", "Sa"],
|
||||
"mechanism" => &["Unspecified", "Flow", "Event", "Operation", "SharedData"],
|
||||
"storage" => &["Value", "Reference"],
|
||||
"direction" if path.contains("/Port/legacy/") => &["In", "Out", "InOut"],
|
||||
"direction" if path.contains("DataField") => {
|
||||
&["Unspecified", "In", "Out", "InOut", "Return"]
|
||||
}
|
||||
@@ -1546,7 +1625,7 @@ fn property<'a>(
|
||||
return view;
|
||||
}
|
||||
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);
|
||||
if !value.is_null() {
|
||||
content = content.push(
|
||||
@@ -1560,6 +1639,29 @@ fn property<'a>(
|
||||
}
|
||||
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 {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -1817,6 +1919,73 @@ fn property<'a>(
|
||||
mod tests {
|
||||
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> {
|
||||
reference_ids(draft, project, path)
|
||||
}
|
||||
|
||||
@@ -45,7 +45,11 @@ pub(super) fn variants(
|
||||
let current = draft.context_value();
|
||||
let sa = current.pointer("/body/Sa").is_some();
|
||||
if !sa {
|
||||
return Some(vec![]);
|
||||
return Some(if draft.semantic_groups {
|
||||
vec![("Legacy generic port".into(), json!("Legacy"))]
|
||||
} else {
|
||||
vec![]
|
||||
});
|
||||
}
|
||||
let owner = current
|
||||
.get("parent")
|
||||
@@ -69,6 +73,9 @@ pub(super) fn variants(
|
||||
json!({"Component":{"kind":"Standard","orientation":"Unset"}}),
|
||||
));
|
||||
}
|
||||
if draft.semantic_groups {
|
||||
choices.push(("Legacy generic port".into(), json!("Legacy")));
|
||||
}
|
||||
return Some(choices);
|
||||
}
|
||||
if path.ends_with("/Port/contract/Component/kind") {
|
||||
|
||||
@@ -8,16 +8,28 @@ use oa_core::{transitions::*, *};
|
||||
use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
mod constructors;
|
||||
mod history;
|
||||
mod keyed;
|
||||
mod message_kind;
|
||||
mod scalar;
|
||||
mod view;
|
||||
pub(crate) use view::summary;
|
||||
#[cfg(test)]
|
||||
mod constructor_tests;
|
||||
#[cfg(test)]
|
||||
mod keyed_tests;
|
||||
#[cfg(test)]
|
||||
mod message_kind_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Tab {
|
||||
Facts,
|
||||
References,
|
||||
Members,
|
||||
History,
|
||||
Impacts,
|
||||
}
|
||||
|
||||
@@ -43,6 +55,16 @@ pub enum Event {
|
||||
RemoveReference(Id),
|
||||
AcceptImpact(usize, bool),
|
||||
Preview,
|
||||
Reset(FacetSelector, bool),
|
||||
Presence(FacetSelector, bool),
|
||||
MemberFamily(keyed::Family),
|
||||
MemberOa(Option<Choice>),
|
||||
MemberSa(Option<Choice>),
|
||||
Literal(String),
|
||||
LiteralSide(Layer, bool),
|
||||
EditPair(FacetSelector),
|
||||
StageMember,
|
||||
DropStaged(FacetSelector),
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
@@ -53,7 +75,19 @@ pub struct State {
|
||||
pub selected: Option<FacetSelector>,
|
||||
pub decisions: BTreeMap<FacetSelector, SemanticDecision>,
|
||||
pub references: BTreeMap<Id, Id>,
|
||||
pub resets: BTreeSet<FacetSelector>,
|
||||
pub staged: BTreeSet<FacetSelector>,
|
||||
pub presence: BTreeMap<FacetSelector, bool>,
|
||||
pub member_family: keyed::Family,
|
||||
pub member_oa: Option<Choice>,
|
||||
pub member_sa: Option<Choice>,
|
||||
pub literal: String,
|
||||
pub literal_oa: bool,
|
||||
pub literal_sa: bool,
|
||||
pub draft: Option<Draft>,
|
||||
// Each closed constructor has an independent draft, including nested kinds.
|
||||
// Scalar and sibling drafts survive decision and constructor changes.
|
||||
pub group_drafts: BTreeMap<FacetSelector, Draft>,
|
||||
pub plan: Option<SemanticTransitionPlan>,
|
||||
pub proposed: Option<Project>,
|
||||
pub error: Option<String>,
|
||||
@@ -95,7 +129,17 @@ impl State {
|
||||
selected: None,
|
||||
decisions: BTreeMap::new(),
|
||||
references: BTreeMap::new(),
|
||||
resets: BTreeSet::new(),
|
||||
staged: BTreeSet::new(),
|
||||
presence: BTreeMap::new(),
|
||||
member_family: keyed::Family::Literal,
|
||||
member_oa: None,
|
||||
member_sa: None,
|
||||
literal: String::new(),
|
||||
literal_oa: true,
|
||||
literal_sa: true,
|
||||
draft: None,
|
||||
group_drafts: BTreeMap::new(),
|
||||
plan: None,
|
||||
proposed: None,
|
||||
error: None,
|
||||
@@ -122,15 +166,41 @@ impl State {
|
||||
fn load_trace(&mut self, project: &Project) {
|
||||
self.facts.clear();
|
||||
self.decisions.clear();
|
||||
self.resets.clear();
|
||||
self.staged.clear();
|
||||
self.presence.clear();
|
||||
self.member_oa = None;
|
||||
self.member_sa = None;
|
||||
self.literal.clear();
|
||||
self.literal_oa = true;
|
||||
self.literal_sa = true;
|
||||
self.selected = None;
|
||||
self.accepted_impacts.clear();
|
||||
self.references.clear();
|
||||
self.draft = self
|
||||
.target(project)
|
||||
.and_then(|id| Draft::from_project(project, id));
|
||||
self.group_drafts.clear();
|
||||
if let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) {
|
||||
for facet in constructors::selectors() {
|
||||
if constructors::available(project, trace, &facet)
|
||||
&& let Some(mut draft) = self
|
||||
.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)) {
|
||||
self.references.insert(trace.oa, trace.sa);
|
||||
}
|
||||
self.member_family = self
|
||||
.member_families(project)
|
||||
.first()
|
||||
.copied()
|
||||
.unwrap_or(keyed::Family::Literal);
|
||||
self.refresh(project);
|
||||
}
|
||||
|
||||
@@ -168,6 +238,17 @@ impl State {
|
||||
{
|
||||
Err("This fact needs the structured review workflow; its history remains unchanged.".into())
|
||||
} else {
|
||||
if keyed::supported(&facet) {
|
||||
let target_presence = self
|
||||
.effective_trace(project)
|
||||
.and_then(|t| capture_facet(project, t.oa, t.sa, &facet).ok())
|
||||
.and_then(|f| scalar::value(&f, self.direction.target_layer()))
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
self.presence
|
||||
.entry(facet.clone())
|
||||
.or_insert(target_presence);
|
||||
}
|
||||
self.decisions.insert(facet, decision);
|
||||
Ok(())
|
||||
}
|
||||
@@ -187,16 +268,9 @@ impl State {
|
||||
Event::Shape(path, value) => {
|
||||
self.edit(project, &path, |d| d.shape_scoped(&path, value))
|
||||
}
|
||||
Event::Variant(path, value) => self.edit(project, &path, |d| {
|
||||
if d.value
|
||||
.pointer(&path)
|
||||
.is_some_and(|v| crate::forms::same_variant(v, &value))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
d.shape_scoped(&path, value)
|
||||
}
|
||||
}),
|
||||
Event::Variant(path, value) => {
|
||||
self.edit(project, &path, |d| d.variant_scoped(&path, value))
|
||||
}
|
||||
Event::Array(path, index, direction) => {
|
||||
self.edit(project, &path, |d| d.array_scoped(&path, index, direction))
|
||||
}
|
||||
@@ -258,6 +332,57 @@ impl State {
|
||||
return;
|
||||
}
|
||||
Event::Preview => Ok(()),
|
||||
Event::Reset(facet, reset) => self.reset_history(project, facet, reset),
|
||||
Event::Presence(facet, present) => {
|
||||
if keyed::supported(&facet)
|
||||
&& self.decisions.get(&facet) == Some(&SemanticDecision::Candidate)
|
||||
{
|
||||
self.presence.insert(facet, present);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Choose Set final value for this member before editing presence.".into())
|
||||
}
|
||||
}
|
||||
Event::MemberFamily(family) => {
|
||||
self.member_family = family;
|
||||
replan = false;
|
||||
Ok(())
|
||||
}
|
||||
Event::MemberOa(choice) => {
|
||||
self.member_oa = choice;
|
||||
replan = false;
|
||||
Ok(())
|
||||
}
|
||||
Event::MemberSa(choice) => {
|
||||
self.member_sa = choice;
|
||||
replan = false;
|
||||
Ok(())
|
||||
}
|
||||
Event::Literal(value) => {
|
||||
self.literal = value;
|
||||
replan = false;
|
||||
Ok(())
|
||||
}
|
||||
Event::LiteralSide(layer, present) => {
|
||||
if layer == Layer::Oa {
|
||||
self.literal_oa = present;
|
||||
} else {
|
||||
self.literal_sa = present;
|
||||
}
|
||||
replan = false;
|
||||
Ok(())
|
||||
}
|
||||
Event::EditPair(facet) => {
|
||||
replan = false;
|
||||
self.edit_pair(project, facet)
|
||||
}
|
||||
Event::StageMember => self.stage_member(project),
|
||||
Event::DropStaged(facet) => {
|
||||
if self.staged.remove(&facet) {
|
||||
self.decisions.remove(&facet);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
if let Err(error) = result {
|
||||
self.error = Some(error);
|
||||
@@ -281,15 +406,23 @@ impl State {
|
||||
if self.decisions.get(facet) != Some(&SemanticDecision::Candidate) {
|
||||
return Err("Choose Set final value before editing this property.".into());
|
||||
}
|
||||
let root = self
|
||||
let roots = self
|
||||
.target(project)
|
||||
.and_then(|id| scalar::binding(project, id, facet))
|
||||
.ok_or("Selected scalar property is unavailable.")?;
|
||||
if path != root && !path.starts_with(&format!("{root}/")) {
|
||||
return Err("Only the selected scalar property can change in this draft.".into());
|
||||
.and_then(|id| constructors::roots(project, id, facet))
|
||||
.ok_or("Selected property is unavailable.")?;
|
||||
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());
|
||||
}
|
||||
let draft = self.draft.as_mut().ok_or("Choose an existing target.")?;
|
||||
// The scoped view sends only descendants of the selected scalar property.
|
||||
let draft = if scalar::grouped(facet) {
|
||||
self.group_drafts.get_mut(facet)
|
||||
} else {
|
||||
self.draft.as_mut()
|
||||
}
|
||||
.ok_or("Choose an existing target.")?;
|
||||
// The scoped view sends only the selected property or closed group roots.
|
||||
// Protected identity and top-level body/perspective controls are never exposed.
|
||||
if path == "/id" || path == "/alias" || path == "/body" || path == "/layer" {
|
||||
return Err("Semantic candidates retain identity, alias and perspective.".into());
|
||||
@@ -298,17 +431,35 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn editable(&self, project: &Project, facet: &FacetSelector) -> bool {
|
||||
if keyed::supported(facet) {
|
||||
return self
|
||||
.effective_trace(project)
|
||||
.is_some_and(|trace| capture_facet(project, trace.oa, trace.sa, facet).is_ok());
|
||||
}
|
||||
self.target(project)
|
||||
.and_then(|id| scalar::binding(project, id, facet))
|
||||
.is_some()
|
||||
&& !scalar::grouped(facet)
|
||||
&& (!scalar::grouped(facet)
|
||||
|| (self.group_drafts.contains_key(facet)
|
||||
&& self
|
||||
.trace
|
||||
.and_then(|id| project.traces.get(&id))
|
||||
.is_some_and(|trace| constructors::available(project, trace, facet))))
|
||||
}
|
||||
|
||||
fn candidate_draft(&self, facet: &FacetSelector) -> Option<&Draft> {
|
||||
if scalar::grouped(facet) {
|
||||
self.group_drafts.get(facet)
|
||||
} else {
|
||||
self.draft.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_acknowledge_constructor(&self, project: &Project, facet: &FacetSelector) -> bool {
|
||||
if !scalar::grouped(facet) {
|
||||
return false;
|
||||
}
|
||||
let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else {
|
||||
let Some(trace) = self.effective_trace(project) else {
|
||||
return false;
|
||||
};
|
||||
if trace
|
||||
@@ -319,13 +470,12 @@ impl State {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.facts
|
||||
.iter()
|
||||
.find(|f| f.selector == *facet)
|
||||
.is_some_and(|f| {
|
||||
scalar::value(&f.current, Layer::Oa).is_some()
|
||||
&& scalar::observation(&f.current, Layer::Oa)
|
||||
== scalar::observation(&f.current, Layer::Sa)
|
||||
capture_facet(project, trace.oa, trace.sa, facet)
|
||||
.ok()
|
||||
.is_some_and(|current| {
|
||||
scalar::value(¤t, Layer::Oa).is_some()
|
||||
&& scalar::observation(¤t, Layer::Oa)
|
||||
== scalar::observation(¤t, Layer::Sa)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -358,7 +508,7 @@ impl State {
|
||||
decision: *decision,
|
||||
})
|
||||
.collect(),
|
||||
reset: vec![],
|
||||
reset: self.resets.iter().cloned().collect(),
|
||||
});
|
||||
request.accepted_impacts = self.accepted_impacts.clone();
|
||||
if self
|
||||
@@ -383,14 +533,11 @@ impl State {
|
||||
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;
|
||||
/// request construction always requires every selected input to parse.
|
||||
fn combined_value(&self, project: &Project, form_context: bool) -> Result<Value, String> {
|
||||
let trace = self
|
||||
.trace
|
||||
.and_then(|id| project.traces.get(&id))
|
||||
.ok_or("Choose a trace.")?;
|
||||
let trace = self.effective_trace(project).ok_or("Choose a trace.")?;
|
||||
let (source, target) = if self.direction == TransitionDirection::OaToSa {
|
||||
(trace.oa, trace.sa)
|
||||
} else {
|
||||
@@ -399,7 +546,6 @@ impl State {
|
||||
let mut final_value = Draft::from_project(project, target)
|
||||
.ok_or("Target has no editable record.")?
|
||||
.value;
|
||||
let draft = self.draft.as_ref().ok_or("Target draft is unavailable.")?;
|
||||
let mut map = BTreeMap::new();
|
||||
for (oa, sa) in &self.references {
|
||||
let (from, to) = if self.direction == TransitionDirection::OaToSa {
|
||||
@@ -412,33 +558,79 @@ impl State {
|
||||
}
|
||||
}
|
||||
map.insert(source, target);
|
||||
for (facet, decision) in &self.decisions {
|
||||
let original = final_value.clone();
|
||||
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 {
|
||||
continue;
|
||||
}
|
||||
let path = scalar::binding(project, target, facet)
|
||||
if keyed::supported(facet) {
|
||||
self.keyed_value(project, &trace, &mut final_value, facet, *decision)?;
|
||||
continue;
|
||||
}
|
||||
let roots = constructors::roots(project, target, facet)
|
||||
.ok_or("Selected property is not applicable to the target.")?;
|
||||
let value = match decision {
|
||||
let supplied = match decision {
|
||||
SemanticDecision::KeepTarget => continue,
|
||||
SemanticDecision::Candidate => {
|
||||
let value = match draft.materialize_scopes(std::slice::from_ref(&path)) {
|
||||
let draft = self
|
||||
.candidate_draft(facet)
|
||||
.ok_or("Target draft is unavailable.")?;
|
||||
match draft.materialize_scopes(&roots) {
|
||||
Ok(value) => value,
|
||||
Err(_) if form_context => draft.value.clone(),
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
value
|
||||
.pointer(&path)
|
||||
.cloned()
|
||||
.ok_or("Candidate property is no longer available.")?
|
||||
}
|
||||
}
|
||||
SemanticDecision::TakeSource if scalar::grouped(facet) => {
|
||||
if self.can_acknowledge_constructor(project, facet) {
|
||||
original.clone()
|
||||
} else {
|
||||
constructors::take_value(project, &trace, self.direction, facet, &map)?
|
||||
}
|
||||
}
|
||||
SemanticDecision::TakeSource => {
|
||||
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
|
||||
}
|
||||
};
|
||||
*final_value
|
||||
.pointer_mut(&path)
|
||||
.ok_or("Final target property is unavailable.")? = value;
|
||||
for path in roots {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if !form_context {
|
||||
for (path, authored) in group_candidates {
|
||||
if !original
|
||||
.pointer(&path)
|
||||
.zip(final_value.pointer(&path))
|
||||
.is_some_and(|(original, final_value)| {
|
||||
constructors::authored_agrees(original, &authored, final_value)
|
||||
})
|
||||
{
|
||||
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)
|
||||
}
|
||||
@@ -449,17 +641,48 @@ impl State {
|
||||
self.error = None;
|
||||
let context = self.combined_value(project, true).ok();
|
||||
if let Some(draft) = &mut self.draft {
|
||||
draft.set_context(context);
|
||||
draft.set_context(context.clone());
|
||||
}
|
||||
let Some(trace) = self.trace.and_then(|id| project.traces.get(&id)) else {
|
||||
for draft in self.group_drafts.values_mut() {
|
||||
draft.set_context(context.clone());
|
||||
}
|
||||
let Some(trace) = self.effective_trace(project) else {
|
||||
self.error = Some(
|
||||
"Several traces are available. Choose the correspondence and review history above."
|
||||
.into(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
match semantic_facets(project, trace, &self.references) {
|
||||
Ok(facts) => self.facts = facts,
|
||||
match semantic_facets(project, &trace, &self.references) {
|
||||
Ok(mut facts) => {
|
||||
// Explicit addresses survive catalog normalization, pairing changes and resets.
|
||||
for selector in self.staged.iter().chain(self.decisions.keys()) {
|
||||
if !facts.iter().any(|f| &f.selector == selector) {
|
||||
match capture_facet(project, trace.oa, trace.sa, selector) {
|
||||
Ok(current) => {
|
||||
let previous = trace
|
||||
.baseline
|
||||
.as_ref()
|
||||
.and_then(|b| b.semantic.as_ref())
|
||||
.and_then(|b| {
|
||||
b.facets.iter().find(|f| f.selector() == *selector)
|
||||
});
|
||||
facts.push(FacetDrift {
|
||||
selector: selector.clone(),
|
||||
drift: facet_drift(¤t, previous),
|
||||
current,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
facts.sort_by(|a, b| a.selector.cmp(&b.selector));
|
||||
self.facts = facts;
|
||||
}
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
return;
|
||||
@@ -499,8 +722,11 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn command(&mut self, project: &Project) -> Result<Command, String> {
|
||||
if self.decisions.is_empty() {
|
||||
return Err("Choose at least one property to review.".into());
|
||||
if self.decisions.is_empty() && self.resets.is_empty() {
|
||||
return Err(
|
||||
"Choose at least one property to review or an exact history observation to reset."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if let Some(error) = &self.error {
|
||||
return Err(error.clone());
|
||||
@@ -514,7 +740,7 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn can_apply(&self) -> bool {
|
||||
!self.decisions.is_empty()
|
||||
(!self.decisions.is_empty() || !self.resets.is_empty())
|
||||
&& self.error.is_none()
|
||||
&& self
|
||||
.plan
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn resettable(facet: &FacetSelector) -> bool {
|
||||
!matches!(facet, FacetSelector::Artifact { .. })
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(super) fn effective_trace(&self, project: &Project) -> Option<Trace> {
|
||||
let mut trace = project.traces.get(&self.trace?)?.clone();
|
||||
if let Some(baseline) = trace.baseline.as_mut().and_then(|b| b.semantic.as_mut()) {
|
||||
baseline
|
||||
.facets
|
||||
.retain(|f| !self.resets.contains(&f.selector()));
|
||||
}
|
||||
Some(trace)
|
||||
}
|
||||
|
||||
pub(super) fn reset_history(
|
||||
&mut self,
|
||||
project: &Project,
|
||||
facet: FacetSelector,
|
||||
reset: bool,
|
||||
) -> Result<(), String> {
|
||||
if !reset && self.resets.remove(&facet) {
|
||||
return Ok(());
|
||||
}
|
||||
let exists = self
|
||||
.trace
|
||||
.and_then(|id| project.traces.get(&id))
|
||||
.and_then(|t| t.baseline.as_ref())
|
||||
.and_then(|b| b.semantic.as_ref())
|
||||
.is_some_and(|b| b.facets.iter().any(|f| f.selector() == facet));
|
||||
if !exists || !resettable(&facet) {
|
||||
return Err("Choose an exact existing non-artifact observation to reset.".into());
|
||||
}
|
||||
if reset {
|
||||
self.resets.insert(facet);
|
||||
} else {
|
||||
self.resets.remove(&facet);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! Closed presence-only adapter. The core independently validates the full final record.
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Family {
|
||||
Member(ReferenceCollection),
|
||||
Literal,
|
||||
}
|
||||
impl std::fmt::Display for Family {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Member(ReferenceCollection::FunctionInputItems) => "Function input items",
|
||||
Self::Member(ReferenceCollection::FunctionOutputItems) => "Function output items",
|
||||
Self::Member(ReferenceCollection::SignalItems) => "Signal items",
|
||||
Self::Member(ReferenceCollection::ExchangeItems) => "Exchange items",
|
||||
Self::Member(ReferenceCollection::TransitionTriggers) => "Transition triggers",
|
||||
Self::Member(ReferenceCollection::TransitionEffects) => "Transition effects",
|
||||
Self::Member(ReferenceCollection::Constrained) => "Constrained identities",
|
||||
Self::Literal => "Enumeration literals",
|
||||
_ => "Ordered membership (read-only)",
|
||||
})
|
||||
}
|
||||
}
|
||||
pub(super) const FAMILIES: [Family; 8] = [
|
||||
Family::Member(ReferenceCollection::FunctionInputItems),
|
||||
Family::Member(ReferenceCollection::FunctionOutputItems),
|
||||
Family::Member(ReferenceCollection::SignalItems),
|
||||
Family::Member(ReferenceCollection::ExchangeItems),
|
||||
Family::Member(ReferenceCollection::TransitionTriggers),
|
||||
Family::Member(ReferenceCollection::TransitionEffects),
|
||||
Family::Member(ReferenceCollection::Constrained),
|
||||
Family::Literal,
|
||||
];
|
||||
pub(super) fn family(facet: &FacetSelector) -> Option<Family> {
|
||||
let family = match facet {
|
||||
FacetSelector::Member { collection, .. } => Family::Member(*collection),
|
||||
FacetSelector::Literal(_) => Family::Literal,
|
||||
_ => return None,
|
||||
};
|
||||
FAMILIES.contains(&family).then_some(family)
|
||||
}
|
||||
pub(super) fn supported(facet: &FacetSelector) -> bool {
|
||||
family(facet).is_some()
|
||||
}
|
||||
|
||||
// Fixed paths for the seven unordered collections and the exact enum literal set.
|
||||
// They are selected by the typed facet; callers cannot supply arbitrary paths.
|
||||
pub(super) fn path(value: &Value, family: Family) -> Option<String> {
|
||||
let prefixes = ["/body/Oa/Shared", "/body/Sa/Shared"];
|
||||
let tails: &[&str] = match family {
|
||||
Family::Member(ReferenceCollection::FunctionInputItems) => {
|
||||
&["Port/contract/FunctionInput/incoming_items"]
|
||||
}
|
||||
Family::Member(ReferenceCollection::FunctionOutputItems) => {
|
||||
&["Port/contract/FunctionOutput/outgoing_items"]
|
||||
}
|
||||
Family::Member(ReferenceCollection::SignalItems) => &["Event/body/Signal/items"],
|
||||
Family::Member(ReferenceCollection::Constrained) => &["Constraint/constrained"],
|
||||
Family::Literal => &["DataType/definition/Enumeration/literals"],
|
||||
Family::Member(ReferenceCollection::ExchangeItems) => {
|
||||
return ["Interaction", "FunctionalExchange", "BoundaryExchange"]
|
||||
.iter()
|
||||
.map(|kind| format!("/body/{kind}/items"))
|
||||
.find(|p| value.pointer(p).is_some_and(Value::is_array));
|
||||
}
|
||||
Family::Member(ReferenceCollection::TransitionTriggers) => {
|
||||
return value
|
||||
.pointer("/body/StateTransition/triggers")
|
||||
.is_some_and(Value::is_array)
|
||||
.then(|| "/body/StateTransition/triggers".into());
|
||||
}
|
||||
Family::Member(ReferenceCollection::TransitionEffects) => {
|
||||
return value
|
||||
.pointer("/body/StateTransition/effects")
|
||||
.is_some_and(Value::is_array)
|
||||
.then(|| "/body/StateTransition/effects".into());
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
prefixes
|
||||
.iter()
|
||||
.flat_map(|prefix| tails.iter().map(move |tail| format!("{prefix}/{tail}")))
|
||||
.find(|p| value.pointer(p).is_some_and(Value::is_array))
|
||||
}
|
||||
fn key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
|
||||
match facet {
|
||||
FacetSelector::Member { keys, .. } => if layer == Layer::Oa { keys.oa } else { keys.sa }
|
||||
.map(|id| serde_json::to_value(id).expect("Id")),
|
||||
FacetSelector::Literal(keys) => if layer == Layer::Oa {
|
||||
keys.oa.as_ref()
|
||||
} else {
|
||||
keys.sa.as_ref()
|
||||
}
|
||||
.map(|v| Value::String(v.clone())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub(super) fn overlap(a: &FacetSelector, b: &FacetSelector) -> bool {
|
||||
if family(a).is_none() || family(a) != family(b) {
|
||||
return false;
|
||||
}
|
||||
[Layer::Oa, Layer::Sa]
|
||||
.into_iter()
|
||||
.any(|layer| key(a, layer).is_some() && key(a, layer) == key(b, layer))
|
||||
}
|
||||
pub(super) fn project_presence(
|
||||
value: &mut Value,
|
||||
facet: &FacetSelector,
|
||||
layer: Layer,
|
||||
present: bool,
|
||||
) -> Result<(), String> {
|
||||
let family = family(facet).ok_or("This keyed family remains read-only.")?;
|
||||
let key = key(facet, layer)
|
||||
.ok_or("Choose the destination identity in Members before setting or taking presence.")?;
|
||||
let path =
|
||||
path(value, family).ok_or("This collection is not applicable to the destination kind.")?;
|
||||
let list = value
|
||||
.pointer_mut(&path)
|
||||
.and_then(Value::as_array_mut)
|
||||
.ok_or("Destination collection is unavailable.")?;
|
||||
if present {
|
||||
if !list.contains(&key) {
|
||||
list.push(key);
|
||||
}
|
||||
} else {
|
||||
list.retain(|v| v != &key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub(super) fn keyed_value(
|
||||
&self,
|
||||
project: &Project,
|
||||
trace: &Trace,
|
||||
value: &mut Value,
|
||||
facet: &FacetSelector,
|
||||
decision: SemanticDecision,
|
||||
) -> Result<(), String> {
|
||||
let present = match decision {
|
||||
SemanticDecision::KeepTarget => return Ok(()),
|
||||
SemanticDecision::TakeSource => {
|
||||
// A missing source identity is a target-local row, not a removal.
|
||||
if key(facet, self.direction.source_layer()).is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let fact = capture_facet(project, trace.oa, trace.sa, facet)?;
|
||||
scalar::value(&fact, self.direction.source_layer()).and_then(|v| v.as_bool()).ok_or("Source collection is not applicable; choose an explicit final presence or Keep.")?
|
||||
}
|
||||
SemanticDecision::Candidate => *self
|
||||
.presence
|
||||
.get(facet)
|
||||
.ok_or("Choose final membership presence.")?,
|
||||
};
|
||||
project_presence(value, facet, self.direction.target_layer(), present)
|
||||
}
|
||||
|
||||
pub(super) fn member_families(&self, project: &Project) -> Vec<Family> {
|
||||
let Some(trace) = self.effective_trace(project) else {
|
||||
return vec![];
|
||||
};
|
||||
let values: Vec<_> = [trace.oa, trace.sa]
|
||||
.into_iter()
|
||||
.filter_map(|id| Draft::from_project(project, id).map(|d| d.value))
|
||||
.collect();
|
||||
FAMILIES
|
||||
.into_iter()
|
||||
.filter(|family| values.iter().any(|value| path(value, *family).is_some()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn member_choices(&self, project: &Project, layer: Layer) -> Vec<Choice> {
|
||||
let Some(trace) = self.effective_trace(project) else {
|
||||
return vec![];
|
||||
};
|
||||
let id = if layer == Layer::Oa {
|
||||
trace.oa
|
||||
} else {
|
||||
trace.sa
|
||||
};
|
||||
let Some(mut draft) = Draft::from_project(project, id) else {
|
||||
return vec![];
|
||||
};
|
||||
if layer == self.direction.target_layer() {
|
||||
draft.set_context(self.combined_value(project, true).ok());
|
||||
}
|
||||
let selected = if layer == Layer::Oa {
|
||||
&self.member_oa
|
||||
} else {
|
||||
&self.member_sa
|
||||
};
|
||||
let Some(path) = path(&draft.context_value(), self.member_family) else {
|
||||
return selected.iter().cloned().collect();
|
||||
};
|
||||
crate::forms::reference_choices(
|
||||
&draft,
|
||||
project,
|
||||
&format!("{path}/0"),
|
||||
selected.as_ref().map_or(Id::nil(), |c| c.id),
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|c| !c.id.is_nil())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn edit_pair(
|
||||
&mut self,
|
||||
project: &Project,
|
||||
facet: FacetSelector,
|
||||
) -> Result<(), String> {
|
||||
self.member_family = family(&facet).ok_or("This keyed family remains read-only.")?;
|
||||
match facet {
|
||||
FacetSelector::Member { keys, .. } => {
|
||||
self.member_oa = keys.oa.map(|id| view::choice(project, id));
|
||||
self.member_sa = keys.sa.map(|id| view::choice(project, id));
|
||||
}
|
||||
FacetSelector::Literal(keys) => {
|
||||
self.literal_oa = keys.oa.is_some();
|
||||
self.literal_sa = keys.sa.is_some();
|
||||
self.literal = keys.oa.or(keys.sa).unwrap_or_default();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.tab = Tab::Members;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn stage_member(&mut self, project: &Project) -> Result<(), String> {
|
||||
if !FAMILIES.contains(&self.member_family) {
|
||||
return Err("This keyed family remains read-only.".into());
|
||||
}
|
||||
let facet = match self.member_family {
|
||||
Family::Member(collection) => FacetSelector::Member {
|
||||
collection,
|
||||
keys: KeyPair {
|
||||
oa: self.member_oa.as_ref().map(|c| c.id),
|
||||
sa: self.member_sa.as_ref().map(|c| c.id),
|
||||
},
|
||||
},
|
||||
Family::Literal => FacetSelector::Literal(KeyPair {
|
||||
oa: self.literal_oa.then(|| self.literal.clone()),
|
||||
sa: self.literal_sa.then(|| self.literal.clone()),
|
||||
}),
|
||||
};
|
||||
let trace = self.effective_trace(project).ok_or("Choose a trace.")?;
|
||||
capture_facet(project, trace.oa, trace.sa, &facet)?;
|
||||
if let FacetSelector::Member {
|
||||
keys:
|
||||
KeyPair {
|
||||
oa: Some(oa),
|
||||
sa: Some(sa),
|
||||
},
|
||||
..
|
||||
} = &facet
|
||||
{
|
||||
self.references.insert(*oa, *sa);
|
||||
}
|
||||
self.staged.insert(facet.clone());
|
||||
self.selected = Some(facet);
|
||||
self.tab = Tab::Facts;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
use super::{tests::*, *};
|
||||
use crate::{app::App, document::Document};
|
||||
|
||||
fn errors(s: &State) -> String {
|
||||
s.error
|
||||
.iter()
|
||||
.chain(s.plan.iter().flat_map(|p| &p.conflicts))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
}
|
||||
fn put(p: &mut Project, owner: Id, facet: &FacetSelector, present: bool) {
|
||||
let layer = p.layer_of(owner).unwrap();
|
||||
let mut value = Draft::from_project(p, owner).unwrap().value;
|
||||
keyed::project_presence(&mut value, facet, layer, present).unwrap();
|
||||
if let Some(element) = p.elements.get_mut(&owner) {
|
||||
*element = serde_json::from_value(value).unwrap();
|
||||
} else {
|
||||
p.relations
|
||||
.insert(owner, serde_json::from_value(value).unwrap());
|
||||
}
|
||||
}
|
||||
fn contains(p: &Project, owner: Id, facet: &FacetSelector, oa: Id, sa: Id) -> bool {
|
||||
scalar::value(
|
||||
&capture_facet(p, oa, sa, facet).unwrap(),
|
||||
p.layer_of(owner).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
.as_bool()
|
||||
.unwrap()
|
||||
}
|
||||
fn history(p: &mut Project, trace: Id, selectors: &[FacetSelector]) {
|
||||
let t = &p.traces[&trace];
|
||||
let baseline = TraceBaseline {
|
||||
oa: p.properties(t.oa).unwrap(),
|
||||
sa: p.properties(t.sa).unwrap(),
|
||||
semantic: Some(SemanticBaseline {
|
||||
facets: selectors
|
||||
.iter()
|
||||
.map(|s| capture_facet(p, t.oa, t.sa, s).unwrap())
|
||||
.collect(),
|
||||
}),
|
||||
};
|
||||
p.traces.get_mut(&trace).unwrap().baseline = Some(baseline);
|
||||
}
|
||||
fn literal(name: &str) -> FacetSelector {
|
||||
FacetSelector::Literal(KeyPair {
|
||||
oa: Some(name.into()),
|
||||
sa: Some(name.into()),
|
||||
})
|
||||
}
|
||||
fn enum_pair() -> (Project, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let mut ids = vec![];
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let id = p.add_element(Kind::DataType, layer, "Enum", None).unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
||||
definition: DataTypeDefinition::Enumeration {
|
||||
literals: vec!["retained".into()],
|
||||
},
|
||||
default: Some(DataValue::Enumeration("retained".into())),
|
||||
unit: String::new(),
|
||||
};
|
||||
ids.push(id);
|
||||
}
|
||||
let trace = add_trace(&mut p, ids[0], ids[1], "Literal review");
|
||||
(p, ids[0], ids[1], trace)
|
||||
}
|
||||
fn member_pair(collection: ReferenceCollection) -> (Project, Id, Id, Id, FacetSelector, [Id; 2]) {
|
||||
let mut p = Project::empty();
|
||||
let mut owners = vec![];
|
||||
let mut keys = vec![];
|
||||
let mut locals = vec![];
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let work = if layer == Layer::Oa {
|
||||
Kind::Activity
|
||||
} else {
|
||||
Kind::Function
|
||||
};
|
||||
let kind = match collection {
|
||||
ReferenceCollection::TransitionTriggers => Kind::Event,
|
||||
ReferenceCollection::TransitionEffects | ReferenceCollection::Constrained => work,
|
||||
_ => Kind::ExchangeItem,
|
||||
};
|
||||
let key = p.add_element(kind, layer, "Mapped member", None).unwrap();
|
||||
let local = p.add_element(kind, layer, "Local member", None).unwrap();
|
||||
let owner = match collection {
|
||||
ReferenceCollection::SignalItems => {
|
||||
let id = p.add_element(Kind::Event, layer, "Signal", None).unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Event {
|
||||
body: EventBody::Signal { items: vec![] },
|
||||
};
|
||||
id
|
||||
}
|
||||
ReferenceCollection::Constrained => p
|
||||
.add_element(Kind::Constraint, layer, "Constraint", None)
|
||||
.unwrap(),
|
||||
ReferenceCollection::ExchangeItems => {
|
||||
let a = p.add_element(work, layer, "From", None).unwrap();
|
||||
let b = p.add_element(work, layer, "To", None).unwrap();
|
||||
let id = p.relate(
|
||||
if layer == Layer::Oa {
|
||||
RelationKind::Interaction
|
||||
} else {
|
||||
RelationKind::FunctionalExchange
|
||||
},
|
||||
"Exchange",
|
||||
a,
|
||||
b,
|
||||
);
|
||||
if let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = p.relations[&id].body
|
||||
{
|
||||
for port in [source_port, target_port] {
|
||||
if let Some(SharedBody::Port { contract, .. }) =
|
||||
p.elements.get_mut(&port).unwrap().shared_mut()
|
||||
{
|
||||
match contract {
|
||||
PortContract::FunctionInput { incoming_items } => {
|
||||
*incoming_items = vec![key, local]
|
||||
}
|
||||
PortContract::FunctionOutput { outgoing_items } => {
|
||||
*outgoing_items = vec![key, local]
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
ReferenceCollection::TransitionTriggers | ReferenceCollection::TransitionEffects => {
|
||||
let machine = p
|
||||
.add_element(Kind::StateMachine, layer, "Machine", None)
|
||||
.unwrap();
|
||||
let region = p
|
||||
.add_element(Kind::Region, layer, "Region", Some(machine))
|
||||
.unwrap();
|
||||
let state = p
|
||||
.add_element(Kind::State, layer, "State", Some(region))
|
||||
.unwrap();
|
||||
p.relate(RelationKind::StateTransition, "Transition", state, state)
|
||||
}
|
||||
ReferenceCollection::FunctionInputItems | ReferenceCollection::FunctionOutputItems => {
|
||||
let parent = p.add_element(work, layer, "Owner", None).unwrap();
|
||||
let id = Id::new_v4();
|
||||
let contract = if layer == Layer::Oa {
|
||||
PortContract::Legacy
|
||||
} else if collection == ReferenceCollection::FunctionInputItems {
|
||||
PortContract::FunctionInput {
|
||||
incoming_items: vec![],
|
||||
}
|
||||
} else {
|
||||
PortContract::FunctionOutput {
|
||||
outgoing_items: vec![],
|
||||
}
|
||||
};
|
||||
let mut port = ports::make_port(&p, id, parent, "Port", contract).unwrap();
|
||||
if layer == Layer::Oa
|
||||
&& let Some(SharedBody::Port { legacy, .. }) = port.shared_mut()
|
||||
{
|
||||
*legacy = Some(LegacyPortContract {
|
||||
direction: PortDirection::InOut,
|
||||
interface: None,
|
||||
});
|
||||
}
|
||||
p.elements.insert(id, port);
|
||||
id
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
owners.push(owner);
|
||||
keys.push(key);
|
||||
locals.push(local);
|
||||
}
|
||||
let trace = add_trace(&mut p, owners[0], owners[1], "Membership review");
|
||||
let facet = FacetSelector::Member {
|
||||
collection,
|
||||
keys: KeyPair {
|
||||
oa: Some(keys[0]),
|
||||
sa: Some(keys[1]),
|
||||
},
|
||||
};
|
||||
(
|
||||
p,
|
||||
owners[0],
|
||||
owners[1],
|
||||
trace,
|
||||
facet,
|
||||
[locals[0], locals[1]],
|
||||
)
|
||||
}
|
||||
fn stage(app: &mut App, facet: &FacetSelector) {
|
||||
send(app, Event::EditPair(facet.clone()));
|
||||
send(app, Event::StageMember);
|
||||
assert_eq!(
|
||||
state(app).selected.as_ref(),
|
||||
Some(facet),
|
||||
"{}",
|
||||
errors(state(app))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unordered_members_keep_take_candidate_preserve_other_rows_both_directions() {
|
||||
for collection in [
|
||||
ReferenceCollection::SignalItems,
|
||||
ReferenceCollection::ExchangeItems,
|
||||
ReferenceCollection::TransitionTriggers,
|
||||
ReferenceCollection::TransitionEffects,
|
||||
ReferenceCollection::Constrained,
|
||||
] {
|
||||
for reverse in [false, true] {
|
||||
for decision in [
|
||||
SemanticDecision::KeepTarget,
|
||||
SemanticDecision::TakeSource,
|
||||
SemanticDecision::Candidate,
|
||||
] {
|
||||
let (mut p, oa, sa, trace, facet, locals) = member_pair(collection);
|
||||
let (source, target, local) = if reverse {
|
||||
(sa, oa, locals[0])
|
||||
} else {
|
||||
(oa, sa, locals[1])
|
||||
};
|
||||
put(&mut p, source, &facet, true);
|
||||
let local_facet = FacetSelector::Member {
|
||||
collection,
|
||||
keys: KeyPair {
|
||||
oa: reverse.then_some(local),
|
||||
sa: (!reverse).then_some(local),
|
||||
},
|
||||
};
|
||||
put(&mut p, target, &local_facet, true);
|
||||
let mut app = App::with_project(p);
|
||||
let before = app.document.snapshot();
|
||||
start(&mut app, source);
|
||||
stage(&mut app, &facet);
|
||||
decide(&mut app, facet.clone(), decision);
|
||||
if decision == SemanticDecision::Candidate {
|
||||
send(&mut app, Event::Presence(facet.clone(), true));
|
||||
}
|
||||
// A target-only Take is an explicit acknowledgment, never implicit removal.
|
||||
decide(&mut app, local_facet.clone(), SemanticDecision::TakeSource);
|
||||
assert!(
|
||||
state(&app).can_apply(),
|
||||
"{collection:?} reverse={reverse} {decision:?}: {}",
|
||||
errors(state(&app))
|
||||
);
|
||||
let proposed = state(&app).proposed.clone().unwrap();
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
apply(&mut app);
|
||||
assert!(app.workflow.is_none(), "{}", app.status);
|
||||
assert_eq!(app.project(), &proposed);
|
||||
assert_eq!(
|
||||
contains(app.project(), target, &facet, oa, sa),
|
||||
decision != SemanticDecision::KeepTarget
|
||||
);
|
||||
assert!(contains(app.project(), target, &local_facet, oa, sa));
|
||||
assert!(app.project().elements.contains_key(&local));
|
||||
assert_eq!(
|
||||
app.project().traces[&trace]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_port_members_use_existing_typed_choices_and_preserve_oa_legacy() {
|
||||
for collection in [
|
||||
ReferenceCollection::FunctionInputItems,
|
||||
ReferenceCollection::FunctionOutputItems,
|
||||
] {
|
||||
let (p, oa, sa, _, facet, _) = member_pair(collection);
|
||||
let FacetSelector::Member { keys, .. } = &facet else {
|
||||
unreachable!()
|
||||
};
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, oa);
|
||||
assert_eq!(
|
||||
state(&app).member_families(app.project()),
|
||||
vec![keyed::Family::Member(collection)]
|
||||
);
|
||||
send(&mut app, Event::EditPair(facet.clone()));
|
||||
assert!(
|
||||
state(&app)
|
||||
.member_choices(app.project(), Layer::Sa)
|
||||
.iter()
|
||||
.any(|c| Some(c.id) == keys.sa)
|
||||
);
|
||||
send(&mut app, Event::StageMember);
|
||||
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(facet.clone(), true));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
apply(&mut app);
|
||||
assert!(contains(app.project(), sa, &facet, oa, sa));
|
||||
assert!(matches!(
|
||||
app.project().elements[&oa].shared(),
|
||||
Some(SharedBody::Port {
|
||||
contract: PortContract::Legacy,
|
||||
..
|
||||
})
|
||||
));
|
||||
start(&mut app, sa);
|
||||
stage(&mut app, &facet);
|
||||
decide(&mut app, facet.clone(), SemanticDecision::KeepTarget);
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(facet, true));
|
||||
assert!(
|
||||
!state(&app).can_apply(),
|
||||
"OA typed membership must remain inapplicable"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_literal_new_rows_and_removals_commit_both_directions_without_default_loss() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, oa, sa, _) = enum_pair();
|
||||
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
||||
let incoming = literal(" New value ");
|
||||
put(&mut p, source, &incoming, true);
|
||||
let removed = literal("old");
|
||||
put(&mut p, target, &removed, true);
|
||||
let created = literal("new value");
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, source);
|
||||
for (facet, decision) in [
|
||||
(&incoming, SemanticDecision::TakeSource),
|
||||
(&removed, SemanticDecision::Candidate),
|
||||
(&created, SemanticDecision::Candidate),
|
||||
] {
|
||||
stage(&mut app, facet);
|
||||
decide(&mut app, facet.clone(), decision);
|
||||
if decision == SemanticDecision::Candidate {
|
||||
send(&mut app, Event::Presence(facet.clone(), facet == &created));
|
||||
}
|
||||
}
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
apply(&mut app);
|
||||
assert!(contains(app.project(), target, &incoming, oa, sa));
|
||||
assert!(contains(app.project(), target, &created, oa, sa));
|
||||
assert!(!contains(app.project(), target, &removed, oa, sa));
|
||||
assert!(
|
||||
matches!(app.project().elements[&target].shared(),Some(SharedBody::DataType { default: Some(DataValue::Enumeration(value)), .. }) if value=="retained")
|
||||
);
|
||||
let reopened = Document::from_bundle(
|
||||
app.document.bundle().clone(),
|
||||
source_project::ProjectLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(reopened.project(), app.project());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_reset_only_is_one_history_source_transaction_and_undo_redo_both_directions() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, oa, sa, trace) = enum_pair();
|
||||
let reset = literal("retained");
|
||||
let kept = FacetSelector::Header(HeaderFacet::Name);
|
||||
history(&mut p, trace, &[reset.clone(), kept.clone()]);
|
||||
let previous = p.traces[&trace].baseline.clone().unwrap();
|
||||
let mut app = App::with_project(p);
|
||||
let mut bundle = app.document.bundle().clone();
|
||||
bundle
|
||||
.files
|
||||
.get_mut(&bundle.entry)
|
||||
.unwrap()
|
||||
.insert_str(0, "// Exact review rationale survives reset.\n");
|
||||
app.document.apply_bundle(bundle).unwrap();
|
||||
let before = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
start(&mut app, if reverse { sa } else { oa });
|
||||
send(&mut app, Event::Reset(reset.clone(), true));
|
||||
assert!(state(&app).decisions.is_empty());
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
assert_eq!(
|
||||
state(&app)
|
||||
.facts
|
||||
.iter()
|
||||
.find(|f| f.selector == reset)
|
||||
.unwrap()
|
||||
.drift,
|
||||
Drift::Unreviewed
|
||||
);
|
||||
send(&mut app, Event::Reset(reset.clone(), false));
|
||||
assert!(!state(&app).can_apply());
|
||||
assert_eq!(
|
||||
state(&app)
|
||||
.facts
|
||||
.iter()
|
||||
.find(|f| f.selector == reset)
|
||||
.unwrap()
|
||||
.drift,
|
||||
Drift::Unchanged
|
||||
);
|
||||
send(&mut app, Event::Reset(reset, true));
|
||||
apply(&mut app);
|
||||
assert_eq!(app.document.revision(), revision + 1);
|
||||
assert_eq!(app.project().elements, before.decoded.project.elements);
|
||||
let after = &app.project().traces[&trace].baseline.as_ref().unwrap();
|
||||
assert_eq!(after.oa, previous.oa);
|
||||
assert_eq!(after.sa, previous.sa);
|
||||
assert_eq!(
|
||||
after
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets
|
||||
.iter()
|
||||
.map(FacetReview::selector)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![kept]
|
||||
);
|
||||
assert!(
|
||||
app.document.bundle().files[&app.document.bundle().entry]
|
||||
.contains("// Exact review rationale survives reset.")
|
||||
);
|
||||
let accepted = app.document.snapshot();
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &accepted.bundle);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_one_sided_pair_migration_requires_exact_reset_and_explicit_overlap_resolution() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, oa, sa, trace, paired, _) = member_pair(ReferenceCollection::SignalItems);
|
||||
let FacetSelector::Member { collection, keys } = &paired else {
|
||||
unreachable!()
|
||||
};
|
||||
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
||||
let previous = FacetSelector::Member {
|
||||
collection: *collection,
|
||||
keys: KeyPair {
|
||||
oa: if reverse { None } else { keys.oa },
|
||||
sa: if reverse { keys.sa } else { None },
|
||||
},
|
||||
};
|
||||
put(&mut p, source, &paired, true);
|
||||
history(&mut p, trace, std::slice::from_ref(&previous));
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, source);
|
||||
decide(&mut app, previous.clone(), SemanticDecision::KeepTarget);
|
||||
stage(&mut app, &paired);
|
||||
decide(&mut app, paired.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(paired.clone(), true));
|
||||
assert!(!state(&app).can_apply());
|
||||
assert_eq!(state(&app).decisions.len(), 2);
|
||||
send(&mut app, Event::Decide(previous.clone(), None));
|
||||
assert!(
|
||||
!state(&app).can_apply(),
|
||||
"Historic pair cannot silently migrate"
|
||||
);
|
||||
send(&mut app, Event::Reset(previous.clone(), true));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
send(&mut app, Event::Reset(previous, false));
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(state(&app).presence[&paired]);
|
||||
let old = state(&app).resets.clone();
|
||||
assert!(old.is_empty());
|
||||
let historic = app.project().traces[&trace]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets[0]
|
||||
.selector();
|
||||
send(&mut app, Event::Reset(historic, true));
|
||||
apply(&mut app);
|
||||
assert!(contains(app.project(), target, &paired, oa, sa));
|
||||
assert_eq!(
|
||||
app.project().traces[&trace]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets[0]
|
||||
.selector(),
|
||||
paired
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_precedes_equal_constructor_candidate_and_preserves_independent_literal_draft() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, oa, sa, trace) = enum_pair();
|
||||
let ctor = FacetSelector::Data(DataFacet::Constructor);
|
||||
history(&mut p, trace, std::slice::from_ref(&ctor));
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, if reverse { sa } else { oa });
|
||||
let created = literal("separate");
|
||||
stage(&mut app, &created);
|
||||
decide(&mut app, created.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(created.clone(), true));
|
||||
send(&mut app, Event::Reset(ctor.clone(), true));
|
||||
decide(&mut app, ctor.clone(), SemanticDecision::Candidate);
|
||||
assert!(state(&app).can_acknowledge_constructor(app.project(), &ctor));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
let reviewed = &state(&app).plan.as_ref().unwrap().reviews[0];
|
||||
assert_eq!(
|
||||
reviewed
|
||||
.acknowledged
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
BTreeSet::from([ctor.clone(), created.clone()])
|
||||
);
|
||||
assert!(reviewed.retired.is_empty());
|
||||
send(&mut app, Event::Reset(ctor.clone(), false));
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(state(&app).presence[&created]);
|
||||
send(&mut app, Event::Reset(ctor, true));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_literal_removal_and_unselected_scalar_buffers_are_retained_and_repairable() {
|
||||
let (p, oa, _, _) = enum_pair();
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, oa);
|
||||
let unit = FacetSelector::Data(DataFacet::Unit);
|
||||
decide(&mut app, unit.clone(), SemanticDecision::Candidate);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input(
|
||||
"/body/Sa/Shared/DataType/unit".into(),
|
||||
" untouched unit ".into(),
|
||||
),
|
||||
);
|
||||
decide(&mut app, unit.clone(), SemanticDecision::KeepTarget);
|
||||
let retained = literal("retained");
|
||||
decide(&mut app, retained.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(retained.clone(), false));
|
||||
assert!(
|
||||
!state(&app).can_apply(),
|
||||
"Existing default must prevent removal"
|
||||
);
|
||||
assert!(!state(&app).presence[&retained]);
|
||||
send(&mut app, Event::Presence(retained.clone(), true));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
assert_eq!(
|
||||
state(&app).draft.as_ref().unwrap().inputs["/body/Sa/Shared/DataType/unit"].raw,
|
||||
" untouched unit "
|
||||
);
|
||||
decide(&mut app, unit, SemanticDecision::Candidate);
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_independent_families_and_missing_identities_do_not_gain_edit_authority() {
|
||||
let (p, oa, _, _) = enum_pair();
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, oa);
|
||||
let unsupported = FacetSelector::Member {
|
||||
collection: ReferenceCollection::Steps,
|
||||
keys: KeyPair {
|
||||
oa: Some(oa),
|
||||
sa: None,
|
||||
},
|
||||
};
|
||||
decide(&mut app, unsupported.clone(), SemanticDecision::Candidate);
|
||||
assert!(!state(&app).decisions.contains_key(&unsupported));
|
||||
send(&mut app, Event::MemberFamily(keyed::Family::Literal));
|
||||
send(&mut app, Event::Literal("unpaired".into()));
|
||||
send(&mut app, Event::LiteralSide(Layer::Sa, false));
|
||||
send(&mut app, Event::StageMember);
|
||||
let facet = state(&app).selected.clone().unwrap();
|
||||
decide(&mut app, facet.clone(), SemanticDecision::Candidate);
|
||||
assert!(!state(&app).can_apply());
|
||||
send(&mut app, Event::LiteralSide(Layer::Oa, false));
|
||||
send(&mut app, Event::StageMember);
|
||||
assert!(state(&app).error.is_some());
|
||||
assert_eq!(
|
||||
state(&app).decisions.get(&facet),
|
||||
Some(&SemanticDecision::Candidate)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleted_member_source_observation_removes_only_mapped_membership_both_directions() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, oa, sa, trace, facet, locals) = member_pair(ReferenceCollection::SignalItems);
|
||||
let FacetSelector::Member { keys, .. } = &facet else {
|
||||
unreachable!()
|
||||
};
|
||||
let (source, target, deleted, local) = if reverse {
|
||||
(sa, oa, keys.sa.unwrap(), locals[0])
|
||||
} else {
|
||||
(oa, sa, keys.oa.unwrap(), locals[1])
|
||||
};
|
||||
let target_only = FacetSelector::Member {
|
||||
collection: ReferenceCollection::SignalItems,
|
||||
keys: KeyPair {
|
||||
oa: reverse.then_some(local),
|
||||
sa: (!reverse).then_some(local),
|
||||
},
|
||||
};
|
||||
put(&mut p, source, &facet, true);
|
||||
put(&mut p, target, &facet, true);
|
||||
put(&mut p, target, &target_only, true);
|
||||
history(&mut p, trace, std::slice::from_ref(&facet));
|
||||
p = prepare_command(&p, &Command::Delete(deleted))
|
||||
.unwrap()
|
||||
.project;
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, source);
|
||||
stage(&mut app, &facet);
|
||||
decide(&mut app, facet.clone(), SemanticDecision::TakeSource);
|
||||
// Force mixed final-record composition, as well as the core Take projection.
|
||||
let name = FacetSelector::Header(HeaderFacet::Name);
|
||||
decide(&mut app, name, SemanticDecision::Candidate);
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
apply(&mut app);
|
||||
assert!(!contains(app.project(), target, &facet, oa, sa));
|
||||
assert!(contains(app.project(), target, &target_only, oa, sa));
|
||||
let retained = if reverse {
|
||||
keys.oa.unwrap()
|
||||
} else {
|
||||
keys.sa.unwrap()
|
||||
};
|
||||
assert!(app.project().elements.contains_key(&retained));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_or_source_failed_reset_and_membership_apply_retain_exact_choices() {
|
||||
let (mut p, oa, _, trace) = enum_pair();
|
||||
let reset = FacetSelector::Header(HeaderFacet::Name);
|
||||
history(&mut p, trace, std::slice::from_ref(&reset));
|
||||
let mut app = App::with_project(p.clone());
|
||||
start(&mut app, oa);
|
||||
send(&mut app, Event::Reset(reset.clone(), true));
|
||||
app.document
|
||||
.execute(Command::Rename(oa, "Changed after preview".into()))
|
||||
.unwrap();
|
||||
let before = app.document.snapshot();
|
||||
apply(&mut app);
|
||||
assert!(state(&app).error.as_ref().unwrap().contains("stale"));
|
||||
assert!(state(&app).resets.contains(&reset));
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
let mut app = App::with_project(p);
|
||||
let bundle = app.document.bundle().clone();
|
||||
let mut limits = source_project::ProjectLimits::default();
|
||||
limits.model.max_source_bytes = bundle.files.values().map(String::len).sum::<usize>() + 10;
|
||||
app.document = Document::from_bundle(bundle, limits).unwrap();
|
||||
start(&mut app, oa);
|
||||
send(&mut app, Event::Reset(reset.clone(), true));
|
||||
let added = literal(&" exact long literal ".repeat(100));
|
||||
stage(&mut app, &added);
|
||||
decide(&mut app, added.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(added.clone(), true));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
let before = app.document.snapshot();
|
||||
apply(&mut app);
|
||||
assert!(state(&app).error.is_some());
|
||||
assert!(state(&app).resets.contains(&reset));
|
||||
assert_eq!(
|
||||
state(&app).decisions.get(&added),
|
||||
Some(&SemanticDecision::Candidate)
|
||||
);
|
||||
assert!(state(&app).presence[&added]);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert!(!app.document.can_undo());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn member_identity_labels_and_new_tabs_preserve_exact_names_and_deleted_keys() {
|
||||
let (mut p, oa, _, trace, facet, _) = member_pair(ReferenceCollection::SignalItems);
|
||||
let FacetSelector::Member { collection, keys } = &facet else {
|
||||
unreachable!()
|
||||
};
|
||||
p.elements.get_mut(&keys.oa.unwrap()).unwrap().name = "Absent intentionally in name".into();
|
||||
let name = view::title(&p, &facet);
|
||||
assert!(name.contains("Absent intentionally in name"));
|
||||
let deleted1 = Id::new_v4();
|
||||
let deleted2 = Id::new_v4();
|
||||
let old1 = FacetSelector::Member {
|
||||
collection: *collection,
|
||||
keys: KeyPair {
|
||||
oa: Some(deleted1),
|
||||
sa: None,
|
||||
},
|
||||
};
|
||||
let old2 = FacetSelector::Member {
|
||||
collection: *collection,
|
||||
keys: KeyPair {
|
||||
oa: Some(deleted2),
|
||||
sa: None,
|
||||
},
|
||||
};
|
||||
history(&mut p, trace, &[old1.clone(), old2.clone()]);
|
||||
assert!(view::title(&p, &old1).contains(&deleted1.to_string()));
|
||||
assert_ne!(view::title(&p, &old1), view::title(&p, &old2));
|
||||
assert_ne!(
|
||||
view::title(&p, &literal("Absent")),
|
||||
view::title(
|
||||
&p,
|
||||
&FacetSelector::Literal(KeyPair {
|
||||
oa: None,
|
||||
sa: Some("Absent".into())
|
||||
})
|
||||
)
|
||||
);
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, oa);
|
||||
for tab in [Tab::History, Tab::Members, Tab::Facts] {
|
||||
send(&mut app, Event::Tab(tab));
|
||||
let _ = state(&app).view(app.project());
|
||||
}
|
||||
send(&mut app, Event::Reset(old1.clone(), true));
|
||||
assert!(state(&app).can_apply(), "{}", errors(state(&app)));
|
||||
assert!(!state(&app).facts.iter().any(|f| f.selector == old1));
|
||||
assert!(state(&app).facts.iter().any(|f| f.selector == old2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staging_removal_and_obsolete_reset_recovery_keep_other_decisions_and_raw_presence() {
|
||||
let (mut p, oa, _, trace) = enum_pair();
|
||||
let reset = FacetSelector::Header(HeaderFacet::Name);
|
||||
history(&mut p, trace, std::slice::from_ref(&reset));
|
||||
let mut app = App::with_project(p.clone());
|
||||
start(&mut app, oa);
|
||||
send(&mut app, Event::Reset(reset.clone(), true));
|
||||
let added = literal("staged draft");
|
||||
stage(&mut app, &added);
|
||||
decide(&mut app, added.clone(), SemanticDecision::Candidate);
|
||||
send(&mut app, Event::Presence(added.clone(), true));
|
||||
send(&mut app, Event::DropStaged(added.clone()));
|
||||
assert!(state(&app).decisions.is_empty());
|
||||
assert!(!state(&app).staged.contains(&added));
|
||||
assert!(state(&app).presence[&added]);
|
||||
assert!(state(&app).can_apply());
|
||||
stage(&mut app, &added);
|
||||
decide(&mut app, added.clone(), SemanticDecision::Candidate);
|
||||
assert!(state(&app).presence[&added]);
|
||||
p.traces
|
||||
.get_mut(&trace)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.facets
|
||||
.clear();
|
||||
let crate::workflow::Workflow::Semantic(s) = app.workflow.as_mut().unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
s.refresh(&p);
|
||||
assert!(!s.can_apply());
|
||||
s.tab = Tab::History;
|
||||
let _ = s.view(&p);
|
||||
s.update(&p, Event::Reset(reset, false));
|
||||
assert!(s.can_apply(), "{}", errors(s));
|
||||
assert_eq!(s.decisions.get(&added), Some(&SemanticDecision::Candidate));
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//! The first supported constructor group is the closed nested MessageKind.
|
||||
//! The core owns its acknowledgment/retirement and validation policies.
|
||||
use super::*;
|
||||
|
||||
pub(super) fn selector() -> FacetSelector {
|
||||
FacetSelector::Occurrence(OccurrenceFacet::MessageKind)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn timer_selector() -> FacetSelector {
|
||||
FacetSelector::Occurrence(OccurrenceFacet::TimerExpression)
|
||||
}
|
||||
pub(super) fn handles(facet: &FacetSelector) -> bool {
|
||||
*facet == selector()
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn value(project: &Project, id: Id) -> Option<&MessageKind> {
|
||||
match project.elements.get(&id)?.shared()? {
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||
..
|
||||
} => Some(message_kind),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,717 @@
|
||||
use super::{tests::*, *};
|
||||
use crate::{app::App, document::Document};
|
||||
use serde_json::json;
|
||||
|
||||
fn fixture(oa_kind: MessageKind, sa_kind: MessageKind) -> (Project, Id, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let mut ids = vec![];
|
||||
for (layer, message_kind) in [(Layer::Oa, oa_kind), (Layer::Sa, sa_kind)] {
|
||||
let scenario = p
|
||||
.add_element(Kind::Scenario, layer, "Scenario", None)
|
||||
.unwrap();
|
||||
let id = p
|
||||
.add_element(Kind::Occurrence, layer, "Message", Some(scenario))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
precondition: "retain precondition".into(),
|
||||
postcondition: "retain postcondition".into(),
|
||||
};
|
||||
p.elements
|
||||
.get_mut(&scenario)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(id);
|
||||
ids.push(id);
|
||||
}
|
||||
let trace = add_trace(&mut p, ids[0], ids[1], "Message-kind review");
|
||||
(p, ids[0], ids[1], trace)
|
||||
}
|
||||
fn timer(value: &str) -> MessageKind {
|
||||
MessageKind::Timer {
|
||||
expression: value.into(),
|
||||
}
|
||||
}
|
||||
fn path(layer: &str) -> String {
|
||||
format!("/body/{layer}/Shared/Occurrence/body/Exchange/message_kind")
|
||||
}
|
||||
fn set_kind(p: &mut Project, id: Id, kind: MessageKind) {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { message_kind, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&id).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*message_kind = kind;
|
||||
}
|
||||
fn seed_history(p: &mut Project, trace: Id) {
|
||||
let t = &p.traces[&trace];
|
||||
let baseline = TraceBaseline {
|
||||
oa: p.properties(t.oa).unwrap(),
|
||||
sa: p.properties(t.sa).unwrap(),
|
||||
semantic: Some(SemanticBaseline {
|
||||
facets: [message_kind::selector(), message_kind::timer_selector()]
|
||||
.iter()
|
||||
.map(|selector| capture_facet(p, t.oa, t.sa, selector).unwrap())
|
||||
.collect(),
|
||||
}),
|
||||
};
|
||||
p.traces.get_mut(&trace).unwrap().baseline = Some(baseline);
|
||||
}
|
||||
fn acknowledged(app: &App) -> BTreeSet<FacetSelector> {
|
||||
state(app).plan.as_ref().unwrap().reviews[0]
|
||||
.acknowledged
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
fn failures(app: &App) -> String {
|
||||
state(app)
|
||||
.error
|
||||
.iter()
|
||||
.chain(state(app).plan.iter().flat_map(|p| &p.conflicts))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_take_full_timer_and_scalar_candidate_are_one_source_history_change_both_directions()
|
||||
{
|
||||
for reverse in [false, true] {
|
||||
let expression = " after β ≥ 5 s\n\tkeep exact whitespace ";
|
||||
let (p, oa, sa, trace) = if reverse {
|
||||
fixture(MessageKind::Asynchronous, timer(expression))
|
||||
} else {
|
||||
fixture(timer(expression), MessageKind::Asynchronous)
|
||||
};
|
||||
let (source, target) = if reverse { (sa, oa) } else { (oa, sa) };
|
||||
let mut app = App::with_project(p);
|
||||
let mut bundle = app.document.bundle().clone();
|
||||
bundle
|
||||
.files
|
||||
.get_mut(&bundle.entry)
|
||||
.unwrap()
|
||||
.insert_str(0, "// Keep the engineering review rationale.\r\n");
|
||||
app.document.apply_bundle(bundle).unwrap();
|
||||
let before = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
start(&mut app, source);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::TakeSource,
|
||||
);
|
||||
decide(
|
||||
&mut app,
|
||||
FacetSelector::Header(HeaderFacet::Name),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input("/name".into(), "Reviewed message".into()),
|
||||
);
|
||||
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
||||
assert_eq!(
|
||||
acknowledged(&app),
|
||||
BTreeSet::from([
|
||||
message_kind::selector(),
|
||||
message_kind::timer_selector(),
|
||||
FacetSelector::Header(HeaderFacet::Name)
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
state(&app).plan.as_ref().unwrap().reviews[0]
|
||||
.retired
|
||||
.is_empty()
|
||||
);
|
||||
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
|
||||
let expected = state(&app).proposed.clone().unwrap();
|
||||
assert_eq!(
|
||||
message_kind::value(&expected, target),
|
||||
Some(&timer(expression))
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
apply(&mut app);
|
||||
assert_eq!(app.project(), &expected);
|
||||
assert_eq!(app.document.revision(), revision + 1);
|
||||
assert_eq!(
|
||||
app.project().elements[&source],
|
||||
before.decoded.project.elements[&source]
|
||||
);
|
||||
let saved = app.document.bundle().clone();
|
||||
assert!(saved.files[&saved.entry].starts_with("// Keep the engineering review rationale."));
|
||||
assert_eq!(
|
||||
Document::from_bundle(saved.clone(), source_project::ProjectLimits::default())
|
||||
.unwrap()
|
||||
.snapshot()
|
||||
.decoded
|
||||
.project,
|
||||
expected
|
||||
);
|
||||
let facts = &app.project().traces[&trace]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets;
|
||||
assert_eq!(facts.len(), 3);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
let _ = app.update(Message::Redo);
|
||||
assert_eq!(app.document.bundle(), &saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_candidate_timer_switches_preserve_both_drafts_and_overlap_is_explicit() {
|
||||
let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("original target"));
|
||||
let mut app = App::with_project(p);
|
||||
let before = app.document.snapshot();
|
||||
let kind = path("Sa");
|
||||
let expression = format!("{kind}/Timer/expression");
|
||||
start(&mut app, oa);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::timer_selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input(expression.clone(), "independent scalar timer".into()),
|
||||
);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
assert_eq!(state(&app).decisions.len(), 2);
|
||||
assert!(!state(&app).can_apply());
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input(expression.clone(), " group β timer\n\t ".into()),
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Text(expression.clone(), text_editor::Action::SelectAll),
|
||||
);
|
||||
let cursor = state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.cursor();
|
||||
let selection = state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.selection();
|
||||
send(
|
||||
&mut app,
|
||||
Event::Variant(kind.clone(), json!("Asynchronous")),
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Variant(kind, json!({"Timer":{"expression":""}})),
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.raw,
|
||||
" group β timer\n\t "
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.cursor(),
|
||||
cursor
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.editor
|
||||
.selection(),
|
||||
selection
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app).draft.as_ref().unwrap().inputs[&expression].raw,
|
||||
"independent scalar timer"
|
||||
);
|
||||
assert!(!state(&app).can_apply());
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
// The visible overlap-resolution actions only change the explicitly chosen
|
||||
// decision. Neither independent candidate buffer is discarded.
|
||||
send(&mut app, Event::Decide(message_kind::selector(), None));
|
||||
assert!(state(&app).can_apply());
|
||||
send(
|
||||
&mut app,
|
||||
Event::Decide(message_kind::timer_selector(), None),
|
||||
);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
||||
assert_eq!(
|
||||
acknowledged(&app),
|
||||
BTreeSet::from([message_kind::selector(), message_kind::timer_selector()])
|
||||
);
|
||||
let _ = state(&app).view(app.project());
|
||||
let expected = state(&app).proposed.clone().unwrap();
|
||||
apply(&mut app);
|
||||
assert_eq!(app.project(), &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_keep_historical_switch_retires_timer_and_completed_no_switch_rejects() {
|
||||
let (mut p, oa, sa, trace) = fixture(timer("old OA"), timer("old SA"));
|
||||
seed_history(&mut p, trace);
|
||||
set_kind(&mut p, oa, MessageKind::Asynchronous);
|
||||
set_kind(&mut p, sa, MessageKind::Asynchronous);
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, oa);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::KeepTarget,
|
||||
);
|
||||
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
||||
assert_eq!(
|
||||
acknowledged(&app),
|
||||
BTreeSet::from([message_kind::selector()])
|
||||
);
|
||||
assert_eq!(
|
||||
state(&app).plan.as_ref().unwrap().reviews[0].retired,
|
||||
vec![message_kind::timer_selector()]
|
||||
);
|
||||
apply(&mut app);
|
||||
assert_eq!(
|
||||
app.project().traces[&trace]
|
||||
.baseline
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.semantic
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.facets
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
start(&mut app, oa);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::KeepTarget,
|
||||
);
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(
|
||||
failures(&app).contains("No constructor switch"),
|
||||
"{}",
|
||||
failures(&app)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_reply_and_reference_take_require_explicit_call_mapping_both_directions() {
|
||||
for reverse in [false, true] {
|
||||
let (mut p, oa, sa, _) = if reverse {
|
||||
fixture(MessageKind::Asynchronous, MessageKind::Reply)
|
||||
} else {
|
||||
fixture(MessageKind::Reply, MessageKind::Asynchronous)
|
||||
};
|
||||
let (source, target, layer) = if reverse {
|
||||
(sa, oa, "Oa")
|
||||
} else {
|
||||
(oa, sa, "Sa")
|
||||
};
|
||||
let mut calls = vec![];
|
||||
for message in [oa, sa] {
|
||||
let scenario = p.elements[&message].parent.unwrap();
|
||||
let layer = p.elements[&message].layer();
|
||||
let call = p
|
||||
.add_element(Kind::Occurrence, layer, "Correlated call", Some(scenario))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&call).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange: None,
|
||||
sender: None,
|
||||
receiver: None,
|
||||
message_kind: MessageKind::Call,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
p.elements
|
||||
.get_mut(&scenario)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.insert(0, call);
|
||||
if message == source {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { reply_to, .. },
|
||||
..
|
||||
}) = p.elements.get_mut(&message).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*reply_to = Some(call);
|
||||
}
|
||||
calls.push(call);
|
||||
}
|
||||
let target_call = if reverse { calls[0] } else { calls[1] };
|
||||
let mut app = App::with_project(p);
|
||||
let before = app.document.snapshot();
|
||||
start(&mut app, source);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(&mut app, Event::Variant(path(layer), json!("Reply")));
|
||||
decide(
|
||||
&mut app,
|
||||
FacetSelector::Occurrence(OccurrenceFacet::ReplyTo),
|
||||
SemanticDecision::TakeSource,
|
||||
);
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(
|
||||
failures(&app).contains("reference counterparts"),
|
||||
"{}",
|
||||
failures(&app)
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::ReferenceOa(Choice {
|
||||
id: calls[0],
|
||||
label: "OA call".into(),
|
||||
}),
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::ReferenceSa(Choice {
|
||||
id: calls[1],
|
||||
label: "SA call".into(),
|
||||
}),
|
||||
);
|
||||
send(&mut app, Event::AddReference);
|
||||
assert!(state(&app).can_apply(), "{}", failures(&app));
|
||||
assert_eq!(
|
||||
acknowledged(&app),
|
||||
BTreeSet::from([
|
||||
message_kind::selector(),
|
||||
FacetSelector::Occurrence(OccurrenceFacet::ReplyTo)
|
||||
])
|
||||
);
|
||||
let final_value =
|
||||
serde_json::to_value(&state(&app).proposed.as_ref().unwrap().elements[&target])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
final_value.pointer(&format!(
|
||||
"/body/{layer}/Shared/Occurrence/body/Exchange/reply_to"
|
||||
)),
|
||||
Some(&json!(target_call))
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
apply(&mut app);
|
||||
assert_eq!(
|
||||
message_kind::value(app.project(), target),
|
||||
Some(&MessageKind::Reply)
|
||||
);
|
||||
for call in calls {
|
||||
assert_eq!(
|
||||
app.project().elements[&call],
|
||||
before.decoded.project.elements[&call]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_candidate_closed_kinds_are_core_validated_in_both_directions() {
|
||||
for reverse in [false, true] {
|
||||
for kind in [
|
||||
MessageKind::Asynchronous,
|
||||
MessageKind::Call,
|
||||
MessageKind::Reply,
|
||||
MessageKind::Create,
|
||||
MessageKind::Delete,
|
||||
timer("after 2 s"),
|
||||
] {
|
||||
let (p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
|
||||
let (source, target, layer) = if reverse {
|
||||
(sa, oa, "Oa")
|
||||
} else {
|
||||
(oa, sa, "Sa")
|
||||
};
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, source);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Variant(path(layer), serde_json::to_value(&kind).unwrap()),
|
||||
);
|
||||
assert!(state(&app).can_apply(), "{kind:?}: {}", failures(&app));
|
||||
assert_eq!(
|
||||
message_kind::value(state(&app).proposed.as_ref().unwrap(), target),
|
||||
Some(&kind)
|
||||
);
|
||||
assert_eq!(
|
||||
acknowledged(&app).contains(&message_kind::timer_selector()),
|
||||
matches!(kind, MessageKind::Timer { .. })
|
||||
);
|
||||
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
|
||||
}
|
||||
}
|
||||
let (mut p, oa, _, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
|
||||
let mut review = State::new(&p, oa).unwrap();
|
||||
let Some(SharedBody::Occurrence { body, .. }) = p.elements.get_mut(&oa).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*body = OccurrenceBody::Action {
|
||||
action: None,
|
||||
lifeline: None,
|
||||
};
|
||||
review.refresh(&p);
|
||||
assert!(
|
||||
!review.editable(&p, &message_kind::selector()),
|
||||
"An externally changed outer occurrence requires its own constructor review"
|
||||
);
|
||||
review.update(
|
||||
&p,
|
||||
Event::Decide(message_kind::selector(), Some(SemanticDecision::Candidate)),
|
||||
);
|
||||
assert!(review.decisions.is_empty());
|
||||
assert!(!review.can_apply());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_timer_does_not_erase_exchange_without_its_own_selected_decision() {
|
||||
let (mut p, oa, sa, _) = fixture(MessageKind::Asynchronous, MessageKind::Asynchronous);
|
||||
let sender = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Sender", None)
|
||||
.unwrap();
|
||||
let receiver = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Receiver", None)
|
||||
.unwrap();
|
||||
let exchange = p.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Message exchange",
|
||||
sender,
|
||||
receiver,
|
||||
);
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
exchange: selected, ..
|
||||
},
|
||||
..
|
||||
}) = p.elements.get_mut(&sa).unwrap().shared_mut()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
*selected = Some(exchange);
|
||||
let mut app = App::with_project(p);
|
||||
let before = app.document.snapshot();
|
||||
start(&mut app, oa);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Variant(path("Sa"), json!({"Timer":{"expression":"after 2 s"}})),
|
||||
);
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(
|
||||
failures(&app).contains("timer_contract"),
|
||||
"{}",
|
||||
failures(&app)
|
||||
);
|
||||
assert_eq!(state(&app).decisions.len(), 1);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
decide(
|
||||
&mut app,
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Exchange),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Shape(
|
||||
"/body/Sa/Shared/Occurrence/body/Exchange/exchange".into(),
|
||||
Value::Null,
|
||||
),
|
||||
);
|
||||
assert!(state(&app).can_apply(), "{}", failures(&app));
|
||||
assert_eq!(
|
||||
acknowledged(&app),
|
||||
BTreeSet::from([
|
||||
message_kind::selector(),
|
||||
message_kind::timer_selector(),
|
||||
FacetSelector::Occurrence(OccurrenceFacet::Exchange)
|
||||
])
|
||||
);
|
||||
assert!(state(&app).plan.as_ref().unwrap().impacts.is_empty());
|
||||
apply(&mut app);
|
||||
assert_eq!(
|
||||
app.project().relations[&exchange],
|
||||
before.decoded.project.relations[&exchange]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_initial_equal_take_acknowledges_only_kind_and_checks_authored_timer_agreement() {
|
||||
let (p, oa, sa, _) = fixture(timer("source timer"), timer("target timer"));
|
||||
let mut app = App::with_project(p);
|
||||
start(&mut app, oa);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::TakeSource,
|
||||
);
|
||||
decide(
|
||||
&mut app,
|
||||
FacetSelector::Header(HeaderFacet::Name),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(&mut app, Event::Input("/name".into(), "Final name".into()));
|
||||
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
||||
assert_eq!(
|
||||
message_kind::value(state(&app).proposed.as_ref().unwrap(), sa),
|
||||
Some(&timer("target timer"))
|
||||
);
|
||||
assert!(!acknowledged(&app).contains(&message_kind::timer_selector()));
|
||||
let expression = format!("{}/Timer/expression", path("Sa"));
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input(expression.clone(), "authored group timer".into()),
|
||||
);
|
||||
assert!(
|
||||
!state(&app).can_apply(),
|
||||
"An unchanged initial kind cannot silently authorize a timer edit"
|
||||
);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::timer_selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input(expression.clone(), "different scalar timer".into()),
|
||||
);
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(
|
||||
state(&app)
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("different final values")
|
||||
);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input(expression, "authored group timer".into()),
|
||||
);
|
||||
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
||||
assert!(acknowledged(&app).contains(&message_kind::timer_selector()));
|
||||
assert_eq!(
|
||||
message_kind::value(state(&app).proposed.as_ref().unwrap(), sa),
|
||||
Some(&timer("authored group timer"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_group_rejects_protected_edits_stale_plan_and_source_failure_without_losing_timer() {
|
||||
for source_failure in [false, true] {
|
||||
let (p, oa, _, _) = fixture(MessageKind::Asynchronous, timer("target"));
|
||||
let mut app = App::with_project(p);
|
||||
if source_failure {
|
||||
let bundle = app.document.bundle().clone();
|
||||
let mut limits = source_project::ProjectLimits::default();
|
||||
limits.model.max_source_bytes =
|
||||
bundle.files.values().map(String::len).sum::<usize>() + 10;
|
||||
app.document = Document::from_bundle(bundle, limits).unwrap();
|
||||
}
|
||||
start(&mut app, oa);
|
||||
decide(
|
||||
&mut app,
|
||||
message_kind::selector(),
|
||||
SemanticDecision::Candidate,
|
||||
);
|
||||
let expression = format!("{}/Timer/expression", path("Sa"));
|
||||
let raw = "retain exact timer β\n".repeat(40);
|
||||
send(&mut app, Event::Input(expression.clone(), raw.clone()));
|
||||
assert!(state(&app).can_apply(), "{:?}", state(&app).error);
|
||||
send(
|
||||
&mut app,
|
||||
Event::Input("/name".into(), "Unselected protected scope".into()),
|
||||
);
|
||||
assert!(!state(&app).can_apply());
|
||||
assert!(
|
||||
state(&app)
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("selected review")
|
||||
);
|
||||
send(&mut app, Event::Preview);
|
||||
assert!(state(&app).can_apply());
|
||||
if !source_failure {
|
||||
app.document
|
||||
.execute(Command::Rename(oa, "External source edit".into()))
|
||||
.unwrap();
|
||||
}
|
||||
let before = app.document.snapshot();
|
||||
let revision = app.document.revision();
|
||||
apply(&mut app);
|
||||
assert!(state(&app).error.is_some());
|
||||
assert_eq!(state(&app).decisions.len(), 1);
|
||||
assert_eq!(
|
||||
state(&app)
|
||||
.group_drafts
|
||||
.get(&message_kind::selector())
|
||||
.unwrap()
|
||||
.inputs[&expression]
|
||||
.raw,
|
||||
raw
|
||||
);
|
||||
assert_eq!(app.document.bundle(), &before.bundle);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@ use super::*;
|
||||
use crate::{app::App, document::Document, workflow::Workflow};
|
||||
use serde_json::json;
|
||||
|
||||
fn send(app: &mut App, event: Event) {
|
||||
pub(super) fn send(app: &mut App, event: Event) {
|
||||
let _ = app.update(Message::Workflow(crate::workflow::Event::Semantic(event)));
|
||||
}
|
||||
fn apply(app: &mut App) {
|
||||
pub(super) fn apply(app: &mut App) {
|
||||
let _ = app.update(Message::Workflow(crate::workflow::Event::Apply));
|
||||
}
|
||||
fn state(app: &App) -> &State {
|
||||
pub(super) fn state(app: &App) -> &State {
|
||||
let Some(Workflow::Semantic(state)) = &app.workflow else {
|
||||
panic!("Semantic workflow missing: {}", app.status)
|
||||
};
|
||||
@@ -35,7 +35,7 @@ fn fixture() -> (Project, Id, Id, Id) {
|
||||
let trace = add_trace(&mut p, oa, sa, "Range rationale");
|
||||
(p, oa, sa, trace)
|
||||
}
|
||||
fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
|
||||
pub(super) fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
|
||||
let id = Id::new_v4();
|
||||
p.traces.insert(
|
||||
id,
|
||||
@@ -52,7 +52,7 @@ fn add_trace(p: &mut Project, oa: Id, sa: Id, name: &str) -> Id {
|
||||
);
|
||||
id
|
||||
}
|
||||
fn start(app: &mut App, source: Id) {
|
||||
pub(super) fn start(app: &mut App, source: Id) {
|
||||
let _ = app.update(Message::StartSemanticReview(source));
|
||||
}
|
||||
fn facet(value: DataFacet) -> FacetSelector {
|
||||
@@ -262,7 +262,7 @@ fn independent_review_keyed_fact_titles_keep_live_deleted_and_qualified_identity
|
||||
assert!(second_title.contains(&b.to_string()));
|
||||
assert_ne!(super::view::title(&p, &annotation(vec![])), first_title);
|
||||
}
|
||||
fn decide(app: &mut App, selected: FacetSelector, decision: SemanticDecision) {
|
||||
pub(super) fn decide(app: &mut App, selected: FacetSelector, decision: SemanticDecision) {
|
||||
send(app, Event::Select(selected.clone()));
|
||||
send(app, Event::Decide(selected, Some(decision)));
|
||||
}
|
||||
@@ -386,7 +386,7 @@ fn invalid_numeric_draft_and_protected_edits_retain_workflow_and_source() {
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("selected scalar")
|
||||
.contains("selected review")
|
||||
);
|
||||
assert_eq!(app.project(), &before.decoded.project);
|
||||
send(&mut app, Event::Preview);
|
||||
@@ -568,7 +568,10 @@ fn read_only_structured_facts_and_partial_rollup_are_honest() {
|
||||
send(
|
||||
&mut app,
|
||||
Event::Decide(
|
||||
facet(DataFacet::Constructor),
|
||||
FacetSelector::Literal(KeyPair {
|
||||
oa: Some("Unpaired".into()),
|
||||
sa: None,
|
||||
}),
|
||||
Some(SemanticDecision::TakeSource),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -71,27 +71,37 @@ pub(super) fn group(facet: &FacetSelector) -> &'static str {
|
||||
}
|
||||
}
|
||||
pub(super) fn title(project: &Project, facet: &FacetSelector) -> String {
|
||||
let keys = |keys: &KeyPair<Id>| {
|
||||
let keys = |keys: &KeyPair<Id>, missing: &str| {
|
||||
format!(
|
||||
"{} ↔ {}",
|
||||
keys.oa
|
||||
.map(|id| choice(project, id).label)
|
||||
.unwrap_or_else(|| "Absent".into()),
|
||||
.unwrap_or_else(|| missing.into()),
|
||||
keys.sa
|
||||
.map(|id| choice(project, id).label)
|
||||
.unwrap_or_else(|| "Absent".into())
|
||||
.unwrap_or_else(|| missing.into())
|
||||
)
|
||||
};
|
||||
match facet {
|
||||
FacetSelector::Member {
|
||||
collection,
|
||||
keys: pair,
|
||||
} => format!("{} · {}", words(&format!("{collection:?}")), keys(pair)),
|
||||
} => format!(
|
||||
"{} · {}",
|
||||
words(&format!("{collection:?}")),
|
||||
keys(pair, "No identity")
|
||||
),
|
||||
FacetSelector::Order(collection) => format!("{} order", words(&format!("{collection:?}"))),
|
||||
FacetSelector::Literal(pair) => format!(
|
||||
"Literal · {} ↔ {}",
|
||||
pair.oa.as_deref().unwrap_or("Absent"),
|
||||
pair.sa.as_deref().unwrap_or("Absent")
|
||||
pair.oa
|
||||
.as_ref()
|
||||
.map(|s| format!("{s:?}"))
|
||||
.unwrap_or_else(|| "No identity".into()),
|
||||
pair.sa
|
||||
.as_ref()
|
||||
.map(|s| format!("{s:?}"))
|
||||
.unwrap_or_else(|| "No identity".into())
|
||||
),
|
||||
FacetSelector::Annotation(pair) => format!(
|
||||
"Message value · {} ↔ {}",
|
||||
@@ -104,11 +114,17 @@ pub(super) fn title(project: &Project, facet: &FacetSelector) -> String {
|
||||
.map(|key| annotation_title(project, key))
|
||||
.unwrap_or_else(|| "Absent".into())
|
||||
),
|
||||
FacetSelector::Binding(pair) => format!("Lifeline binding · {}", keys(pair)),
|
||||
FacetSelector::Child(pair) => format!("Owned child · {}", keys(pair)),
|
||||
FacetSelector::Incident { keys: pair, end } => format!("Incident {end:?} · {}", keys(pair)),
|
||||
FacetSelector::Binding(pair) => format!("Lifeline binding · {}", keys(pair, "Absent")),
|
||||
FacetSelector::Child(pair) => format!("Owned child · {}", keys(pair, "Absent")),
|
||||
FacetSelector::Incident { keys: pair, end } => {
|
||||
format!("Incident {end:?} · {}", keys(pair, "Absent"))
|
||||
}
|
||||
FacetSelector::Artifact { keys: pair, facet } => {
|
||||
format!("Artifact {} · {}", words(&format!("{facet:?}")), keys(pair))
|
||||
format!(
|
||||
"Artifact {} · {}",
|
||||
words(&format!("{facet:?}")),
|
||||
keys(pair, "Absent")
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
let json = serde_json::to_value(facet).expect("Typed selector");
|
||||
@@ -199,6 +215,12 @@ fn observed(project: &Project, fact: Option<&FacetReview>, layer: Layer) -> Stri
|
||||
let value = scalar::observation(fact, layer);
|
||||
if value.as_str() == Some("NotApplicable") {
|
||||
"Not applicable".into()
|
||||
} else if keyed::supported(&fact.selector()) {
|
||||
if value["Value"].as_bool() == Some(true) {
|
||||
"Present".into()
|
||||
} else {
|
||||
"Absent".into()
|
||||
}
|
||||
} else {
|
||||
formatted(project, &value["Value"], &scalar::references(fact))
|
||||
}
|
||||
@@ -237,11 +259,13 @@ impl State {
|
||||
action(direction, Event::Direction(if self.direction == TransitionDirection::OaToSa { TransitionDirection::SaToOa } else { TransitionDirection::OaToSa })),
|
||||
].spacing(8),
|
||||
text("Changing trace or direction starts a new review draft. Other pending edits remain separate.").size(10).color(style::MUTED),
|
||||
row![action("Properties", Event::Tab(Tab::Facts)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6),
|
||||
row![action("Properties", Event::Tab(Tab::Facts)), action("Members / literals", Event::Tab(Tab::Members)), action(format!("History ({})", self.resets.len()), Event::Tab(Tab::History)), action(format!("Reference pairs ({})", self.references.len()), Event::Tab(Tab::References)), action(format!("Additional changes ({})", self.plan.as_ref().map_or(0, |p| p.impacts.len())), Event::Tab(Tab::Impacts)), Space::new().width(Fill), action("Refresh preview", Event::Preview)].spacing(6).wrap(),
|
||||
].spacing(8);
|
||||
let body = match self.tab {
|
||||
Tab::Facts => self.facts_view(project),
|
||||
Tab::References => self.references_view(project),
|
||||
Tab::Members => self.members_view(project),
|
||||
Tab::History => self.history_view(project),
|
||||
Tab::Impacts => self.impacts_view(project),
|
||||
};
|
||||
content = content.push(body);
|
||||
@@ -266,7 +290,7 @@ impl State {
|
||||
.iter()
|
||||
.filter(|f| f.drift == Drift::Unreviewed)
|
||||
.count();
|
||||
content = content.push(text(format!("{} selected decisions · {unreviewed} currently unreviewed facts · Constructors and structured collections require further review.", self.decisions.len())).size(10).color(style::MUTED));
|
||||
content = content.push(text(format!("{} decisions · {} exact resets · {unreviewed} unreviewed facts · Ordered, annotation, binding and attached-record decisions remain pending.", self.decisions.len(), self.resets.len())).size(10).color(style::MUTED));
|
||||
if let Some(trace) = trace {
|
||||
content = content.push(text(summary(project, trace)).size(10).color(style::MUTED));
|
||||
}
|
||||
@@ -364,7 +388,43 @@ impl State {
|
||||
.and_then(|t| t.baseline.as_ref())
|
||||
.and_then(|b| b.semantic.as_ref())
|
||||
.and_then(|b| b.facets.iter().find(|f| f.selector() == fact.selector));
|
||||
let is_group = scalar::grouped(&fact.selector);
|
||||
let group_observations = |project: &Project, facts: Vec<&FacetReview>, layer| {
|
||||
facts
|
||||
.into_iter()
|
||||
.filter(|f| constructors::contains(&fact.selector, &f.selector()))
|
||||
.map(|f| {
|
||||
format!(
|
||||
"{}: {}",
|
||||
title(project, &f.selector()),
|
||||
observed(project, Some(f), layer)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
let side = |layer, label: &str| {
|
||||
let mut before = observed(project, previous, layer);
|
||||
let mut current = observed(project, Some(&fact.current), layer);
|
||||
if is_group {
|
||||
before = group_observations(
|
||||
project,
|
||||
trace
|
||||
.and_then(|t| t.baseline.as_ref())
|
||||
.and_then(|b| b.semantic.as_ref())
|
||||
.map(|b| b.facets.iter().collect())
|
||||
.unwrap_or_default(),
|
||||
layer,
|
||||
);
|
||||
if before.is_empty() {
|
||||
before = "No previous group observations".into();
|
||||
}
|
||||
current = group_observations(
|
||||
project,
|
||||
self.facts.iter().map(|f| &f.current).collect(),
|
||||
layer,
|
||||
);
|
||||
}
|
||||
column![
|
||||
text(format!(
|
||||
"{label}{}",
|
||||
@@ -377,9 +437,9 @@ impl State {
|
||||
.size(12)
|
||||
.color(style::TEAL),
|
||||
text("Previously reviewed").size(10).color(style::MUTED),
|
||||
text(observed(project, previous, layer)).size(12),
|
||||
text(before).size(12),
|
||||
text("Current").size(10).color(style::MUTED),
|
||||
text(observed(project, Some(&fact.current), layer)).size(12),
|
||||
text(current).size(12),
|
||||
]
|
||||
.spacing(5)
|
||||
.width(Fill)
|
||||
@@ -390,6 +450,38 @@ impl State {
|
||||
row![side(Layer::Oa, "Operational"), side(Layer::Sa, "System")].spacing(14)
|
||||
]
|
||||
.spacing(10);
|
||||
if previous.is_some() && history::resettable(&fact.selector) {
|
||||
let facet = fact.selector.clone();
|
||||
detail = detail.push(
|
||||
checkbox(self.resets.contains(&facet))
|
||||
.label("Reset this exact previous observation before review")
|
||||
.text_size(11)
|
||||
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
|
||||
);
|
||||
}
|
||||
if keyed::supported(&fact.selector) {
|
||||
detail = detail.push(text("Presence changes only this collection entry; referenced objects stay intact. Take preserves a row with no source identity. An explicit source identity that is absent can remove its mapped destination entry.").size(11))
|
||||
.push(action("Choose / change exact row identities…", Event::EditPair(fact.selector.clone())));
|
||||
if let Some(baseline) = trace
|
||||
.and_then(|t| t.baseline.as_ref())
|
||||
.and_then(|b| b.semantic.as_ref())
|
||||
{
|
||||
for prior in &baseline.facets {
|
||||
let prior = prior.selector();
|
||||
if prior != fact.selector && keyed::overlap(&prior, &fact.selector) {
|
||||
detail = detail.push(
|
||||
checkbox(self.resets.contains(&prior))
|
||||
.label(format!(
|
||||
"Reset overlapping history: {}",
|
||||
title(project, &prior)
|
||||
))
|
||||
.text_size(11)
|
||||
.on_toggle(move |v| event(Event::Reset(prior.clone(), v))),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(proposed) = &self.proposed
|
||||
&& let Some(trace) = trace
|
||||
&& let Ok(after) = capture_facet(proposed, trace.oa, trace.sa, &fact.selector)
|
||||
@@ -401,11 +493,20 @@ impl State {
|
||||
.color(style::TEAL),
|
||||
)
|
||||
.push(
|
||||
text(observed(
|
||||
proposed,
|
||||
Some(&after),
|
||||
self.direction.target_layer(),
|
||||
))
|
||||
text(if is_group {
|
||||
semantic_facets(proposed, &proposed.traces[&trace.id], &self.references)
|
||||
.ok()
|
||||
.map(|facts| {
|
||||
group_observations(
|
||||
proposed,
|
||||
facts.iter().map(|f| &f.current).collect(),
|
||||
self.direction.target_layer(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "Group payload is unavailable.".into())
|
||||
} else {
|
||||
observed(proposed, Some(&after), self.direction.target_layer())
|
||||
})
|
||||
.size(12),
|
||||
);
|
||||
} else {
|
||||
@@ -416,6 +517,13 @@ impl State {
|
||||
);
|
||||
}
|
||||
if self.editable(project, &fact.selector) {
|
||||
if is_group {
|
||||
detail = detail.push(text(constructors::description(&fact.selector)).size(12));
|
||||
detail = detail.push(text("Replacing a kind reviews its complete payload. Review every removed reference, row and prior observation in the exact scope below; changing kind retains your draft for switching back.").size(11));
|
||||
if self.can_acknowledge_constructor(project, &fact.selector) {
|
||||
detail = detail.push(text("These initial kinds are equal. Keep or Take acknowledges only the kind; an unchanged Candidate kind needs independent descendant decisions for payload edits.").size(11).color(style::MUTED));
|
||||
}
|
||||
}
|
||||
let destination = if self.direction.target_layer() == Layer::Sa {
|
||||
"System"
|
||||
} else {
|
||||
@@ -438,30 +546,120 @@ impl State {
|
||||
]
|
||||
.spacing(5);
|
||||
buttons = buttons.push(action(
|
||||
"Set final value…",
|
||||
if is_group {
|
||||
"Set final kind and payload…"
|
||||
} else {
|
||||
"Set final value…"
|
||||
},
|
||||
Event::Decide(fact.selector.clone(), Some(SemanticDecision::Candidate)),
|
||||
));
|
||||
detail = detail.push(buttons).push(action(
|
||||
detail = detail.push(buttons.wrap()).push(action(
|
||||
"Leave unselected",
|
||||
Event::Decide(fact.selector.clone(), None),
|
||||
));
|
||||
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
|
||||
&& let Some(draft) = &self.draft
|
||||
&& let Some(path) = self
|
||||
.target(project)
|
||||
.and_then(|id| scalar::binding(project, id, &fact.selector))
|
||||
&& keyed::supported(&fact.selector)
|
||||
{
|
||||
detail = detail.push(text("Final destination value").size(12)).push(
|
||||
crate::forms::scoped(draft, project, &path, &title(project, &fact.selector))
|
||||
.map(form_event),
|
||||
let facet = fact.selector.clone();
|
||||
detail = detail.push(
|
||||
checkbox(self.presence.get(&facet).copied().unwrap_or(false))
|
||||
.label(
|
||||
"Include in destination collection (unchecked removes only this entry)",
|
||||
)
|
||||
.text_size(12)
|
||||
.on_toggle(move |v| event(Event::Presence(facet.clone(), v))),
|
||||
);
|
||||
}
|
||||
if self.decisions.get(&fact.selector) == Some(&SemanticDecision::Candidate)
|
||||
&& let Some(draft) = self.candidate_draft(&fact.selector)
|
||||
&& let Some(paths) = self
|
||||
.target(project)
|
||||
.and_then(|id| constructors::roots(project, id, &fact.selector))
|
||||
{
|
||||
detail = detail.push(text("Final destination value").size(12));
|
||||
for path in paths {
|
||||
let label = if path.ends_with("/default") {
|
||||
"Default"
|
||||
} else if path.ends_with("/legacy") {
|
||||
"Generic association"
|
||||
} else {
|
||||
"Kind and payload"
|
||||
};
|
||||
detail = detail.push(
|
||||
crate::forms::scoped(
|
||||
draft,
|
||||
project,
|
||||
&path,
|
||||
if is_group { label } else { "Final value" },
|
||||
)
|
||||
.map(form_event),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if self.can_acknowledge_constructor(project, &fact.selector) {
|
||||
detail = detail.push(text("Both constructor kinds are equal and have no prior observation. Acknowledge only the kind; its properties remain independent.").size(12))
|
||||
.push(action("Acknowledge equal kind only", Event::Decide(fact.selector.clone(), Some(SemanticDecision::KeepTarget))))
|
||||
.push(action("Leave unselected", Event::Decide(fact.selector.clone(), None)));
|
||||
} else {
|
||||
detail = detail.push(text(if scalar::grouped(&fact.selector) { "Constructor groups are shown for comparison. Group replacement and its complete descendant review are a later milestone." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED));
|
||||
detail = detail.push(text(if scalar::grouped(&fact.selector) { "This constructor is unavailable on one of the current counterparts. Review the outer constructor before its nested kind." } else { "This structured or inapplicable fact is read-only here. Its review history is retained." }).size(12).color(style::MUTED));
|
||||
}
|
||||
if self.staged.contains(&fact.selector) {
|
||||
detail = detail.push(action(
|
||||
"Remove staged address and its decision",
|
||||
Event::DropStaged(fact.selector.clone()),
|
||||
));
|
||||
}
|
||||
let overlaps: Vec<_> = self
|
||||
.decisions
|
||||
.keys()
|
||||
.filter(|other| {
|
||||
**other != fact.selector
|
||||
&& (constructors::contains(&fact.selector, other)
|
||||
|| constructors::contains(other, &fact.selector)
|
||||
|| keyed::overlap(other, &fact.selector))
|
||||
})
|
||||
.collect();
|
||||
if self.decisions.contains_key(&fact.selector) && !overlaps.is_empty() {
|
||||
detail = detail.push(text("Related decisions are selected. Paired rows cannot review the same OA or SA identity twice. Replacing groups require one combined decision; first equal-kind acknowledgments may coexist with descendants. 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),
|
||||
));
|
||||
}
|
||||
detail = detail.push(action(
|
||||
"Leave this decision unselected; use the related decisions",
|
||||
Event::Decide(fact.selector.clone(), None),
|
||||
));
|
||||
}
|
||||
if (is_group
|
||||
|| keyed::supported(&fact.selector)
|
||||
|| constructors::selectors().iter().any(|group| {
|
||||
self.decisions.contains_key(group) && constructors::contains(group, &fact.selector)
|
||||
}))
|
||||
&& let Some(review) = self.plan.as_ref().and_then(|p| p.reviews.first())
|
||||
{
|
||||
detail = detail.push(text("Exact review scope from preview").size(13));
|
||||
for (label, selectors) in [
|
||||
("Acknowledge", &review.acknowledged),
|
||||
("Retire previous observation", &review.retired),
|
||||
] {
|
||||
detail = detail.push(
|
||||
text(format!(
|
||||
"{label}: {}",
|
||||
if selectors.is_empty() {
|
||||
"None".into()
|
||||
} else {
|
||||
selectors
|
||||
.iter()
|
||||
.map(|s| title(project, s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
))
|
||||
.size(11),
|
||||
);
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
fact.selector,
|
||||
@@ -474,10 +672,106 @@ impl State {
|
||||
detail.into()
|
||||
}
|
||||
|
||||
fn members_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
|
||||
let families = self.member_families(project);
|
||||
let available = families.contains(&self.member_family);
|
||||
let mut content = column![
|
||||
text("Review an individual member or literal").size(16),
|
||||
text("Choose an address, then review its presence in Properties. This also stages a new row in an empty collection. No model object is created.").size(12),
|
||||
pick_list(families, Some(self.member_family), |v| event(Event::MemberFamily(v))).text_size(12).padding(7),
|
||||
].spacing(9);
|
||||
if !available {
|
||||
content = content.push(text("This collection is not available on the current counterparts. Choose an available collection above; existing staged addresses remain removable below.").size(12));
|
||||
} else if self.member_family == keyed::Family::Literal {
|
||||
content = content.push(text("Literal identity is exact, including case and spaces. Both sides use the same string; a rename requires separate old/new rows.").size(11))
|
||||
.push(text_input("Exact literal", &self.literal).on_input(|v| event(Event::Literal(v))).size(12).padding(7))
|
||||
.push(row![checkbox(self.literal_oa).label("Operational identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Oa,v))), checkbox(self.literal_sa).label("System identity").text_size(12).on_toggle(|v| event(Event::LiteralSide(Layer::Sa,v)))].spacing(12));
|
||||
} else {
|
||||
content = content.push(text("Identities may exist outside the collection. Choosing both explicitly sets their OA → SA correspondence. No identity means an unpaired side, distinct from an absent member.").size(11))
|
||||
.push(row![pick_list(self.member_choices(project, Layer::Oa), self.member_oa.clone(), |v| event(Event::MemberOa(Some(v)))).placeholder("Operational identity").text_size(12).padding(7).width(Fill), action("No OA identity", Event::MemberOa(None))].spacing(7))
|
||||
.push(row![pick_list(self.member_choices(project, Layer::Sa), self.member_sa.clone(), |v| event(Event::MemberSa(Some(v)))).placeholder("System identity").text_size(12).padding(7).width(Fill), action("No SA identity", Event::MemberSa(None))].spacing(7));
|
||||
}
|
||||
content = content.push(button(text("Use these identities and review presence").size(12)).padding([7,9]).style(style::nav).on_press_maybe(available.then_some(event(Event::StageMember))))
|
||||
.push(text("Changing a row address retains its earlier decisions and drafts. Reset conflicting historical observations and leave overlapping decisions unselected explicitly.").size(11).color(style::MUTED));
|
||||
if !self.staged.is_empty() {
|
||||
content = content.push(text("Staged addresses in this draft").size(13));
|
||||
for facet in &self.staged {
|
||||
content = content.push(
|
||||
row![
|
||||
text(title(project, facet)).size(11).width(Fill),
|
||||
action(
|
||||
"Remove address / decision",
|
||||
Event::DropStaged(facet.clone())
|
||||
)
|
||||
]
|
||||
.spacing(7),
|
||||
);
|
||||
}
|
||||
}
|
||||
scrollable(content).height(305).into()
|
||||
}
|
||||
|
||||
fn history_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
|
||||
let mut content = column![text("Exact previous observations").size(16), text("Check only the observations to forget. Resets run before this review's decisions; reset alone changes history, not model values. Uncheck to restore the pending observation. Prose history remains separate.").size(12)].spacing(9);
|
||||
let baseline = self
|
||||
.trace
|
||||
.and_then(|id| project.traces.get(&id))
|
||||
.and_then(|t| t.baseline.as_ref())
|
||||
.and_then(|b| b.semantic.as_ref());
|
||||
if baseline.is_none_or(|b| b.facets.is_empty()) {
|
||||
content =
|
||||
content.push(text("No previous semantic observations on this trace.").size(12));
|
||||
}
|
||||
if let Some(baseline) = baseline {
|
||||
for fact in &baseline.facets {
|
||||
let facet = fact.selector();
|
||||
let mut item = column![
|
||||
text(title(project, &facet)).size(13),
|
||||
text(format!(
|
||||
"Operational: {}\nSystem: {}",
|
||||
observed(project, Some(fact), Layer::Oa),
|
||||
observed(project, Some(fact), Layer::Sa)
|
||||
))
|
||||
.size(11)
|
||||
]
|
||||
.spacing(4);
|
||||
if history::resettable(&facet) {
|
||||
item = item.push(
|
||||
checkbox(self.resets.contains(&facet))
|
||||
.label("Reset this exact observation")
|
||||
.text_size(11)
|
||||
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
|
||||
);
|
||||
} else {
|
||||
item = item.push(
|
||||
text("Artifact history reset remains pending with artifact review.")
|
||||
.size(11),
|
||||
);
|
||||
}
|
||||
content = content.push(container(item).padding(7));
|
||||
}
|
||||
}
|
||||
for pending in &self.resets {
|
||||
if baseline.is_none_or(|b| !b.facets.iter().any(|f| f.selector() == *pending)) {
|
||||
let facet = pending.clone();
|
||||
content = content.push(
|
||||
checkbox(true)
|
||||
.label(format!(
|
||||
"Pending reset no longer in current history: {}",
|
||||
title(project, &facet)
|
||||
))
|
||||
.text_size(11)
|
||||
.on_toggle(move |v| event(Event::Reset(facet.clone(), v))),
|
||||
);
|
||||
}
|
||||
}
|
||||
scrollable(content).height(305).into()
|
||||
}
|
||||
|
||||
fn references_view<'a>(&'a self, project: &'a Project) -> Element<'a, Message> {
|
||||
let mut content = column![
|
||||
text("Reference correspondence · always Operational → System").size(15),
|
||||
text("Choose explicit counterpart identities before taking reference values. Names do not establish correspondence. Changing an already reviewed keyed pair requires reset in the structured workflow.").size(12),
|
||||
text("Choose explicit counterpart identities before taking reference values. Names do not establish correspondence. Changing an already reviewed keyed pair requires an exact reset in History. Existing decisions are retained; resolve their overlapping row identities explicitly.").size(12),
|
||||
text_input("Filter reference choices by name or alias", &self.reference_search).on_input(|v| event(Event::ReferenceSearch(v))).padding(7).size(12),
|
||||
row![
|
||||
pick_list(self.all_references(project, Layer::Oa), self.reference_oa.clone(), |c| event(Event::ReferenceOa(c))).placeholder("Operational reference").padding(7).text_size(12).width(Fill),
|
||||
|
||||
@@ -736,6 +736,7 @@ fn traces(app: &App) -> View<'_, Message> {
|
||||
action("+ Artifact", Message::AddArtifact)
|
||||
]
|
||||
.spacing(8)
|
||||
.wrap()
|
||||
]
|
||||
.spacing(16);
|
||||
for t in p.traces.values() {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Arcadia Studio server
|
||||
|
||||
`arcadia-server` is a headless Rust REST service for the same schema-4 model, commands, source language, validation, artifacts and OA/SA traces used by the desktop. It stores each model and its authored source project together in an atomic JSON envelope. The service provides a deployment artifact and configuration examples; this repository does not assert that a remote instance has been deployed.
|
||||
`arcadia-server` is a headless Rust REST service for the same schema-5 model, commands, source language, validation, artifacts and OA/SA traces used by the desktop. It stores each model and its authored source project together in an atomic JSON envelope. The service provides a deployment artifact and configuration examples; this repository does not assert that a remote instance has been deployed.
|
||||
|
||||
The complete HTTP contract is [api.rst](api.rst); machine-readable schemas are in [openapi.json](openapi.json), also served at `/api/v1/openapi.json`. The original [service research](research-v2-service.md) discusses broader alternatives; retained history, SQLite, idempotency receipts and stored transition plans from that research are **not** the implemented storage contract.
|
||||
|
||||
|
||||
@@ -116,3 +116,18 @@ typed facts can outlive former constructors/families and deleted references. The
|
||||
typed reference visitor supports explicit reidentification but does not add live
|
||||
deletion dependencies. Derived linked-definition drift remains bounded and exposes
|
||||
missing review coverage instead of copying a shared definition into every baseline.
|
||||
|
||||
## Hosted destination identity and bounded independent panes — 2026-09-06
|
||||
|
||||
A hosted save binding belongs to one installed document, not the connection dialog.
|
||||
Installing any local source workspace, including invalid repair source, detaches
|
||||
it; a failed or cancelled load does not. Verified regression prevented accidental
|
||||
write to a previously opened server model after repairing local text. Catalog
|
||||
freshness is explicit and scoped to its service; observing a changed bound head
|
||||
invalidates even empty filtered results. Background polling is unnecessary.
|
||||
|
||||
Pending process forms use an independent bounded scroll pane with pinned actions,
|
||||
while the diagram retains its own camera/scroll state. Nesting both under the page
|
||||
scroll supplied infinite height and clipped the fields at the supported minimum.
|
||||
Real Iced layout tests verify finite field space; native recheck remains required.
|
||||
|
||||
|
||||
@@ -68,3 +68,13 @@ Tests must cover user-visible meaning and cross-representation invariants, not j
|
||||
## Definition of done
|
||||
|
||||
Every concrete requirement in the new objective and release coverage table has authoritative current evidence. Primary OA/SA engineering workflows work graphically, textually and through the API; help is navigable and useful; native Windows/Linux UI is intentionally designed and exercised; no blocking/high-impact defect remains; strict checks pass; independent findings are closed; durable docs accurately reflect the release; another engineer can build/run/test and use the supplied packages. V2 completion alone does not satisfy this goal.
|
||||
|
||||
## Current milestone progress — hosted integration
|
||||
|
||||
Milestone4 desktop adapter is implemented on the existing Document/REST boundary.
|
||||
Windows native publish, graphical/text save, rejected invalid source, API artifact
|
||||
reload and stale-revision recovery are demonstrated. Offline/retry and exact local-copy/catalog Open native acceptance passed on Windows.
|
||||
Destination and catalog defects found afterward are fixed/reviewed/integrated;
|
||||
recheck those and Linux native next. Constructor groups and reset/unordered keyed
|
||||
GUI are integrated/reviewed. Annotation/Binding and Home/recents are active; order,
|
||||
attachments, artifacts, multi-source GUI and checkpoint recovery remain required.
|
||||
|
||||
+76
-132
@@ -1,152 +1,96 @@
|
||||
# First release — current engineering state
|
||||
# Current release state — 2026-09-06
|
||||
|
||||
## Objective and phase
|
||||
The first-release goal remains active and incomplete. Authoritative objective:
|
||||
attachment10134df9-bea2-4c02-a820-67b53d6b86f7/goal-objective.md. PLAN/TASK_LEDGER
|
||||
retain complete OA/SA, graphical/source/includes/REST parity and release acceptance.
|
||||
|
||||
The first-release goal is active and incomplete: usable Rust/Iced OA/SA,
|
||||
equivalent graphical/source/REST semantics, traceability, direct behavior editing,
|
||||
offline guidance, lifecycle/hosted workflows and Windows/Linux evidence.
|
||||
Authoritative objective reread 2026-09-06: attachment10134df9-bea2-4c02-a820-67b53d6b86f7.
|
||||
PLAN retains full scope; no subsystem checkpoint completes the release.
|
||||
## Ownership and integrated state
|
||||
|
||||
Main remains a documentation pointer on production a8f451e. Current integration is
|
||||
D:/CodexBuilds/Capella2-data-integration, branch codex/data-contract-integration.
|
||||
Production merge332e46b + formatc6db9d4 + evidenced114a5d integrate DATA/scenario/
|
||||
process60d162c and semantic6ad3cb. Scalar fixes5a51a75 integrated as e7fa061 and evidence767634a;
|
||||
322 desktop tests/strict checks/build pass on both platforms. That worker owns DATA/Linux indexes and targets;
|
||||
root owns this file, other native ledgers, README and all native input.
|
||||
- Main C:/Users/sorok/OneDrive/Documents/ChatGPT/Capella 2 is the continuation
|
||||
pointer, codex/release1-engineering,4fa9591; production work is in DATA below.
|
||||
- DATA D:/CodexBuilds/Capella2-data-integration, codex/data-contract-integration,
|
||||
production14b8e18. Root owns index and Capella2-data-target (idle).
|
||||
- Linux D:/CodexBuilds/Capella2-data-linux-review, codex/data-linux-review,
|
||||
equivalent9c61d0d. Root owns index and data-linux-target (idle).
|
||||
- SEM D:/CodexBuilds/Capella2-semantic-review, codex/semantic-review,6fb01ae.
|
||||
semantic_gui_design owns slice2 Annotation/Binding GUI, index and
|
||||
semantic-desktop-target. Slice1 exact reset/unordered Member/Literal is integrated
|
||||
and independently reviewed; ordered/attachment/artifact/multi-source work remains.
|
||||
- HOST D:/CodexBuilds/Capella2-hosted-desktop, codex/hosted-desktop,e807086.
|
||||
semantic_integration owns Home/start/recents, index and scenario-target. Child
|
||||
owns new Home tests; no overlap with root or SEM. Recovery is not implemented.
|
||||
- Root is sole native operator. Agents may not issue native input.
|
||||
|
||||
## Accepted foundations and evidence
|
||||
## Verified integration
|
||||
|
||||
- Typed ports and migration: release-port-{contract,core,migration}.md and
|
||||
release-capella-workflow-benchmark.md.
|
||||
- Explorer grouping/deep hierarchy, contextual port creation, routing, labels,
|
||||
anchors, direct exchange drawing, menus and source tree: release-explorer-usability.md,
|
||||
release-routing-{verification,linux}.md, release-port-drawing.md,
|
||||
release-context-menus.md.
|
||||
- Modular source/includes, provenance-aware writes, exact source/model history,
|
||||
retained drafts and coupled hosted revisions: release-source-integration.md.
|
||||
- Direct state creation/movement/transitions/reconnect, typed forms, fold and
|
||||
camera/tool lifetime through316dc55, Windows/Linux native: release-state-editing.md.
|
||||
R6 data contracts, direct scenario/process gestures, source5 core/REST and scalar/
|
||||
constructor GUI are integrated. Full Windows767/Linux770 workspace tests passed
|
||||
at DATA28ebbbb/Linux04210b3, one intentional benchmark ignore each; strict checks
|
||||
and optimized desktop/server builds passed. Live REST184 schemas/19 typed values/
|
||||
9 malformed rejections passed. See scoped ledgers for limits.
|
||||
|
||||
## Integrated semantics and verification
|
||||
Newest DATA14b8e18/Linux9c61d0d:392 desktop tests each, strict all-target desktop
|
||||
Clippy, workspace fmt and release builds pass. Sessions24877/10919 closed0.
|
||||
Logs integrated-review-{test,clippy,build}.log in Windows data-target and Linux
|
||||
checkout target. Initial Windows log redirect failed before Cargo (nonexistent
|
||||
checkout target); rerun used the existing dedicated target and passed.
|
||||
|
||||
R6 data payloads/defaults/inheritance/associations/interface allocations/qualified
|
||||
message-value paths are integrated. Source3 migration preserves comments/nesting
|
||||
until Save All. Typed review now uses source/schema5 with explicit migration.
|
||||
See release-data-{contract,implementation,verification}.md.
|
||||
Independent reviews closed constructorc149f2f (299 tests), reset/unordered6fb01ae
|
||||
(311 tests), invalid-local-source destinationb04c4b3 and catalog12e3020/e807086.
|
||||
Catalog review found and corrected false empty filtered results when Check observes
|
||||
a changed head outside displayed rows; unchanged-head/cross-service controls pass.
|
||||
Constructor and keyed core changes43370de/8a75294 have independent probes and359
|
||||
core tests passing. Full graphical parity is not yet established.
|
||||
|
||||
Semantic core supports finite typed facts, historical references, mappings/reset,
|
||||
constructor groups, canonical agreement and exact prepared impacts. Independent
|
||||
equal-discriminator review found and fixed first acknowledgment reachability:
|
||||
initial equal unreviewed discriminators acknowledge only that scalar; true and
|
||||
historical switches retain grouped rules.353 core tests and independent36 focused/
|
||||
8 adversarial probes passed. Normative clarification6ad3cb is integrated.
|
||||
## Native state and evidence
|
||||
|
||||
REST query/preview/apply is implemented and verified after explicit user approval.
|
||||
Exact prepared candidates preserve generated identities, conditional ETags and
|
||||
coupled model/source commits. Fingerprints express consistency, not authentication.
|
||||
48 server tests pass; optimized live loopback checks validate184 schemas and19
|
||||
actual/typed values, reject9 malformed shapes. Strict reST/catalog checks pass.
|
||||
No deployment was requested or performed. See release-semantic-integration.md.
|
||||
Windows hosted6fcc238 trial71940 closed0/empty stderr. Default/minimum publish,
|
||||
graph/text Save, invalid source retention, artifact API Check/review/Reload/Undo,
|
||||
stale ETag conflict/local retention, offline/retry, exact three-file local Save As
|
||||
and catalog Open passed. Isolated loopback serverPID20984 remains at127.0.0.1:8787,
|
||||
resource d28b2fc9-fbbd-456b-8ba8-a1195d1c8f33 revision6. Evidence/store is
|
||||
C:/Users/sorok/AppData/Local/Temp/capella2-hosted-native-20260906.
|
||||
|
||||
d114a5d integration: Windows718 / Linux721 tests pass, one intentional inheritance
|
||||
benchmark ignored on each. Strict workspace all-target Clippy, formatting and
|
||||
optimized desktop/server builds pass. Linux exec80687 exited0; logs are
|
||||
semantic-workspace-test.log, semantic-workspace-clippy.log and
|
||||
semantic-release-build.log in Capella2-data-linux-target.
|
||||
Invalid local repair source used to retain hosted destination and overwrite that
|
||||
resource after repair. Fix is integrated/reviewed; actual fixed native flow remains
|
||||
pending. Catalog freshness is likewise integrated/reviewed, native check pending.
|
||||
|
||||
Scalar GUI milestones1–2 provide existing-trace scoped Keep/Take/Candidate review,
|
||||
typed forms, exact ordinary Batch, impact review and source/history. Independent
|
||||
review found three P2 issues: unrelated invalid draft blocking scoped actions,
|
||||
combined type-decision form context, and hidden historical keyed identities.
|
||||
Worker5a51a75 fixes all three;277 desktop tests/Clippy/build pass in SEM. Fresh
|
||||
independent recheck passed280 tests (277 repository +3 independent probes),
|
||||
closing all three P2 findings; integrated platform verification also passed. Native scalar
|
||||
acceptance and structured milestone3 are still required; do not claim full parity.
|
||||
Current Windows constructor trial84997/window4327540 uses frozen cf04ff1 at
|
||||
ui-constructors-windows-cf04ff1, SHA86C86A0A4E9E7BDEB5BBB64F6787AA5D3A8715D109979A57D056AEB686538E38.
|
||||
At1100x700, fixture01 Data reviewed Take, exact source/history, Undo/Redo and Save
|
||||
passed; parsedProject matched independently core-generated expected. Candidate
|
||||
Real retained raw '-' across Integer/Real changes, blocked incompatible Boolean
|
||||
default, accepted repaired -1.5..100/default3.5, applied as one Undo/Redo and saved.
|
||||
Current accepted file is fixtures/01-data-switch.arcadia with Real candidate;
|
||||
native property values/history and disk comments/unitcm confirmed. Independent
|
||||
candidate exactProject comparison is pending. No modal currently open.
|
||||
|
||||
## Native behavior evidence and current environment
|
||||
Remaining five pristine constructor fixtures and expected outputs are in
|
||||
C:/Users/sorok/AppData/Local/Temp/capella2-constructor-native-20260906. Do not rerun
|
||||
the UUID-generating fixture generator. Copy originals before editing.
|
||||
|
||||
Frozen60d162c binaries live under D:/CodexBuilds/Capella2-release1-01a06e2e:
|
||||
ui-behavior-windows-60d162c and ui-behavior-linux-60d162c. Hashes in the linked ledgers.
|
||||
Windows prior scenario native passed middle/ALT insertion, guarded correlated
|
||||
Call/Reply, qualified values, Undo/Redo, save/reopen, minimum layout and viewport
|
||||
retention. See release-scenario-editing.md. Remaining advanced tasks are explicit.
|
||||
New frozen accepted builds (native pending) under Capella2-release1-01a06e2e:
|
||||
ui-integrated-review-windows-14b8e18 and ui-integrated-review-linux-9c61d0d.
|
||||
Manifests contain hashes. Earlier scalar native accepted; process camera/owner
|
||||
accepted, compact pending form fixf08507a needs native recheck. See native ledgers.
|
||||
|
||||
Linux1440x900 Wayland native: numeric Retry budget0..4 rejects default9 without
|
||||
losing draft; default2 saves, Undo/Redo works. Recursive Destination/Next/Latitude
|
||||
browser preserves independent Origin expression; save/reopen passed. Created
|
||||
Validated route Call/Reply in the middle, Undo/Redo and save/reopen passed. Created
|
||||
SA chain with two existing functions, drew guarded ready link, inserted existing
|
||||
Navigation request into it, verified guard and original link ID, Undo/Redo and saved
|
||||
69objects/7links/3expected checks. Final chain reopen and advanced process cases
|
||||
remain. See release-data-verification.md and release-process-editing.md.
|
||||
|
||||
Wayland exec61870 closed normally, exit0 and no serious runtime output. Current Windows semantic trial is exec62786/window2099614, frozen e7fa061,
|
||||
1100x700. Root owns input. Re-inventory; never reuse closed window4852212.
|
||||
Original Capella395654 remains available. Do not modify protected user windows.
|
||||
Wayland compact resizing is unverified: capture separates frame and client into
|
||||
bounded surfaces. X11 launch fails in dependency xkbcommon-dl because Ubuntu lacks
|
||||
libxkbcommon-x11-0. sudo installation requires the user's password; user has been
|
||||
given `sudo apt-get install libxkbcommon-x11-0`. Windows/Wayland testing continues.
|
||||
Do not bypass authentication or treat this optional environment issue as goal blocked.
|
||||
Linux uses Ubuntu-24.04, Rust1.98.1 PATH/home/sorokinoleg/.cache/arcadia/rust/bin,
|
||||
--config target/linux-cargo.toml and dedicated target. Wayland works with
|
||||
WGPU_BACKEND=vulkan and VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json.
|
||||
Optional X11 missing libxkbcommon-x11-0; user has sudo apt-get install command.
|
||||
Do not bypass authentication. Windows/Wayland/REST are unblocked.
|
||||
|
||||
## Exact next actions
|
||||
|
||||
1. Finish5a51a75 integration/recheck, update immutable binaries/hashes, operate scalar
|
||||
review in actual Windows/Linux app including reverse, failure and compact forms.
|
||||
2. Complete native R6/scenario/process cases (both layers, branching/reuse/reconnect,
|
||||
cancellation, minimum size and source reopen); fix from observed behavior.
|
||||
3. Complete structured semantic GUI: constructor groups, keyed/order/reset/artifact
|
||||
operations and multi-source final-candidate review; retain shared core planner.
|
||||
4. Finish coverage/filter, Home/recent/recovery, hosted desktop ETag and offline help
|
||||
requirements. Fresh end-state reviews, full platform/MSRV checks and packaging.
|
||||
1. Complete native constructor fixtures and compact process pane acceptance.
|
||||
2. Operate corrected invalid-local-open and stale catalog on newest Windows build;
|
||||
complete Linux hosted/semantic native acceptance.
|
||||
3. Independently review/integrate SEM Annotation/Binding and HOST Home/recents,
|
||||
then ordered/attachment/artifact/multi-source GUI and recoverable checkpoints.
|
||||
4. Finish advanced OA/process/scenario, help/coverage and fresh final reviews;
|
||||
full platforms/MSRV/docs/package verification. No release handoff yet.
|
||||
|
||||
Use dedicated Cargo targets. Windows: cargo test --locked --offline --workspace;
|
||||
Build: cargo test --locked --offline --workspace;
|
||||
cargo clippy --locked --offline --workspace --all-targets -- -D warnings;
|
||||
cargo build --locked --offline --release -p oa-desktop -p oa-server.
|
||||
WSL Ubuntu24.04 uses target/linux-cargo.toml, Rust1.98.1 at
|
||||
/home/sorokinoleg/.cache/arcadia/rust/bin and Mesa software Vulkan. No remote push,
|
||||
publication, usage reset or broad recursive target deletion is authorized.
|
||||
|
||||
## Latest native and process checkpoint — 2026-09-06
|
||||
|
||||
Process cf33785 + evidence1b0b072 fixes forced zoom on palette changes and canvas
|
||||
remount/scroll loss on pending forms, with persistent two-path field labels.
|
||||
325 desktop tests/strict checks/release pass on each platform. Independent24-test
|
||||
review closed both original defects but found owner-to-owner Scrollable leakage.
|
||||
Fix8c60008 resets actual observer child only on (document,owner) changes;25 focused
|
||||
red/green checks pass. Full platforms and fresh semantic_http_review are active.
|
||||
semantic_integration owns DATA/Linux indexes and targets for this narrow follow-up.
|
||||
|
||||
Windows60d162c reopened Linux69-object bundle. At1100x700, inserted named Choice
|
||||
Route available? on the ready path, then Two paths added Choice/two existing-work
|
||||
occurrences/Merge with ready/else guards. Undo74→70 and Redo70→74; Save All wrote
|
||||
74objects/13links/5 incomplete-model checks. Original link identity/ready preserved.
|
||||
Trial3607004 closed; saved bundle remains in ui-behavior-linux-60d162c/model.
|
||||
Final camera/owner fixes and advanced OA/process native cases remain unverified.
|
||||
|
||||
Scalar Windows e7fa061 actual native: source5 fixture, numeric prior/current
|
||||
comparison; Keep SA minimum1, Take OA maximum20, candidate default99 rejects and
|
||||
retains all three decisions; corrected7. Error, editor and Apply fit1100x700.
|
||||
Additional changes0; Apply atomically changes SA to1..20/default7, OA remains
|
||||
0..20/default5. Native text verifies comment and values. Undo restores saved state,
|
||||
Redo restores review; Save All succeeds. Field Type Take OA Boolean + Default
|
||||
Candidate creates Boolean(false), previews/applies/saves successfully at1100x700.
|
||||
Current saved working copy: C:/Users/sorok/AppData/Local/Temp/
|
||||
capella2-semantic-native-fixture-20260906/windows-scalar.arcadia. Runtime stderr empty.
|
||||
Reverse, mapping, historical identity, reopen and Linux scalar cases remain.
|
||||
|
||||
semantic_gui_design owns SEM message-kind/TimerExpression constructor slice and
|
||||
compact Traceability toolbar clipping fix (views.rs), plus its semantic module/forms
|
||||
adapter/GUI ledger and semantic-desktop-target. Scope is one existing trace and a
|
||||
complete nested constructor family; no API/core change or full parity claim.
|
||||
semantic_http_review temporarily owns semantic-desktop-target for isolated process
|
||||
review; GUI worker must wait for release before builds. Root owns all native input.
|
||||
|
||||
Latest process closure: DATA/Linux f4cb486, production8c60008.327 desktop tests,
|
||||
strict Clippy/fmt/build pass on both. Independent26 tests (4 actual Iced observer,
|
||||
22 process) close owner-scroll findings. Both targets are idle and root-owned.
|
||||
Frozen ui-process-owner-{windows,linux}-8c60008 await root native verification.
|
||||
semantic_http_review used a separate Temp target and released it; SEM GUI owns
|
||||
semantic-desktop-target exclusively. Hosted lifecycle mapping is read-only parallel
|
||||
work by semantic_integration; no shared index or target reservation for that task.
|
||||
Dedicated targets only. No remote push, usage reset or broad target deletion.
|
||||
|
||||
@@ -98,3 +98,42 @@ have actual native evidence in their ledgers; advanced and compact cases remain.
|
||||
X11 lacks libxkbcommon-x11-0, installation requires user's sudo password. User has
|
||||
the exact fix command; Windows/Wayland progress is independent. This environment
|
||||
issue is not evidence of a semantic defect or reason to mark the release blocked.
|
||||
|
||||
2026-09-06: DATA67ed8e0/Linux43dd566 integrates bounded MessageKind/TimerExpression
|
||||
review.335 desktop tests and strict checks/build pass per platform; independent22
|
||||
module tests+288 differential cases close no findings. Windows native scalar now
|
||||
includes named mapping/default, historical identities, reopen and reverse Undo;
|
||||
trial closed0/empty stderr. Exact scope and pending native cases:
|
||||
release-semantic-native.md. Hosted desktop implementation is isolated in
|
||||
Capella2-hosted-desktop; all release completion claims remain pending.
|
||||
|
||||
## Hosted/native and constructor checkpoint — 2026-09-06
|
||||
|
||||
Verified: optional hosted desktop is integrated; native publish, exact authored
|
||||
bundle preservation, graphical/text Save, invalid-source rejection, API artifact
|
||||
review/reload/Undo/Redo and competing-revision local retention passed on Windows.
|
||||
Minimum hosted dialog controls remain reachable. Offline/reopen/local Save As and
|
||||
Linux native hosted acceptance continue. No external server was deployed.
|
||||
|
||||
Independent core review closed43370de/8a75294 with31 semantic tests, optional
|
||||
Binding/Annotation both-direction probes,3 reverse Port boundary and13 port
|
||||
regressions. Full owner core suite359 passes. Semantics preserve target-local keyed
|
||||
members without resurrecting source removals, mandatory reset and OA Legacy rules.
|
||||
Remaining constructor GUIc149f2f passes299 SEM tests; fresh review is active.
|
||||
|
||||
Native process camera/owner fix acceptance passed. New compact form finding was
|
||||
fixed and passed real Iced layout+360 desktop tests; native fix acceptance pending.
|
||||
Strategy remains measured release completion, not treating these slices as full
|
||||
source/UI/API parity. Keyed/order/reset, artifacts/multi-source, remaining advanced
|
||||
native cases, Home/recovery/help/coverage and final acceptance are still required.
|
||||
|
||||
## Reviewed corrections and native Data evidence — 2026-09-06
|
||||
|
||||
Verified: constructorc149f2f/reset-unordered6fb01ae and hostedb04c4b3/cataloge807086
|
||||
are independently reviewed/integrated. Both platforms pass392 desktop tests and
|
||||
strict gates. Native Data Take matched the whole core-generated Project/history;
|
||||
Candidate raw invalid/type mismatch/repair/compact Apply/Undo/Redo/Save passed.
|
||||
Candidate exactProject and remaining constructors/Linux native are still pending.
|
||||
Strategy: finish structured parity while Home/recents progresses independently;
|
||||
recovery, ordered/attachments/artifacts/multi-source and final acceptance remain.
|
||||
|
||||
|
||||
@@ -55,3 +55,89 @@ Frozen executable: `D:/CodexBuilds/Capella2-release1-01a06e2e/ui-hosted-windows-
|
||||
SHA-256: `51E4C02F1265D3E9B140C2BCF34A8B11E25E5B0D1D32C83E987FEA4BEB4FC061`.
|
||||
|
||||
The release output and frozen copy hashes match. A sibling `manifest.json` records the production commit, gates and pending acceptance. No existing trial artifact was replaced. The assigned scenario target and HOST index are idle and returned to the lead after this evidence checkpoint.
|
||||
|
||||
## Windows native checkpoint — 2026-09-06
|
||||
|
||||
Root operated frozen6fcc238 (SHA51E4C02F1265D3E9B140C2BCF34A8B11E25E5B0D1D32C83E987FEA4BEB4FC061)
|
||||
at1440x900 and1100x700. Isolated loopback service127.0.0.1:8787 uses
|
||||
C:/Users/sorok/AppData/Local/Temp/capella2-hosted-native-20260906/store.
|
||||
Resource d28b2fc9-fbbd-456b-8ba8-a1195d1c8f33; no external deployment.
|
||||
|
||||
- Native Open loaded the isolated74-element/13-link/three-file process model.
|
||||
Connect displayed an empty catalog. Publish created revision1; REST envelope
|
||||
comparison confirmed all three authored file strings exactly matched disk.
|
||||
- Edited project description graphically; Save to server applied its pending
|
||||
property draft and saved revision2. REST confirmed content and bundle counts.
|
||||
- Typed a source comment without its newline, making the header invalid.
|
||||
Save retained the draft, showed syntax lint and left server revision2 unchanged.
|
||||
Inserted newline; Ctrl+S saved valid source/comment as revision3.
|
||||
- Conditional REST CreateArtifact attached Navigation CAD contract to Request
|
||||
navigation, revision4. Native Check server displayed changed project.arcadia;
|
||||
read-only View server text preserved local state. Reload exposed the artifact
|
||||
with Implements, URI and revision. Undo removed the local attachment; Redo
|
||||
restored it and returned to the server-saved state, without network writes.
|
||||
- Entered a different local project description; competing REST EditProject
|
||||
created revision5. Native Save was rejected by stale revision protection and
|
||||
automatically loaded a separate review; the local description remained intact.
|
||||
Independent REST read confirmed server revision5 and server description.
|
||||
- At1100x700, conflict body scrolls while bottom actions remain visible; review,
|
||||
changed-file action and Reload are reachable. Reload adopted revision5 and
|
||||
Undo restored the exact competing local description as unsaved local content.
|
||||
|
||||
Evidence responses: published.json, graphical-save.json, textual-save.json,
|
||||
artifact-transaction.json and competing-transaction.json in the isolated directory.
|
||||
Root harness mistakes (wrong route prefix, PowerShell array-valued ETag) failed
|
||||
before successful corrected requests; these were not application failures.
|
||||
Current window2493046 remains open. Server11260 was identity-checked and stopped
|
||||
for the next offline-save trial; offline result/restart and Save as local/catalog
|
||||
Open/Linux acceptance are still pending at this checkpoint.
|
||||
|
||||
Integrated Windowsf08507a passed360 desktop tests, strict all-target Clippy, fmt
|
||||
and optimized build. Linuxba9c117 passed359 tests and the same gates before the
|
||||
compact form fix. DATA28ebbbb/Linux04210b3 now include independently reviewed
|
||||
core constructor-history/explicit-Port fixes; their full workspace gates are active.
|
||||
An initial Linux invocation selected unconfigured Ubuntu; corrected to Ubuntu-24.04.
|
||||
|
||||
## Windows offline, local-copy and catalog completion
|
||||
|
||||
Native6fcc238 at1100x700: stopped only identity-checked isolated server11260.
|
||||
Save reported unreachable/interrupted service and retained the local model/source.
|
||||
Restarted same binary/store as20984; independent read found revision5 unchanged.
|
||||
Explicit Save retry succeeded revision6 with the restored local description and
|
||||
CAD attachment. No automatic replay was involved.
|
||||
|
||||
Save as local wrote a new .arcadia project under
|
||||
C:/Users/sorok/AppData/Local/Temp/capella2-hosted-native-20260906/local-copy.
|
||||
Header changed from Save to server/Hosted to Save all/All changes saved. All three
|
||||
disk file strings compared exactly equal to server revision6, including the comment
|
||||
and both includes. Evidence: offline-recovery.json and local-copy-equality.json.
|
||||
Connect/refresh then displayed the resource; selecting it and Open selected loaded
|
||||
the hosted revision6, restored Save to server and the authored model. Trial71940
|
||||
closed normally, exit0, empty native.stderr.log. No user model was modified.
|
||||
|
||||
Native P2: after Publish/Save the catalog retained its initial empty result and
|
||||
misleading No models found text until manual refresh. A separate follow-up is pending.
|
||||
Code review then reproduced a high-impact destination defect with actual App/HTTP:
|
||||
opening invalid local source from hosted, repairing and saving could overwrite the
|
||||
old resource. The local repair-workspace branch needs to detach its previous hosted
|
||||
handle; true read failure/Cancel must retain it. HOST owns red/green fix; fresh review
|
||||
and native fixed-flow verification are required before hosted acceptance closes.
|
||||
|
||||
DATA28ebbbb/Linux04210b3 full workspace gates completed:767/770 tests respectively,
|
||||
one intentional ignored benchmark each, strict workspace all-target Clippy, fmt and
|
||||
optimized desktop/server builds pass. Logs hosted-core-{workspace-test,workspace-clippy,
|
||||
build}.log in dedicated data/data-linux targets. GUI constructors and help are now
|
||||
integrated at DATAcf04ff1/Linuxb5db730; desktop gates3782/6616 are active.
|
||||
|
||||
## Destination and catalog correction checkpoint — 2026-09-06
|
||||
|
||||
b04c4b3 detaches a hosted handle when installing invalid local repair source;
|
||||
failed/cancelled Open retains the current document and destination. Actual-App
|
||||
HTTP regression failed before the fix and passed afterward; independent review
|
||||
closed.12e3020/e807086 identify stale catalogs after writes/changed heads, including
|
||||
empty filtered results with a bound resource outside the displayed rows. Fresh
|
||||
review reproduced that case red, then closed correction with negative unchanged-
|
||||
head and cross-service controls. Both fixes are integrated in DATA14b8e18 and
|
||||
Linux9c61d0d;392 desktop tests/platform and strict gates pass. Native correction
|
||||
checks remain pending. No polling, automatic overwrite or persisted token added.
|
||||
|
||||
|
||||
@@ -272,3 +272,23 @@ executable access was checked without launching. Both build targets are idle and
|
||||
returned to the lead. This task used no native input. Root must verify preserved
|
||||
Fit scale and the remotely scrolled region through tool/form changes and inspect
|
||||
the persistent compact-form labels on the actual application.
|
||||
|
||||
## Actual Windows camera/owner verification and compact finding
|
||||
|
||||
Root operated67ed8e0 at1440x900,1440x700 and1100x700. Opening the isolated source4
|
||||
model and explicit migration/save produced source5 with74 elements,13 relations,
|
||||
the same five incomplete-engineering checks and all three files retained.
|
||||
Fit28% displayed the entire chain; selecting the OR alternate kept28% scale.
|
||||
At100%, horizontal scrolling to the remote Choice/Merge region survived opening
|
||||
Two paths and Cancel. Changing owner to Navigation conversation and back reset
|
||||
the actual mounted canvas to the first Request navigation occurrence. Compact
|
||||
Traceability Artifact action wrapped and remained visible. Trial closed normally,
|
||||
exit0 and empty stderr. Source original was unchanged; only the isolated copy migrated.
|
||||
|
||||
Native minimum-size trial found P2: pending contribution forms competed with the
|
||||
canvas inside an outer page scroll; only74px of fields could be visible. Fixf08507a
|
||||
makes the form an independent pane with pinned actions and scrollable fields,
|
||||
keeps the diagram scroll/canvas mounted and divides available height while pending.
|
||||
Real Iced layout regression covers570/770px,203/303px field viewports and retained
|
||||
page scroll open/cancel.23 focused tests and strict Clippy pass; integrated360
|
||||
desktop tests/build pass. Actual native verification of this fix remains pending.
|
||||
|
||||
+207
-19
@@ -1,7 +1,8 @@
|
||||
# Semantic desktop review — milestones 1 and 2
|
||||
# Semantic desktop review — scalars and closed constructor groups
|
||||
|
||||
Status: milestones 1–2 implemented and desktop tests/build verified; not full GUI
|
||||
parity, native acceptance or release acceptance.
|
||||
Status: milestones 1–2 and the closed constructor group forms are implemented.
|
||||
Independent keyed/ordered review, historical pairing reset, multi-source review,
|
||||
full GUI parity, native acceptance and release acceptance remain outstanding.
|
||||
Base: `d130013`. Dedicated build target:
|
||||
`D:/CodexBuilds/Capella2-semantic-desktop-target`. Native verification is root-owned.
|
||||
|
||||
@@ -16,9 +17,10 @@ Base: `d130013`. Dedicated build target:
|
||||
- Support every applicable scalar leaf through Keep, Take and typed final-value
|
||||
forms. Candidate records contain the complete combined selected result. Identity,
|
||||
alias and layer are protected. The closed core remains the validation boundary.
|
||||
- State.Kind is a single structural scalar. Other constructor switches, keyed
|
||||
membership/order/annotation/binding/attachment decisions and pairing resets belong
|
||||
to milestone 3; show them read-only with an honest explanation.
|
||||
- State.Kind is a single structural scalar. Data, Port, Occurrence, nested
|
||||
MessageKind and Event support their closed constructor groups and complete
|
||||
typed payloads. Independent keyed membership/order/annotation/binding/attachment
|
||||
decisions and pairing resets remain visibly read-only outside a replacing group.
|
||||
- Core `6bb2583` permits first equal constructor acknowledgment without descendant
|
||||
expansion. The GUI exposes **Acknowledge equal kind only** when applicable;
|
||||
previously reviewed constructors remain governed by the core's switch rules.
|
||||
@@ -36,14 +38,16 @@ Base: `d130013`. Dedicated build target:
|
||||
2. Scalar decisions and typed candidates; immutable initial observations; combined
|
||||
final records; exact impact acceptance; atomic document/source commit. Focused
|
||||
App event/source/history tests, strict Clippy and a dedicated desktop build.
|
||||
3. Constructor closure, keyed and ordered facts, reset, complete artifact lifecycle,
|
||||
multi-source review and remaining source/REST parity cases. Still outstanding.
|
||||
3. All closed constructor forms are implemented, including nested MessageKind.
|
||||
Independent keyed and ordered facts, reset, complete artifact lifecycle,
|
||||
multi-source review and remaining source/REST parity cases are outstanding.
|
||||
4. Root-owned native review, cross-surface fixtures, package/release acceptance.
|
||||
|
||||
## Verification
|
||||
|
||||
Verified after the independent scalar review fixes on SEM `6ad3cb`, using only the
|
||||
dedicated desktop target (core behavior remains based on `6bb2583`):
|
||||
Scalar fixes were verified on SEM `6ad3cb`; the nested message-kind slice builds
|
||||
on scalar commit `5a51a75`. Verification uses only the dedicated desktop target
|
||||
(core behavior remains based on `6bb2583`):
|
||||
|
||||
```powershell
|
||||
$env:CARGO_TARGET_DIR='D:/CodexBuilds/Capella2-semantic-desktop-target'
|
||||
@@ -52,8 +56,8 @@ cargo clippy --locked -p oa-desktop --all-targets -- -D warnings
|
||||
cargo build --locked -p oa-desktop
|
||||
```
|
||||
|
||||
All **277 desktop tests passed**, including **11 original semantic review tests**
|
||||
and **5 scalar review regression tests**.
|
||||
All **285 desktop tests passed**, including **11 original semantic review tests**,
|
||||
**5 scalar review regression tests**, and **8 nested message-kind tests**.
|
||||
Strict Clippy and the desktop build passed. The existing migration test's header
|
||||
assertion now follows `SCHEMA_VERSION` instead of expecting schema 4.
|
||||
|
||||
@@ -85,19 +89,203 @@ The independent scalar review found three gaps, now corrected:
|
||||
The regression tests exercise App events, the actual default-form helpers and
|
||||
button availability, exact source retention before Apply, and one-step undo.
|
||||
|
||||
## Bounded constructor slice
|
||||
|
||||
Occurrence.MessageKind is a complete closed nested family: Asynchronous, Call,
|
||||
Reply, Create, Delete and Timer. It is available only when both traced counterparts
|
||||
are Exchange occurrences. The only constructor-specific payload is the timer
|
||||
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.
|
||||
Taking a replacing group copies the typed MessageKind, including its expression.
|
||||
Taking an initial equal discriminator does not copy an unselected timer value.
|
||||
- Candidate MessageKind has its own scoped draft. The independent TimerExpression
|
||||
scalar draft survives group decisions; the group timer's exact raw text, editor
|
||||
cursor and selection survive switching away from Timer and back.
|
||||
- An overlapping kind/timer draft retains both decisions and blocks Apply until
|
||||
explicitly resolved. The dialog offers **Replace timer decision with message-kind
|
||||
group** and **Leave message kind unselected; use timer decision**. First equal-kind
|
||||
acknowledgment can coexist with an independent timer decision. Divergent authored
|
||||
candidate values reject; neither candidate silently overwrites the other.
|
||||
- The kind detail includes baseline/current timer observations and the complete
|
||||
proposed kind. It displays the exact plan-provided acknowledged/retired lists.
|
||||
Exchange, reply-to, lifelines, annotations and pre/postconditions stay separately
|
||||
scoped. Incompatible Timer contracts remain core errors; no dependency or payload
|
||||
is silently removed. Existing additional-impact approval remains in force.
|
||||
- Group changes compose with scalar choices and execute the exact prepared Document
|
||||
batch. Tests cover both directions, all six kinds, mapped reply correlation,
|
||||
historical retirement, repeated no-switch rejection, invalid combinations,
|
||||
protected scopes, stale preview, source failure, comments and one-step undo/redo.
|
||||
- The Traceability contextual toolbar now wraps at the available width so +Artifact
|
||||
remains reachable in the compact layout. This is a single `row.wrap()` change in
|
||||
`views.rs`; other toolbars are outside this slice.
|
||||
|
||||
Root reported native scalar coverage on frozen integrated `e7fa061`: mixed numeric
|
||||
Keep/Take/Candidate, invalid default retention/repair, zero additional changes,
|
||||
atomic Apply, comment/value inspection, Undo/Redo and Save All at 1440×900 and
|
||||
1100×700. Reverse/type/reference/history/reopen native checks were still continuing
|
||||
when this slice was implemented. This task launched no native application.
|
||||
|
||||
Built executable:
|
||||
`D:/CodexBuilds/Capella2-semantic-desktop-target/debug/arcadia-studio.exe`.
|
||||
The build target is idle. No native application was launched by this task.
|
||||
|
||||
## Remaining work
|
||||
|
||||
Milestone 3 must add explicit constructor closure/replacement, keyed and ordered
|
||||
decisions, historical pairing reset, complete artifact lifecycle and multi-source
|
||||
review. These facts are currently inspectable, with unsupported writes visibly
|
||||
Milestone 3 must add independent keyed and ordered decisions, historical pairing
|
||||
reset, complete artifact lifecycle and multi-source review. These facts are currently inspectable, with unsupported writes visibly
|
||||
identified. The reference table supplies explicit mappings for scalar reference
|
||||
Take; it does not claim to complete a reset or establish keyed acknowledgments.
|
||||
|
||||
Fresh independent scalar GUI review and root-owned native verification are still
|
||||
required before merging this milestone into release acceptance. In particular,
|
||||
validate the 1100×700 layout, keyboard/focus continuity, long historical values,
|
||||
reference selection, source reopening and prepared-impact inspection in the app.
|
||||
Fresh independent requirements/correctness review and root-owned native checks of
|
||||
the message-kind slice are required before release acceptance. Verify the compact
|
||||
kind picker, timer text/focus retention, overlap choices, exact closure/retirement,
|
||||
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.
|
||||
|
||||
## Independent unordered membership and exact reset slice
|
||||
|
||||
Current scope: seven unordered reference collections (function input/output items,
|
||||
signal/exchange items, transition triggers/effects, constrained identities) and exact
|
||||
enumeration literals. Stage an explicit OA/SA address, then choose Keep, Take or
|
||||
Candidate presence. New rows never create referenced model objects or infer pairs.
|
||||
Pair changes preserve existing decisions and require explicit history resets and
|
||||
overlap resolution. Literal pairing uses the same exact string on both sides.
|
||||
|
||||
History lists exact previous observations, with reversible pending resets applied
|
||||
before catalog construction, constructor scope and candidate composition. A reset-only
|
||||
Apply uses the same core prepared command batch and atomic source/history undo.
|
||||
Artifact history stays deferred; other read-only value families can have their
|
||||
exact historical observations reset without exposing value editors.
|
||||
|
||||
Independent annotation, binding, ordered/owned/incident/artifact decisions and
|
||||
multi-source review remain later slices. Existing constructor forms remain intact.
|
||||
The member-family picker offers collections available on the current counterparts.
|
||||
Identity pickers reuse the ordinary property editor's typed choices. Presence and
|
||||
all scalar/group drafts remain separate; leaving a decision or removing a staged
|
||||
address retains its raw presence draft. A missing source key means a target-local
|
||||
row that Take preserves; an explicit absent source key can remove only the mapped
|
||||
entry. Existing model objects and unrelated entries remain intact. Previous member
|
||||
identities, deleted UUIDs and quoted exact literals remain distinguishable.
|
||||
|
||||
History and Members use the existing compact scroll area, and the contextual tabs
|
||||
wrap. Users can explicitly remove staged addresses and their decisions or uncheck
|
||||
obsolete pending resets after refresh, without discarding other work. Candidate
|
||||
projection changes only the selected boolean member entries; the core independently
|
||||
checks the complete typed candidate and supplies all acknowledged/retired/impact
|
||||
scope. The GUI never synthesizes accepted history or additional-change commands.
|
||||
|
||||
On base `c149f2f`, all **311 desktop tests pass**, including 12 new workflow tests.
|
||||
The suite covers 30 five-family/direction/Keep-Take-Candidate cases, both typed
|
||||
function-port item families with reverse Legacy applicability, exact literal
|
||||
add/remove and default retention, historical pair migration with explicit overlap
|
||||
resolution, constructor reset before equal-kind acknowledgment, deleted source
|
||||
members and target-local preservation, exact reset-only source/Undo/Redo, source
|
||||
reopen/comments, retained drafts, stale/source-write failures and recovery.
|
||||
Owned-file rustfmt check, strict Clippy (`--all-targets -- -D warnings`) and
|
||||
`cargo build --locked -p oa-desktop` all pass with the dedicated desktop target.
|
||||
No core, API, schema or shared header changes were needed.
|
||||
|
||||
Root-owned native acceptance uses the fixtures mirrored by `keyed_tests.rs`:
|
||||
- Signal pair: source contains a mapped item; destination contains a separate local
|
||||
item. In Members, choose Signal items and both mapped identities, then Take in
|
||||
Properties. Inspect the proposed present row and retained local row; apply, undo,
|
||||
redo and inspect source. Repeat in reverse. Test Candidate exclusion as well.
|
||||
- Enum pair: both contain `retained` with that default; source also contains
|
||||
` New value ` and destination contains `old`. Stage exact paired literals; Take
|
||||
the incoming row, Candidate-remove `old`, Candidate-add `new value`. Inspect the
|
||||
distinct exact strings, unchanged default and one source/history transaction.
|
||||
- Historical pair: an existing one-sided member observation is replaced by an
|
||||
explicitly paired address. Apply stays unavailable until that exact old history
|
||||
is reset and any overlapping earlier decision is explicitly left unselected.
|
||||
Use History for a reset-only Apply; values and prose history stay unchanged, and
|
||||
one Undo restores the exact prior source and semantic history.
|
||||
- At 1100x700, verify wrapped tabs, scrollable member/history content, long/deleted
|
||||
identity labels, retained errors/decisions, staged-address removal, source failure
|
||||
repair and apply/cancel access; repeat a normal transaction at 1440x900.
|
||||
|
||||
No native application was launched here. Independent review, actual native
|
||||
acceptance, later keyed/ordered slices and full GUI parity remain open.
|
||||
|
||||
@@ -6,7 +6,7 @@ Trial exec62786/window2099614; stderr/stdout captured under ui-semantic-windows-
|
||||
Source fixture and exact GUI steps: C:/Users/sorok/AppData/Local/Temp/
|
||||
capella2-semantic-native-fixture-20260906. Original source SHA256
|
||||
54BA9528F6904428B3DE2E41E3D8CAC6861FA652E5F83F11C1516FB3DB40E937.
|
||||
Root edits only windows-scalar.arcadia, preserving the generator's starting source.
|
||||
Root edited windows-scalar.arcadia, preserving the generator's starting source.
|
||||
|
||||
## Observed Windows workflows
|
||||
|
||||
@@ -27,9 +27,80 @@ All persists it. This exercises combined type context through actual controls.
|
||||
|
||||
## Remaining checks and findings
|
||||
|
||||
Reverse decisions, raw invalid bound scoped clearing, named-type mapping, deleted
|
||||
historical identities, saved reopen and Linux remain required. Error evidence above
|
||||
Raw invalid bound scoped clearing and Linux remain required. Error evidence above
|
||||
covers a normal validation message, not arbitrarily large diagnostics.
|
||||
Native found compact Traceability toolbar clips Artifact after the semantic button;
|
||||
semantic_gui_design owns a narrow wrapping correction. No scalar-modal defect found
|
||||
in the tested paths. Structured review is a separate incomplete milestone.
|
||||
|
||||
## Mapping, history, reopen and reverse review — 2026-09-06
|
||||
|
||||
At1100x700, selected Take Operational Type on OA Selected count → SA Selected count.
|
||||
Preview rejected the missing explicit OA Count range correspondence and disabled
|
||||
Apply. Reference pairs OA Count range → SA Count range resolved it. The proposed
|
||||
named type was @sa_count_range; a candidate literal default initialized to Integer1,
|
||||
using the destination range minimum. Apply and Save All persisted both atomically.
|
||||
Disk source confirms Named @sa_count_range and Integer1, with0 model checks.
|
||||
|
||||
Selected the record trace and inspected both historical deleted children. Detail
|
||||
headings distinguish full UUIDs ending2329 and232a, despite identical former names;
|
||||
prior OA presence and current absence are retained. Cancel changed nothing.
|
||||
|
||||
Opened the saved windows-scalar.arcadia through the native file dialog. It reopened
|
||||
with9objects/0links/0checks, original comment, accepted values and reviewed history.
|
||||
On the range trace, switched to System→Operational and included unchanged facts.
|
||||
Previously reviewed/current minimum values were OA0/SA1 and defaults OA5/SA7.
|
||||
Take System minimum and default previewed1/7; Apply selected the OA destination.
|
||||
Native source showed OA default7. One Undo restored OA default5 and All changes
|
||||
saved, retaining the earlier three saved reviews. The reverse exercise was left
|
||||
undone intentionally. The separate windows-reverse.arcadia remains an unused copy.
|
||||
|
||||
Closed trial62786 normally, exit0. Captured stderr was empty. No new scalar defect
|
||||
was found. This still does not establish complete structured review acceptance.
|
||||
|
||||
## Integrated message-kind group verification — 2026-09-06
|
||||
|
||||
SEM f73551f integrates as DATA67ed8e0 and Linux43dd566. Both platforms passed335
|
||||
complete desktop tests, strict all-target desktop Clippy, workspace formatting and
|
||||
optimized desktop build. Core/server are unchanged from the full integrated gates.
|
||||
Independent review on a fresh Temp archive passed22 semantic-review tests and a
|
||||
288-case differential matrix against core (all6 kinds, both directions, baseline
|
||||
and no-baseline, Keep/Take with unrelated Header candidate). No material findings.
|
||||
Temp probe: capella2-message-review-f73551f/semantic_review/message_kind_tests.rs
|
||||
under the desktop crate. Root native checks of the new group remain required.
|
||||
|
||||
Frozen binaries under D:/CodexBuilds/Capella2-release1-01a06e2e:
|
||||
- ui-semantic-groups-windows-67ed8e0/arcadia-studio.exe, SHA256
|
||||
CC741CC0FE21B7873E545DE7763E74CCF18AB8401D1EEBD8F90DA493B7262DD8.
|
||||
- ui-semantic-groups-linux-67ed8e0/arcadia-studio, SHA256
|
||||
B50BD6462C239C52AEF93FD235FC3652901C519E5289938DD881523357C891D4.
|
||||
|
||||
Windows build/test session97666 and Linux2903 exited0. Linux logs are
|
||||
message-kind-{test,clippy,build}.log in Capella2-data-linux-target. Frozen manifests
|
||||
record source/platform/hash. The Windows freeze contains an isolated copy of the
|
||||
74-object process fixture for native camera, owner-scroll and branch checks.
|
||||
|
||||
## Constructor and keyed integration checkpoint — 2026-09-06
|
||||
|
||||
Integrated c149f2f constructor and6fb01ae exact reset/unordered Member/Literal,
|
||||
independent reviews closed. DATA14b8e18/Linux9c61d0d pass392 desktop tests each,
|
||||
strict Clippy/fmt/release. Builds include hosted destination/catalog fixes too.
|
||||
|
||||
Native Windows cf04ff1 trial84997: fixture01 Data Boolean→Integer Take copied
|
||||
0..100/default42 while retaining unitcm. Preview acknowledged Constructor,
|
||||
IntegerMinimum/Maximum and Default. Apply selected SA; one Undo restored Boolean
|
||||
and old history; Redo restored accepted Integer. Save retained authored comments;
|
||||
independent Rust verifier matched the complete parsed Project including history to
|
||||
expected/01-data-switch-after-primary.arcadia, with zero structural errors.
|
||||
|
||||
Then Undo and Candidate Real: incompatible Boolean default blocked Apply; raw '-'
|
||||
lower bound survived Real→Integer→Real. Repair -1.5, max100 and default Real3.5
|
||||
enabled Apply. At actual1100x700 all fields and pinned actions were reachable;
|
||||
exact scope listed Constructor, Integer/Real min/max and Default, no retirement.
|
||||
Apply updated model/source/history together. Undo restored Booleanfalse/unitcm and
|
||||
old history, Redo restored Real bounds/default and unchanged-since-review state.
|
||||
Save succeeded; disk confirmed OA unchanged, SA Real -1.5..100/default3.5/unitcm and
|
||||
both authored comments. Independent exact candidate comparison is pending.
|
||||
Minor misleading 'Minimum count' error label reported to SEM for local correction.
|
||||
Other constructor families, reset/keyed and Linux native are not yet accepted.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user