Files
ARCADIA-Studio/crates/oa-core/src/operations.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
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.
2026-09-10 10:06:55 -07:00

346 lines
11 KiB
Rust

//! 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],
},
}
}