Model typed SA ports and atomic exchange allocation workflows

This commit is contained in:
2026-09-05 18:22:00 +03:00
parent c20a2073d7
commit 0d14438b7b
13 changed files with 2402 additions and 256 deletions
+105 -12
View File
@@ -6,7 +6,7 @@ use std::{
use uuid::Uuid;
pub type Id = Uuid;
pub const SCHEMA_VERSION: u32 = 2;
pub const SCHEMA_VERSION: u32 = 3;
pub const MAX_COORDINATE: f32 = 1_000_000.;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@@ -293,6 +293,54 @@ pub enum PortDirection {
InOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentPortKind {
Standard,
Flow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComponentPortOrientation {
Unset,
In,
Out,
InOut,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LegacyPortContract {
pub direction: PortDirection,
pub interface: Option<Id>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum PortContract {
FunctionInput {
incoming_items: Vec<Id>,
},
FunctionOutput {
outgoing_items: Vec<Id>,
},
Component {
kind: ComponentPortKind,
orientation: ComponentPortOrientation,
},
Legacy,
}
impl PortContract {
pub fn items(&self) -> &[Id] {
match self {
Self::FunctionInput { incoming_items } => incoming_items,
Self::FunctionOutput { outgoing_items } => outgoing_items,
_ => &[],
}
}
pub fn is_function(&self) -> bool {
matches!(
self,
Self::FunctionInput { .. } | Self::FunctionOutput { .. }
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StateKind {
State,
Mode,
@@ -468,8 +516,8 @@ pub enum SharedBody {
items: Vec<Id>,
},
Port {
direction: PortDirection,
interface: Option<Id>,
contract: PortContract,
legacy: Option<LegacyPortContract>,
},
Scenario {
#[serde(default)]
@@ -635,10 +683,8 @@ impl ElementBody {
unit: String::new(),
},
Kind::Interface => SharedBody::Interface { items: vec![] },
Kind::Port => SharedBody::Port {
direction: PortDirection::InOut,
interface: None,
},
// Port creation requires an explicit contract and owner.
Kind::Port => return None,
Kind::Scenario => SharedBody::Scenario {
viewpoint: if layer == Layer::Oa {
ScenarioViewpoint::OperationalActivity
@@ -707,6 +753,7 @@ impl SharedBody {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RelationKind {
Allocation,
PortAllocation,
Interaction,
CapabilityInvolvement,
ChainInvolvement,
@@ -728,8 +775,9 @@ pub enum RelationKind {
StateAction,
}
impl RelationKind {
pub const ALL: [Self; 20] = [
pub const ALL: [Self; 21] = [
Self::Allocation,
Self::PortAllocation,
Self::Interaction,
Self::CapabilityInvolvement,
Self::ChainInvolvement,
@@ -753,6 +801,7 @@ impl RelationKind {
pub fn permits(self, from: Kind, to: Kind) -> bool {
match self {
Self::Allocation => from.is_performer() && to.is_work(),
Self::PortAllocation => from == Kind::Port && to == Kind::Port,
Self::Interaction => from == Kind::Activity && to == Kind::Activity,
Self::CapabilityInvolvement => {
(from.is_performer() || from.is_work() || from == Kind::Role)
@@ -807,6 +856,7 @@ impl RelationKind {
pub fn hint(self) -> &'static str {
match self {
Self::Allocation => "Performer → leaf activity/function",
Self::PortAllocation => "Component port → allocated function port",
Self::Interaction => "OA activity → OA activity",
Self::CapabilityInvolvement => "Participant/activity/function → capability",
Self::ChainInvolvement => "Process/chain → capability",
@@ -837,6 +887,7 @@ impl fmt::Display for RelationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Allocation => "Allocation",
Self::PortAllocation => "Port allocation",
Self::Interaction => "Interaction",
Self::CapabilityInvolvement => "Capability involvement",
Self::ChainInvolvement => "Chain involvement",
@@ -875,6 +926,10 @@ pub enum RelationBody {
source: Id,
target: Id,
},
PortAllocation {
source: Id,
target: Id,
},
Interaction {
source: Id,
target: Id,
@@ -899,13 +954,14 @@ pub enum RelationBody {
FunctionalExchange {
source: Id,
target: Id,
source_port: Option<Id>,
target_port: Option<Id>,
source_port: Id,
target_port: Id,
items: Vec<Id>,
},
BoundaryExchange {
source: Id,
target: Id,
oriented: bool,
items: Vec<Id>,
},
MissionCapability {
@@ -984,6 +1040,7 @@ impl RelationBody {
pub fn kind(&self) -> RelationKind {
match self {
Self::Allocation { .. } => RelationKind::Allocation,
Self::PortAllocation { .. } => RelationKind::PortAllocation,
Self::Interaction { .. } => RelationKind::Interaction,
Self::CapabilityInvolvement { .. } => RelationKind::CapabilityInvolvement,
Self::ChainInvolvement { .. } => RelationKind::ChainInvolvement,
@@ -1008,6 +1065,7 @@ impl RelationBody {
pub fn endpoints(&self) -> (Id, Id) {
match self {
Self::Allocation { source, target }
| Self::PortAllocation { source, target }
| Self::Interaction { source, target, .. }
| Self::CapabilityInvolvement { source, target }
| Self::ChainInvolvement { source, target }
@@ -1032,6 +1090,7 @@ impl RelationBody {
pub fn set_endpoints(&mut self, a: Id, b: Id) {
match self {
Self::Allocation { source, target }
| Self::PortAllocation { source, target }
| Self::Interaction { source, target, .. }
| Self::CapabilityInvolvement { source, target }
| Self::ChainInvolvement { source, target }
@@ -1067,6 +1126,7 @@ impl RelationBody {
pub fn new(kind: RelationKind, source: Id, target: Id) -> Self {
match kind {
RelationKind::Allocation => Self::Allocation { source, target },
RelationKind::PortAllocation => Self::PortAllocation { source, target },
RelationKind::Interaction => Self::Interaction {
source,
target,
@@ -1079,13 +1139,15 @@ impl RelationBody {
RelationKind::FunctionalExchange => Self::FunctionalExchange {
source,
target,
source_port: None,
target_port: None,
// Unbound drafts must be completed before candidate admission.
source_port: Id::nil(),
target_port: Id::nil(),
items: vec![],
},
RelationKind::BoundaryExchange => Self::BoundaryExchange {
source,
target,
oriented: true,
items: vec![],
},
RelationKind::MissionCapability => Self::MissionCapability { source, target },
@@ -1584,6 +1646,37 @@ impl Project {
pub fn relate(&mut self, kind: RelationKind, name: &str, source: Id, target: Id) -> Id {
let id = Id::new_v4();
let alias = self.fresh_alias(name, "relation", id);
if kind == RelationKind::FunctionalExchange
&& self.elements.contains_key(&source)
&& self.elements.contains_key(&target)
{
let request = crate::ports::FunctionalExchangeRequest {
id,
alias,
name: name.into(),
description: String::new(),
source,
target,
source_port: crate::ports::PortChoice::Auto,
target_port: crate::ports::PortChoice::Auto,
items: vec![],
};
// Project is the unchecked builder; Session validates the complete batch.
for command in crate::ports::functional_exchange_commands(self, &request)
.expect("fresh exchange identities")
{
match command {
crate::Command::CreateElement(e) => {
self.elements.insert(e.id, e);
}
crate::Command::CreateRelation(r) => {
self.relations.insert(r.id, r);
}
_ => unreachable!("functional creation only creates ports and an exchange"),
}
}
return id;
}
let mut body = RelationBody::new(kind, source, target);
if self
.elements