Files
ARCADIA-Studio/crates/oa-core/src/ports.rs
T

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.",
);
}
}