Keep port endpoint feasibility independent of model-wide advisory scans

This commit is contained in:
2026-09-06 01:20:28 +03:00
parent 4e8575bd11
commit 93fd32c684
2 changed files with 53 additions and 29 deletions
+48 -28
View File
@@ -847,6 +847,11 @@ 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 },
@@ -1060,34 +1065,6 @@ pub(crate) fn validate_port_relation(p: &Project, r: &Relation, out: &mut Vec<Is
"This boundary exchange retains unresolved generic port semantics.",
);
}
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
&& role.target() == required.target()
}) {
warning(
out,
required.id,
"unsatisfied_interface_requirement",
"target",
"The connected partner has not declared this required interface as provided.",
);
}
}
}
}
RelationBody::PortAllocation { source, target } => {
let (Some(component), Some(function)) =
@@ -1167,6 +1144,49 @@ pub(crate) fn validate_port_relation(p: &Project, r: &Relation, out: &mut Vec<Is
_ => {}
}
}
/// 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
&& 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 {
+5 -1
View File
@@ -643,6 +643,7 @@ pub fn validate(p: &Project) -> Vec<Issue> {
text(&mut out, r.id, "name", &r.name);
text(&mut out, r.id, "description", &r.description);
validate_relation(p, r, &mut out);
crate::ports::validate_boundary_interface_requirements(p, r, &mut out);
validate_relation_context(p, r, &mut out);
}
for (key, d) in &p.diagrams {
@@ -2010,7 +2011,10 @@ fn relation_path_exists(p: &Project, kind: RelationKind, from: Id, to: Id) -> bo
fn validate_relation(p: &Project, r: &Relation, out: &mut Vec<Issue>) {
let (source, target) = r.body.endpoints();
for id in crate::references::relation_references(r) {
if !p.contains(id) {
// Most relation references name elements. Avoid constructing all model
// identities per endpoint while retaining the general lookup semantics
// for other record kinds and malformed map keys.
if !p.elements.get(&id).is_some_and(|element| element.id == id) && !p.contains(id) {
error(
out,
r.id,