//! 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, } #[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, pub outgoing_items: Vec, } /// 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 { 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> { 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 { 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, ) -> Result { 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) -> Result, ModelError> { execute_batch(p, &commands)?; Ok(commands) } pub fn plan_functional_exchange( p: &Project, request: &FunctionalExchangeRequest, ) -> Result, 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, 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, ) -> 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, 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, 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, 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, ) -> Result, 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, ) -> 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) { 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) { // 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) { 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, ) { 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) { let (Some(boundary), Some(work)) = (p.relations.get(&r.source()), p.relations.get(&r.target())) else { return; }; if let RelationBody::CommunicationMean { source, target, oriented, .. } = &boundary.body { let actual = ( actual_performer(p, work.source()), actual_performer(p, work.target()), ); let compatible = |a, b| { actual.0.is_none_or(|owner| owner == a) && actual.1.is_none_or(|owner| owner == b) }; if !compatible(*source, *target) && (*oriented || !compatible(*target, *source)) { error( out, r.id, "exchange_allocation_performer", "endpoints", "Actual activity allocations must agree with one permitted communication direction; review the means or the activity allocations.", ); } else if actual.0.is_none() || actual.1.is_none() { warning( out, r.id, "incomplete_exchange_allocation", "endpoints", "Add or review actual activity-to-participant Allocations; role responsibility alone does not resolve communication allocation.", ); } if !items_satisfy(p, work.body.items(), boundary.body.items()) { error( out, r.id, "exchange_allocation_items", "items", "Communication means must declare every allocated interaction item; review the carried item contract explicitly.", ); } 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", if p.layer_of(boundary.id) == Some(Layer::Oa) { "This preserved OA boundary uses legacy ports; review its communication representation without inventing SA port contracts." } else { "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.", ); } }