Complete source 6 engineering workflows and preserve release validation evidence

This commit is contained in:
Codex
2026-09-06 21:49:04 -07:00
parent 2b2d2bc18b
commit 4b6b5fc197
290 changed files with 48271 additions and 1791 deletions
+135 -5
View File
@@ -669,6 +669,7 @@ fn exchanges(p: &Project, d: &Draft) -> Vec<Choice> {
&& matches!(
r.kind(),
RelationKind::Interaction
| RelationKind::CommunicationMean
| RelationKind::FunctionalExchange
| RelationKind::BoundaryExchange
)
@@ -677,7 +678,9 @@ fn exchanges(p: &Project, d: &Draft) -> Vec<Choice> {
ScenarioViewpoint::Functional => r.kind() == RelationKind::FunctionalExchange,
ScenarioViewpoint::ParticipantInteraction => matches!(
r.kind(),
RelationKind::Interaction | RelationKind::BoundaryExchange
RelationKind::Interaction
| RelationKind::CommunicationMean
| RelationKind::BoundaryExchange
),
ScenarioViewpoint::BoundaryInterface => matches!(
r.kind(),
@@ -687,17 +690,37 @@ fn exchanges(p: &Project, d: &Draft) -> Vec<Choice> {
}
&& (!d.fixed_endpoints
|| d.sender.zip(d.receiver).is_some_and(|(sender, receiver)| {
validation::lifeline_matches_endpoint(p, sender, r.source()) == Some(true)
&& validation::lifeline_matches_endpoint(p, receiver, r.target())
== Some(true)
let mut body = d.message_body();
if let OccurrenceBody::Exchange { exchange, .. } = &mut body {
*exchange = Some(r.id);
}
validation::message_endpoints(p, &body).is_some_and(|(source, target)| {
validation::lifeline_matches_endpoint(p, sender, source) == Some(true)
&& validation::lifeline_matches_endpoint(p, receiver, target)
== Some(true)
})
}))
})
.map(|r| Choice {
id: r.id,
label: format!(
"{} · {} → {}",
"{} · {} {} {}",
r.name,
p.named(r.source()),
if matches!(
r.body,
RelationBody::CommunicationMean {
oriented: false,
..
} | RelationBody::BoundaryExchange {
oriented: false,
..
}
) {
"↔"
} else {
"→"
},
p.named(r.target())
),
})
@@ -837,6 +860,38 @@ impl Draft {
lines(p, self.owner)
.into_iter()
.filter(|line| {
if self
.exchange
.and_then(|id| p.relations.get(&id))
.is_some_and(|r| {
matches!(
r.body,
RelationBody::CommunicationMean { .. }
| RelationBody::BoundaryExchange {
oriented: false,
..
}
)
})
{
let mut body = self.message_body();
if let OccurrenceBody::Exchange {
sender: from,
receiver: to,
..
} = &mut body
{
if sender {
*from = Some(line.id);
} else {
*to = Some(line.id);
}
}
return !matches!(
validation::resolve_message_endpoints(p, &body),
validation::MessageEndpointResolution::Conflict
);
}
endpoint.is_none_or(|endpoint| {
validation::lifeline_matches_endpoint(p, line.id, endpoint) == Some(true)
})
@@ -2315,6 +2370,81 @@ pub(crate) mod direct_tests {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reverse_communication_canvas_choices_and_correlated_reply_share_core_direction() {
let mut p = Project::empty();
let a = p
.add_element(Kind::Entity, Layer::Oa, "Crew", None)
.unwrap();
let b = p
.add_element(Kind::Actor, Layer::Oa, "Station", None)
.unwrap();
let exchange = p.relate(RelationKind::CommunicationMean, "Radio", a, b);
let owner = scenario(&mut p, "Participant dialogue");
if let Some(SharedBody::Scenario { viewpoint, .. }) =
p.elements.get_mut(&owner).unwrap().shared_mut()
{
*viewpoint = ScenarioViewpoint::ParticipantInteraction;
}
let la = lifeline(&mut p, owner, a);
let lb = lifeline(&mut p, owner, b);
let mut state = State::default();
state
.begin_message(
&p,
direct_tests::context(owner),
Slot {
parent: owner,
before: None,
},
lb,
la,
MessageStyle::CallReply,
)
.unwrap();
assert!(
exchanges(&p, state.draft.as_ref().unwrap())
.iter()
.any(|c| c.id == exchange)
);
state
.update(&p, Event::Exchange(named_choice(&p, exchange)))
.unwrap();
let mut document =
crate::document::Document::from_project(p.clone(), Default::default()).unwrap();
let original = document.bundle().clone();
document
.execute(state.prepare_pending(&p).unwrap())
.unwrap();
let accepted = document.project();
let steps = accepted.elements[&owner].steps().unwrap();
assert_eq!(steps.len(), 2);
for (id, expected) in [(steps[0], (b, a)), (steps[1], (a, b))] {
let Some(SharedBody::Occurrence { body, .. }) = accepted.elements[&id].shared() else {
unreachable!()
};
assert_eq!(
validation::message_endpoints(accepted, body),
Some(expected)
);
}
let after = document.bundle().clone();
assert!(document.undo());
assert_eq!(document.bundle(), &original);
assert!(document.redo());
assert_eq!(document.bundle(), &after);
let mut directed = p.clone();
if let RelationBody::CommunicationMean { oriented, .. } =
&mut directed.relations.get_mut(&exchange).unwrap().body
{
*oriented = true;
}
assert!(
!exchanges(&directed, state.draft.as_ref().unwrap())
.iter()
.any(|c| c.id == exchange)
);
}
fn scenario(p: &mut Project, name: &str) -> Id {
let mut e = p
.make_element(Kind::Scenario, Layer::Oa, name, None)
@@ -1538,6 +1538,7 @@ mod tests {
projection: ProjectionKind::StateMachine { machine },
nodes: BTreeMap::new(),
routes: vec![],
view_options: Default::default(),
},
);
for (element, x, y, width, height) in [
@@ -293,6 +293,7 @@ pub fn create_view(project: &Project, machine: Id) -> Result<Plan, ModelError> {
projection: ProjectionKind::StateMachine { machine },
nodes: BTreeMap::new(),
routes: vec![],
view_options: Default::default(),
},
);
let members: Vec<_> = project
@@ -41,6 +41,7 @@ pub fn popup(app: &App, opened: &Opened, clean: bool) -> Option<Menu<Message>> {
if let Some(context) = context.filter(|context| context.diagram == id) {
entries.extend([
item("View properties", Message::EditProperties(id), true),
item("Duplicate view", Message::DuplicateDiagram(id), true),
item("Fit diagram", diagram_event(Event::Fit(context)), true),
item(
"Create another state view",