Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
Codex
2026-09-10 10:06:55 -07:00
parent 4b6b5fc197
commit e2ecfea69b
162 changed files with 17595 additions and 926 deletions
+164 -16
View File
@@ -5,6 +5,10 @@ use super::*;
use iced::widget::{Column, column, text_input};
use std::{cell::RefCell, collections::BTreeSet};
#[cfg(test)]
#[path = "scenario_operation_tests.rs"]
mod operation_tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Tool {
Lifeline,
@@ -125,6 +129,9 @@ pub enum Event {
Receiver(Choice),
Call(Choice),
Style(MessageStyle),
OperationIntent(bool),
Operation(Choice),
ClearOperation,
Parent(Choice),
Expression(String),
SecondGuard(String),
@@ -175,6 +182,10 @@ struct Draft {
receiver: Option<Id>,
call: Option<Id>,
style: MessageStyle,
#[serde(default)]
operation: Option<OperationUse>,
#[serde(default)]
disabled_operation: Option<OperationUse>,
expression: String,
second_guard: String,
operator: FragmentOperator,
@@ -330,6 +341,8 @@ impl State {
receiver: None,
call: None,
style: MessageStyle::CallReply,
operation: None,
disabled_operation: None,
expression: String::new(),
second_guard: "else".into(),
operator: FragmentOperator::Alternative,
@@ -351,6 +364,27 @@ impl State {
Event::Name(s) => d.name = s,
Event::Expression(s) => d.expression = s,
Event::SecondGuard(s) => d.second_guard = s,
Event::OperationIntent(enabled) => {
if enabled {
d.operation = Some(
d.operation
.clone()
.or_else(|| d.disabled_operation.take())
.unwrap_or(OperationUse { allocation: None }),
);
} else {
d.disabled_operation =
d.operation.take().or_else(|| d.disabled_operation.take());
}
}
Event::Operation(c) => {
d.operation = Some(OperationUse {
allocation: Some(c.id),
})
}
Event::ClearOperation => {
d.operation = Some(OperationUse { allocation: None })
}
Event::Target(c) => {
d.target = Some(c.id);
d.bindings.clear();
@@ -358,7 +392,9 @@ impl State {
Event::Exchange(c) => {
if d.exchange != Some(c.id) {
d.exchange = Some(c.id);
d.reset_values();
if !d.has_operation_intent(p) {
d.reset_values();
}
if !d.fixed_endpoints {
d.sender = None;
d.receiver = None;
@@ -394,7 +430,9 @@ impl State {
std::mem::swap(&mut d.sender, &mut d.receiver);
d.call = None;
}
d.retain_value_paths(p);
if !d.has_operation_intent(p) {
d.retain_value_paths(p);
}
}
}
Event::Call(c) => {
@@ -410,7 +448,7 @@ impl State {
..
}) = p.elements.get(&c.id).and_then(|e| e.shared())
{
if d.exchange != *exchange {
if d.exchange != *exchange && !d.has_operation_intent(p) {
d.reset_values();
}
d.exchange = *exchange;
@@ -426,18 +464,24 @@ impl State {
d.values.insert(id, s);
}
Event::ValueFocus(key)
if d.exchange
.and_then(|id| p.relations.get(&id))
.and_then(|r| crate::data_values::resolve(p, r, &d.kind(), &key))
if d.signature(p)
.ok()
.flatten()
.and_then(|r| {
crate::data_values::resolve(p, &r, &d.value_kind(), &key)
})
.is_some() =>
{
d.value_focus = Some(key);
}
Event::ValueBrowse(key) => {
if key.as_ref().is_none_or(|key| {
d.exchange
.and_then(|id| p.relations.get(&id))
.and_then(|r| crate::data_values::resolve(p, r, &d.kind(), key))
d.signature(p)
.ok()
.flatten()
.and_then(|r| {
crate::data_values::resolve(p, &r, &d.value_kind(), key)
})
.is_some()
}) {
d.value_browse = key;
@@ -767,6 +811,33 @@ fn calls(p: &Project, d: &Draft) -> Vec<Choice> {
.collect()
}
impl Draft {
fn has_operation_intent(&self, p: &Project) -> bool {
self.operation.is_some()
|| self
.call
.and_then(|id| p.elements.get(&id))
.is_some_and(|e| {
matches!(
e.shared(),
Some(SharedBody::Occurrence {
body: OccurrenceBody::Exchange {
operation: Some(_),
..
},
..
})
)
})
}
fn proposed_message(&self, p: &Project) -> oa_core::Element {
let mut message = self.append(p, "Operation preview", self.message_body());
// This is a transient resolver input; no generated identity reaches a command.
message.id = Id::nil();
message
}
fn signature(&self, p: &Project) -> Result<Option<Relation>, String> {
crate::data_values::message_signature(p, &self.proposed_message(p))
}
fn reset_values(&mut self) {
self.values.clear();
self.value_focus = None;
@@ -804,8 +875,8 @@ impl Draft {
exchange: &Relation,
) -> Result<Vec<crate::data_values::FieldPathChoice>, String> {
match &self.value_browse {
Some(key) => crate::data_values::children(p, exchange, &self.kind(), key),
None => Ok(crate::data_values::roots(p, exchange, &self.kind())),
Some(key) => crate::data_values::children(p, exchange, &self.value_kind(), key),
None => Ok(crate::data_values::roots(p, exchange, &self.value_kind())),
}
}
fn authored_values(&self) -> Vec<(&crate::data_values::FieldPathKey, &str)> {
@@ -815,6 +886,9 @@ impl Draft {
.map(|(key, value)| (key, value.as_str()))
.collect()
}
fn value_kind(&self) -> MessageKind {
crate::data_values::operation_value_phase(&self.kind(), self.operation.as_ref())
}
fn kind(&self) -> MessageKind {
match self.style {
MessageStyle::Asynchronous => MessageKind::Asynchronous,
@@ -841,6 +915,14 @@ impl Draft {
self.receiver
},
message_kind: self.kind(),
operation: if matches!(
self.style,
MessageStyle::Call | MessageStyle::CallReply | MessageStyle::Asynchronous
) {
self.operation.clone()
} else {
None
},
reply_to: self.call,
values: self
.values
@@ -974,6 +1056,7 @@ impl Draft {
sender: self.receiver,
receiver: self.sender,
message_kind: MessageKind::Reply,
operation: None,
reply_to: Some(call_id),
values: vec![],
},
@@ -1136,7 +1219,7 @@ fn value_panel<'a>(p: &Project, d: &'a Draft, exchange: &Relation) -> Element<'a
use crate::data_values;
let mut out = column![text("Message values").size(14)].spacing(7);
if let Some(browse) = &d.value_browse {
let title = data_values::resolve(p, exchange, &d.kind(), browse)
let title = data_values::resolve(p, exchange, &d.value_kind(), browse)
.map(|choice| format!("Fields inside {}", choice.label))
.unwrap_or_else(|| "This record path is no longer available in this message.".into());
out = out.push(text(title).size(11)).push(
@@ -1182,7 +1265,7 @@ fn value_panel<'a>(p: &Project, d: &'a Draft, exchange: &Relation) -> Element<'a
match d
.value_focus
.as_ref()
.map(|key| data_values::resolve(p, exchange, &d.kind(), key))
.map(|key| data_values::resolve(p, exchange, &d.value_kind(), key))
{
Some(Some(focus)) => {
let key = focus.key;
@@ -1233,7 +1316,7 @@ fn value_panel<'a>(p: &Project, d: &'a Draft, exchange: &Relation) -> Element<'a
ValueSection::Authored,
));
for (key, expression) in &authored[value_page(authored.len(), d.value_authored_page)] {
let title = data_values::resolve(p, exchange, &d.kind(), key)
let title = data_values::resolve(p, exchange, &d.value_kind(), key)
.map(|choice| choice.label)
.unwrap_or_else(|| format!("Unavailable field {} · review this value", key.field));
out = out.push(
@@ -1579,8 +1662,71 @@ pub fn view<'a>(app: &'a App, owner: Id, mut out: Column<'a, Message>) -> Elemen
.size(12),
);
}
if let Some(exchange) = d.exchange.and_then(|id| p.relations.get(&id)) {
form = form.push(value_panel(p, d, exchange));
if p.layer_of(owner) == Some(Layer::Sa)
&& viewpoint(p, owner) == ScenarioViewpoint::BoundaryInterface
&& matches!(
d.style,
MessageStyle::Call | MessageStyle::CallReply | MessageStyle::Asynchronous
)
{
form = form.push(
iced::widget::checkbox(d.operation.is_some())
.label("Invoke a specific interface operation")
.on_toggle(|enabled| event(Event::OperationIntent(enabled))),
);
if let Some(operation) = &d.operation {
form = form
.push(bounded_picker(
crate::data_values::operation_choices(p, &d.proposed_message(p)),
d,
operation.allocation,
"Choose the exact interface operation allocation…",
Event::Operation,
))
.push(
button("Keep operation unresolved")
.on_press(event(Event::ClearOperation)),
);
if let Some(id) = operation.allocation {
form = form.push(label(crate::data_values::operation_identity(p, id)));
}
}
}
if let Some(description) =
crate::data_values::operation_label(p, &d.proposed_message(p))
{
form = form.push(label(description));
}
match d.signature(p) {
Ok(Some(exchange)) => form = form.push(value_panel(p, d, &exchange)),
Err(problem) => {
form = form.push(label(problem));
for (key, expression) in d.authored_values() {
form = form
.push(
text(format!(
"Retained value · {} / {}",
key.path
.iter()
.map(|id| p.named(*id))
.collect::<Vec<_>>()
.join(" → "),
p.named(key.field)
))
.size(11),
)
.push(
text_input("Value expression", expression)
.on_input({
let key = key.clone();
move |raw| event(Event::Value(key.clone(), raw))
})
.padding(8)
.size(12),
);
}
}
Ok(None) => {}
}
if d.style == MessageStyle::CallReply {
form=form.push(label("Creates the call and reversed, correlated reply together in one undoable operation."));
@@ -2072,6 +2218,7 @@ pub(crate) mod direct_tests {
f.owner,
&format!("Call {index}"),
OccurrenceBody::Exchange {
operation: None,
exchange: Some(f.exchange),
sender: Some(f.sender),
receiver: Some(f.receiver),
@@ -3173,6 +3320,7 @@ mod data_contract_tests {
f.scenario,
name,
OccurrenceBody::Exchange {
operation: None,
exchange: Some(exchange),
sender: Some(lifelines[0]),
receiver: Some(lifelines[1]),