Skip to main content

peat_protocol/cell/
capability_aggregation.rs

1//! Cell Capability Aggregation
2//!
3//! This module implements capability aggregation across squad members following ADR-004
4//! human-machine teaming principles. It collects individual platform capabilities and
5//! composes them into emergent cell-level capabilities with human authority integration.
6//!
7//! # Key Concepts
8//!
9//! - **Capability Collection**: Gathers capabilities from all squad members
10//! - **Emergent Capabilities**: Squad-level capabilities that emerge from member composition
11//! - **Human Authority**: Integrates operator authority levels into capability confidence
12//! - **Confidence Aggregation**: Combines individual confidence scores with authority weights
13//!
14//! # Human-Machine Integration
15//!
16//! Following ADR-004, capability aggregation factors in:
17//! - Operator authority levels (Monitoring, Conditional, Full)
18//! - Human oversight requirements for critical capabilities
19//! - Hybrid confidence scoring (technical capability + human authority)
20
21use crate::models::{
22    AuthorityLevel, CapabilityExt, CapabilityType, HumanMachinePair, HumanMachinePairExt,
23    NodeConfig, NodeState, NodeStateExt,
24};
25use crate::{Error, Result};
26use serde::{Deserialize, Serialize};
27use std::collections::HashMap;
28
29/// Aggregated cell-level capability with human authority integration
30#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct AggregatedCapability {
32    /// Capability type
33    pub capability_type: CapabilityType,
34    /// Aggregated confidence score (0.0-1.0)
35    pub confidence: f32,
36    /// Number of nodes contributing this capability
37    pub contributor_count: usize,
38    /// Contributing platform IDs
39    pub contributors: Vec<String>,
40    /// Highest authority level among contributors
41    pub max_authority: Option<AuthorityLevel>,
42    /// Requires human oversight for mission-critical operations
43    pub requires_oversight: bool,
44}
45
46impl AggregatedCapability {
47    /// Create a new aggregated capability
48    pub fn new(
49        capability_type: CapabilityType,
50        confidence: f32,
51        contributors: Vec<String>,
52        max_authority: Option<AuthorityLevel>,
53    ) -> Self {
54        let contributor_count = contributors.len();
55        let requires_oversight = Self::check_oversight_requirement(capability_type, max_authority);
56
57        Self {
58            capability_type,
59            confidence,
60            contributor_count,
61            contributors,
62            max_authority,
63            requires_oversight,
64        }
65    }
66
67    /// Check if this capability type requires human oversight
68    fn check_oversight_requirement(
69        capability_type: CapabilityType,
70        max_authority: Option<AuthorityLevel>,
71    ) -> bool {
72        // Mission-critical capabilities require oversight unless Commander authority present
73        match capability_type {
74            CapabilityType::Payload => {
75                // Weapons require Commander authority or oversight
76                !matches!(max_authority, Some(AuthorityLevel::Commander))
77            }
78            CapabilityType::Communication => {
79                // Critical comms require at least Commander authority
80                matches!(max_authority, None | Some(AuthorityLevel::Observer))
81            }
82            _ => false, // Other capabilities don't require oversight by default
83        }
84    }
85
86    /// Check if this capability is mission-ready (high confidence and appropriate authority)
87    pub fn is_mission_ready(&self) -> bool {
88        let confidence_threshold = if self.requires_oversight { 0.8 } else { 0.7 };
89
90        self.confidence >= confidence_threshold
91            && self.contributor_count > 0
92            && (!self.requires_oversight || self.max_authority.is_some())
93    }
94
95    /// Get effective confidence factoring in authority and oversight
96    pub fn effective_confidence(&self) -> f32 {
97        let mut confidence = self.confidence;
98
99        // Reduce confidence if oversight required but no high authority present
100        if self.requires_oversight {
101            match self.max_authority {
102                Some(AuthorityLevel::Commander) => confidence *= 1.0, // No penalty
103                Some(AuthorityLevel::Supervisor) => confidence *= 0.85, // Slight penalty
104                Some(AuthorityLevel::Advisor) => confidence *= 0.7,   // Moderate penalty
105                Some(AuthorityLevel::Observer) => confidence *= 0.6,  // Significant penalty
106                Some(AuthorityLevel::Unspecified) => confidence *= 0.5, // Major penalty
107                None => confidence *= 0.5, // Major penalty for autonomous-only
108            }
109        }
110
111        confidence.min(1.0)
112    }
113}
114
115/// Cell capability aggregator
116pub struct CapabilityAggregator;
117
118impl CapabilityAggregator {
119    /// Aggregate capabilities from a list of squad members
120    ///
121    /// # Arguments
122    /// * `members` - List of (NodeConfig, NodeState) tuples for each squad member
123    ///
124    /// # Returns
125    /// HashMap of CapabilityType to AggregatedCapability
126    pub fn aggregate_capabilities(
127        members: &[(NodeConfig, NodeState)],
128    ) -> Result<HashMap<CapabilityType, AggregatedCapability>> {
129        let mut capability_map: HashMap<
130            CapabilityType,
131            Vec<(String, f32, Option<AuthorityLevel>)>,
132        > = HashMap::new();
133
134        // Collect capabilities from all members
135        for (config, state) in members {
136            // Skip if platform is not operational
137            if !state.is_operational() {
138                continue;
139            }
140
141            // Get authority level from operator binding
142            let authority = config
143                .operator_binding
144                .as_ref()
145                .and_then(Self::get_max_authority);
146
147            // Add each capability to the map
148            for cap in &config.capabilities {
149                capability_map
150                    .entry(cap.get_capability_type())
151                    .or_default()
152                    .push((config.id.clone(), cap.confidence, authority));
153            }
154        }
155
156        // Aggregate each capability type
157        let mut aggregated = HashMap::new();
158        for (cap_type, contributors) in capability_map {
159            let agg_cap = Self::aggregate_capability_type(cap_type, contributors)?;
160            aggregated.insert(cap_type, agg_cap);
161        }
162
163        Ok(aggregated)
164    }
165
166    /// Aggregate a single capability type from multiple contributors
167    fn aggregate_capability_type(
168        capability_type: CapabilityType,
169        contributors: Vec<(String, f32, Option<AuthorityLevel>)>,
170    ) -> Result<AggregatedCapability> {
171        if contributors.is_empty() {
172            return Err(Error::config_error(
173                "Cannot aggregate capability with no contributors",
174                None,
175            ));
176        }
177
178        // Calculate aggregated confidence
179        // Strategy: Take weighted average with redundancy bonus
180        let avg_confidence: f32 =
181            contributors.iter().map(|(_, conf, _)| conf).sum::<f32>() / contributors.len() as f32;
182
183        // Redundancy bonus: more contributors = higher confidence
184        let redundancy_bonus = match contributors.len() {
185            1 => 0.0,
186            2 => 0.05,
187            3..=4 => 0.10,
188            _ => 0.15, // Cap at 0.15 bonus for 5+ contributors
189        };
190
191        let base_confidence = (avg_confidence + redundancy_bonus).min(1.0);
192
193        // Authority bonus: higher authority increases confidence
194        let max_authority = contributors.iter().filter_map(|(_, _, auth)| *auth).max();
195
196        let authority_bonus = match max_authority {
197            Some(AuthorityLevel::Commander) => 0.10,
198            Some(AuthorityLevel::Supervisor) => 0.05,
199            Some(AuthorityLevel::Advisor) => 0.03,
200            Some(AuthorityLevel::Observer) => 0.0,
201            Some(AuthorityLevel::Unspecified) => 0.0,
202            None => 0.0,
203        };
204
205        let final_confidence = (base_confidence + authority_bonus).min(1.0);
206
207        let contributor_ids: Vec<String> = contributors.into_iter().map(|(id, _, _)| id).collect();
208
209        Ok(AggregatedCapability::new(
210            capability_type,
211            final_confidence,
212            contributor_ids,
213            max_authority,
214        ))
215    }
216
217    /// Get the maximum authority level from a human-machine pair
218    fn get_max_authority(binding: &HumanMachinePair) -> Option<AuthorityLevel> {
219        binding.max_authority()
220    }
221
222    /// Calculate squad readiness score based on aggregated capabilities
223    ///
224    /// Returns a score from 0.0-1.0 indicating overall squad capability readiness
225    pub fn calculate_readiness_score(
226        capabilities: &HashMap<CapabilityType, AggregatedCapability>,
227    ) -> f32 {
228        if capabilities.is_empty() {
229            return 0.0;
230        }
231
232        // Weight different capability types
233        let weights: HashMap<CapabilityType, f32> = [
234            (CapabilityType::Communication, 0.30), // Critical for coordination
235            (CapabilityType::Sensor, 0.25),        // Important for awareness
236            (CapabilityType::Compute, 0.20),       // Important for processing
237            (CapabilityType::Payload, 0.15),       // Important for mission execution
238            (CapabilityType::Mobility, 0.10),      // Important for positioning
239        ]
240        .into_iter()
241        .collect();
242
243        let mut total_score = 0.0;
244        let mut total_weight = 0.0;
245
246        for (cap_type, agg_cap) in capabilities {
247            let weight = weights.get(cap_type).copied().unwrap_or(0.05);
248            let score = agg_cap.effective_confidence();
249            total_score += score * weight;
250            total_weight += weight;
251        }
252
253        if total_weight > 0.0 {
254            total_score / total_weight
255        } else {
256            0.0
257        }
258    }
259
260    /// Identify capability gaps in the squad
261    ///
262    /// Returns a list of missing or weak capability types
263    pub fn identify_gaps(
264        capabilities: &HashMap<CapabilityType, AggregatedCapability>,
265        required_capabilities: &[CapabilityType],
266    ) -> Vec<CapabilityType> {
267        let mut gaps = Vec::new();
268
269        for &cap_type in required_capabilities {
270            match capabilities.get(&cap_type) {
271                None => gaps.push(cap_type), // Missing entirely
272                Some(agg_cap) if !agg_cap.is_mission_ready() => gaps.push(cap_type), // Present but weak
273                _ => {}                                                              // Adequate
274            }
275        }
276
277        gaps
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::models::{
285        Capability, HealthStatus, HumanMachinePairExt, NodeConfigExt, NodeStateExt, Operator,
286        OperatorExt, OperatorRank,
287    };
288
289    fn create_test_platform(
290        id: &str,
291        capabilities: Vec<(CapabilityType, f32)>,
292        operator: Option<Operator>,
293    ) -> (NodeConfig, NodeState) {
294        let mut config = NodeConfig::new("Test".to_string());
295        config.id = id.to_string();
296
297        for (cap_type, confidence) in capabilities {
298            config.add_capability(Capability::new(
299                format!("{}_{:?}", id, cap_type),
300                format!("{:?}", cap_type),
301                cap_type,
302                confidence,
303            ));
304        }
305
306        if let Some(op) = operator {
307            let binding = HumanMachinePair::new(
308                vec![op],
309                vec![id.to_string()],
310                crate::models::BindingType::OneToOne,
311            );
312            config.operator_binding = Some(binding);
313        }
314
315        let state = NodeState::new((0.0, 0.0, 0.0));
316
317        (config, state)
318    }
319
320    #[test]
321    fn test_aggregate_single_platform() {
322        let platform = create_test_platform(
323            "p1",
324            vec![
325                (CapabilityType::Sensor, 0.8),
326                (CapabilityType::Communication, 0.9),
327            ],
328            None,
329        );
330
331        let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
332
333        assert_eq!(result.len(), 2);
334        assert!(result.contains_key(&CapabilityType::Sensor));
335        assert!(result.contains_key(&CapabilityType::Communication));
336
337        let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
338        assert_eq!(sensor_cap.contributor_count, 1);
339        assert_eq!(sensor_cap.confidence, 0.8); // No redundancy bonus for single contributor
340    }
341
342    #[test]
343    fn test_aggregate_multiple_platforms_redundancy() {
344        let p1 = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.7)], None);
345        let p2 = create_test_platform("p2", vec![(CapabilityType::Sensor, 0.8)], None);
346        let p3 = create_test_platform("p3", vec![(CapabilityType::Sensor, 0.75)], None);
347
348        let result = CapabilityAggregator::aggregate_capabilities(&[p1, p2, p3]).unwrap();
349
350        let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
351        assert_eq!(sensor_cap.contributor_count, 3);
352
353        // Average: (0.7 + 0.8 + 0.75) / 3 = 0.75
354        // Redundancy bonus for 3 contributors: 0.10
355        // Expected: 0.85
356        assert!((sensor_cap.confidence - 0.85).abs() < 0.01);
357    }
358
359    #[test]
360    fn test_authority_integration() {
361        let operator = Operator::new(
362            "op1".to_string(),
363            "John Doe".to_string(),
364            OperatorRank::E5,
365            AuthorityLevel::Commander,
366            "19D".to_string(),
367        );
368
369        let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.7)], Some(operator));
370
371        let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
372
373        let payload_cap = result.get(&CapabilityType::Payload).unwrap();
374        assert_eq!(payload_cap.max_authority, Some(AuthorityLevel::Commander));
375
376        // Base: 0.7, Authority bonus: 0.10 = 0.80
377        assert!((payload_cap.confidence - 0.80).abs() < 0.01);
378    }
379
380    #[test]
381    fn test_oversight_requirements() {
382        // Payload capability without operator - requires oversight
383        let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.9)], None);
384        let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
385        let payload_cap = result.get(&CapabilityType::Payload).unwrap();
386        assert!(payload_cap.requires_oversight);
387
388        // Payload capability with DirectControl authority - no oversight required
389        let operator = Operator::new(
390            "op1".to_string(),
391            "Jane Smith".to_string(),
392            OperatorRank::E6,
393            AuthorityLevel::Commander,
394            "11B".to_string(),
395        );
396        let p2 = create_test_platform("p2", vec![(CapabilityType::Payload, 0.9)], Some(operator));
397        let result2 = CapabilityAggregator::aggregate_capabilities(&[p2]).unwrap();
398        let payload_cap2 = result2.get(&CapabilityType::Payload).unwrap();
399        assert!(!payload_cap2.requires_oversight);
400    }
401
402    #[test]
403    fn test_mission_readiness() {
404        let operator = Operator::new(
405            "op1".to_string(),
406            "Bob Johnson".to_string(),
407            OperatorRank::E5,
408            AuthorityLevel::Commander,
409            "11B".to_string(),
410        );
411
412        let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.85)], Some(operator));
413        let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
414        let payload_cap = result.get(&CapabilityType::Payload).unwrap();
415
416        // High confidence + DirectControl authority = mission ready
417        assert!(payload_cap.is_mission_ready());
418    }
419
420    #[test]
421    fn test_effective_confidence_with_authority() {
422        // Observer authority on Payload capability - reduced confidence
423        let operator = Operator::new(
424            "op1".to_string(),
425            "Alice Brown".to_string(),
426            OperatorRank::E4,
427            AuthorityLevel::Observer,
428            "11B".to_string(),
429        );
430
431        let p1 = create_test_platform("p1", vec![(CapabilityType::Payload, 0.9)], Some(operator));
432        let result = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
433        let payload_cap = result.get(&CapabilityType::Payload).unwrap();
434
435        // Base: 0.9, but reduced by 0.6x for Observer authority on oversight-required capability
436        let effective = payload_cap.effective_confidence();
437        assert!(effective < 0.9);
438        assert!((effective - 0.54).abs() < 0.01); // 0.9 * 0.6
439    }
440
441    #[test]
442    fn test_readiness_score() {
443        let operator = Operator::new(
444            "op1".to_string(),
445            "Charlie Davis".to_string(),
446            OperatorRank::E5,
447            AuthorityLevel::Commander,
448            "11B".to_string(),
449        );
450
451        let p1 = create_test_platform(
452            "p1",
453            vec![
454                (CapabilityType::Communication, 0.9),
455                (CapabilityType::Sensor, 0.8),
456            ],
457            Some(operator.clone()),
458        );
459
460        let p2 = create_test_platform(
461            "p2",
462            vec![
463                (CapabilityType::Compute, 0.85),
464                (CapabilityType::Payload, 0.8),
465            ],
466            Some(operator),
467        );
468
469        let capabilities = CapabilityAggregator::aggregate_capabilities(&[p1, p2]).unwrap();
470        let score = CapabilityAggregator::calculate_readiness_score(&capabilities);
471
472        // Should be high with good capabilities across multiple types
473        assert!(score > 0.7);
474        assert!(score <= 1.0);
475    }
476
477    #[test]
478    fn test_identify_gaps() {
479        let p1 = create_test_platform(
480            "p1",
481            vec![
482                (CapabilityType::Sensor, 0.8),
483                (CapabilityType::Communication, 0.9),
484            ],
485            None,
486        );
487
488        let capabilities = CapabilityAggregator::aggregate_capabilities(&[p1]).unwrap();
489
490        let required = vec![
491            CapabilityType::Sensor,
492            CapabilityType::Communication,
493            CapabilityType::Payload,
494            CapabilityType::Compute,
495        ];
496
497        let gaps = CapabilityAggregator::identify_gaps(&capabilities, &required);
498
499        // Communication without operator requires oversight and is not mission-ready
500        // Payload and Compute are missing entirely
501        assert_eq!(gaps.len(), 3);
502        assert!(gaps.contains(&CapabilityType::Communication));
503        assert!(gaps.contains(&CapabilityType::Payload));
504        assert!(gaps.contains(&CapabilityType::Compute));
505    }
506
507    #[test]
508    fn test_skip_non_operational_platforms() {
509        let mut platform = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.9)], None);
510
511        // Set platform to degraded state
512        platform.1.health = HealthStatus::Degraded as i32;
513
514        let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
515
516        // Should still include degraded nodes (they're operational)
517        assert_eq!(result.len(), 1);
518
519        // Critical nodes are still operational (only Failed is non-operational)
520        let mut platform2 = create_test_platform("p2", vec![(CapabilityType::Sensor, 0.9)], None);
521        platform2.1.health = HealthStatus::Critical as i32;
522
523        let result2 = CapabilityAggregator::aggregate_capabilities(&[platform2]).unwrap();
524
525        // Critical is still operational
526        assert_eq!(result2.len(), 1);
527
528        // Now test with Failed status (truly non-operational)
529        let mut platform3 = create_test_platform("p3", vec![(CapabilityType::Sensor, 0.9)], None);
530        platform3.1.health = HealthStatus::Failed as i32;
531
532        let result3 = CapabilityAggregator::aggregate_capabilities(&[platform3]).unwrap();
533
534        // Should exclude failed platforms
535        assert_eq!(result3.len(), 0);
536    }
537
538    #[test]
539    fn test_empty_squad_aggregation() {
540        // Edge case: Empty squad should return empty capabilities
541        let result = CapabilityAggregator::aggregate_capabilities(&[]).unwrap();
542        assert_eq!(result.len(), 0);
543
544        // Readiness score for empty squad should be 0
545        let readiness = CapabilityAggregator::calculate_readiness_score(&result);
546        assert_eq!(readiness, 0.0);
547
548        // Gap identification should show all required capabilities as missing
549        let required = vec![CapabilityType::Communication, CapabilityType::Sensor];
550        let gaps = CapabilityAggregator::identify_gaps(&result, &required);
551        assert_eq!(gaps.len(), 2);
552        assert!(gaps.contains(&CapabilityType::Communication));
553        assert!(gaps.contains(&CapabilityType::Sensor));
554    }
555
556    #[test]
557    fn test_all_platforms_non_operational() {
558        // Edge case: All nodes failed/non-operational
559        let mut platform1 = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.9)], None);
560        platform1.1.health = HealthStatus::Failed as i32;
561
562        let mut platform2 =
563            create_test_platform("p2", vec![(CapabilityType::Communication, 0.8)], None);
564        platform2.1.health = HealthStatus::Failed as i32;
565
566        let result = CapabilityAggregator::aggregate_capabilities(&[platform1, platform2]).unwrap();
567
568        // All nodes excluded, should be empty
569        assert_eq!(result.len(), 0);
570    }
571
572    #[test]
573    fn test_zero_confidence_capability() {
574        // Edge case: Capability with 0.0 confidence
575        let platform = create_test_platform("p1", vec![(CapabilityType::Sensor, 0.0)], None);
576
577        let result = CapabilityAggregator::aggregate_capabilities(&[platform]).unwrap();
578
579        // Should still aggregate, but with low confidence
580        assert_eq!(result.len(), 1);
581        let sensor_cap = result.get(&CapabilityType::Sensor).unwrap();
582        assert!(sensor_cap.confidence < 0.1);
583    }
584}