Model typed SA ports and atomic exchange allocation workflows
This commit is contained in:
@@ -34,11 +34,33 @@ fn occurrence(
|
||||
id
|
||||
}
|
||||
fn port(p: &mut Project, owner: Id, name: &str, direction: PortDirection, interface: Id) -> Id {
|
||||
let id = add(p, Kind::Port, Layer::Sa, name, Some(owner));
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Port {
|
||||
direction,
|
||||
interface: Some(interface),
|
||||
let items = match p.elements[&interface].shared() {
|
||||
Some(SharedBody::Interface { items }) => items.clone(),
|
||||
_ => vec![],
|
||||
};
|
||||
let contract = if p.elements[&owner].kind() == Kind::Function {
|
||||
if direction == PortDirection::Out {
|
||||
PortContract::FunctionOutput {
|
||||
outgoing_items: items,
|
||||
}
|
||||
} else {
|
||||
PortContract::FunctionInput {
|
||||
incoming_items: items,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: match direction {
|
||||
PortDirection::In => ComponentPortOrientation::In,
|
||||
PortDirection::Out => ComponentPortOrientation::Out,
|
||||
PortDirection::InOut => ComponentPortOrientation::InOut,
|
||||
},
|
||||
}
|
||||
};
|
||||
let id = Id::new_v4();
|
||||
let element = crate::ports::make_port(p, id, owner, name, contract).unwrap();
|
||||
p.elements.insert(id, element);
|
||||
id
|
||||
}
|
||||
fn diagram(
|
||||
@@ -369,8 +391,8 @@ pub fn coastal_rescue() -> Project {
|
||||
p.relations.get_mut(&exchange).unwrap().body = RelationBody::FunctionalExchange {
|
||||
source: transmit,
|
||||
target: coordinate,
|
||||
source_port: Some(function_out),
|
||||
target_port: Some(function_in),
|
||||
source_port: function_out,
|
||||
target_port: function_in,
|
||||
items: vec![information],
|
||||
};
|
||||
let boundary = p.relate(
|
||||
@@ -384,6 +406,18 @@ pub fn coastal_rescue() -> Project {
|
||||
{
|
||||
items.push(information);
|
||||
}
|
||||
p.relate(
|
||||
RelationKind::PortAllocation,
|
||||
"Vessel output represents report output",
|
||||
vessel_port,
|
||||
function_out,
|
||||
);
|
||||
p.relate(
|
||||
RelationKind::PortAllocation,
|
||||
"System input represents assessment input",
|
||||
system_port,
|
||||
function_in,
|
||||
);
|
||||
p.relate(
|
||||
RelationKind::ExchangeAllocation,
|
||||
"Report carried by connection",
|
||||
@@ -393,13 +427,13 @@ pub fn coastal_rescue() -> Project {
|
||||
p.relate(
|
||||
RelationKind::InterfaceProvision,
|
||||
"Reporting service provided",
|
||||
actor,
|
||||
vessel_port,
|
||||
interface,
|
||||
);
|
||||
p.relate(
|
||||
RelationKind::InterfaceRequirement,
|
||||
"Reporting service required",
|
||||
system,
|
||||
system_port,
|
||||
interface,
|
||||
);
|
||||
let sa_occurrence = occurrence(
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
pub mod language;
|
||||
pub mod model;
|
||||
pub mod persistence;
|
||||
pub mod ports;
|
||||
pub mod projection;
|
||||
pub mod references;
|
||||
pub mod session;
|
||||
pub mod transitions;
|
||||
pub mod validation;
|
||||
pub use model::*;
|
||||
pub use ports::*;
|
||||
pub use session::*;
|
||||
mod demo;
|
||||
mod state_validation;
|
||||
|
||||
+105
-12
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ pub struct ProjectedEdge {
|
||||
pub source: Id,
|
||||
pub target: Id,
|
||||
pub trace: bool,
|
||||
pub oriented: bool,
|
||||
pub source_port: Option<Id>,
|
||||
pub target_port: Option<Id>,
|
||||
}
|
||||
@@ -27,7 +28,36 @@ pub struct ProjectedPort {
|
||||
/// Placement identity of the owning visible function/participant.
|
||||
pub owner: Id,
|
||||
pub position: Position,
|
||||
pub direction: PortDirection,
|
||||
pub side: PortAnchorSide,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PortAnchorSide {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
pub fn port_anchor_side(e: &Element) -> Option<PortAnchorSide> {
|
||||
let Some(SharedBody::Port { contract, legacy }) = e.shared() else {
|
||||
return None;
|
||||
};
|
||||
Some(match contract {
|
||||
PortContract::FunctionInput { .. } => PortAnchorSide::Left,
|
||||
PortContract::FunctionOutput { .. } => PortAnchorSide::Right,
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::In,
|
||||
} => PortAnchorSide::Left,
|
||||
PortContract::Component {
|
||||
kind: ComponentPortKind::Flow,
|
||||
orientation: ComponentPortOrientation::Out,
|
||||
} => PortAnchorSide::Right,
|
||||
PortContract::Legacy => match legacy.as_ref().map(|l| l.direction) {
|
||||
Some(PortDirection::In) => PortAnchorSide::Left,
|
||||
Some(PortDirection::Out) => PortAnchorSide::Right,
|
||||
_ => PortAnchorSide::Bottom,
|
||||
},
|
||||
_ => PortAnchorSide::Bottom,
|
||||
})
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
|
||||
pub struct ProjectedView {
|
||||
@@ -36,10 +66,25 @@ pub struct ProjectedView {
|
||||
pub ports: Vec<ProjectedPort>,
|
||||
}
|
||||
pub(crate) fn minimum_node_size(p: &Project, element: Id, mut size: Size) -> Size {
|
||||
let count = [PortDirection::In, PortDirection::Out].into_iter().map(|direction| p.children(element).filter(|port| matches!(port.shared(), Some(SharedBody::Port {direction:d,..}) if *d==direction)).count()).max().unwrap_or(0);
|
||||
let count = [PortAnchorSide::Left, PortAnchorSide::Right]
|
||||
.into_iter()
|
||||
.map(|side| {
|
||||
p.children(element)
|
||||
.filter(|port| port_anchor_side(port) == Some(side))
|
||||
.count()
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
if count > 0 {
|
||||
size.height = size.height.max(76. + 28. * (count + 1) as f32);
|
||||
}
|
||||
let bottom = p
|
||||
.children(element)
|
||||
.filter(|port| port_anchor_side(port) == Some(PortAnchorSide::Bottom))
|
||||
.count();
|
||||
if bottom > 0 {
|
||||
size.width = size.width.max(28. * (bottom + 1) as f32);
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
@@ -205,20 +250,27 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.collect();
|
||||
children.sort_by_key(|e| (&e.alias, e.id));
|
||||
for direction in [PortDirection::In, PortDirection::Out, PortDirection::InOut] {
|
||||
let side: Vec<_> = children.iter().filter(|e| matches!(e.shared(), Some(SharedBody::Port {direction: d, ..}) if *d == direction)).collect();
|
||||
for anchor_side in [
|
||||
PortAnchorSide::Left,
|
||||
PortAnchorSide::Right,
|
||||
PortAnchorSide::Bottom,
|
||||
] {
|
||||
let side: Vec<_> = children
|
||||
.iter()
|
||||
.filter(|e| port_anchor_side(e) == Some(anchor_side))
|
||||
.collect();
|
||||
for (index, port) in side.iter().enumerate() {
|
||||
let fraction = (index + 1) as f32 / (side.len() + 1) as f32;
|
||||
let position = match direction {
|
||||
PortDirection::In => Position {
|
||||
let position = match anchor_side {
|
||||
PortAnchorSide::Left => Position {
|
||||
x: node.position.x,
|
||||
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
|
||||
},
|
||||
PortDirection::Out => Position {
|
||||
PortAnchorSide::Right => Position {
|
||||
x: node.position.x + node.size.width,
|
||||
y: node.position.y + 64. + (node.size.height - 76.).max(0.) * fraction,
|
||||
},
|
||||
PortDirection::InOut => Position {
|
||||
PortAnchorSide::Bottom => Position {
|
||||
x: node.position.x + node.size.width * fraction,
|
||||
y: node.position.y + node.size.height,
|
||||
},
|
||||
@@ -227,7 +279,7 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
|
||||
element: port.id,
|
||||
owner: node.id,
|
||||
position,
|
||||
direction,
|
||||
side: anchor_side,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -270,10 +322,9 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => (*source_port, *target_port),
|
||||
RelationBody::BoundaryExchange { source, target, .. } => {
|
||||
(Some(*source), Some(*target))
|
||||
}
|
||||
} => (Some(*source_port), Some(*target_port)),
|
||||
RelationBody::BoundaryExchange { source, target, .. }
|
||||
| RelationBody::PortAllocation { source, target } => (Some(*source), Some(*target)),
|
||||
_ => (None, None),
|
||||
};
|
||||
edges.push(ProjectedEdge {
|
||||
@@ -281,6 +332,13 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
|
||||
source,
|
||||
target,
|
||||
trace: false,
|
||||
oriented: !matches!(
|
||||
&r.body,
|
||||
RelationBody::BoundaryExchange {
|
||||
oriented: false,
|
||||
..
|
||||
}
|
||||
),
|
||||
source_port: source_port.filter(|id| ports.iter().any(|p| p.element == *id)),
|
||||
target_port: target_port.filter(|id| ports.iter().any(|p| p.element == *id)),
|
||||
});
|
||||
@@ -296,6 +354,7 @@ pub fn project_view(p: &Project, diagram: Id) -> ProjectedView {
|
||||
source,
|
||||
target,
|
||||
trace: true,
|
||||
oriented: true,
|
||||
source_port: None,
|
||||
target_port: None,
|
||||
});
|
||||
|
||||
@@ -45,10 +45,20 @@ pub fn visit_element_refs(e: &mut Element, mut visit: impl FnMut(&mut Id)) {
|
||||
visit(id);
|
||||
}
|
||||
}
|
||||
SharedBody::Port {
|
||||
interface: Some(id),
|
||||
..
|
||||
} => visit(id),
|
||||
SharedBody::Port { contract, legacy } => {
|
||||
match contract {
|
||||
PortContract::FunctionInput { incoming_items } => {
|
||||
incoming_items.iter_mut().for_each(&mut visit)
|
||||
}
|
||||
PortContract::FunctionOutput { outgoing_items } => {
|
||||
outgoing_items.iter_mut().for_each(&mut visit)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(id) = legacy.as_mut().and_then(|old| old.interface.as_mut()) {
|
||||
visit(id);
|
||||
}
|
||||
}
|
||||
SharedBody::Scenario { context, steps, .. } => {
|
||||
if let Some(id) = context {
|
||||
visit(id);
|
||||
@@ -145,7 +155,7 @@ pub fn visit_relation_refs(r: &mut Relation, mut visit: impl FnMut(&mut Id)) {
|
||||
items,
|
||||
..
|
||||
} => {
|
||||
for id in [source_port, target_port].into_iter().flatten() {
|
||||
for id in [source_port, target_port] {
|
||||
visit(id);
|
||||
}
|
||||
for id in items {
|
||||
@@ -207,7 +217,49 @@ pub fn deletion_impact(p: &Project, id: Id) -> BTreeSet<Id> {
|
||||
}
|
||||
}
|
||||
for r in p.relations.values() {
|
||||
if removed.contains(&r.source()) || removed.contains(&r.target()) {
|
||||
let port_removed = matches!(&r.body, RelationBody::FunctionalExchange { source_port, target_port, .. } if removed.contains(source_port) || removed.contains(target_port));
|
||||
let work_allocation_removed = r.kind() == RelationKind::PortAllocation
|
||||
&& p.relations.values().any(|allocation| {
|
||||
allocation.kind() == RelationKind::Allocation
|
||||
&& removed.contains(&allocation.id)
|
||||
&& p.elements.get(&r.source()).and_then(|e| e.parent)
|
||||
== Some(allocation.source())
|
||||
&& p.elements.get(&r.target()).and_then(|e| e.parent)
|
||||
== Some(allocation.target())
|
||||
});
|
||||
if removed.contains(&r.source())
|
||||
|| removed.contains(&r.target())
|
||||
|| port_removed
|
||||
|| work_allocation_removed
|
||||
{
|
||||
removed.insert(r.id);
|
||||
}
|
||||
}
|
||||
// Exchange allocation depends on both explicit endpoint allocations.
|
||||
for r in p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::ExchangeAllocation)
|
||||
{
|
||||
let (Some(boundary), Some(work)) =
|
||||
(p.relations.get(&r.source()), p.relations.get(&r.target()))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = &work.body
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if p.relations.values().any(|allocation| {
|
||||
allocation.kind() == RelationKind::PortAllocation
|
||||
&& removed.contains(&allocation.id)
|
||||
&& [boundary.source(), boundary.target()].contains(&allocation.source())
|
||||
&& [*source_port, *target_port].contains(&allocation.target())
|
||||
}) {
|
||||
removed.insert(r.id);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +315,20 @@ pub fn delete_cascade(p: &mut Project, id: Id) {
|
||||
SharedBody::Event {
|
||||
body: EventBody::Signal { items },
|
||||
} => items.retain(|id| !removed.contains(id)),
|
||||
SharedBody::Port { interface, .. } => clear(interface),
|
||||
SharedBody::Port { contract, legacy } => {
|
||||
match contract {
|
||||
PortContract::FunctionInput { incoming_items } => {
|
||||
incoming_items.retain(|id| !removed.contains(id))
|
||||
}
|
||||
PortContract::FunctionOutput { outgoing_items } => {
|
||||
outgoing_items.retain(|id| !removed.contains(id))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(old) = legacy {
|
||||
clear(&mut old.interface);
|
||||
}
|
||||
}
|
||||
SharedBody::Scenario { context, .. } => clear(context),
|
||||
SharedBody::Lifeline { represented } => clear(represented),
|
||||
SharedBody::Constraint { constrained, .. } => {
|
||||
@@ -336,15 +401,8 @@ pub fn delete_cascade(p: &mut Project, id: Id) {
|
||||
| RelationBody::BoundaryExchange { items, .. } => {
|
||||
items.retain(|id| !removed.contains(id))
|
||||
}
|
||||
RelationBody::FunctionalExchange {
|
||||
items,
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => {
|
||||
RelationBody::FunctionalExchange { items, .. } => {
|
||||
items.retain(|id| !removed.contains(id));
|
||||
clear(source_port);
|
||||
clear(target_port);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -453,6 +453,24 @@ fn apply_command(p: &mut Project, c: &Command, depth: usize) -> Result<Option<Id
|
||||
Some(*id)
|
||||
}
|
||||
Command::Connect(kind, name, source, target) => {
|
||||
if *kind == RelationKind::FunctionalExchange {
|
||||
let id = Id::new_v4();
|
||||
let commands = crate::ports::functional_exchange_commands(
|
||||
p,
|
||||
&FunctionalExchangeRequest {
|
||||
id,
|
||||
alias: p.fresh_alias(name, "relation", id),
|
||||
name: name.clone(),
|
||||
description: String::new(),
|
||||
source: *source,
|
||||
target: *target,
|
||||
source_port: PortChoice::Auto,
|
||||
target_port: PortChoice::Auto,
|
||||
items: vec![],
|
||||
},
|
||||
)?;
|
||||
return apply_command(p, &Command::Batch(commands), depth + 1);
|
||||
}
|
||||
Some(p.relate(*kind, name, *source, *target))
|
||||
}
|
||||
Command::Delete(id) => {
|
||||
|
||||
@@ -196,6 +196,11 @@ fn inferred_kind(kind: Kind, direction: TransitionDirection) -> Option<Kind> {
|
||||
})
|
||||
}
|
||||
fn mapped_body(e: &Element, kind: Kind, layer: Layer) -> Option<ElementBody> {
|
||||
if layer == Layer::Oa
|
||||
&& crate::ports::port_contract(e).is_some_and(|contract| *contract != PortContract::Legacy)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some(shared) = e.shared()
|
||||
&& kind == e.kind()
|
||||
{
|
||||
@@ -232,8 +237,8 @@ fn mapped_relation(r: &Relation, direction: TransitionDirection) -> Option<Relat
|
||||
RelationBody::FunctionalExchange {
|
||||
source,
|
||||
target,
|
||||
source_port: None,
|
||||
target_port: None,
|
||||
source_port: Id::nil(),
|
||||
target_port: Id::nil(),
|
||||
items: items.clone(),
|
||||
}
|
||||
}
|
||||
@@ -320,6 +325,47 @@ fn value(props: &TracedProperties, field: TransitionField) -> &String {
|
||||
TransitionField::Description => &props.description,
|
||||
}
|
||||
}
|
||||
|
||||
/// SA port implementation details have no fabricated OA counterpart. Propagate
|
||||
/// omissions through dependent records, except the FE whose ports are removed
|
||||
/// by its explicit Interaction mapping.
|
||||
fn sa_port_omissions(p: &Project) -> BTreeSet<Id> {
|
||||
let mut omitted: BTreeSet<_> = p
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| {
|
||||
e.layer() == Layer::Sa
|
||||
&& crate::ports::port_contract(e).is_some_and(|c| *c != PortContract::Legacy)
|
||||
})
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
loop {
|
||||
let before = omitted.len();
|
||||
for r in p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() != RelationKind::FunctionalExchange)
|
||||
{
|
||||
if crate::references::relation_references(r)
|
||||
.iter()
|
||||
.any(|id| omitted.contains(id))
|
||||
{
|
||||
omitted.insert(r.id);
|
||||
}
|
||||
}
|
||||
for e in p.elements.values().filter(|e| e.layer() == Layer::Sa) {
|
||||
if crate::references::element_references(e)
|
||||
.iter()
|
||||
.any(|id| omitted.contains(id))
|
||||
{
|
||||
omitted.insert(e.id);
|
||||
}
|
||||
}
|
||||
if before == omitted.len() {
|
||||
return omitted;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn set(props: &mut TracedProperties, field: TransitionField, value: String) {
|
||||
match field {
|
||||
TransitionField::Name => props.name = value,
|
||||
@@ -341,6 +387,12 @@ pub fn plan_transition(
|
||||
commands: vec![],
|
||||
};
|
||||
let mut selected: BTreeSet<_> = request.sources.iter().copied().collect();
|
||||
let omitted = if request.direction == TransitionDirection::SaToOa {
|
||||
sa_port_omissions(project)
|
||||
} else {
|
||||
BTreeSet::new()
|
||||
};
|
||||
let mut reported_omissions = BTreeSet::new();
|
||||
if selected.len() != request.sources.len() {
|
||||
conflict(
|
||||
&mut plan,
|
||||
@@ -364,6 +416,9 @@ pub fn plan_transition(
|
||||
let before = selected.len();
|
||||
let existing: Vec<_> = selected.iter().copied().collect();
|
||||
for id in existing {
|
||||
if omitted.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(e) = project.elements.get(&id) {
|
||||
selected.extend(crate::references::element_references(e));
|
||||
// Contextual links are owned by the process/chain shared by
|
||||
@@ -398,7 +453,27 @@ pub fn plan_transition(
|
||||
selected.extend(project.children(id).map(|e| e.id));
|
||||
}
|
||||
} else if let Some(r) = project.relations.get(&id) {
|
||||
selected.extend(crate::references::relation_references(r));
|
||||
if request.direction == TransitionDirection::SaToOa
|
||||
&& let RelationBody::FunctionalExchange {
|
||||
source,
|
||||
target,
|
||||
source_port,
|
||||
target_port,
|
||||
items,
|
||||
} = &r.body
|
||||
{
|
||||
// An Interaction needs the work/items, never either SA
|
||||
// endpoint port, including an unresolved legacy one.
|
||||
selected.extend([*source, *target]);
|
||||
selected.extend(items);
|
||||
for port in [*source_port, *target_port] {
|
||||
if reported_omissions.insert(port) {
|
||||
plan.warnings.push(format!("SA-only omission: functional endpoint {} ({port}) is not copied into the portless OA interaction; the original is preserved.", project.named(port)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selected.extend(crate::references::relation_references(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
if selected.len() == before {
|
||||
@@ -406,6 +481,12 @@ pub fn plan_transition(
|
||||
}
|
||||
}
|
||||
}
|
||||
selected.retain(|id| {
|
||||
if omitted.contains(id) {
|
||||
if reported_omissions.insert(*id) { plan.warnings.push(format!("SA-only omission: {} ({id}) has no OA port counterpart; the original is preserved.", project.named(*id))); }
|
||||
false
|
||||
} else { true }
|
||||
});
|
||||
let mut choices = BTreeMap::new();
|
||||
for mapping in &request.mappings {
|
||||
if choices.insert(mapping.source, mapping).is_some() {
|
||||
@@ -569,7 +650,17 @@ pub fn plan_transition(
|
||||
}
|
||||
}
|
||||
let mut working = project.clone();
|
||||
for source in selected.iter().filter(|id| creating.contains(id)) {
|
||||
let ordered_creations: Vec<_> = selected
|
||||
.iter()
|
||||
.filter(|id| creating.contains(id) && project.elements.contains_key(id))
|
||||
.chain(
|
||||
selected
|
||||
.iter()
|
||||
.filter(|id| creating.contains(id) && project.relations.contains_key(id)),
|
||||
)
|
||||
.copied()
|
||||
.collect();
|
||||
for source in &ordered_creations {
|
||||
let Some(target) = targets.get(source).copied() else {
|
||||
continue;
|
||||
};
|
||||
@@ -643,6 +734,12 @@ pub fn plan_transition(
|
||||
}
|
||||
});
|
||||
for dependency in crate::references::relation_references(&created) {
|
||||
if dependency.is_nil()
|
||||
&& request.direction == TransitionDirection::OaToSa
|
||||
&& r.kind() == RelationKind::Interaction
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !targets.values().any(|id| *id == dependency)
|
||||
&& working.layer_of(dependency) != Some(request.direction.target_layer())
|
||||
{
|
||||
@@ -660,8 +757,52 @@ pub fn plan_transition(
|
||||
if !request.fields.contains(&TransitionField::Description) {
|
||||
created.description.clear();
|
||||
}
|
||||
working.relations.insert(target, created.clone());
|
||||
plan.commands.push(Command::CreateRelation(created));
|
||||
if request.direction == TransitionDirection::OaToSa
|
||||
&& let RelationBody::FunctionalExchange {
|
||||
source,
|
||||
target: destination,
|
||||
items,
|
||||
..
|
||||
} = &created.body
|
||||
{
|
||||
let request = FunctionalExchangeRequest {
|
||||
id: created.id,
|
||||
alias: created.alias.clone(),
|
||||
name: created.name.clone(),
|
||||
description: created.description.clone(),
|
||||
source: *source,
|
||||
target: *destination,
|
||||
source_port: PortChoice::Auto,
|
||||
target_port: PortChoice::Auto,
|
||||
items: items.clone(),
|
||||
};
|
||||
match crate::ports::functional_exchange_commands(&working, &request) {
|
||||
Ok(commands) => {
|
||||
for command in &commands {
|
||||
match command {
|
||||
Command::CreateElement(e) => {
|
||||
working.elements.insert(e.id, e.clone());
|
||||
}
|
||||
Command::CreateRelation(r) => {
|
||||
working.relations.insert(r.id, r.clone());
|
||||
}
|
||||
_ => unreachable!("functional creation only creates objects"),
|
||||
}
|
||||
}
|
||||
plan.commands.extend(commands);
|
||||
}
|
||||
Err(err) => conflict(
|
||||
&mut plan,
|
||||
Some(*source),
|
||||
Some(target),
|
||||
None,
|
||||
format!("Functional endpoint creation: {err}"),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
working.relations.insert(target, created.clone());
|
||||
plan.commands.push(Command::CreateRelation(created));
|
||||
}
|
||||
}
|
||||
}
|
||||
for source in &selected {
|
||||
|
||||
@@ -450,21 +450,7 @@ pub fn validate(p: &Project) -> Vec<Issue> {
|
||||
});
|
||||
}
|
||||
}
|
||||
SharedBody::Port { interface, .. } => {
|
||||
if let Some(id) = interface {
|
||||
expect_element(p, &mut out, e.id, *id, "interface", |k| {
|
||||
k == Kind::Interface
|
||||
});
|
||||
} else {
|
||||
warning(
|
||||
&mut out,
|
||||
e.id,
|
||||
"missing_interface",
|
||||
"interface",
|
||||
"Choose a port interface or add an explicit provision/requirement contract.",
|
||||
);
|
||||
}
|
||||
}
|
||||
SharedBody::Port { .. } => crate::ports::validate_port(p, e, &mut out),
|
||||
SharedBody::Scenario {
|
||||
viewpoint,
|
||||
context,
|
||||
@@ -2140,82 +2126,7 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
||||
for item in r.body.items() {
|
||||
expect_element(p, out, r.id, *item, "items", |k| k == Kind::ExchangeItem);
|
||||
}
|
||||
if let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = &r.body
|
||||
{
|
||||
for (port, owner, outgoing, field) in [
|
||||
(*source_port, source, true, "source_port"),
|
||||
(*target_port, target, false, "target_port"),
|
||||
] {
|
||||
if let Some(port) = port {
|
||||
match p.elements.get(&port) {
|
||||
Some(e)
|
||||
if e.parent == Some(owner)
|
||||
&& matches!(e.shared(),Some(SharedBody::Port{direction,..})if if outgoing{*direction!=PortDirection::In}else{*direction!=PortDirection::Out}) =>
|
||||
{}
|
||||
_ => error(
|
||||
out,
|
||||
r.id,
|
||||
"functional_port",
|
||||
field,
|
||||
"Functional port must belong to the endpoint function with compatible direction.",
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if r.kind() == RelationKind::BoundaryExchange {
|
||||
for (e, outgoing, field) in [(a, true, "source"), (b, false, "target")] {
|
||||
match e.shared() {
|
||||
Some(SharedBody::Port { direction, .. })
|
||||
if e.parent
|
||||
.and_then(|id| p.elements.get(&id))
|
||||
.is_some_and(|e| e.kind().is_performer())
|
||||
&& (if outgoing {
|
||||
*direction != PortDirection::In
|
||||
} else {
|
||||
*direction != PortDirection::Out
|
||||
}) => {}
|
||||
_ => error(
|
||||
out,
|
||||
r.id,
|
||||
"boundary_port",
|
||||
field,
|
||||
"Boundary exchange uses participant ports with output/input compatible direction.",
|
||||
),
|
||||
}
|
||||
}
|
||||
let interface_items = |port: &Element| {
|
||||
if let Some(SharedBody::Port {
|
||||
interface: Some(id),
|
||||
..
|
||||
}) = port.shared()
|
||||
&& let Some(SharedBody::Interface { items }) =
|
||||
p.elements.get(id).and_then(Element::shared)
|
||||
{
|
||||
return Some(items);
|
||||
}
|
||||
None
|
||||
};
|
||||
for port in [a, b] {
|
||||
if let Some(items) = interface_items(port) {
|
||||
for item in r.body.items() {
|
||||
if !items.contains(item) {
|
||||
error(
|
||||
out,
|
||||
r.id,
|
||||
"interface_contract",
|
||||
"items",
|
||||
"Exchange item is not included in a referenced port interface.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::ports::validate_port_relation(p, r, out);
|
||||
if let RelationBody::StateTransition {
|
||||
trigger,
|
||||
guard,
|
||||
@@ -2417,51 +2328,7 @@ pub fn allocation_summary(p: &Project, work: Id) -> Option<Id> {
|
||||
}
|
||||
|
||||
fn validate_exchange_allocation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
||||
let (Some(boundary), Some(work)) = (p.relations.get(&r.source()), p.relations.get(&r.target()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
for (function, port, field) in [
|
||||
(work.source(), boundary.source(), "source"),
|
||||
(work.target(), boundary.target(), "target"),
|
||||
] {
|
||||
let performer = p
|
||||
.relations
|
||||
.values()
|
||||
.find(|a| a.kind() == RelationKind::Allocation && a.target() == function)
|
||||
.map(Relation::source);
|
||||
let owner = p.elements.get(&port).and_then(|e| e.parent);
|
||||
if let (Some(performer), Some(owner)) = (performer, owner) {
|
||||
if performer != owner {
|
||||
error(
|
||||
out,
|
||||
r.id,
|
||||
"exchange_allocation_performer",
|
||||
field,
|
||||
"Boundary port owner must perform the corresponding work-exchange endpoint.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
warning(
|
||||
out,
|
||||
r.id,
|
||||
"incomplete_exchange_allocation",
|
||||
field,
|
||||
"Allocate work endpoints to the boundary-port participants.",
|
||||
);
|
||||
}
|
||||
}
|
||||
for item in work.body.items() {
|
||||
if !boundary.body.items().contains(item) {
|
||||
error(
|
||||
out,
|
||||
r.id,
|
||||
"exchange_allocation_items",
|
||||
"items",
|
||||
"The boundary exchange must carry every item of the allocated work exchange.",
|
||||
);
|
||||
}
|
||||
}
|
||||
crate::ports::validate_exchange_allocation(p, r, out);
|
||||
}
|
||||
|
||||
pub fn effective_performer(p: &Project, work: Id) -> Option<Id> {
|
||||
@@ -2534,59 +2401,6 @@ fn validate_relation_context(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
||||
"A boundary exchange connects different participants.",
|
||||
);
|
||||
}
|
||||
let ports: Vec<_> = match &r.body {
|
||||
RelationBody::BoundaryExchange { source, target, .. } => vec![*source, *target],
|
||||
RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} => [*source_port, *target_port].into_iter().flatten().collect(),
|
||||
_ => vec![],
|
||||
};
|
||||
for port in ports {
|
||||
let mut interfaces = BTreeSet::new();
|
||||
if let Some(SharedBody::Port {
|
||||
interface: Some(id),
|
||||
..
|
||||
}) = p.elements.get(&port).and_then(Element::shared)
|
||||
{
|
||||
interfaces.insert(*id);
|
||||
}
|
||||
interfaces.extend(
|
||||
p.relations
|
||||
.values()
|
||||
.filter(|rel| {
|
||||
matches!(
|
||||
rel.kind(),
|
||||
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement
|
||||
) && rel.source() == port
|
||||
})
|
||||
.map(Relation::target),
|
||||
);
|
||||
if !interfaces.is_empty() {
|
||||
let allowed: BTreeSet<_> = interfaces
|
||||
.iter()
|
||||
.filter_map(|id| p.elements.get(id))
|
||||
.filter_map(|e| match e.shared() {
|
||||
Some(SharedBody::Interface { items }) => Some(items),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
.copied()
|
||||
.collect();
|
||||
for item in r.body.items() {
|
||||
if !allowed.contains(item) {
|
||||
error(
|
||||
out,
|
||||
r.id,
|
||||
"port_interface_items",
|
||||
"items",
|
||||
"The exchange carries an item outside a port's declared interface contract.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
r.kind(),
|
||||
RelationKind::Availability | RelationKind::StateAction
|
||||
|
||||
Reference in New Issue
Block a user