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
+118 -7
View File
@@ -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
)
)
}