Compare commits

..
Author SHA1 Message Date
sorokin.oleg.edu c091f80e6e docs: record integrated review gates and native constructor evidence
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
2026-09-06 08:22:58 +03:00
sorokin.oleg.edu 14b8e187ea fix(desktop): invalidate filtered catalog on changed hosted head 2026-09-06 08:15:31 +03:00
sorokin.oleg.edu 00ac6eb86c fix(desktop): identify stale hosted catalog results 2026-09-06 08:15:31 +03:00
sorokin.oleg.edu 843635ed26 Add exact semantic history resets and unordered member review 2026-09-06 08:09:44 +03:00
sorokin.oleg.edu 2c0f93839f fix(desktop): detach hosted destination for invalid local source 2026-09-06 08:07:47 +03:00
sorokin.oleg.edu 09b7ebddc4 Record hosted native recovery and destination review findings 2026-09-06 07:57:02 +03:00
sorokin.oleg.edu cf04ff1442 Document hosted desktop workflows and local save behavior 2026-09-06 07:49:02 +03:00
sorokin.oleg.edu 1a68ae0795 Review closed semantic constructor groups with retained typed drafts 2026-09-06 07:49:01 +03:00
sorokin.oleg.edu 0e47384532 Record native hosted conflict and process verification 2026-09-06 07:48:15 +03:00
sorokin.oleg.edu 28ebbbbc4c Allow review of explicitly mapped existing OA Legacy ports 2026-09-06 07:39:43 +03:00
sorokin.oleg.edu 4f5906b0ff Normalize constructor review members without losing local history 2026-09-06 07:39:43 +03:00
sorokin.oleg.edu f08507a789 Keep process contribution editors fully visible in compact windows 2026-09-06 07:29:57 +03:00
sorokin.oleg.edu 691ef26a01 Record verified hosted Windows trial and reproducible checks 2026-09-06 07:23:18 +03:00
sorokin.oleg.edu 5930f91ed6 Polish hosted request controls and record full desktop checks 2026-09-06 07:23:18 +03:00
sorokin.oleg.edu 1b80671dd0 Add optional hosted desktop editing with conditional source saves 2026-09-06 07:23:18 +03:00
sorokin.oleg.edu 49a2446f85 Record native scalar review and integrated constructor verification 2026-09-06 07:11:44 +03:00
sorokin.oleg.edu 67ed8e09be Review nested message kind groups with exact timer scope 2026-09-06 07:05:50 +03:00
24 changed files with 5138 additions and 264 deletions
@@ -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);
}
}
+60 -15
View File
@@ -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;
+171 -2
View File
@@ -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(&current, &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(&current));
let next_key = (path.to_owned(), tag(&proposed));
let mut inputs = BTreeMap::new();
for key in self
.inputs
.keys()
.filter(|key| within(key, path))
.cloned()
.collect::<Vec<_>>()
{
inputs.insert(key.clone(), self.inputs.remove(&key).unwrap());
}
self.constructor_drafts
.insert(previous_key, (current.clone(), inputs));
let saved = self.constructor_drafts.remove(&next_key);
let (value, restored) = if let Some(saved) = saved {
saved
} else {
let value = if path.ends_with("/Port/contract") {
port_fields::switch_contract(
port_current.as_ref().unwrap_or(&current),
proposed,
&mut BTreeMap::new(),
)
} else {
proposed
};
(value, BTreeMap::new())
};
self.shape_scoped(path, value)?;
self.inputs.extend(restored);
Ok(())
}
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)
}
+8 -1
View File
@@ -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") {
+279 -53
View File
@@ -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(&current, Layer::Oa).is_some()
&& scalar::observation(&current, Layer::Oa)
== scalar::observation(&current, 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(&current, 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(&current, Layer::Oa) != scalar::observation(&current, Layer::Sa) {
return Ok(value);
}
let references = if direction == TransitionDirection::OaToSa {
map.clone()
} else {
map.iter().map(|(sa, oa)| (*oa, *sa)).collect()
};
let old = trace.baseline.as_ref().and_then(|b| b.semantic.as_ref());
for fact in semantic_facets(project, trace, &references)? {
if !contains(facet, &fact.selector) {
continue;
}
let Some((path, key)) =
target_row(project, target, &fact.selector, direction.target_layer())
else {
continue;
};
if row_present(&fact.current, direction.source_layer())
|| !row_present(&fact.current, direction.target_layer())
{
continue;
}
let was_paired = old.is_some_and(|b| {
b.facets.iter().any(|previous| {
row_present(previous, direction.source_layer())
&& target_row(
project,
target,
&previous.selector(),
direction.target_layer(),
)
.as_ref()
== Some(&(path.clone(), key.clone()))
})
});
if was_paired {
continue;
}
let row = scalar::value(&fact.current, direction.target_layer())
.ok_or("Target row is inapplicable.")?;
let items = value
.pointer_mut(&path)
.and_then(Value::as_array_mut)
.ok_or("Target collection is unavailable.")?;
match &fact.selector {
FacetSelector::Member { .. } | FacetSelector::Literal(_) => {
if row == Value::Bool(true) && !items.contains(&key) {
items.push(key);
} else if row == Value::Bool(false) {
items.retain(|v| v != &key);
}
}
FacetSelector::Annotation(_) => {
items.retain(|v| {
serde_json::from_value::<MessageValue>(v.clone())
.ok()
.map(|v| serde_json::to_value(AnnotationKey::from(&v)).unwrap())
.as_ref()
!= Some(&key)
});
if !row.is_null() {
items.push(row);
}
}
FacetSelector::Binding(_) => {
items.retain(|v| v.get("referenced") != Some(&key));
if !row.is_null() {
items.push(row);
}
}
_ => unreachable!(),
}
}
Ok(value)
}
fn row_present(fact: &FacetReview, layer: Layer) -> bool {
match fact {
FacetReview::Member { .. } | FacetReview::Literal { .. } => {
scalar::value(fact, layer) == Some(Value::Bool(true))
}
FacetReview::Annotation { .. } | FacetReview::Binding { .. } => {
scalar::value(fact, layer).is_some_and(|value| !value.is_null())
}
_ => false,
}
}
fn row_key(facet: &FacetSelector, layer: Layer) -> Option<Value> {
match facet {
FacetSelector::Member { keys, .. } | FacetSelector::Binding(keys) => {
chosen(keys, layer).map(|v| serde_json::json!(v))
}
FacetSelector::Literal(keys) => chosen(keys, layer).map(|v| serde_json::json!(v)),
FacetSelector::Annotation(keys) => chosen(keys, layer).map(|v| serde_json::json!(v)),
_ => None,
}
}
fn target_row(
project: &Project,
target: Id,
facet: &FacetSelector,
layer: Layer,
) -> Option<(String, Value)> {
let key = row_key(facet, layer)?;
let (group, suffix) = match facet {
FacetSelector::Literal(_) => (
FacetSelector::Data(DataFacet::Constructor),
"/Enumeration/literals",
),
FacetSelector::Binding(_) => (
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
"/ScenarioUse/bindings",
),
FacetSelector::Annotation(_) => (
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
"/Exchange/values",
),
FacetSelector::Member { collection, .. } => match collection {
ReferenceCollection::FunctionInputItems => (
FacetSelector::Port(PortFacet::Constructor),
"/FunctionInput/incoming_items",
),
ReferenceCollection::FunctionOutputItems => (
FacetSelector::Port(PortFacet::Constructor),
"/FunctionOutput/outgoing_items",
),
ReferenceCollection::SignalItems => (
FacetSelector::Event(EventFacet::Constructor),
"/Signal/items",
),
ReferenceCollection::Steps => (
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
"/Branch/occurrences",
),
ReferenceCollection::FragmentBranches => (
FacetSelector::Occurrence(OccurrenceFacet::Constructor),
"/Fragment/branches",
),
_ => return None,
},
_ => return None,
};
Some((
format!("{}{suffix}", scalar::binding(project, target, &group)?),
key,
))
}
/// A selected descendant may fill untouched group fields during first-equal
/// acknowledgment, but cannot silently replace an authored group field.
pub(super) fn authored_agrees(original: &Value, authored: &Value, final_value: &Value) -> bool {
if original == authored || authored == final_value {
return true;
}
match (original, authored, final_value) {
(Value::Object(a), Value::Object(b), Value::Object(c)) if a.keys().eq(b.keys()) => {
b.iter().all(|(key, v)| {
c.get(key)
.is_some_and(|final_value| authored_agrees(&a[key], v, final_value))
})
}
_ => false,
}
}
@@ -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);
}
}
+11 -8
View File
@@ -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),
),
);
+324 -30
View File
@@ -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),
+1
View File
@@ -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
View File
@@ -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.
+15
View File
@@ -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.
+10
View File
@@ -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
View File
@@ -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.
+39
View File
@@ -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.
+86
View File
@@ -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.
+20
View File
@@ -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
View File
@@ -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.
+74 -3
View File
@@ -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.