Skip to main content

peat_protocol/composition/
rules.rs

1//! Composition rule trait and implementations
2//!
3//! This module defines the core abstraction for capability composition rules.
4//! Rules analyze sets of capabilities and detect composed/emergent capabilities.
5
6use crate::models::capability::Capability;
7use crate::Result;
8use async_trait::async_trait;
9
10use crate::models::{AuthorityLevel, NodeConfig};
11
12/// Context for rule execution
13///
14/// Provides additional information that rules may need for composition decisions.
15#[derive(Debug, Clone)]
16pub struct CompositionContext {
17    /// Node IDs contributing capabilities
18    pub node_ids: Vec<String>,
19
20    /// Cell or squad ID if composing within a cell
21    pub cell_id: Option<String>,
22
23    /// Timestamp of composition (for temporal analysis)
24    pub timestamp: std::time::SystemTime,
25
26    /// Node configurations (for operator/authority checks)
27    pub node_configs: Vec<NodeConfig>,
28}
29
30impl CompositionContext {
31    /// Create a new composition context
32    pub fn new(node_ids: Vec<String>) -> Self {
33        Self {
34            node_ids,
35            cell_id: None,
36            timestamp: std::time::SystemTime::now(),
37            node_configs: Vec::new(),
38        }
39    }
40
41    /// Set the cell ID for this composition
42    pub fn with_cell_id(mut self, cell_id: String) -> Self {
43        self.cell_id = Some(cell_id);
44        self
45    }
46
47    /// Add node configurations for operator/authority checks
48    pub fn with_node_configs(mut self, configs: Vec<NodeConfig>) -> Self {
49        self.node_configs = configs;
50        self
51    }
52
53    /// Get the maximum authority level among all operators in the context
54    pub fn max_authority(&self) -> Option<AuthorityLevel> {
55        use crate::models::HumanMachinePairExt;
56
57        self.node_configs
58            .iter()
59            .filter_map(|config| config.operator_binding.as_ref())
60            .filter_map(|binding| binding.max_authority())
61            .max()
62    }
63
64    /// Check if any node has an operator with Commander authority
65    pub fn has_commander(&self) -> bool {
66        self.max_authority() == Some(AuthorityLevel::Commander)
67    }
68
69    /// Get authorization bonus (0-5 scale) based on max authority
70    pub fn authorization_bonus(&self) -> i32 {
71        use crate::models::AuthorityLevelExt;
72
73        match self.max_authority() {
74            Some(auth) => (auth.to_score() * 5.0).round() as i32,
75            None => 0,
76        }
77    }
78}
79
80impl Default for CompositionContext {
81    fn default() -> Self {
82        Self {
83            node_ids: Vec::new(),
84            cell_id: None,
85            timestamp: std::time::SystemTime::now(),
86            node_configs: Vec::new(),
87        }
88    }
89}
90
91/// Result of applying a composition rule
92#[derive(Debug, Clone)]
93pub struct CompositionResult {
94    /// Composed capabilities detected by the rule
95    pub composed_capabilities: Vec<Capability>,
96
97    /// Confidence in the composition (0.0 - 1.0)
98    pub confidence: f32,
99
100    /// Input capabilities that contributed to this composition
101    pub contributing_capabilities: Vec<String>, // capability IDs
102}
103
104impl CompositionResult {
105    /// Create a new composition result
106    pub fn new(composed_capabilities: Vec<Capability>, confidence: f32) -> Self {
107        Self {
108            composed_capabilities,
109            confidence,
110            contributing_capabilities: Vec::new(),
111        }
112    }
113
114    /// Add contributing capability IDs
115    pub fn with_contributors(mut self, capability_ids: Vec<String>) -> Self {
116        self.contributing_capabilities = capability_ids;
117        self
118    }
119
120    /// Check if any capabilities were composed
121    pub fn has_compositions(&self) -> bool {
122        !self.composed_capabilities.is_empty()
123    }
124}
125
126/// Trait for capability composition rules
127///
128/// Composition rules analyze a set of capabilities and detect:
129/// - Additive compositions (summed capabilities)
130/// - Emergent compositions (new capabilities from combinations)
131/// - Redundant compositions (reliability from redundancy)
132/// - Constraint compositions (team limits from individual constraints)
133#[async_trait]
134pub trait CompositionRule: Send + Sync {
135    /// Human-readable name for this rule
136    fn name(&self) -> &str;
137
138    /// Description of what this rule detects
139    fn description(&self) -> &str;
140
141    /// Check if this rule applies to the given set of capabilities
142    ///
143    /// Rules should return true if they can meaningfully compose
144    /// any of the provided capabilities.
145    fn applies_to(&self, capabilities: &[Capability]) -> bool;
146
147    /// Apply the composition rule to a set of capabilities
148    ///
149    /// Returns composed capabilities with confidence scores and
150    /// references to contributing capabilities.
151    async fn compose(
152        &self,
153        capabilities: &[Capability],
154        context: &CompositionContext,
155    ) -> Result<CompositionResult>;
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::models::capability::{CapabilityExt, CapabilityType};
162
163    #[test]
164    fn test_composition_context_creation() {
165        let node_ids = vec!["node1".to_string(), "node2".to_string()];
166        let ctx = CompositionContext::new(node_ids.clone());
167
168        assert_eq!(ctx.node_ids, node_ids);
169        assert_eq!(ctx.cell_id, None);
170    }
171
172    #[test]
173    fn test_composition_context_with_cell() {
174        let ctx = CompositionContext::new(vec!["node1".to_string()])
175            .with_cell_id("cell_alpha".to_string());
176
177        assert_eq!(ctx.cell_id, Some("cell_alpha".to_string()));
178    }
179
180    #[test]
181    fn test_composition_result_creation() {
182        let capability = Capability::new(
183            "test".to_string(),
184            "Test Capability".to_string(),
185            CapabilityType::Emergent,
186            0.9,
187        );
188
189        let result = CompositionResult::new(vec![capability], 0.8);
190
191        assert_eq!(result.composed_capabilities.len(), 1);
192        assert_eq!(result.confidence, 0.8);
193        assert!(result.has_compositions());
194    }
195
196    #[test]
197    fn test_composition_result_with_contributors() {
198        let capability = Capability::new(
199            "emergent".to_string(),
200            "Emergent".to_string(),
201            CapabilityType::Emergent,
202            0.9,
203        );
204
205        let result = CompositionResult::new(vec![capability], 0.8)
206            .with_contributors(vec!["cap1".to_string(), "cap2".to_string()]);
207
208        assert_eq!(result.contributing_capabilities.len(), 2);
209        assert!(result
210            .contributing_capabilities
211            .contains(&"cap1".to_string()));
212    }
213
214    #[test]
215    fn test_empty_composition_result() {
216        let result = CompositionResult::new(vec![], 0.0);
217
218        assert!(!result.has_compositions());
219        assert_eq!(result.composed_capabilities.len(), 0);
220    }
221
222    #[test]
223    fn test_composition_context_with_node_configs() {
224        use crate::models::{NodeConfig, NodeConfigExt};
225
226        let config = NodeConfig::new("UAV".to_string());
227        let ctx =
228            CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
229
230        assert_eq!(ctx.node_configs.len(), 1);
231    }
232
233    #[test]
234    fn test_composition_context_max_authority_none() {
235        let ctx = CompositionContext::new(vec!["node1".to_string()]);
236
237        assert!(ctx.max_authority().is_none());
238        assert!(!ctx.has_commander());
239        assert_eq!(ctx.authorization_bonus(), 0);
240    }
241
242    #[test]
243    fn test_composition_context_max_authority_with_commander() {
244        use crate::models::{
245            HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt, Operator,
246            OperatorExt, OperatorRank,
247        };
248
249        let operator = Operator::new(
250            "op1".to_string(),
251            "CPT Smith".to_string(),
252            OperatorRank::O3,
253            AuthorityLevel::Commander,
254            "11A".to_string(),
255        );
256
257        let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
258        let config = NodeConfig::with_operator("Command Post".to_string(), binding);
259
260        let ctx =
261            CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
262
263        assert_eq!(ctx.max_authority(), Some(AuthorityLevel::Commander));
264        assert!(ctx.has_commander());
265        assert_eq!(ctx.authorization_bonus(), 4); // 0.8 * 5 = 4
266    }
267
268    #[test]
269    fn test_composition_context_authorization_bonus_levels() {
270        use crate::models::{
271            HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt, Operator,
272            OperatorExt, OperatorRank,
273        };
274
275        // Test with Supervisor authority (0.5 * 5 = 2.5 rounds to 2 or 3)
276        let operator = Operator::new(
277            "op1".to_string(),
278            "SGT Jones".to_string(),
279            OperatorRank::E5,
280            AuthorityLevel::Supervisor,
281            "11B".to_string(),
282        );
283
284        let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
285        let config = NodeConfig::with_operator("Control Station".to_string(), binding);
286
287        let ctx =
288            CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
289
290        assert_eq!(ctx.max_authority(), Some(AuthorityLevel::Supervisor));
291        assert!(!ctx.has_commander());
292        // 0.5 * 5 = 2.5, rounds to 2 or 3
293        let bonus = ctx.authorization_bonus();
294        assert!((2..=3).contains(&bonus));
295    }
296
297    #[test]
298    fn test_composition_context_default() {
299        let ctx = CompositionContext::default();
300
301        assert!(ctx.node_ids.is_empty());
302        assert!(ctx.cell_id.is_none());
303        assert!(ctx.node_configs.is_empty());
304    }
305}