Skip to main content

peat_protocol/cell/
coordinator.rs

1//! Cell Formation Coordinator (E4.5)
2//!
3//! Coordinates cell formation completion by integrating role assignment (E4.3) and
4//! capability aggregation (E4.4). Detects when formation is complete and manages
5//! phase transitions with human approval workflow following ADR-004.
6//!
7//! # Formation Completion Criteria
8//!
9//! A cell formation is considered complete when:
10//! 1. Minimum squad size met (configurable, default 3)
11//! 2. Leader elected and confirmed
12//! 3. All members have assigned roles
13//! 4. Minimum capability coverage achieved (Communication + Sensor required)
14//! 5. Cell readiness score above threshold (default 0.7)
15//! 6. Human approval obtained (if required by authority levels)
16//!
17//! # Phase Transition Workflow
18//!
19//! SquadFormation -> OperationalReady (with human approval if needed):
20//! - Check formation completion criteria
21//! - Calculate squad readiness score
22//! - Request human approval if any mission-critical capabilities lack DirectControl authority
23//! - Transition to OperationalReady once approved
24
25use crate::cell::{AggregatedCapability, CapabilityAggregator};
26use crate::models::{CellRole, NodeConfig, NodeState};
27use crate::traits::Phase;
28use crate::{Error, Result};
29use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31use std::time::{SystemTime, UNIX_EPOCH};
32
33/// Cell formation status
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub enum FormationStatus {
36    /// Formation in progress
37    Forming,
38    /// Formation complete, awaiting human approval
39    AwaitingApproval,
40    /// Formation complete and approved, ready for operations
41    Ready,
42    /// Formation failed or incomplete
43    Failed(String),
44}
45
46/// Cell formation coordinator
47pub struct CellCoordinator {
48    /// Cell ID
49    pub squad_id: String,
50    /// Minimum squad size
51    pub min_size: usize,
52    /// Minimum readiness score (0.0-1.0)
53    pub min_readiness: f32,
54    /// Required capability types for formation
55    pub required_capabilities: Vec<crate::models::CapabilityType>,
56    /// Formation status
57    pub status: FormationStatus,
58    /// Human approval received
59    pub human_approved: bool,
60    /// Formation start timestamp
61    pub formation_start: u64,
62    /// Formation completion timestamp
63    pub formation_complete: Option<u64>,
64}
65
66impl CellCoordinator {
67    /// Create a new squad coordinator
68    pub fn new(squad_id: String) -> Self {
69        let now = SystemTime::now()
70            .duration_since(UNIX_EPOCH)
71            .unwrap()
72            .as_secs();
73
74        Self {
75            squad_id,
76            min_size: 3,
77            min_readiness: 0.7,
78            required_capabilities: vec![
79                crate::models::CapabilityType::Communication,
80                crate::models::CapabilityType::Sensor,
81            ],
82            status: FormationStatus::Forming,
83            human_approved: false,
84            formation_start: now,
85            formation_complete: None,
86        }
87    }
88
89    /// Check if formation is complete
90    ///
91    /// # Arguments
92    /// * `members` - Cell members (config, state, role)
93    /// * `leader_id` - Optional ID of elected leader
94    ///
95    /// # Returns
96    /// True if formation meets all criteria
97    pub fn check_formation_complete(
98        &mut self,
99        members: &[(NodeConfig, NodeState, Option<CellRole>)],
100        leader_id: Option<&str>,
101    ) -> Result<bool> {
102        // Criterion 1: Minimum size
103        if members.len() < self.min_size {
104            self.status = FormationStatus::Failed(format!(
105                "Insufficient members: {} < {}",
106                members.len(),
107                self.min_size
108            ));
109            return Ok(false);
110        }
111
112        // Criterion 2: Leader elected
113        if leader_id.is_none() {
114            return Ok(false); // Still forming, no failure
115        }
116
117        // Criterion 3: All members have roles
118        let unassigned = members.iter().filter(|(_, _, role)| role.is_none()).count();
119        if unassigned > 0 {
120            return Ok(false); // Still assigning roles
121        }
122
123        // Criterion 4 & 5: Capability coverage and readiness
124        let members_for_agg: Vec<(NodeConfig, NodeState)> = members
125            .iter()
126            .map(|(c, s, _)| (c.clone(), s.clone()))
127            .collect();
128
129        let capabilities = CapabilityAggregator::aggregate_capabilities(&members_for_agg)?;
130
131        // Check required capabilities
132        let gaps = CapabilityAggregator::identify_gaps(&capabilities, &self.required_capabilities);
133        if !gaps.is_empty() {
134            self.status =
135                FormationStatus::Failed(format!("Missing required capabilities: {:?}", gaps));
136            return Ok(false);
137        }
138
139        // Check readiness score
140        let readiness = CapabilityAggregator::calculate_readiness_score(&capabilities);
141        if readiness < self.min_readiness {
142            self.status = FormationStatus::Failed(format!(
143                "Insufficient readiness: {:.2} < {:.2}",
144                readiness, self.min_readiness
145            ));
146            return Ok(false);
147        }
148
149        // Formation criteria met - check if human approval needed
150        let needs_approval = self.needs_human_approval(&capabilities);
151
152        if needs_approval && !self.human_approved {
153            self.status = FormationStatus::AwaitingApproval;
154            return Ok(false); // Complete but awaiting approval
155        }
156
157        // All criteria met
158        self.status = FormationStatus::Ready;
159        if self.formation_complete.is_none() {
160            self.formation_complete = Some(
161                SystemTime::now()
162                    .duration_since(UNIX_EPOCH)
163                    .unwrap()
164                    .as_secs(),
165            );
166        }
167
168        Ok(true)
169    }
170
171    /// Check if human approval is needed for formation
172    ///
173    /// Approval required if any oversight-required capabilities are present
174    fn needs_human_approval(
175        &self,
176        capabilities: &HashMap<crate::models::CapabilityType, AggregatedCapability>,
177    ) -> bool {
178        capabilities.values().any(|cap| cap.requires_oversight)
179    }
180
181    /// Approve formation (human operator decision)
182    pub fn approve_formation(&mut self) -> Result<()> {
183        if self.status != FormationStatus::AwaitingApproval {
184            return Err(Error::InvalidTransition {
185                from: format!("{:?}", self.status),
186                to: "Ready".to_string(),
187                reason: "Cannot approve formation not awaiting approval".to_string(),
188            });
189        }
190
191        self.human_approved = true;
192        self.status = FormationStatus::Ready;
193
194        if self.formation_complete.is_none() {
195            self.formation_complete = Some(
196                SystemTime::now()
197                    .duration_since(UNIX_EPOCH)
198                    .unwrap()
199                    .as_secs(),
200            );
201        }
202
203        Ok(())
204    }
205
206    /// Reject formation (human operator decision)
207    pub fn reject_formation(&mut self, reason: String) -> Result<()> {
208        if self.status != FormationStatus::AwaitingApproval {
209            return Err(Error::InvalidTransition {
210                from: format!("{:?}", self.status),
211                to: "Failed".to_string(),
212                reason: "Cannot reject formation not awaiting approval".to_string(),
213            });
214        }
215
216        self.status = FormationStatus::Failed(format!("Human rejected: {}", reason));
217        Ok(())
218    }
219
220    /// Get formation duration in seconds
221    pub fn formation_duration(&self) -> u64 {
222        let end = self.formation_complete.unwrap_or_else(|| {
223            SystemTime::now()
224                .duration_since(UNIX_EPOCH)
225                .unwrap()
226                .as_secs()
227        });
228
229        end - self.formation_start
230    }
231
232    /// Check if squad can transition to Hierarchical phase
233    pub fn can_transition_to_hierarchical(&self) -> bool {
234        self.status == FormationStatus::Ready
235    }
236
237    /// Get transition to Hierarchical phase
238    pub fn get_hierarchical_phase(&self) -> Result<Phase> {
239        if !self.can_transition_to_hierarchical() {
240            return Err(Error::InvalidTransition {
241                from: "Squad".to_string(),
242                to: "Hierarchical".to_string(),
243                reason: format!("Cannot transition with status: {:?}", self.status),
244            });
245        }
246
247        Ok(Phase::Hierarchy)
248    }
249
250    /// Reset formation (for retry scenarios)
251    pub fn reset(&mut self) {
252        self.status = FormationStatus::Forming;
253        self.human_approved = false;
254        self.formation_complete = None;
255        self.formation_start = SystemTime::now()
256            .duration_since(UNIX_EPOCH)
257            .unwrap()
258            .as_secs();
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::models::{
266        AuthorityLevel, Capability, CapabilityExt, CapabilityType, HumanMachinePair,
267        HumanMachinePairExt, NodeConfigExt, NodeStateExt, Operator, OperatorExt, OperatorRank,
268    };
269
270    fn create_test_member(
271        id: &str,
272        capabilities: Vec<CapabilityType>,
273        role: Option<CellRole>,
274        operator: Option<Operator>,
275    ) -> (NodeConfig, NodeState, Option<CellRole>) {
276        let mut config = NodeConfig::new("Test".to_string());
277        config.id = id.to_string();
278
279        for cap_type in capabilities {
280            config.add_capability(Capability::new(
281                format!("{}_{:?}", id, cap_type),
282                format!("{:?}", cap_type),
283                cap_type,
284                0.9,
285            ));
286        }
287
288        if let Some(op) = operator {
289            let binding = HumanMachinePair::new(
290                vec![op],
291                vec![id.to_string()],
292                crate::models::BindingType::OneToOne,
293            );
294            config.operator_binding = Some(binding);
295        }
296
297        let state = NodeState::new((0.0, 0.0, 0.0));
298
299        (config, state, role)
300    }
301
302    #[test]
303    fn test_coordinator_creation() {
304        let coord = CellCoordinator::new("cell1".to_string());
305        assert_eq!(coord.squad_id, "cell1");
306        assert_eq!(coord.status, FormationStatus::Forming);
307        assert_eq!(coord.min_size, 3);
308        assert!(!coord.human_approved);
309    }
310
311    #[test]
312    fn test_insufficient_members() {
313        let mut coord = CellCoordinator::new("cell1".to_string());
314
315        let members = vec![
316            create_test_member(
317                "p1",
318                vec![CapabilityType::Communication, CapabilityType::Sensor],
319                Some(CellRole::Leader),
320                None,
321            ),
322            create_test_member(
323                "p2",
324                vec![CapabilityType::Sensor],
325                Some(CellRole::Sensor),
326                None,
327            ),
328        ];
329
330        let complete = coord
331            .check_formation_complete(&members, Some("p1"))
332            .unwrap();
333
334        assert!(!complete);
335        assert!(matches!(
336            coord.status,
337            FormationStatus::Failed(ref msg) if msg.contains("Insufficient members")
338        ));
339    }
340
341    #[test]
342    fn test_no_leader() {
343        let mut coord = CellCoordinator::new("cell1".to_string());
344
345        let members = vec![
346            create_test_member(
347                "p1",
348                vec![CapabilityType::Communication],
349                Some(CellRole::Follower),
350                None,
351            ),
352            create_test_member(
353                "p2",
354                vec![CapabilityType::Sensor],
355                Some(CellRole::Sensor),
356                None,
357            ),
358            create_test_member(
359                "p3",
360                vec![CapabilityType::Compute],
361                Some(CellRole::Compute),
362                None,
363            ),
364        ];
365
366        let complete = coord.check_formation_complete(&members, None).unwrap();
367
368        assert!(!complete);
369        assert_eq!(coord.status, FormationStatus::Forming); // Not failed, just incomplete
370    }
371
372    #[test]
373    fn test_unassigned_roles() {
374        let mut coord = CellCoordinator::new("cell1".to_string());
375
376        let members = vec![
377            create_test_member(
378                "p1",
379                vec![CapabilityType::Communication],
380                Some(CellRole::Leader),
381                None,
382            ),
383            create_test_member(
384                "p2",
385                vec![CapabilityType::Sensor],
386                Some(CellRole::Sensor),
387                None,
388            ),
389            create_test_member("p3", vec![CapabilityType::Compute], None, None), // No role
390        ];
391
392        let complete = coord
393            .check_formation_complete(&members, Some("p1"))
394            .unwrap();
395
396        assert!(!complete);
397        assert_eq!(coord.status, FormationStatus::Forming);
398    }
399
400    #[test]
401    fn test_missing_required_capabilities() {
402        let mut coord = CellCoordinator::new("cell1".to_string());
403
404        let members = vec![
405            create_test_member(
406                "p1",
407                vec![CapabilityType::Communication],
408                Some(CellRole::Leader),
409                None,
410            ),
411            create_test_member(
412                "p2",
413                vec![CapabilityType::Compute],
414                Some(CellRole::Compute),
415                None,
416            ), // Missing Sensor
417            create_test_member(
418                "p3",
419                vec![CapabilityType::Mobility],
420                Some(CellRole::Follower),
421                None,
422            ),
423        ];
424
425        let complete = coord
426            .check_formation_complete(&members, Some("p1"))
427            .unwrap();
428
429        assert!(!complete);
430        assert!(matches!(
431            coord.status,
432            FormationStatus::Failed(ref msg) if msg.contains("Missing required capabilities")
433        ));
434    }
435
436    #[test]
437    fn test_formation_complete_no_approval_needed() {
438        let mut coord = CellCoordinator::new("cell1".to_string());
439
440        // Create operator with Commander authority for Communication
441        let operator = Operator::new(
442            "op1".to_string(),
443            "Test Operator".to_string(),
444            OperatorRank::E5,
445            AuthorityLevel::Commander,
446            "11B".to_string(),
447        );
448
449        let members = vec![
450            create_test_member(
451                "p1",
452                vec![CapabilityType::Communication, CapabilityType::Sensor],
453                Some(CellRole::Leader),
454                Some(operator),
455            ),
456            create_test_member(
457                "p2",
458                vec![CapabilityType::Sensor],
459                Some(CellRole::Sensor),
460                None,
461            ),
462            create_test_member(
463                "p3",
464                vec![CapabilityType::Compute],
465                Some(CellRole::Compute),
466                None,
467            ),
468        ];
469
470        let complete = coord
471            .check_formation_complete(&members, Some("p1"))
472            .unwrap();
473
474        assert!(complete);
475        assert_eq!(coord.status, FormationStatus::Ready);
476        assert!(coord.formation_complete.is_some());
477    }
478
479    #[test]
480    fn test_formation_awaiting_approval() {
481        let mut coord = CellCoordinator::new("cell1".to_string());
482
483        // Create operator with Commander authority for Communication (p1)
484        let operator1 = Operator::new(
485            "op1".to_string(),
486            "Test Operator 1".to_string(),
487            OperatorRank::E5,
488            AuthorityLevel::Commander,
489            "11B".to_string(),
490        );
491
492        // Create operator with Observer authority for Payload (p3) - requires oversight
493        let operator3 = Operator::new(
494            "op3".to_string(),
495            "Test Operator 3".to_string(),
496            OperatorRank::E5,
497            AuthorityLevel::Observer, // Observer on Payload requires oversight
498            "11B".to_string(),
499        );
500
501        let members = vec![
502            create_test_member(
503                "p1",
504                vec![CapabilityType::Communication, CapabilityType::Sensor],
505                Some(CellRole::Leader),
506                Some(operator1),
507            ),
508            create_test_member(
509                "p2",
510                vec![CapabilityType::Sensor],
511                Some(CellRole::Sensor),
512                None,
513            ),
514            create_test_member(
515                "p3",
516                vec![CapabilityType::Payload], // Oversight-required with low authority
517                Some(CellRole::Follower),
518                Some(operator3),
519            ),
520        ];
521
522        let complete = coord
523            .check_formation_complete(&members, Some("p1"))
524            .unwrap();
525
526        assert!(!complete); // Not complete until approved
527        assert_eq!(coord.status, FormationStatus::AwaitingApproval);
528    }
529
530    #[test]
531    fn test_human_approval_workflow() {
532        let mut coord = CellCoordinator::new("cell1".to_string());
533        coord.status = FormationStatus::AwaitingApproval;
534
535        coord.approve_formation().unwrap();
536
537        assert_eq!(coord.status, FormationStatus::Ready);
538        assert!(coord.human_approved);
539        assert!(coord.formation_complete.is_some());
540    }
541
542    #[test]
543    fn test_human_rejection() {
544        let mut coord = CellCoordinator::new("cell1".to_string());
545        coord.status = FormationStatus::AwaitingApproval;
546
547        coord
548            .reject_formation("Insufficient capability coverage".to_string())
549            .unwrap();
550
551        assert!(matches!(
552            coord.status,
553            FormationStatus::Failed(ref msg) if msg.contains("Human rejected")
554        ));
555    }
556
557    #[test]
558    fn test_phase_transition() {
559        let mut coord = CellCoordinator::new("cell1".to_string());
560        coord.status = FormationStatus::Ready;
561
562        assert!(coord.can_transition_to_hierarchical());
563
564        let phase = coord.get_hierarchical_phase().unwrap();
565        assert_eq!(phase, Phase::Hierarchy);
566    }
567
568    #[test]
569    fn test_cannot_transition_when_not_ready() {
570        let coord = CellCoordinator::new("cell1".to_string());
571
572        assert!(!coord.can_transition_to_hierarchical());
573        assert!(coord.get_hierarchical_phase().is_err());
574    }
575
576    #[test]
577    fn test_formation_duration() {
578        let coord = CellCoordinator::new("cell1".to_string());
579
580        std::thread::sleep(std::time::Duration::from_secs(1));
581
582        let duration = coord.formation_duration();
583        assert!(duration >= 1);
584    }
585
586    #[test]
587    fn test_reset_formation() {
588        let mut coord = CellCoordinator::new("cell1".to_string());
589        coord.status = FormationStatus::Ready;
590        coord.human_approved = true;
591        coord.formation_complete = Some(12345);
592
593        coord.reset();
594
595        assert_eq!(coord.status, FormationStatus::Forming);
596        assert!(!coord.human_approved);
597        assert!(coord.formation_complete.is_none());
598    }
599
600    #[test]
601    fn test_single_member_squad() {
602        // Edge case: Single member (< min_size)
603        let mut coord = CellCoordinator::new("cell1".to_string());
604
605        let operator = Operator::new(
606            "op1".to_string(),
607            "Test Operator".to_string(),
608            OperatorRank::E5,
609            AuthorityLevel::Commander,
610            "11B".to_string(),
611        );
612
613        let members = vec![create_test_member(
614            "p1",
615            vec![CapabilityType::Communication, CapabilityType::Sensor],
616            Some(CellRole::Leader),
617            Some(operator),
618        )];
619
620        let complete = coord
621            .check_formation_complete(&members, Some("p1"))
622            .unwrap();
623
624        assert!(!complete);
625        assert!(matches!(
626            coord.status,
627            FormationStatus::Failed(ref msg) if msg.contains("Insufficient members")
628        ));
629    }
630
631    #[test]
632    fn test_exact_minimum_size_squad() {
633        // Boundary case: Exactly min_size (3) members
634        let mut coord = CellCoordinator::new("cell1".to_string());
635        assert_eq!(coord.min_size, 3);
636
637        let operator = Operator::new(
638            "op1".to_string(),
639            "Test Operator".to_string(),
640            OperatorRank::E5,
641            AuthorityLevel::Commander,
642            "11B".to_string(),
643        );
644
645        let members = vec![
646            create_test_member(
647                "p1",
648                vec![CapabilityType::Communication, CapabilityType::Sensor],
649                Some(CellRole::Leader),
650                Some(operator),
651            ),
652            create_test_member(
653                "p2",
654                vec![CapabilityType::Sensor],
655                Some(CellRole::Sensor),
656                None,
657            ),
658            create_test_member(
659                "p3",
660                vec![CapabilityType::Compute],
661                Some(CellRole::Compute),
662                None,
663            ),
664        ];
665
666        let complete = coord
667            .check_formation_complete(&members, Some("p1"))
668            .unwrap();
669
670        // Should succeed with exactly min_size
671        assert!(complete);
672        assert_eq!(coord.status, FormationStatus::Ready);
673    }
674
675    #[test]
676    fn test_empty_squad_formation() {
677        // Edge case: Zero members
678        let mut coord = CellCoordinator::new("cell1".to_string());
679
680        let complete = coord.check_formation_complete(&[], None).unwrap();
681
682        assert!(!complete);
683        assert!(matches!(
684            coord.status,
685            FormationStatus::Failed(ref msg) if msg.contains("Insufficient members: 0 < 3")
686        ));
687    }
688
689    #[test]
690    fn test_approval_idempotency() {
691        // Edge case: Multiple approval calls should be idempotent
692        let mut coord = CellCoordinator::new("cell1".to_string());
693        coord.status = FormationStatus::AwaitingApproval;
694
695        // First approval
696        coord.approve_formation().unwrap();
697        assert_eq!(coord.status, FormationStatus::Ready);
698        assert!(coord.human_approved);
699
700        // Second approval should fail (not awaiting approval anymore)
701        let result = coord.approve_formation();
702        assert!(result.is_err());
703    }
704}