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
@@ -0,0 +1,304 @@
//! Explicit SA endpoint decisions for an operational communication counterpart.
use super::*;
use iced::widget::column;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct PortDraft {
source: Option<CommunicationPortChoice>,
target: Option<CommunicationPortChoice>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PortOption {
Reuse(Id),
Create(ComponentPortKind, ComponentPortOrientation),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PortMenuChoice {
option: PortOption,
label: String,
}
impl std::fmt::Display for PortMenuChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.label)
}
}
fn selected(p: &Project, value: &CommunicationPortChoice) -> PortMenuChoice {
match value {
CommunicationPortChoice::Reuse { port } => PortMenuChoice {
option: PortOption::Reuse(*port),
label: format!("Reuse {}", choice(p, *port).label),
},
CommunicationPortChoice::Create {
kind, orientation, ..
} => creation(*kind, *orientation),
}
}
fn creation(kind: ComponentPortKind, orientation: ComponentPortOrientation) -> PortMenuChoice {
PortMenuChoice {
option: PortOption::Create(kind, orientation),
label: match kind {
ComponentPortKind::Standard => "Create Standard port · bidirectional".into(),
ComponentPortKind::Flow => format!("Create Flow port · {orientation:?}"),
},
}
}
impl Transition {
pub(super) fn sync_communication_ports(&mut self) {
self.request.communication_endpoints = self
.communication_ports
.iter()
.filter_map(|(id, draft)| {
Some(CommunicationEndpointDecision {
source: *id,
source_port: draft.source.clone()?,
target_port: draft.target.clone()?,
})
})
.collect();
}
pub(super) fn choose_communication_port(
&mut self,
p: &Project,
exchange: Id,
source: bool,
choice: PortMenuChoice,
) -> Result<(), String> {
if self.request.direction != TransitionDirection::OaToSa
|| !p
.relations
.get(&exchange)
.is_some_and(|r| r.kind() == RelationKind::CommunicationMean)
{
return Err("Choose an operational communication means in this transition.".into());
}
let value = match choice.option {
PortOption::Reuse(port) => CommunicationPortChoice::Reuse { port },
PortOption::Create(kind, orientation) => {
let id = Id::new_v4();
let side = if source { "source" } else { "target" };
let name = format!("{} · {side} port", p.named(exchange));
CommunicationPortChoice::Create {
id,
alias: p.fresh_alias(&name, "port", id),
name,
description: String::new(),
kind,
orientation,
}
}
};
let draft = self.communication_ports.entry(exchange).or_default();
if source {
draft.source = Some(value);
} else {
draft.target = Some(value);
}
self.sync_communication_ports();
Ok(())
}
pub(super) fn communication_view<'a>(
&'a self,
p: &'a Project,
exchange: Id,
) -> Option<Element<'a, Message>> {
if self.request.direction != TransitionDirection::OaToSa {
return None;
}
let relation = p.relations.get(&exchange)?;
if relation.kind() != RelationKind::CommunicationMean {
return None;
}
let mut content = column![
text("System communication endpoints").size(14),
text("Choose both component ports explicitly. Participant correspondence still maps entities to participants. New ports are created together with the reviewed exchange; interface and functional-port allocation decisions remain explicit.").size(12),
].spacing(8).width(Fill);
for source in [true, false] {
let participant = if source {
relation.source()
} else {
relation.target()
};
let owner = self
.request
.mappings
.iter()
.find(|m| m.source == participant)
.and_then(|m| m.target)
.or_else(|| {
self.plan
.changes
.iter()
.find(|c| c.source == participant)
.map(|c| c.target)
});
let mut options = vec![creation(
ComponentPortKind::Standard,
ComponentPortOrientation::Unset,
)];
for orientation in [
ComponentPortOrientation::Out,
ComponentPortOrientation::In,
ComponentPortOrientation::InOut,
] {
options.push(creation(ComponentPortKind::Flow, orientation));
}
options.extend(
p.elements
.values()
.filter(|e| {
e.layer() == Layer::Sa
&& owner == e.parent
&& owner.is_some()
&& matches!(port_contract(e), Some(PortContract::Component { .. }))
})
.map(|e| selected(p, &CommunicationPortChoice::Reuse { port: e.id })),
);
let draft = self.communication_ports.get(&exchange);
let value = draft
.and_then(|d| {
if source {
d.source.as_ref()
} else {
d.target.as_ref()
}
})
.or_else(|| {
self.request
.communication_endpoints
.iter()
.find(|d| d.source == exchange)
.map(|d| {
if source {
&d.source_port
} else {
&d.target_port
}
})
});
let value = value.map(|v| selected(p, v));
if let Some(value) = &value
&& !options.contains(value)
{
options.push(value.clone());
}
content = content
.push(
text(format!(
"{} · {}",
if source {
"Source participant"
} else {
"Target participant"
},
p.named(participant)
))
.size(12),
)
.push(
pick_list(options, value, move |choice| {
Message::Workflow(Event::CommunicationPort(exchange, source, choice))
})
.placeholder("Choose a component port")
.padding(8)
.text_size(12)
.width(Fill),
);
}
Some(content.into())
}
}
pub(super) fn from_request(request: &TransitionRequest) -> BTreeMap<Id, PortDraft> {
request
.communication_endpoints
.iter()
.map(|d| {
(
d.source,
PortDraft {
source: Some(d.source_port.clone()),
target: Some(d.target_port.clone()),
},
)
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn communication_requires_two_explicit_ports_and_recovers_the_partial_choice() {
let mut p = Project::empty();
let oa_a = p
.add_element(Kind::Entity, Layer::Oa, "Controller", None)
.unwrap();
let oa_b = p
.add_element(Kind::Actor, Layer::Oa, "Partner", None)
.unwrap();
let sa_a = p
.add_element(Kind::System, Layer::Sa, "Controller", None)
.unwrap();
let sa_b = p
.add_element(Kind::SystemActor, Layer::Sa, "Partner", None)
.unwrap();
let exchange = p.relate(RelationKind::CommunicationMean, "Radio", oa_a, oa_b);
let mut workflow = Workflow::transition(&p, exchange).unwrap();
let Workflow::Transition(t) = &mut workflow else {
unreachable!()
};
for (source, target) in [(oa_a, sa_a), (oa_b, sa_b)] {
t.request.mappings.push(TransitionMapping {
source,
target: Some(target),
target_kind: None,
});
}
workflow.update(&p, Event::Preview).unwrap();
let make_port = || creation(ComponentPortKind::Standard, ComponentPortOrientation::Unset);
workflow
.update(&p, Event::CommunicationPort(exchange, true, make_port()))
.unwrap();
let Workflow::Transition(t) = &workflow else {
unreachable!()
};
assert!(t.request.communication_endpoints.is_empty());
assert!(!t.plan.conflicts.is_empty());
let raw = workflow.recovery_snapshot().unwrap();
let mut restored = Workflow::from_recovery(&p, raw.clone()).unwrap();
assert_eq!(restored.recovery_snapshot().unwrap(), raw);
restored
.update(&p, Event::CommunicationPort(exchange, false, make_port()))
.unwrap();
let Workflow::Transition(t) = &restored else {
unreachable!()
};
assert!(t.plan.conflicts.is_empty(), "{:?}", t.plan.conflicts);
let decision = t.request.communication_endpoints[0].clone();
let CommunicationPortChoice::Create { id: from, .. } = decision.source_port else {
unreachable!()
};
let CommunicationPortChoice::Create { id: to, .. } = decision.target_port else {
unreachable!()
};
let before = p.clone();
let accepted = restored.update(&p, Event::Apply).unwrap().unwrap();
assert_eq!(accepted.elements[&from].parent, Some(sa_a));
assert_eq!(accepted.elements[&to].parent, Some(sa_b));
assert!(accepted.relations.values().any(|r| matches!(r.body,
RelationBody::BoundaryExchange { source, target, oriented: false, .. }
if source == from && target == to)));
assert_eq!(p, before);
}
}
+57 -2
View File
@@ -30,6 +30,8 @@ enum Saved {
Transition {
base: String,
request: TransitionRequest,
#[serde(default)]
communication_ports: std::collections::BTreeMap<Id, communication::PortDraft>,
},
}
pub(crate) fn endpoint(p: &Project, port: PortChoice) -> EndpointChoice {
@@ -52,6 +54,40 @@ fn check(p: &Project, base: &str) -> Result<(), String> {
Ok(())
}
impl Workflow {
pub(crate) fn from_recovery_after_source5_upgrade(
p: &Project,
legacy_base: &str,
value: serde_json::Value,
) -> Result<Self, String> {
let mut saved: Saved = serde_json::from_value(value).map_err(|e| e.to_string())?;
match &mut saved {
Saved::Semantic(value) => {
return crate::semantic_review::State::from_recovery_after_source5_upgrade(
p,
legacy_base,
value.take(),
)
.map(|state| Self::Semantic(Box::new(state)));
}
Saved::Ports(value) => {
return crate::port_workflow::State::from_recovery_after_source5_upgrade(
p,
legacy_base,
value.take(),
)
.map(|state| Self::Ports(Box::new(state)));
}
Saved::Relation { base, .. }
| Saved::ManualTrace { base, .. }
| Saved::Transition { base, .. } => {
if base != legacy_base {
return Err("Workflow recovery does not match the accepted source-5 model identity and revision.".into());
}
*base = project_fingerprint(p);
}
}
Self::from_recovery(p, serde_json::to_value(saved).map_err(|e| e.to_string())?)
}
pub fn recovery_snapshot(&self) -> Result<serde_json::Value, String> {
let saved = match self {
Self::Semantic(state) => Saved::Semantic(state.recovery_snapshot()?),
@@ -84,6 +120,7 @@ impl Workflow {
Self::Transition(d) => Saved::Transition {
base: d.plan.base_fingerprint.clone(),
request: d.request.clone(),
communication_ports: d.communication_ports.clone(),
},
};
serde_json::to_value(saved).map_err(|e| e.to_string())
@@ -148,10 +185,28 @@ impl Workflow {
}
Ok(workflow)
}
Saved::Transition { base, request } => {
Saved::Transition {
base,
request,
mut communication_ports,
} => {
check(p, &base)?;
if communication_ports.len() > crate::source_workspace::LIMITS.model.max_records {
return Err("Recovered transition exceeds endpoint draft limits.".into());
}
for (id, draft) in communication::from_request(&request) {
communication_ports.entry(id).or_insert(draft);
}
let plan = plan_transition(p, &request).map_err(|e| e.to_string())?;
Ok(Self::Transition(Box::new(Transition { request, plan })))
let mut transition = Transition {
request,
plan,
communication_ports,
};
transition.sync_communication_ports();
transition.plan =
plan_transition(p, &transition.request).map_err(|e| e.to_string())?;
Ok(Self::Transition(Box::new(transition)))
}
}
}