Model typed SA ports and atomic exchange allocation workflows

This commit is contained in:
2026-09-05 18:22:00 +03:00
parent c20a2073d7
commit 0d14438b7b
13 changed files with 2402 additions and 256 deletions
+3 -189
View File
@@ -450,21 +450,7 @@ pub fn validate(p: &Project) -> Vec<Issue> {
});
}
}
SharedBody::Port { interface, .. } => {
if let Some(id) = interface {
expect_element(p, &mut out, e.id, *id, "interface", |k| {
k == Kind::Interface
});
} else {
warning(
&mut out,
e.id,
"missing_interface",
"interface",
"Choose a port interface or add an explicit provision/requirement contract.",
);
}
}
SharedBody::Port { .. } => crate::ports::validate_port(p, e, &mut out),
SharedBody::Scenario {
viewpoint,
context,
@@ -2140,82 +2126,7 @@ fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
for item in r.body.items() {
expect_element(p, out, r.id, *item, "items", |k| k == Kind::ExchangeItem);
}
if let RelationBody::FunctionalExchange {
source_port,
target_port,
..
} = &r.body
{
for (port, owner, outgoing, field) in [
(*source_port, source, true, "source_port"),
(*target_port, target, false, "target_port"),
] {
if let Some(port) = port {
match p.elements.get(&port) {
Some(e)
if e.parent == Some(owner)
&& matches!(e.shared(),Some(SharedBody::Port{direction,..})if if outgoing{*direction!=PortDirection::In}else{*direction!=PortDirection::Out}) =>
{}
_ => error(
out,
r.id,
"functional_port",
field,
"Functional port must belong to the endpoint function with compatible direction.",
),
}
}
}
}
if r.kind() == RelationKind::BoundaryExchange {
for (e, outgoing, field) in [(a, true, "source"), (b, false, "target")] {
match e.shared() {
Some(SharedBody::Port { direction, .. })
if e.parent
.and_then(|id| p.elements.get(&id))
.is_some_and(|e| e.kind().is_performer())
&& (if outgoing {
*direction != PortDirection::In
} else {
*direction != PortDirection::Out
}) => {}
_ => error(
out,
r.id,
"boundary_port",
field,
"Boundary exchange uses participant ports with output/input compatible direction.",
),
}
}
let interface_items = |port: &Element| {
if let Some(SharedBody::Port {
interface: Some(id),
..
}) = port.shared()
&& let Some(SharedBody::Interface { items }) =
p.elements.get(id).and_then(Element::shared)
{
return Some(items);
}
None
};
for port in [a, b] {
if let Some(items) = interface_items(port) {
for item in r.body.items() {
if !items.contains(item) {
error(
out,
r.id,
"interface_contract",
"items",
"Exchange item is not included in a referenced port interface.",
);
}
}
}
}
}
crate::ports::validate_port_relation(p, r, out);
if let RelationBody::StateTransition {
trigger,
guard,
@@ -2417,51 +2328,7 @@ pub fn allocation_summary(p: &Project, work: Id) -> Option<Id> {
}
fn validate_exchange_allocation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
let (Some(boundary), Some(work)) = (p.relations.get(&r.source()), p.relations.get(&r.target()))
else {
return;
};
for (function, port, field) in [
(work.source(), boundary.source(), "source"),
(work.target(), boundary.target(), "target"),
] {
let performer = p
.relations
.values()
.find(|a| a.kind() == RelationKind::Allocation && a.target() == function)
.map(Relation::source);
let owner = p.elements.get(&port).and_then(|e| e.parent);
if let (Some(performer), Some(owner)) = (performer, owner) {
if performer != owner {
error(
out,
r.id,
"exchange_allocation_performer",
field,
"Boundary port owner must perform the corresponding work-exchange endpoint.",
);
}
} else {
warning(
out,
r.id,
"incomplete_exchange_allocation",
field,
"Allocate work endpoints to the boundary-port participants.",
);
}
}
for item in work.body.items() {
if !boundary.body.items().contains(item) {
error(
out,
r.id,
"exchange_allocation_items",
"items",
"The boundary exchange must carry every item of the allocated work exchange.",
);
}
}
crate::ports::validate_exchange_allocation(p, r, out);
}
pub fn effective_performer(p: &Project, work: Id) -> Option<Id> {
@@ -2534,59 +2401,6 @@ fn validate_relation_context(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
"A boundary exchange connects different participants.",
);
}
let ports: Vec<_> = match &r.body {
RelationBody::BoundaryExchange { source, target, .. } => vec![*source, *target],
RelationBody::FunctionalExchange {
source_port,
target_port,
..
} => [*source_port, *target_port].into_iter().flatten().collect(),
_ => vec![],
};
for port in ports {
let mut interfaces = BTreeSet::new();
if let Some(SharedBody::Port {
interface: Some(id),
..
}) = p.elements.get(&port).and_then(Element::shared)
{
interfaces.insert(*id);
}
interfaces.extend(
p.relations
.values()
.filter(|rel| {
matches!(
rel.kind(),
RelationKind::InterfaceProvision | RelationKind::InterfaceRequirement
) && rel.source() == port
})
.map(Relation::target),
);
if !interfaces.is_empty() {
let allowed: BTreeSet<_> = interfaces
.iter()
.filter_map(|id| p.elements.get(id))
.filter_map(|e| match e.shared() {
Some(SharedBody::Interface { items }) => Some(items),
_ => None,
})
.flatten()
.copied()
.collect();
for item in r.body.items() {
if !allowed.contains(item) {
error(
out,
r.id,
"port_interface_items",
"items",
"The exchange carries an item outside a port's declared interface contract.",
);
}
}
}
}
if matches!(
r.kind(),
RelationKind::Availability | RelationKind::StateAction