Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
//! One operation resolver for admission, forms and scenario labels.
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ResolvedOperation {
|
||||
/// The actual Call/Asynchronous declaration, also when resolving its Reply.
|
||||
pub invocation: Id,
|
||||
pub allocation: Id,
|
||||
pub interface: Id,
|
||||
pub item: Id,
|
||||
pub caller_port: Id,
|
||||
pub provider_port: Id,
|
||||
pub transmission: TransmissionProtocol,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum OperationResolution {
|
||||
Ordinary,
|
||||
Unresolved { invocation: Id, reason: String },
|
||||
Selected(ResolvedOperation),
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("{message}")]
|
||||
pub struct OperationError {
|
||||
pub code: &'static str,
|
||||
pub message: String,
|
||||
}
|
||||
fn error(code: &'static str, message: &str) -> OperationError {
|
||||
OperationError {
|
||||
code,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(p: &Project, occurrence: Id) -> Result<OperationResolution, OperationError> {
|
||||
let e = p
|
||||
.elements
|
||||
.get(&occurrence)
|
||||
.ok_or_else(|| error("operation_context", "The message no longer exists."))?;
|
||||
let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
operation,
|
||||
message_kind,
|
||||
reply_to,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = e.shared()
|
||||
else {
|
||||
return Ok(OperationResolution::Ordinary);
|
||||
};
|
||||
let invocation = if *message_kind == MessageKind::Reply {
|
||||
if operation.is_some() {
|
||||
return Err(error(
|
||||
"reply_operation",
|
||||
"A Reply derives its operation from its exact Call; it cannot store a second selection.",
|
||||
));
|
||||
}
|
||||
let Some(call) = reply_to else {
|
||||
return Ok(OperationResolution::Ordinary);
|
||||
};
|
||||
if !matches!(
|
||||
p.elements.get(call).and_then(Element::shared),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Call,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
) {
|
||||
return Ok(OperationResolution::Ordinary);
|
||||
}
|
||||
*call
|
||||
} else {
|
||||
occurrence
|
||||
};
|
||||
let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
body @ OccurrenceBody::Exchange {
|
||||
operation: Some(operation),
|
||||
message_kind,
|
||||
exchange,
|
||||
sender,
|
||||
receiver,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = p.elements.get(&invocation).and_then(Element::shared)
|
||||
else {
|
||||
return Ok(OperationResolution::Ordinary);
|
||||
};
|
||||
if p.layer_of(invocation) != Some(Layer::Sa)
|
||||
|| !matches!(message_kind, MessageKind::Call | MessageKind::Asynchronous)
|
||||
|| validation::ancestor_of_kind(p, invocation, Kind::Scenario)
|
||||
.and_then(|id| p.elements.get(&id))
|
||||
.and_then(Element::shared)
|
||||
.is_none_or(|body| {
|
||||
!matches!(
|
||||
body,
|
||||
SharedBody::Scenario {
|
||||
viewpoint: ScenarioViewpoint::BoundaryInterface,
|
||||
..
|
||||
}
|
||||
)
|
||||
})
|
||||
{
|
||||
return Err(error(
|
||||
"operation_context",
|
||||
"Operation use belongs to a Call or Asynchronous message in an SA BoundaryInterface scenario.",
|
||||
));
|
||||
}
|
||||
let unresolved = |reason: &str| {
|
||||
Ok(OperationResolution::Unresolved {
|
||||
invocation,
|
||||
reason: reason.into(),
|
||||
})
|
||||
};
|
||||
if exchange
|
||||
.and_then(|id| p.relations.get(&id))
|
||||
.is_some_and(|r| r.kind() != RelationKind::BoundaryExchange)
|
||||
{
|
||||
return Err(error(
|
||||
"operation_channel",
|
||||
"Operation use requires an explicit boundary exchange channel.",
|
||||
));
|
||||
}
|
||||
let Some(allocation) = operation.allocation else {
|
||||
return unresolved(
|
||||
"Choose the interface operation allocation; existing value expressions are preserved.",
|
||||
);
|
||||
};
|
||||
let Some(relation) = p.relations.get(&allocation) else {
|
||||
return Err(error(
|
||||
"operation_allocation",
|
||||
"The selected interface operation allocation no longer exists.",
|
||||
));
|
||||
};
|
||||
let RelationBody::InterfaceItemAllocation {
|
||||
source: interface,
|
||||
target: item,
|
||||
transmission,
|
||||
..
|
||||
} = relation.body
|
||||
else {
|
||||
return Err(error(
|
||||
"operation_allocation",
|
||||
"Choose an InterfaceItemAllocation, not an item or another relationship.",
|
||||
));
|
||||
};
|
||||
if p.layer_of(allocation) != Some(Layer::Sa)
|
||||
|| !matches!(
|
||||
p.elements.get(&item).and_then(Element::shared),
|
||||
Some(SharedBody::ExchangeItem {
|
||||
kind: ExchangeItemKind::Information,
|
||||
mechanism: ExchangeMechanism::Operation,
|
||||
..
|
||||
})
|
||||
)
|
||||
{
|
||||
return Err(error(
|
||||
"operation_allocation",
|
||||
"The selected SA allocation must declare an Information item with Operation mechanism.",
|
||||
));
|
||||
}
|
||||
let Some(channel) = exchange.and_then(|id| p.relations.get(&id)) else {
|
||||
return Err(error(
|
||||
"operation_channel",
|
||||
"A selected operation requires an existing boundary exchange carrying its item; clear the allocation to retain unresolved intent.",
|
||||
));
|
||||
};
|
||||
if channel.kind() != RelationKind::BoundaryExchange
|
||||
|| !channel
|
||||
.body
|
||||
.items()
|
||||
.iter()
|
||||
.any(|carried| data::item_satisfies(p, *carried, item))
|
||||
{
|
||||
return Err(error(
|
||||
"operation_channel",
|
||||
"The boundary exchange must carry the selected operation item or an admitted subtype.",
|
||||
));
|
||||
}
|
||||
if sender.is_none() || receiver.is_none() {
|
||||
return Err(error(
|
||||
"operation_lifelines",
|
||||
"A selected operation requires actual caller and provider lifelines; clear the allocation to retain unresolved intent.",
|
||||
));
|
||||
}
|
||||
let (caller_port, provider_port) = match validation::resolve_message_endpoints(p, body) {
|
||||
validation::MessageEndpointResolution::Resolved { source, target } => (source, target),
|
||||
validation::MessageEndpointResolution::Conflict => {
|
||||
return Err(error(
|
||||
"operation_direction",
|
||||
"The selected lifelines conflict with the boundary channel's actual caller/provider direction.",
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
return Err(error(
|
||||
"operation_direction",
|
||||
"A selected operation requires a proven actual caller/provider channel direction; clear the allocation to retain unresolved intent.",
|
||||
));
|
||||
}
|
||||
};
|
||||
for port in [caller_port, provider_port] {
|
||||
if !matches!(
|
||||
p.elements.get(&port).and_then(ports::port_contract),
|
||||
Some(PortContract::Component {
|
||||
kind: ComponentPortKind::Standard,
|
||||
..
|
||||
})
|
||||
) {
|
||||
return Err(error(
|
||||
"operation_ports",
|
||||
"Operation proof requires concrete Standard component ports.",
|
||||
));
|
||||
}
|
||||
}
|
||||
let exposes = |interface| {
|
||||
data::effective_interface_allocations(p, interface)
|
||||
.iter()
|
||||
.any(|r| r.id == allocation)
|
||||
};
|
||||
let providers: Vec<_> = p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| {
|
||||
r.kind() == RelationKind::InterfaceProvision
|
||||
&& r.source() == provider_port
|
||||
&& exposes(r.target())
|
||||
})
|
||||
.map(Relation::target)
|
||||
.collect();
|
||||
let valid = p.relations.values().any(|r| {
|
||||
r.kind() == RelationKind::InterfaceRequirement
|
||||
&& r.source() == caller_port
|
||||
&& exposes(r.target())
|
||||
&& providers
|
||||
.iter()
|
||||
.any(|provided| data::interface_satisfies(p, *provided, r.target()))
|
||||
});
|
||||
if !valid {
|
||||
return Err(error(
|
||||
"operation_roles",
|
||||
"The actual receiver port must provide, and sender port require, compatible interfaces exposing this exact allocation.",
|
||||
));
|
||||
}
|
||||
if !matches!(
|
||||
(message_kind, transmission),
|
||||
(_, TransmissionProtocol::Unspecified)
|
||||
| (MessageKind::Call, TransmissionProtocol::Synchronous)
|
||||
| (
|
||||
MessageKind::Asynchronous,
|
||||
TransmissionProtocol::Asynchronous
|
||||
)
|
||||
) {
|
||||
return Err(error(
|
||||
"operation_transmission",
|
||||
"Calls require synchronous transmission; asynchronous operation messages require asynchronous transmission.",
|
||||
));
|
||||
}
|
||||
let selected = ResolvedOperation {
|
||||
invocation,
|
||||
allocation,
|
||||
interface,
|
||||
item,
|
||||
caller_port,
|
||||
provider_port,
|
||||
transmission,
|
||||
};
|
||||
if occurrence != invocation {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: reply_exchange,
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = e.shared()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
if let Some(response) = reply_exchange.and_then(|id| p.relations.get(&id))
|
||||
&& response.id != channel.id
|
||||
&& !response
|
||||
.body
|
||||
.items()
|
||||
.iter()
|
||||
.any(|carried| data::item_satisfies(p, *carried, item))
|
||||
{
|
||||
return Err(error(
|
||||
"operation_reply_channel",
|
||||
"A separate reply channel must carry the Call's selected operation item or an admitted subtype, including parameterless returns.",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(OperationResolution::Selected(selected))
|
||||
}
|
||||
|
||||
/// Valid selections under the proposed message context, without mutating the model.
|
||||
pub fn eligible_allocations(p: &Project, occurrence: Id) -> Vec<Id> {
|
||||
let mut candidate = p.clone();
|
||||
let ids: Vec<_> = p
|
||||
.relations
|
||||
.values()
|
||||
.filter(|r| r.kind() == RelationKind::InterfaceItemAllocation)
|
||||
.map(|r| r.id)
|
||||
.collect();
|
||||
ids.into_iter()
|
||||
.filter(|id| {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { operation, .. },
|
||||
..
|
||||
}) = candidate
|
||||
.elements
|
||||
.get_mut(&occurrence)
|
||||
.and_then(Element::shared_mut)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
*operation = Some(OperationUse {
|
||||
allocation: Some(*id),
|
||||
});
|
||||
matches!(
|
||||
resolve(&candidate, occurrence),
|
||||
Ok(OperationResolution::Selected(_))
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Reuse the existing bounded field-path engine with exactly one selected item.
|
||||
pub fn signature_relation(operation: &ResolvedOperation) -> Relation {
|
||||
Relation {
|
||||
id: operation.allocation,
|
||||
alias: String::new(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
body: RelationBody::Interaction {
|
||||
source: operation.caller_port,
|
||||
target: operation.provider_port,
|
||||
items: vec![operation.item],
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user