Complete source 6 engineering workflows and preserve release validation evidence
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
use crate::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
mod communication;
|
||||
pub(crate) use communication::review_endpoint_fact;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TransitionDirection {
|
||||
@@ -53,6 +55,30 @@ pub struct FieldResolution {
|
||||
pub field: TransitionField,
|
||||
pub decision: FieldDecision,
|
||||
}
|
||||
/// Technical endpoint choices are scoped to one OA exchange. Participant
|
||||
/// correspondence remains in `mappings`, never in these port identities.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum CommunicationPortChoice {
|
||||
Reuse {
|
||||
port: Id,
|
||||
},
|
||||
Create {
|
||||
id: Id,
|
||||
alias: String,
|
||||
name: String,
|
||||
description: String,
|
||||
kind: ComponentPortKind,
|
||||
orientation: ComponentPortOrientation,
|
||||
},
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CommunicationEndpointDecision {
|
||||
pub source: Id,
|
||||
pub source_port: CommunicationPortChoice,
|
||||
pub target_port: CommunicationPortChoice,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct TransitionRequest {
|
||||
@@ -63,6 +89,8 @@ pub struct TransitionRequest {
|
||||
pub fields: Vec<TransitionField>,
|
||||
pub resolutions: Vec<FieldResolution>,
|
||||
pub destination_diagram: Option<Id>,
|
||||
#[serde(default)]
|
||||
pub communication_endpoints: Vec<CommunicationEndpointDecision>,
|
||||
}
|
||||
impl TransitionRequest {
|
||||
pub fn new(direction: TransitionDirection, sources: Vec<Id>) -> Self {
|
||||
@@ -74,6 +102,7 @@ impl TransitionRequest {
|
||||
fields: vec![TransitionField::Name, TransitionField::Description],
|
||||
resolutions: vec![],
|
||||
destination_diagram: None,
|
||||
communication_endpoints: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,6 +206,7 @@ fn inferred_kind(kind: Kind, direction: TransitionDirection) -> Option<Kind> {
|
||||
|| matches!(
|
||||
k,
|
||||
Kind::ExchangeItem
|
||||
| Kind::ExchangeCategory
|
||||
| Kind::DataType
|
||||
| Kind::DataField
|
||||
| Kind::Interface
|
||||
@@ -255,6 +285,28 @@ fn mapped_relation(r: &Relation, direction: TransitionDirection) -> Option<Relat
|
||||
items: items.clone(),
|
||||
}
|
||||
}
|
||||
(
|
||||
RelationBody::CommunicationMean {
|
||||
oriented, items, ..
|
||||
},
|
||||
TransitionDirection::OaToSa,
|
||||
) => RelationBody::BoundaryExchange {
|
||||
source,
|
||||
target,
|
||||
oriented: *oriented,
|
||||
items: items.clone(),
|
||||
},
|
||||
(
|
||||
RelationBody::BoundaryExchange {
|
||||
oriented, items, ..
|
||||
},
|
||||
TransitionDirection::SaToOa,
|
||||
) => RelationBody::CommunicationMean {
|
||||
source,
|
||||
target,
|
||||
oriented: *oriented,
|
||||
items: items.clone(),
|
||||
},
|
||||
(
|
||||
RelationBody::Allocation { .. }
|
||||
| RelationBody::InterfaceItemAllocation { .. }
|
||||
@@ -318,7 +370,7 @@ fn trace_kind(p: &Project, oa: Id, sa: Id) -> TraceKind {
|
||||
_ => TraceKind::Manual,
|
||||
},
|
||||
_ => {
|
||||
if matches!((p.relations.get(&oa),p.relations.get(&sa)),(Some(a),Some(b))if a.kind()==RelationKind::Interaction&&b.kind()==RelationKind::FunctionalExchange)
|
||||
if matches!((p.relations.get(&oa),p.relations.get(&sa)),(Some(a),Some(b))if exchange_counterparts(a.kind(), b.kind()))
|
||||
{
|
||||
TraceKind::Exchange
|
||||
} else {
|
||||
@@ -349,11 +401,12 @@ fn sa_port_omissions(p: &Project) -> BTreeSet<Id> {
|
||||
.collect();
|
||||
loop {
|
||||
let before = omitted.len();
|
||||
for r in p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() != RelationKind::FunctionalExchange)
|
||||
{
|
||||
for r in p.relations.values().filter(|r| {
|
||||
!matches!(
|
||||
r.kind(),
|
||||
RelationKind::FunctionalExchange | RelationKind::BoundaryExchange
|
||||
)
|
||||
}) {
|
||||
if crate::references::relation_references(r)
|
||||
.iter()
|
||||
.any(|id| omitted.contains(id))
|
||||
@@ -499,6 +552,15 @@ pub fn plan_transition(
|
||||
}
|
||||
} else if let Some(r) = project.relations.get(&id) {
|
||||
if request.direction == TransitionDirection::SaToOa
|
||||
&& r.kind() == RelationKind::BoundaryExchange
|
||||
{
|
||||
selected.extend(
|
||||
[r.source(), r.target()].into_iter().filter_map(|port| {
|
||||
project.elements.get(&port).and_then(|e| e.parent)
|
||||
}),
|
||||
);
|
||||
selected.extend(r.body.items());
|
||||
} else if request.direction == TransitionDirection::SaToOa
|
||||
&& let RelationBody::FunctionalExchange {
|
||||
source,
|
||||
target,
|
||||
@@ -695,6 +757,8 @@ pub fn plan_transition(
|
||||
}
|
||||
}
|
||||
let mut working = project.clone();
|
||||
let communication_pairs =
|
||||
communication::prepare_ports(project, &mut working, &targets, &mut plan);
|
||||
let ordered_creations: Vec<_> = selected
|
||||
.iter()
|
||||
.filter(|id| creating.contains(id) && project.elements.contains_key(id))
|
||||
@@ -764,6 +828,25 @@ pub fn plan_transition(
|
||||
let mut created = r.clone();
|
||||
created.id = target;
|
||||
created.body = body;
|
||||
// SA technical ports project through their reviewed owners; never
|
||||
// place a port→participant pair in the global correspondence map.
|
||||
if request.direction == TransitionDirection::SaToOa
|
||||
&& r.kind() == RelationKind::BoundaryExchange
|
||||
{
|
||||
let owners = [r.source(), r.target()]
|
||||
.map(|port| project.elements.get(&port).and_then(|e| e.parent));
|
||||
if let [Some(a), Some(b)] = owners {
|
||||
created.body.set_endpoints(a, b);
|
||||
} else {
|
||||
conflict(
|
||||
&mut plan,
|
||||
Some(*source),
|
||||
Some(target),
|
||||
None,
|
||||
"Boundary endpoints need participant owners before communication transition.",
|
||||
);
|
||||
}
|
||||
}
|
||||
created.alias = working.fresh_alias(
|
||||
&format!(
|
||||
"{}_{}",
|
||||
@@ -778,6 +861,21 @@ pub fn plan_transition(
|
||||
*id = *mapped;
|
||||
}
|
||||
});
|
||||
if request.direction == TransitionDirection::OaToSa
|
||||
&& r.kind() == RelationKind::CommunicationMean
|
||||
{
|
||||
if let Some((a, b)) = communication_pairs.get(source) {
|
||||
created.body.set_endpoints(*a, *b);
|
||||
} else {
|
||||
conflict(
|
||||
&mut plan,
|
||||
Some(*source),
|
||||
Some(target),
|
||||
None,
|
||||
"Choose an explicit SA component-port pair for this communication means; participant mappings cannot stand in for ports.",
|
||||
);
|
||||
}
|
||||
}
|
||||
for dependency in crate::references::relation_references(&created) {
|
||||
if dependency.is_nil()
|
||||
&& request.direction == TransitionDirection::OaToSa
|
||||
@@ -1139,9 +1237,22 @@ fn compatible_existing(
|
||||
(Some(a), Some(b)) => compatible_kinds(a.kind(), b.kind(), direction),
|
||||
_ => match (p.relations.get(&source), p.relations.get(&target)) {
|
||||
(Some(a), Some(b)) => {
|
||||
mapped_relation(a, direction).is_some_and(|body| body.kind() == b.kind())
|
||||
(a.kind() == RelationKind::BoundaryExchange
|
||||
&& b.kind() == RelationKind::BoundaryExchange)
|
||||
|| mapped_relation(a, direction).is_some_and(|body| body.kind() == b.kind())
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn exchange_counterparts(oa: RelationKind, sa: RelationKind) -> bool {
|
||||
matches!(
|
||||
(oa, sa),
|
||||
(RelationKind::Interaction, RelationKind::FunctionalExchange)
|
||||
| (
|
||||
RelationKind::CommunicationMean,
|
||||
RelationKind::BoundaryExchange
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user