1286 lines
43 KiB
Rust
1286 lines
43 KiB
Rust
//! Typed port contracts and concrete, atomic authoring plans.
|
|
use crate::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::BTreeSet;
|
|
use validation::{Issue, error, warning};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum PortChoice {
|
|
Auto,
|
|
Reuse(Id),
|
|
Create,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct FunctionalExchangeRequest {
|
|
pub id: Id,
|
|
pub alias: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub source: Id,
|
|
pub target: Id,
|
|
pub source_port: PortChoice,
|
|
pub target_port: PortChoice,
|
|
pub items: Vec<Id>,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub enum ComponentExchangeChoice {
|
|
Create {
|
|
id: Id,
|
|
alias: String,
|
|
name: String,
|
|
description: String,
|
|
port_kind: ComponentPortKind,
|
|
oriented: bool,
|
|
source_port: PortChoice,
|
|
target_port: PortChoice,
|
|
},
|
|
Reuse(Id),
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct ComponentExchangeRequest {
|
|
pub functional_exchange: Id,
|
|
pub boundary: ComponentExchangeChoice,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LegacyPortResolution {
|
|
pub port: Id,
|
|
pub contract: PortContract,
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum FunctionPortSide {
|
|
Input,
|
|
Output,
|
|
}
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LegacyWorkPortSplitRequest {
|
|
pub port: Id,
|
|
pub primary: FunctionPortSide,
|
|
pub incoming_items: Vec<Id>,
|
|
pub outgoing_items: Vec<Id>,
|
|
}
|
|
|
|
/// A specialized carried item satisfies a declared base item.
|
|
pub fn item_satisfies(project: &Project, carried: Id, declared: Id) -> bool {
|
|
crate::data::item_satisfies(project, carried, declared)
|
|
}
|
|
pub fn items_satisfy(project: &Project, carried: &[Id], declared: &[Id]) -> bool {
|
|
carried.iter().all(|item| {
|
|
declared
|
|
.iter()
|
|
.any(|allowed| item_satisfies(project, *item, *allowed))
|
|
})
|
|
}
|
|
pub fn port_contract(e: &Element) -> Option<&PortContract> {
|
|
match e.shared()? {
|
|
SharedBody::Port { contract, .. } => Some(contract),
|
|
_ => None,
|
|
}
|
|
}
|
|
pub fn actual_performer(p: &Project, function: Id) -> Option<Id> {
|
|
let mut owners = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::Allocation && r.target() == function)
|
|
.map(Relation::source);
|
|
let owner = owners.next()?;
|
|
owners.next().is_none().then_some(owner)
|
|
}
|
|
pub fn component_allows(contract: &PortContract, outgoing: bool) -> bool {
|
|
match contract {
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
..
|
|
} => true,
|
|
PortContract::Component { orientation, .. } => match orientation {
|
|
ComponentPortOrientation::Unset | ComponentPortOrientation::InOut => true,
|
|
ComponentPortOrientation::In => !outgoing,
|
|
ComponentPortOrientation::Out => outgoing,
|
|
},
|
|
_ => false,
|
|
}
|
|
}
|
|
pub fn has_port_allocation(p: &Project, component: Id, function: Id) -> bool {
|
|
p.relations.values().any(|r| {
|
|
r.kind() == RelationKind::PortAllocation
|
|
&& r.source() == component
|
|
&& r.target() == function
|
|
})
|
|
}
|
|
/// Explicit role declarations only: participant interfaces are not inherited.
|
|
fn declared_interface_items(p: &Project, port: Id) -> Option<Vec<Id>> {
|
|
let interfaces: Vec<_> = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| {
|
|
matches!(
|
|
r.kind(),
|
|
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement
|
|
) && r.source() == port
|
|
})
|
|
.map(Relation::target)
|
|
.collect();
|
|
if interfaces.is_empty() {
|
|
return None;
|
|
}
|
|
Some(
|
|
interfaces
|
|
.into_iter()
|
|
.flat_map(|id| crate::data::interface_items(p, id))
|
|
.collect(),
|
|
)
|
|
}
|
|
fn compatible(p: &Project, existing: &Element, desired: &Element, items: &[Id]) -> bool {
|
|
if existing.parent != desired.parent || existing.layer() != Layer::Sa {
|
|
return false;
|
|
}
|
|
match (port_contract(existing), port_contract(desired)) {
|
|
(
|
|
Some(PortContract::FunctionInput { incoming_items }),
|
|
Some(PortContract::FunctionInput { .. }),
|
|
) => items_satisfy(p, items, incoming_items),
|
|
(
|
|
Some(PortContract::FunctionOutput { outgoing_items }),
|
|
Some(PortContract::FunctionOutput { .. }),
|
|
) => items_satisfy(p, items, outgoing_items),
|
|
(
|
|
Some(actual @ PortContract::Component { kind, .. }),
|
|
Some(PortContract::Component {
|
|
kind: expected,
|
|
orientation,
|
|
}),
|
|
) => {
|
|
kind == expected
|
|
&& component_allows(actual, *orientation != ComponentPortOrientation::In)
|
|
&& declared_interface_items(p, existing.id)
|
|
.is_none_or(|allowed| items_satisfy(p, items, &allowed))
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
fn vacant(p: &Project, id: Id) -> Result<(), ModelError> {
|
|
if p.contains(id) {
|
|
Err(ModelError(format!(
|
|
"port_id_collision: Generated/requested identity {id} already exists."
|
|
)))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
pub fn make_port(
|
|
p: &Project,
|
|
id: Id,
|
|
owner: Id,
|
|
name: &str,
|
|
contract: PortContract,
|
|
) -> Result<Element, ModelError> {
|
|
let layer = p
|
|
.elements
|
|
.get(&owner)
|
|
.ok_or_else(|| ModelError("port_owner: Choose an existing owner.".into()))?
|
|
.layer();
|
|
Ok(Element {
|
|
id,
|
|
alias: p.fresh_alias(name, "port", id),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
parent: Some(owner),
|
|
body: ElementBody::shared_in(
|
|
layer,
|
|
SharedBody::Port {
|
|
contract,
|
|
legacy: None,
|
|
},
|
|
),
|
|
})
|
|
}
|
|
fn select_port(
|
|
p: &mut Project,
|
|
choice: PortChoice,
|
|
desired: Element,
|
|
items: &[Id],
|
|
commands: &mut Vec<Command>,
|
|
) -> Result<Id, ModelError> {
|
|
let candidates: Vec<_> = p
|
|
.elements
|
|
.values()
|
|
.filter(|e| compatible(p, e, &desired, items))
|
|
.map(|e| e.id)
|
|
.collect();
|
|
match choice {
|
|
PortChoice::Reuse(id) if candidates.contains(&id) => return Ok(id),
|
|
PortChoice::Reuse(_) => {
|
|
return Err(ModelError(
|
|
"port_reuse: Selected port has an incompatible owner, direction, or item contract."
|
|
.into(),
|
|
));
|
|
}
|
|
PortChoice::Auto if candidates.len() == 1 => return Ok(candidates[0]),
|
|
_ => {}
|
|
}
|
|
vacant(p, desired.id)?;
|
|
let id = desired.id;
|
|
p.elements.insert(id, desired.clone());
|
|
commands.push(Command::CreateElement(desired));
|
|
Ok(id)
|
|
}
|
|
fn finish(p: &Project, commands: Vec<Command>) -> Result<Vec<Command>, ModelError> {
|
|
execute_batch(p, &commands)?;
|
|
Ok(commands)
|
|
}
|
|
pub fn plan_functional_exchange(
|
|
p: &Project,
|
|
request: &FunctionalExchangeRequest,
|
|
) -> Result<Vec<Command>, ModelError> {
|
|
finish(p, functional_exchange_commands(p, request)?)
|
|
}
|
|
/// Used inside an existing transaction; its final candidate owns validation.
|
|
pub(crate) fn functional_exchange_commands(
|
|
p: &Project,
|
|
request: &FunctionalExchangeRequest,
|
|
) -> Result<Vec<Command>, ModelError> {
|
|
vacant(p, request.id)?;
|
|
let mut working = p.clone();
|
|
let mut relation = Relation {
|
|
id: request.id,
|
|
alias: request.alias.clone(),
|
|
name: request.name.clone(),
|
|
description: request.description.clone(),
|
|
body: RelationBody::new(
|
|
RelationKind::FunctionalExchange,
|
|
request.source,
|
|
request.target,
|
|
),
|
|
};
|
|
// Reserve the authored alias as well as identity before naming generated
|
|
// endpoints. This draft exists only in the planner's private candidate.
|
|
working.relations.insert(relation.id, relation.clone());
|
|
let mut commands = Vec::new();
|
|
let label = if request.name.trim().is_empty() {
|
|
"Exchange"
|
|
} else {
|
|
&request.name
|
|
};
|
|
let output = make_port(
|
|
&working,
|
|
Id::new_v5(&request.id, b"function-source-port"),
|
|
request.source,
|
|
&format!("{label} output"),
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: request.items.clone(),
|
|
},
|
|
)?;
|
|
let source_port = select_port(
|
|
&mut working,
|
|
request.source_port,
|
|
output,
|
|
&request.items,
|
|
&mut commands,
|
|
)?;
|
|
let input = make_port(
|
|
&working,
|
|
Id::new_v5(&request.id, b"function-target-port"),
|
|
request.target,
|
|
&format!("{label} input"),
|
|
PortContract::FunctionInput {
|
|
incoming_items: request.items.clone(),
|
|
},
|
|
)?;
|
|
let target_port = select_port(
|
|
&mut working,
|
|
request.target_port,
|
|
input,
|
|
&request.items,
|
|
&mut commands,
|
|
)?;
|
|
relation.body = RelationBody::FunctionalExchange {
|
|
source: request.source,
|
|
target: request.target,
|
|
source_port,
|
|
target_port,
|
|
items: request.items.clone(),
|
|
};
|
|
commands.push(Command::CreateRelation(relation));
|
|
Ok(commands)
|
|
}
|
|
fn add_allocation(
|
|
p: &mut Project,
|
|
kind: RelationKind,
|
|
source: Id,
|
|
target: Id,
|
|
commands: &mut Vec<Command>,
|
|
) -> Result<(), ModelError> {
|
|
if p.relations
|
|
.values()
|
|
.any(|r| r.kind() == kind && r.source() == source && r.target() == target)
|
|
{
|
|
return Ok(());
|
|
}
|
|
let prefix = if kind == RelationKind::PortAllocation {
|
|
"port-allocation"
|
|
} else {
|
|
"exchange-allocation"
|
|
};
|
|
let id = Id::new_v5(&p.id, format!("{prefix}:{source}:{target}").as_bytes());
|
|
vacant(p, id)?;
|
|
let name = format!("{} / {}", p.named(source), p.named(target));
|
|
let relation = Relation {
|
|
id,
|
|
alias: p.fresh_alias(&name, prefix, id),
|
|
name,
|
|
description: String::new(),
|
|
body: RelationBody::new(kind, source, target),
|
|
};
|
|
p.relations.insert(id, relation.clone());
|
|
commands.push(Command::CreateRelation(relation));
|
|
Ok(())
|
|
}
|
|
pub fn plan_component_exchange(
|
|
p: &Project,
|
|
request: &ComponentExchangeRequest,
|
|
) -> Result<Vec<Command>, ModelError> {
|
|
let fe = p
|
|
.relations
|
|
.get(&request.functional_exchange)
|
|
.ok_or_else(|| {
|
|
ModelError("functional_exchange: Select an existing functional exchange.".into())
|
|
})?;
|
|
let RelationBody::FunctionalExchange {
|
|
source,
|
|
target,
|
|
source_port: output,
|
|
target_port: input,
|
|
items,
|
|
} = &fe.body
|
|
else {
|
|
return Err(ModelError(
|
|
"functional_exchange: Select a functional exchange.".into(),
|
|
));
|
|
};
|
|
if !p
|
|
.elements
|
|
.get(output)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::FunctionOutput { .. })))
|
|
|| !p
|
|
.elements
|
|
.get(input)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::FunctionInput { .. })))
|
|
{
|
|
return Err(ModelError(
|
|
"legacy_port: Resolve functional endpoints before component exchange generation."
|
|
.into(),
|
|
));
|
|
}
|
|
let (Some(from), Some(to)) = (actual_performer(p, *source), actual_performer(p, *target))
|
|
else {
|
|
return Err(ModelError(
|
|
"port_allocation_owner: Allocate both functions to actual participants first.".into(),
|
|
));
|
|
};
|
|
let mut working = p.clone();
|
|
let mut commands = Vec::new();
|
|
let (boundary, from_port, to_port) = match &request.boundary {
|
|
ComponentExchangeChoice::Create {
|
|
id,
|
|
alias,
|
|
name,
|
|
description,
|
|
port_kind,
|
|
oriented,
|
|
source_port,
|
|
target_port,
|
|
} => {
|
|
vacant(&working, *id)?;
|
|
let mut relation = Relation {
|
|
id: *id,
|
|
alias: alias.clone(),
|
|
name: name.clone(),
|
|
description: description.clone(),
|
|
body: RelationBody::new(RelationKind::BoundaryExchange, Id::nil(), Id::nil()),
|
|
};
|
|
working.relations.insert(*id, relation.clone());
|
|
let contract = |outgoing| PortContract::Component {
|
|
kind: *port_kind,
|
|
orientation: if *port_kind == ComponentPortKind::Standard {
|
|
ComponentPortOrientation::Unset
|
|
} else if outgoing {
|
|
ComponentPortOrientation::Out
|
|
} else {
|
|
ComponentPortOrientation::In
|
|
},
|
|
};
|
|
let output_port = make_port(
|
|
&working,
|
|
Id::new_v5(id, b"component-source-port"),
|
|
from,
|
|
&format!("{name} source port"),
|
|
contract(true),
|
|
)?;
|
|
let a = select_port(
|
|
&mut working,
|
|
*source_port,
|
|
output_port,
|
|
items,
|
|
&mut commands,
|
|
)?;
|
|
let input_port = make_port(
|
|
&working,
|
|
Id::new_v5(id, b"component-target-port"),
|
|
to,
|
|
&format!("{name} target port"),
|
|
contract(false),
|
|
)?;
|
|
let b = select_port(&mut working, *target_port, input_port, items, &mut commands)?;
|
|
relation.body = RelationBody::BoundaryExchange {
|
|
source: a,
|
|
target: b,
|
|
oriented: *oriented,
|
|
items: items.clone(),
|
|
};
|
|
working.relations.insert(*id, relation.clone());
|
|
commands.push(Command::CreateRelation(relation));
|
|
(*id, a, b)
|
|
}
|
|
ComponentExchangeChoice::Reuse(id) => {
|
|
let mut boundary = working.relations.get(id).cloned().ok_or_else(|| {
|
|
ModelError("boundary_exchange: Select an existing boundary exchange.".into())
|
|
})?;
|
|
let RelationBody::BoundaryExchange {
|
|
source: a,
|
|
target: b,
|
|
oriented,
|
|
items: carried,
|
|
} = &mut boundary.body
|
|
else {
|
|
return Err(ModelError(
|
|
"boundary_exchange: Select a boundary exchange.".into(),
|
|
));
|
|
};
|
|
let owner = |port| p.elements.get(&port).and_then(|e| e.parent);
|
|
let endpoints = if owner(*a) == Some(from) && owner(*b) == Some(to) {
|
|
(*a, *b)
|
|
} else if !*oriented && owner(*b) == Some(from) && owner(*a) == Some(to) {
|
|
(*b, *a)
|
|
} else {
|
|
return Err(ModelError("boundary_owner: Boundary participants do not perform the selected functions in a compatible direction.".into()));
|
|
};
|
|
for item in items {
|
|
if !carried.contains(item) {
|
|
carried.push(*item);
|
|
}
|
|
}
|
|
if working.relations[id] != boundary {
|
|
working.relations.insert(*id, boundary.clone());
|
|
commands.push(Command::EditRelation(*id, boundary));
|
|
}
|
|
(*id, endpoints.0, endpoints.1)
|
|
}
|
|
};
|
|
add_allocation(
|
|
&mut working,
|
|
RelationKind::PortAllocation,
|
|
from_port,
|
|
*output,
|
|
&mut commands,
|
|
)?;
|
|
add_allocation(
|
|
&mut working,
|
|
RelationKind::PortAllocation,
|
|
to_port,
|
|
*input,
|
|
&mut commands,
|
|
)?;
|
|
add_allocation(
|
|
&mut working,
|
|
RelationKind::ExchangeAllocation,
|
|
boundary,
|
|
fe.id,
|
|
&mut commands,
|
|
)?;
|
|
finish(p, commands)
|
|
}
|
|
pub fn plan_legacy_port_resolution(
|
|
p: &Project,
|
|
request: &LegacyPortResolution,
|
|
) -> Result<Vec<Command>, ModelError> {
|
|
let mut element = p
|
|
.elements
|
|
.get(&request.port)
|
|
.cloned()
|
|
.ok_or_else(|| ModelError("legacy_port: Select an existing legacy port.".into()))?;
|
|
let Some(SharedBody::Port {
|
|
contract,
|
|
legacy: Some(_),
|
|
}) = element.shared_mut()
|
|
else {
|
|
return Err(ModelError(
|
|
"legacy_port: Select a preserved legacy port.".into(),
|
|
));
|
|
};
|
|
if *contract != PortContract::Legacy || request.contract == PortContract::Legacy {
|
|
return Err(ModelError(
|
|
"legacy_port: Resolution requires an unresolved port and a typed contract.".into(),
|
|
));
|
|
}
|
|
*contract = request.contract.clone();
|
|
finish(p, vec![Command::EditElement(element.id, element)])
|
|
}
|
|
pub fn plan_legacy_work_port_split(
|
|
p: &Project,
|
|
request: &LegacyWorkPortSplitRequest,
|
|
) -> Result<Vec<Command>, ModelError> {
|
|
let mut primary = p
|
|
.elements
|
|
.get(&request.port)
|
|
.cloned()
|
|
.ok_or_else(|| ModelError("legacy_port: Select an existing legacy port.".into()))?;
|
|
if primary.layer() != Layer::Sa
|
|
|| !primary
|
|
.parent
|
|
.and_then(|id| p.elements.get(&id))
|
|
.is_some_and(|e| e.kind() == Kind::Function)
|
|
|| !matches!(
|
|
primary.shared(),
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::Legacy,
|
|
legacy: Some(LegacyPortContract {
|
|
direction: PortDirection::InOut,
|
|
..
|
|
})
|
|
})
|
|
)
|
|
{
|
|
return Err(ModelError(
|
|
"legacy_port_split: Select an unresolved SA function InOut port.".into(),
|
|
));
|
|
}
|
|
let other_id = Id::new_v5(&primary.id, b"split-opposite-port");
|
|
vacant(p, other_id)?;
|
|
let input_contract = PortContract::FunctionInput {
|
|
incoming_items: request.incoming_items.clone(),
|
|
};
|
|
let output_contract = PortContract::FunctionOutput {
|
|
outgoing_items: request.outgoing_items.clone(),
|
|
};
|
|
let (contract, opposite, suffix, input, output) = if request.primary == FunctionPortSide::Input
|
|
{
|
|
(
|
|
input_contract,
|
|
output_contract,
|
|
"output",
|
|
primary.id,
|
|
other_id,
|
|
)
|
|
} else {
|
|
(
|
|
output_contract,
|
|
input_contract,
|
|
"input",
|
|
other_id,
|
|
primary.id,
|
|
)
|
|
};
|
|
if let Some(SharedBody::Port {
|
|
contract: current, ..
|
|
}) = primary.shared_mut()
|
|
{
|
|
*current = contract;
|
|
}
|
|
let sibling = make_port(
|
|
p,
|
|
other_id,
|
|
primary.parent.unwrap(),
|
|
&format!("{} {suffix}", primary.name),
|
|
opposite,
|
|
)?;
|
|
let mut commands = vec![
|
|
Command::EditElement(primary.id, primary),
|
|
Command::CreateElement(sibling),
|
|
];
|
|
for r in p.relations.values() {
|
|
let mut changed = r.clone();
|
|
if let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = &mut changed.body
|
|
{
|
|
if *source_port == request.port {
|
|
*source_port = output;
|
|
}
|
|
if *target_port == request.port {
|
|
*target_port = input;
|
|
}
|
|
}
|
|
if changed != *r {
|
|
commands.push(Command::EditRelation(r.id, changed));
|
|
}
|
|
}
|
|
finish(p, commands)
|
|
}
|
|
|
|
/// Explicit synchronization preserves port identity; final validation protects all users.
|
|
pub fn plan_function_port_items(
|
|
p: &Project,
|
|
port: Id,
|
|
items: Vec<Id>,
|
|
) -> Result<Vec<Command>, ModelError> {
|
|
let mut e =
|
|
p.elements.get(&port).cloned().ok_or_else(|| {
|
|
ModelError("functional_port: Select an existing function port.".into())
|
|
})?;
|
|
match e.shared_mut() {
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::FunctionInput { incoming_items },
|
|
..
|
|
}) => *incoming_items = items,
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::FunctionOutput { outgoing_items },
|
|
..
|
|
}) => *outgoing_items = items,
|
|
_ => {
|
|
return Err(ModelError(
|
|
"functional_port: Select a typed function port.".into(),
|
|
));
|
|
}
|
|
}
|
|
let commands = if p.elements.get(&port) == Some(&e) {
|
|
vec![]
|
|
} else {
|
|
vec![Command::EditElement(port, e)]
|
|
};
|
|
finish(p, commands)
|
|
}
|
|
|
|
/// Whether removing these allocation facts breaks a typed exchange mapping.
|
|
/// Legacy or malformed endpoints are left to their own validation rules; this
|
|
/// helper only determines the conditional dependency on PortAllocation facts.
|
|
pub(crate) fn missing_exchange_port_allocations(
|
|
p: &Project,
|
|
relation: &Relation,
|
|
removed: &BTreeSet<Id>,
|
|
) -> bool {
|
|
let Some(Relation {
|
|
body:
|
|
RelationBody::BoundaryExchange {
|
|
source: a,
|
|
target: b,
|
|
oriented,
|
|
..
|
|
},
|
|
..
|
|
}) = p.relations.get(&relation.source())
|
|
else {
|
|
return false;
|
|
};
|
|
let Some(Relation {
|
|
body:
|
|
RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
},
|
|
..
|
|
}) = p.relations.get(&relation.target())
|
|
else {
|
|
return false;
|
|
};
|
|
if ![*a, *b].iter().all(|id| {
|
|
p.elements
|
|
.get(id)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::Component { .. })))
|
|
}) || !p
|
|
.elements
|
|
.get(source_port)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::FunctionOutput { .. })))
|
|
|| !p
|
|
.elements
|
|
.get(target_port)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::FunctionInput { .. })))
|
|
{
|
|
return false;
|
|
}
|
|
let allocated = |component, function| {
|
|
p.relations.values().any(|allocation| {
|
|
allocation.kind() == RelationKind::PortAllocation
|
|
&& !removed.contains(&allocation.id)
|
|
&& allocation.source() == component
|
|
&& allocation.target() == function
|
|
})
|
|
};
|
|
!((allocated(*a, *source_port) && allocated(*b, *target_port))
|
|
|| (!*oriented && allocated(*b, *source_port) && allocated(*a, *target_port)))
|
|
}
|
|
|
|
pub(crate) fn validate_port(p: &Project, e: &Element, out: &mut Vec<Issue>) {
|
|
let Some(SharedBody::Port { contract, legacy }) = e.shared() else {
|
|
return;
|
|
};
|
|
let owner = e.parent.and_then(|id| p.elements.get(&id));
|
|
if let Some(id) = legacy.as_ref().and_then(|old| old.interface)
|
|
&& !p.elements.get(&id).is_some_and(|interface| {
|
|
interface.kind() == Kind::Interface && interface.layer() == e.layer()
|
|
})
|
|
{
|
|
error(
|
|
out,
|
|
e.id,
|
|
"port_legacy_interface",
|
|
"interface",
|
|
"Preserved interface must exist in the port's layer.",
|
|
);
|
|
}
|
|
if *contract == PortContract::Legacy {
|
|
if legacy.is_none() {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"port_legacy_missing",
|
|
"legacy",
|
|
"An unresolved port requires its preserved generic contract.",
|
|
);
|
|
}
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"legacy_port",
|
|
"contract",
|
|
"Review this preserved generic port before assigning a typed SA contract.",
|
|
);
|
|
return;
|
|
}
|
|
let valid_owner = e.layer() == Layer::Sa
|
|
&& owner.is_some_and(|owner| {
|
|
owner.layer() == Layer::Sa
|
|
&& if contract.is_function() {
|
|
owner.kind() == Kind::Function
|
|
} else {
|
|
matches!(owner.kind(), Kind::System | Kind::SystemActor)
|
|
}
|
|
});
|
|
if !valid_owner {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"port_owner",
|
|
"parent",
|
|
"Typed ports belong to an SA Function or an SA participant according to their contract; OA has no typed ports.",
|
|
);
|
|
}
|
|
match contract {
|
|
PortContract::FunctionInput { incoming_items }
|
|
| PortContract::FunctionOutput {
|
|
outgoing_items: incoming_items,
|
|
} => {
|
|
let field = if matches!(contract, PortContract::FunctionInput { .. }) {
|
|
"incoming_items"
|
|
} else {
|
|
"outgoing_items"
|
|
};
|
|
let mut seen = BTreeSet::new();
|
|
for id in incoming_items {
|
|
if !seen.insert(*id) {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"duplicate_reference",
|
|
field,
|
|
"Port item declarations contain unique identities.",
|
|
);
|
|
}
|
|
if !p.elements.get(id).is_some_and(|item| {
|
|
item.kind() == Kind::ExchangeItem && item.layer() == e.layer()
|
|
}) {
|
|
error(
|
|
out,
|
|
e.id,
|
|
"port_item",
|
|
field,
|
|
"Port items must be exchange items in the same layer.",
|
|
);
|
|
}
|
|
}
|
|
if incoming_items.is_empty() {
|
|
warning(
|
|
out,
|
|
e.id,
|
|
"empty_port_contract",
|
|
field,
|
|
"This function port has no declared exchange items.",
|
|
);
|
|
}
|
|
}
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation,
|
|
} if *orientation != ComponentPortOrientation::Unset => error(
|
|
out,
|
|
e.id,
|
|
"standard_port_orientation",
|
|
"orientation",
|
|
"Standard ports have no Flow orientation; use Unset.",
|
|
),
|
|
PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
} => warning(
|
|
out,
|
|
e.id,
|
|
"unset_port_orientation",
|
|
"orientation",
|
|
"Choose the Flow port's orientation after reviewing its communication direction.",
|
|
),
|
|
_ => {}
|
|
}
|
|
}
|
|
fn legacy_allows(e: &Element, outgoing: bool) -> bool {
|
|
matches!(e.shared(), Some(SharedBody::Port { contract: PortContract::Legacy, legacy: Some(LegacyPortContract { direction, .. }) }) if if outgoing { *direction != PortDirection::In } else { *direction != PortDirection::Out })
|
|
}
|
|
fn validate_carried_contract(p: &Project, r: &Relation, port: &Element, out: &mut Vec<Issue>) {
|
|
// Every carried-item constraint is a subset check. An empty exchange cannot
|
|
// violate one, so endpoint feasibility need not scan all interface roles.
|
|
if r.body.items().is_empty() {
|
|
return;
|
|
}
|
|
match port.shared() {
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::FunctionInput { incoming_items },
|
|
..
|
|
}) => {
|
|
if !items_satisfy(p, r.body.items(), incoming_items) {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"function_port_items",
|
|
"items",
|
|
"An exchange item is outside its input port's incoming declaration.",
|
|
);
|
|
}
|
|
}
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::FunctionOutput { outgoing_items },
|
|
..
|
|
}) => {
|
|
if !items_satisfy(p, r.body.items(), outgoing_items) {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"function_port_items",
|
|
"items",
|
|
"An exchange item is outside its output port's outgoing declaration.",
|
|
);
|
|
}
|
|
}
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::Component { .. },
|
|
..
|
|
}) => {
|
|
if let Some(items) = declared_interface_items(p, port.id)
|
|
&& !items_satisfy(p, r.body.items(), &items)
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"port_interface_items",
|
|
"items",
|
|
"The component exchange carries an item outside this port's declared interfaces.",
|
|
);
|
|
}
|
|
}
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::Legacy,
|
|
legacy,
|
|
..
|
|
}) => {
|
|
let mut allowed = declared_interface_items(p, port.id);
|
|
if let Some(interface) = legacy.as_ref().and_then(|l| l.interface) {
|
|
let items = crate::data::interface_items(p, interface);
|
|
if r.kind() == RelationKind::BoundaryExchange
|
|
&& !items_satisfy(p, r.body.items(), &items)
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"interface_contract",
|
|
"items",
|
|
"Exchange items must fit the preserved legacy interface association.",
|
|
);
|
|
}
|
|
allowed.get_or_insert_with(Vec::new).extend(items);
|
|
}
|
|
if let Some(items) = allowed
|
|
&& !items_satisfy(p, r.body.items(), &items)
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"port_interface_items",
|
|
"items",
|
|
"The exchange carries an item outside its legacy interface declarations.",
|
|
);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
pub(crate) fn validate_port_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
|
|
match &r.body {
|
|
RelationBody::FunctionalExchange {
|
|
source,
|
|
target,
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} => {
|
|
for (id, owner, outgoing, field) in [
|
|
(*source_port, *source, true, "source_port"),
|
|
(*target_port, *target, false, "target_port"),
|
|
] {
|
|
let Some(port) = p.elements.get(&id) else {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"functional_port",
|
|
field,
|
|
"A functional exchange requires an existing port at each endpoint.",
|
|
);
|
|
continue;
|
|
};
|
|
let typed = matches!(
|
|
(port_contract(port), outgoing),
|
|
(Some(PortContract::FunctionOutput { .. }), true)
|
|
| (Some(PortContract::FunctionInput { .. }), false)
|
|
);
|
|
if port.parent != Some(owner)
|
|
|| port.layer() != Layer::Sa
|
|
|| !(typed || legacy_allows(port, outgoing))
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"functional_port",
|
|
field,
|
|
"Functional endpoints must have the matching function owner and output/input contract.",
|
|
);
|
|
}
|
|
validate_carried_contract(p, r, port, out);
|
|
}
|
|
}
|
|
RelationBody::BoundaryExchange {
|
|
source,
|
|
target,
|
|
oriented,
|
|
..
|
|
} => {
|
|
let (Some(a), Some(b)) = (p.elements.get(source), p.elements.get(target)) else {
|
|
return;
|
|
};
|
|
for (port, outgoing, field) in [(a, true, "source"), (b, false, "target")] {
|
|
let participant = port
|
|
.parent
|
|
.and_then(|id| p.elements.get(&id))
|
|
.is_some_and(|owner| owner.kind().is_performer());
|
|
let allowed = match port_contract(port) {
|
|
Some(contract @ PortContract::Component { .. }) => {
|
|
port.layer() == Layer::Sa
|
|
&& (!*oriented || component_allows(contract, outgoing))
|
|
}
|
|
Some(PortContract::Legacy) => legacy_allows(port, outgoing),
|
|
_ => false,
|
|
};
|
|
if !participant || !allowed {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"boundary_port",
|
|
field,
|
|
"Boundary endpoints require compatible component ports owned by participants (or preserved legacy participant ports).",
|
|
);
|
|
}
|
|
validate_carried_contract(p, r, port, out);
|
|
}
|
|
if !*oriented
|
|
&& let (
|
|
Some(
|
|
ac @ PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
..
|
|
},
|
|
),
|
|
Some(
|
|
bc @ PortContract::Component {
|
|
kind: ComponentPortKind::Flow,
|
|
..
|
|
},
|
|
),
|
|
) = (port_contract(a), port_contract(b))
|
|
&& !((component_allows(ac, true) && component_allows(bc, false))
|
|
|| (component_allows(ac, false) && component_allows(bc, true)))
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"boundary_orientation",
|
|
"oriented",
|
|
"These Flow ports have no compatible communication direction.",
|
|
);
|
|
}
|
|
if let (
|
|
Some(PortContract::Component { kind: ak, .. }),
|
|
Some(PortContract::Component { kind: bk, .. }),
|
|
) = (port_contract(a), port_contract(b))
|
|
&& ak != bk
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"mixed_component_ports",
|
|
"endpoints",
|
|
"A component exchange must bind ports of the same kind (both Standard or both Flow).",
|
|
);
|
|
}
|
|
if [a, b]
|
|
.iter()
|
|
.any(|e| matches!(port_contract(e), Some(PortContract::Legacy)))
|
|
{
|
|
warning(
|
|
out,
|
|
r.id,
|
|
"legacy_boundary",
|
|
"endpoints",
|
|
"This boundary exchange retains unresolved generic port semantics.",
|
|
);
|
|
}
|
|
}
|
|
RelationBody::PortAllocation { source, target } => {
|
|
let (Some(component), Some(function)) =
|
|
(p.elements.get(source), p.elements.get(target))
|
|
else {
|
|
return;
|
|
};
|
|
let outgoing = matches!(
|
|
port_contract(function),
|
|
Some(PortContract::FunctionOutput { .. })
|
|
);
|
|
if component.layer() != Layer::Sa
|
|
|| function.layer() != Layer::Sa
|
|
|| !port_contract(function).is_some_and(PortContract::is_function)
|
|
|| !port_contract(component).is_some_and(|c| component_allows(c, outgoing))
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"port_allocation_contract",
|
|
"endpoints",
|
|
"Port allocation requires an SA component port and a compatible typed function port.",
|
|
);
|
|
}
|
|
if function.parent.and_then(|id| actual_performer(p, id)) != component.parent
|
|
|| component.parent.is_none()
|
|
{
|
|
error(
|
|
out,
|
|
r.id,
|
|
"port_allocation_owner",
|
|
"endpoints",
|
|
"The function must have an actual work allocation to the allocating port's component.",
|
|
);
|
|
}
|
|
if p.relations.values().any(|other| {
|
|
other.id != r.id
|
|
&& other.kind() == RelationKind::PortAllocation
|
|
&& other.source() == *source
|
|
&& other.target() == *target
|
|
}) {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"duplicate_port_allocation",
|
|
"endpoints",
|
|
"There is already a port allocation for these endpoints.",
|
|
);
|
|
}
|
|
}
|
|
RelationBody::InterfaceProvision { source, .. }
|
|
| RelationBody::InterfaceRequirement { source, .. } => {
|
|
let Some(port) = p.elements.get(source) else {
|
|
return;
|
|
};
|
|
if let Some(SharedBody::Port { contract, legacy }) = port.shared() {
|
|
if *contract == PortContract::Legacy || (contract.is_function() && legacy.is_some())
|
|
{
|
|
warning(
|
|
out,
|
|
r.id,
|
|
"legacy_interface_role",
|
|
"source",
|
|
"This preserved legacy interface role does not replace the typed function item contract.",
|
|
);
|
|
} else if contract.is_function() {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"function_interface_role",
|
|
"source",
|
|
"Use function input/output item declarations; interface roles belong to component ports or participants.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
/// Whole-model advisory checks. Missing peer declarations are warnings, so they
|
|
/// must not make endpoint feasibility scan the relation graph per candidate.
|
|
pub(crate) fn validate_boundary_interface_requirements(
|
|
p: &Project,
|
|
r: &Relation,
|
|
out: &mut Vec<Issue>,
|
|
) {
|
|
let RelationBody::BoundaryExchange { source, target, .. } = &r.body else {
|
|
return;
|
|
};
|
|
let (Some(a), Some(b)) = (p.elements.get(source), p.elements.get(target)) else {
|
|
return;
|
|
};
|
|
for (port, peer) in [(a, b), (b, a)] {
|
|
if !matches!(
|
|
port_contract(port),
|
|
Some(PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
..
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
for required in p.relations.values().filter(|role| {
|
|
role.kind() == RelationKind::InterfaceRequirement && role.source() == port.id
|
|
}) {
|
|
if !p.relations.values().any(|role| {
|
|
role.kind() == RelationKind::InterfaceProvision
|
|
&& role.source() == peer.id
|
|
&& crate::data::interface_satisfies(p, role.target(), required.target())
|
|
}) {
|
|
warning(
|
|
out,
|
|
required.id,
|
|
"unsatisfied_interface_requirement",
|
|
"target",
|
|
"The connected partner has not declared this required interface as provided.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) 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;
|
|
};
|
|
let RelationBody::BoundaryExchange {
|
|
source: a,
|
|
target: b,
|
|
oriented,
|
|
..
|
|
} = &boundary.body
|
|
else {
|
|
return;
|
|
};
|
|
let owner = |port| p.elements.get(&port).and_then(|e| e.parent);
|
|
let direct = owner(*a) == actual_performer(p, work.source())
|
|
&& owner(*b) == actual_performer(p, work.target());
|
|
let reverse = !*oriented
|
|
&& owner(*b) == actual_performer(p, work.source())
|
|
&& owner(*a) == actual_performer(p, work.target());
|
|
let legacy = [*a, *b].iter().any(|id| {
|
|
p.elements
|
|
.get(id)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::Legacy)))
|
|
}) || match &work.body {
|
|
RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} => [*source_port, *target_port].iter().any(|id| {
|
|
p.elements
|
|
.get(id)
|
|
.is_some_and(|e| matches!(port_contract(e), Some(PortContract::Legacy)))
|
|
}),
|
|
_ => true,
|
|
};
|
|
if legacy {
|
|
warning(
|
|
out,
|
|
r.id,
|
|
"legacy_exchange_allocation",
|
|
"endpoints",
|
|
"Resolve legacy ports and explicitly allocate the corresponding typed ports.",
|
|
);
|
|
for (function, port) in [(work.source(), *a), (work.target(), *b)] {
|
|
match (actual_performer(p, function), owner(port)) {
|
|
(Some(actual), Some(owner)) if actual != owner => error(
|
|
out,
|
|
r.id,
|
|
"exchange_allocation_performer",
|
|
"endpoints",
|
|
"Boundary owner must perform the corresponding work endpoint.",
|
|
),
|
|
(None, _) | (_, None) => warning(
|
|
out,
|
|
r.id,
|
|
"incomplete_exchange_allocation",
|
|
"endpoints",
|
|
"Allocate the corresponding work endpoints to participants.",
|
|
),
|
|
_ => {}
|
|
}
|
|
}
|
|
} else {
|
|
if !direct && !reverse {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"exchange_allocation_performer",
|
|
"endpoints",
|
|
"Boundary participants must perform the corresponding function endpoints.",
|
|
);
|
|
}
|
|
if let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = &work.body
|
|
{
|
|
let mapped = (direct
|
|
&& has_port_allocation(p, *a, *source_port)
|
|
&& has_port_allocation(p, *b, *target_port))
|
|
|| (reverse
|
|
&& has_port_allocation(p, *b, *source_port)
|
|
&& has_port_allocation(p, *a, *target_port));
|
|
if !mapped {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"exchange_port_allocation",
|
|
"endpoints",
|
|
"The component exchange must explicitly allocate both corresponding function ports.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if !items_satisfy(p, work.body.items(), boundary.body.items()) {
|
|
error(
|
|
out,
|
|
r.id,
|
|
"exchange_allocation_items",
|
|
"items",
|
|
"The boundary exchange must carry every allocated work-exchange item.",
|
|
);
|
|
}
|
|
}
|