175 lines
6.3 KiB
Rust
175 lines
6.3 KiB
Rust
//! Advisory presence of explicit behavior evidence, never inferred execution.
|
|
use super::*;
|
|
|
|
#[derive(Default)]
|
|
pub(super) struct Index {
|
|
pub grouping: BTreeSet<Id>,
|
|
pub supported_capabilities: BTreeSet<Id>,
|
|
contextual_capabilities: BTreeSet<Id>,
|
|
supported_missions: BTreeSet<Id>,
|
|
}
|
|
|
|
impl Index {
|
|
pub fn new(p: &Project) -> Self {
|
|
let mut index = Self::default();
|
|
for element in p.elements.values() {
|
|
if let Some(parent) = element.parent.and_then(|id| p.elements.get(&id))
|
|
&& parent.kind() == element.kind()
|
|
&& parent.layer() == element.layer()
|
|
{
|
|
index.grouping.insert(parent.id);
|
|
}
|
|
if let Some(SharedBody::Scenario {
|
|
context: Some(id), ..
|
|
}) = element.shared()
|
|
&& p.elements.get(id).is_some_and(|context| {
|
|
matches!(context.kind(), Kind::Capability | Kind::SystemCapability)
|
|
&& context.layer() == element.layer()
|
|
})
|
|
{
|
|
index.contextual_capabilities.insert(*id);
|
|
index.supported_capabilities.insert(*id);
|
|
}
|
|
}
|
|
for relation in p.relations.values() {
|
|
let (Some(source), Some(target)) = (
|
|
p.elements.get(&relation.source()),
|
|
p.elements.get(&relation.target()),
|
|
) else {
|
|
continue;
|
|
};
|
|
if source.layer() != target.layer()
|
|
|| !relation.kind().permits(source.kind(), target.kind())
|
|
{
|
|
continue;
|
|
}
|
|
match relation.kind() {
|
|
RelationKind::CapabilityInvolvement => {
|
|
index.supported_capabilities.insert(target.id);
|
|
}
|
|
RelationKind::ChainInvolvement => {
|
|
index.supported_capabilities.insert(target.id);
|
|
index.contextual_capabilities.insert(target.id);
|
|
}
|
|
RelationKind::MissionCapability => {
|
|
index.supported_missions.insert(source.id);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
index
|
|
}
|
|
|
|
pub fn validate(&self, p: &Project, out: &mut Vec<Issue>) {
|
|
let (work, exchanges) = included_evidence(p);
|
|
for element in p.elements.values() {
|
|
if element.kind().is_work()
|
|
&& !self.grouping.contains(&element.id)
|
|
&& !work.contains(&element.id)
|
|
{
|
|
warning(
|
|
out,
|
|
element.id,
|
|
"work_without_behavior_context",
|
|
"",
|
|
"Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.",
|
|
);
|
|
}
|
|
if matches!(element.kind(), Kind::OperationalMission | Kind::Mission)
|
|
&& !self.supported_missions.contains(&element.id)
|
|
{
|
|
warning(
|
|
out,
|
|
element.id,
|
|
"mission_without_capability",
|
|
"",
|
|
"Connect this mission to a capability that delivers it.",
|
|
);
|
|
}
|
|
if matches!(element.kind(), Kind::Capability | Kind::SystemCapability)
|
|
&& !self.grouping.contains(&element.id)
|
|
&& self.supported_capabilities.contains(&element.id)
|
|
&& !self.contextual_capabilities.contains(&element.id)
|
|
{
|
|
warning(
|
|
out,
|
|
element.id,
|
|
"capability_without_behavior_context",
|
|
"",
|
|
"Add an explicit behavior context: link a process/chain with Chain involvement, or set a scenario's Context to this capability.",
|
|
);
|
|
}
|
|
}
|
|
for relation in p.relations.values().filter(|r| work_exchange(r.kind())) {
|
|
let mut non_leaf = false;
|
|
for (field, endpoint) in [("source", relation.source()), ("target", relation.target())]
|
|
{
|
|
if self.grouping.contains(&endpoint) {
|
|
non_leaf = true;
|
|
warning(
|
|
out,
|
|
relation.id,
|
|
"non_leaf_work_exchange",
|
|
field,
|
|
"Choose the leaf activity/function responsible for this exchange; keep any typed port owned by that function.",
|
|
);
|
|
}
|
|
}
|
|
if !non_leaf && !exchanges.contains(&relation.id) {
|
|
warning(
|
|
out,
|
|
relation.id,
|
|
"exchange_without_behavior_context",
|
|
"",
|
|
"Use this exchange in a process/chain or scenario.",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn work_exchange(kind: RelationKind) -> bool {
|
|
matches!(
|
|
kind,
|
|
RelationKind::Interaction | RelationKind::FunctionalExchange
|
|
)
|
|
}
|
|
|
|
/// Only ordered, owned occurrences contribute. Each identity is visited once,
|
|
/// including reused containers; this remains bounded even on malformed drafts.
|
|
fn included_evidence(p: &Project) -> (BTreeSet<Id>, BTreeSet<Id>) {
|
|
let crate::view_options::BehaviorEvidence { work, exchanges } =
|
|
crate::view_options::all_behavior_evidence(p);
|
|
(work, exchanges)
|
|
}
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn malformed_self_reuse_is_bounded_even_without_structural_gate() {
|
|
let mut p = Project::empty();
|
|
let scenario = p
|
|
.add_element(Kind::Scenario, Layer::Oa, "Self reuse", None)
|
|
.unwrap();
|
|
let occurrence = p
|
|
.add_element(Kind::Occurrence, Layer::Oa, "Cycle", Some(scenario))
|
|
.unwrap();
|
|
if let Some(SharedBody::Occurrence { body, .. }) =
|
|
p.elements.get_mut(&occurrence).unwrap().shared_mut()
|
|
{
|
|
*body = OccurrenceBody::ScenarioUse {
|
|
scenario: Some(scenario),
|
|
bindings: vec![],
|
|
};
|
|
}
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(occurrence);
|
|
assert_eq!(included_evidence(&p), (BTreeSet::new(), BTreeSet::new()));
|
|
}
|
|
}
|