//! Advisory presence of explicit behavior evidence, never inferred execution. use super::*; #[derive(Default)] pub(super) struct Index { pub grouping: BTreeSet, pub supported_capabilities: BTreeSet, contextual_capabilities: BTreeSet, supported_missions: BTreeSet, } 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) { 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, BTreeSet) { 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())); } }