Index semantic containment and start boundary connections from component ports

This commit is contained in:
2026-09-05 22:38:47 +03:00
parent 3c87576ed8
commit 60131e610a
2 changed files with 544 additions and 0 deletions
+144
View File
@@ -292,6 +292,17 @@ impl Workflow {
})
}
pub fn relationship(p: &Project, selected: Id, layer: Layer) -> Self {
if let Some(PortContract::Component { orientation, .. }) =
p.elements.get(&selected).and_then(port_contract)
{
return Self::relationship_kind(
p,
selected,
layer,
RelationKind::BoundaryExchange,
*orientation == ComponentPortOrientation::In,
);
}
let kind = match p.elements.get(&selected).map(oa_core::Element::kind) {
Some(Kind::OperationalMission | Kind::Mission) => RelationKind::MissionCapability,
Some(Kind::Capability | Kind::SystemCapability) => RelationKind::CapabilityInclude,
@@ -1115,6 +1126,30 @@ impl Workflow {
}
}
fn choice(p: &Project, id: Id) -> Choice {
if let Some(element) = p.elements.get(&id)
&& let Some(contract) = port_contract(element)
{
let contract = match contract {
PortContract::FunctionInput { .. } => "Input".into(),
PortContract::FunctionOutput { .. } => "Output".into(),
PortContract::Component {
kind: ComponentPortKind::Standard,
..
} => "Standard".into(),
PortContract::Component { orientation, .. } => format!("Flow {orientation:?}"),
PortContract::Legacy => "Legacy".into(),
};
return Choice {
id,
label: format!(
"{} / {} · {} · @{}",
short(element.parent.map_or("Unowned", |owner| p.named(owner)), 18),
short(&element.name, 20),
contract,
element.alias
),
};
}
Choice {
id,
label: format!(
@@ -1128,6 +1163,115 @@ fn choice(p: &Project, id: Id) -> Choice {
#[cfg(test)]
mod port_authoring_tests {
use super::*;
#[test]
fn generic_component_port_connection_keeps_its_endpoint_and_direction() {
for (kind, orientation) in [
(ComponentPortKind::Flow, ComponentPortOrientation::In),
(ComponentPortKind::Flow, ComponentPortOrientation::Out),
(ComponentPortKind::Flow, ComponentPortOrientation::InOut),
(ComponentPortKind::Flow, ComponentPortOrientation::Unset),
(ComponentPortKind::Standard, ComponentPortOrientation::Unset),
] {
let mut p = Project::empty();
let actor = p
.add_element(Kind::SystemActor, Layer::Sa, "Actor", None)
.unwrap();
let system = p
.add_element(Kind::System, Layer::Sa, "System", None)
.unwrap();
let incoming = orientation == ComponentPortOrientation::In;
let other_orientation = if kind == ComponentPortKind::Standard {
ComponentPortOrientation::Unset
} else if incoming {
ComponentPortOrientation::Out
} else {
ComponentPortOrientation::In
};
let mut ports = vec![];
for (owner, orientation) in [(actor, orientation), (system, other_orientation)] {
let id = Id::new_v4();
let port = make_port(
&p,
id,
owner,
"Data",
PortContract::Component { kind, orientation },
)
.unwrap();
p.elements.insert(id, port);
ports.push(id);
}
let before = p.clone();
let mut workflow = Workflow::relationship(&p, ports[0], Layer::Sa);
let Workflow::Relation(draft) = &workflow else {
unreachable!()
};
assert_eq!(draft.kind, RelationKind::BoundaryExchange);
if incoming {
assert!(draft.source.is_none());
assert_eq!(draft.target.as_ref().unwrap().id, ports[0]);
} else {
assert_eq!(draft.source.as_ref().unwrap().id, ports[0]);
assert!(draft.target.is_none());
}
assert_eq!(draft.oriented, kind == ComponentPortKind::Flow);
workflow
.update(
&p,
if incoming {
Event::Source(choice(&p, ports[1]))
} else {
Event::Target(choice(&p, ports[1]))
},
)
.unwrap();
let accepted = workflow.update(&p, Event::Apply).unwrap().unwrap();
assert_eq!(p, before, "The modal never mutates its input model");
let (expected_source, expected_target) = if incoming {
(ports[1], ports[0])
} else {
(ports[0], ports[1])
};
assert!(accepted.relations.values().any(|relation| matches!(relation.body, RelationBody::BoundaryExchange { source, target, oriented, .. } if source == expected_source && target == expected_target && oriented == (kind == ComponentPortKind::Flow))));
}
}
#[test]
fn duplicate_port_names_show_owner_contract_and_unique_alias() {
let mut p = Project::empty();
let actor = p
.add_element(Kind::SystemActor, Layer::Sa, "Operator", None)
.unwrap();
let system = p
.add_element(Kind::System, Layer::Sa, "Rescue", None)
.unwrap();
let mut labels = BTreeSet::new();
for owner in [actor, actor, system] {
let id = Id::new_v4();
let port = make_port(
&p,
id,
owner,
"Data",
PortContract::Component {
kind: ComponentPortKind::Flow,
orientation: ComponentPortOrientation::Out,
},
)
.unwrap();
p.elements.insert(id, port);
let label = choice(&p, id).label;
assert!(label.contains(&format!("{} / Data", p.named(owner))));
assert!(label.contains("Flow Out"));
assert!(label.contains(&format!("@{}", p.elements[&id].alias)));
assert!(
labels.insert(label),
"Duplicate names must still identify distinct ports"
);
}
}
#[test]
fn boundary_picker_recomputes_for_unoriented_reverse_endpoints_without_relaxing_contracts() {
let mut p = Project::empty();