Skip to main content

peat_protocol/composition/
emergent.rs

1//! Emergent composition rules
2//!
3//! This module implements composition rules for emergent capabilities - new
4//! capabilities that arise from specific combinations of individual capabilities.
5//!
6//! Examples:
7//! - ISR Chain: Sensor + Compute + Communication → Intelligence gathering
8//! - 3D Mapping: Camera + Lidar + Compute → Detailed 3D maps
9//! - Strike Chain: ISR + Strike + BDA → Complete targeting cycle
10
11use crate::composition::rules::{CompositionContext, CompositionResult, CompositionRule};
12use crate::models::capability::{Capability, CapabilityType};
13use crate::models::CapabilityExt;
14use crate::Result;
15use async_trait::async_trait;
16use serde_json::{json, Value};
17
18/// Rule for detecting ISR (Intelligence, Surveillance, Reconnaissance) chain capability
19///
20/// Requires: Sensor + Compute + Communication capabilities
21/// Emergent capability: Complete ISR chain for intelligence gathering
22pub struct IsrChainRule {
23    /// Minimum confidence threshold for each component
24    min_confidence: f32,
25}
26
27impl IsrChainRule {
28    /// Create a new ISR chain rule
29    pub fn new(min_confidence: f32) -> Self {
30        Self { min_confidence }
31    }
32}
33
34impl Default for IsrChainRule {
35    fn default() -> Self {
36        Self::new(0.7)
37    }
38}
39
40#[async_trait]
41impl CompositionRule for IsrChainRule {
42    fn name(&self) -> &str {
43        "isr_chain"
44    }
45
46    fn description(&self) -> &str {
47        "Detects emergent ISR chain capability from sensor + compute + communication"
48    }
49
50    fn applies_to(&self, capabilities: &[Capability]) -> bool {
51        let has_sensor = capabilities.iter().any(|c| {
52            c.get_capability_type() == CapabilityType::Sensor && c.confidence >= self.min_confidence
53        });
54
55        let has_compute = capabilities.iter().any(|c| {
56            c.get_capability_type() == CapabilityType::Compute
57                && c.confidence >= self.min_confidence
58        });
59
60        let has_comms = capabilities.iter().any(|c| {
61            c.get_capability_type() == CapabilityType::Communication
62                && c.confidence >= self.min_confidence
63        });
64
65        has_sensor && has_compute && has_comms
66    }
67
68    async fn compose(
69        &self,
70        capabilities: &[Capability],
71        _context: &CompositionContext,
72    ) -> Result<CompositionResult> {
73        // Find best capability of each required type
74        let best_sensor = capabilities
75            .iter()
76            .filter(|c| c.get_capability_type() == CapabilityType::Sensor)
77            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
78
79        let best_compute = capabilities
80            .iter()
81            .filter(|c| c.get_capability_type() == CapabilityType::Compute)
82            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
83
84        let best_comms = capabilities
85            .iter()
86            .filter(|c| c.get_capability_type() == CapabilityType::Communication)
87            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
88
89        // Check if we have all required components
90        if let (Some(sensor), Some(compute), Some(comms)) = (best_sensor, best_compute, best_comms)
91        {
92            // Emergent capability confidence is minimum of components (weakest link)
93            let chain_confidence = sensor
94                .confidence
95                .min(compute.confidence)
96                .min(comms.confidence);
97
98            let mut composed = Capability::new(
99                format!("emergent_isr_chain_{}", uuid::Uuid::new_v4()),
100                "ISR Chain".to_string(),
101                CapabilityType::Emergent,
102                chain_confidence,
103            );
104            composed.metadata_json = serde_json::to_string(&json!({
105                "composition_type": "emergent",
106                "pattern": "isr_chain",
107                "components": {
108                    "sensor": sensor.id,
109                    "compute": compute.id,
110                    "communication": comms.id
111                },
112                "description": "Complete intelligence gathering capability"
113            }))
114            .unwrap_or_default();
115
116            let contributors = vec![sensor.id.clone(), compute.id.clone(), comms.id.clone()];
117
118            return Ok(CompositionResult::new(vec![composed], chain_confidence)
119                .with_contributors(contributors));
120        }
121
122        Ok(CompositionResult::new(vec![], 0.0))
123    }
124}
125
126/// Rule for detecting 3D mapping capability
127///
128/// Requires: Camera + Lidar + Compute capabilities
129/// Emergent capability: Detailed 3D environment mapping
130pub struct Mapping3dRule {
131    /// Minimum confidence threshold for each component
132    min_confidence: f32,
133}
134
135impl Mapping3dRule {
136    /// Create a new 3D mapping rule
137    pub fn new(min_confidence: f32) -> Self {
138        Self { min_confidence }
139    }
140}
141
142impl Default for Mapping3dRule {
143    fn default() -> Self {
144        Self::new(0.7)
145    }
146}
147
148#[async_trait]
149impl CompositionRule for Mapping3dRule {
150    fn name(&self) -> &str {
151        "mapping_3d"
152    }
153
154    fn description(&self) -> &str {
155        "Detects emergent 3D mapping capability from camera + lidar + compute"
156    }
157
158    fn applies_to(&self, capabilities: &[Capability]) -> bool {
159        // Look for camera sensor
160        let has_camera = capabilities.iter().any(|c| {
161            c.get_capability_type() == CapabilityType::Sensor
162                && c.confidence >= self.min_confidence
163                && serde_json::from_str::<Value>(&c.metadata_json)
164                    .ok()
165                    .and_then(|v| {
166                        v.get("sensor_type")
167                            .and_then(|s| s.as_str())
168                            .map(|s| s == "camera")
169                    })
170                    .unwrap_or(false)
171        });
172
173        // Look for lidar sensor
174        let has_lidar = capabilities.iter().any(|c| {
175            c.get_capability_type() == CapabilityType::Sensor
176                && c.confidence >= self.min_confidence
177                && serde_json::from_str::<Value>(&c.metadata_json)
178                    .ok()
179                    .and_then(|v| {
180                        v.get("sensor_type")
181                            .and_then(|s| s.as_str())
182                            .map(|s| s == "lidar")
183                    })
184                    .unwrap_or(false)
185        });
186
187        let has_compute = capabilities.iter().any(|c| {
188            c.get_capability_type() == CapabilityType::Compute
189                && c.confidence >= self.min_confidence
190        });
191
192        has_camera && has_lidar && has_compute
193    }
194
195    async fn compose(
196        &self,
197        capabilities: &[Capability],
198        _context: &CompositionContext,
199    ) -> Result<CompositionResult> {
200        // Find required capabilities
201        let camera = capabilities.iter().find(|c| {
202            c.get_capability_type() == CapabilityType::Sensor
203                && serde_json::from_str::<Value>(&c.metadata_json)
204                    .ok()
205                    .and_then(|v| {
206                        v.get("sensor_type")
207                            .and_then(|s| s.as_str())
208                            .map(|s| s == "camera")
209                    })
210                    .unwrap_or(false)
211        });
212
213        let lidar = capabilities.iter().find(|c| {
214            c.get_capability_type() == CapabilityType::Sensor
215                && serde_json::from_str::<Value>(&c.metadata_json)
216                    .ok()
217                    .and_then(|v| {
218                        v.get("sensor_type")
219                            .and_then(|s| s.as_str())
220                            .map(|s| s == "lidar")
221                    })
222                    .unwrap_or(false)
223        });
224
225        let compute = capabilities
226            .iter()
227            .filter(|c| c.get_capability_type() == CapabilityType::Compute)
228            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
229
230        if let (Some(camera), Some(lidar), Some(compute)) = (camera, lidar, compute) {
231            // Confidence is minimum of all components
232            let mapping_confidence = camera
233                .confidence
234                .min(lidar.confidence)
235                .min(compute.confidence);
236
237            let mut composed = Capability::new(
238                format!("emergent_3d_mapping_{}", uuid::Uuid::new_v4()),
239                "3D Mapping".to_string(),
240                CapabilityType::Emergent,
241                mapping_confidence,
242            );
243            composed.metadata_json = serde_json::to_string(&json!({
244                "composition_type": "emergent",
245                "pattern": "3d_mapping",
246                "components": {
247                    "camera": camera.id,
248                    "lidar": lidar.id,
249                    "compute": compute.id
250                },
251                "description": "Real-time 3D environment mapping"
252            }))
253            .unwrap_or_default();
254
255            let contributors = vec![camera.id.clone(), lidar.id.clone(), compute.id.clone()];
256
257            return Ok(CompositionResult::new(vec![composed], mapping_confidence)
258                .with_contributors(contributors));
259        }
260
261        Ok(CompositionResult::new(vec![], 0.0))
262    }
263}
264
265/// Rule for detecting strike chain capability
266///
267/// Requires: ISR + Strike + BDA (Battle Damage Assessment) capabilities
268/// Emergent capability: Complete targeting and assessment cycle
269pub struct StrikeChainRule {
270    /// Minimum confidence threshold for each component
271    min_confidence: f32,
272}
273
274impl StrikeChainRule {
275    /// Create a new strike chain rule
276    pub fn new(min_confidence: f32) -> Self {
277        Self { min_confidence }
278    }
279}
280
281impl Default for StrikeChainRule {
282    fn default() -> Self {
283        Self::new(0.8) // Higher threshold for lethal operations
284    }
285}
286
287#[async_trait]
288impl CompositionRule for StrikeChainRule {
289    fn name(&self) -> &str {
290        "strike_chain"
291    }
292
293    fn description(&self) -> &str {
294        "Detects emergent strike chain capability from ISR + strike + BDA"
295    }
296
297    fn applies_to(&self, capabilities: &[Capability]) -> bool {
298        // Look for ISR capability (could be emergent from another rule)
299        let has_isr = capabilities.iter().any(|c| {
300            c.get_capability_type() == CapabilityType::Emergent
301                && c.confidence >= self.min_confidence
302                && serde_json::from_str::<Value>(&c.metadata_json)
303                    .ok()
304                    .map(|v| {
305                        let is_isr_pattern =
306                            v.get("pattern").and_then(|p| p.as_str()) == Some("isr_chain");
307                        let is_isr_capable = v
308                            .get("isr_capable")
309                            .and_then(|i| i.as_bool())
310                            .unwrap_or(false);
311                        is_isr_pattern || is_isr_capable
312                    })
313                    .unwrap_or(false)
314        });
315
316        // Look for strike/payload capability
317        let has_strike = capabilities.iter().any(|c| {
318            c.get_capability_type() == CapabilityType::Payload
319                && c.confidence >= self.min_confidence
320                && serde_json::from_str::<Value>(&c.metadata_json)
321                    .ok()
322                    .and_then(|v| v.get("strike_capable").and_then(|s| s.as_bool()))
323                    .unwrap_or(false)
324        });
325
326        // Look for BDA capability (sensor for assessment)
327        let has_bda = capabilities.iter().any(|c| {
328            c.get_capability_type() == CapabilityType::Sensor && c.confidence >= self.min_confidence
329        });
330
331        has_isr && has_strike && has_bda
332    }
333
334    async fn compose(
335        &self,
336        capabilities: &[Capability],
337        _context: &CompositionContext,
338    ) -> Result<CompositionResult> {
339        // Find ISR capability
340        let isr = capabilities.iter().find(|c| {
341            c.get_capability_type() == CapabilityType::Emergent
342                && serde_json::from_str::<Value>(&c.metadata_json)
343                    .ok()
344                    .and_then(|v| {
345                        v.get("pattern")
346                            .and_then(|p| p.as_str())
347                            .map(|s| s == "isr_chain")
348                    })
349                    .unwrap_or(false)
350        });
351
352        // Find strike capability
353        let strike = capabilities
354            .iter()
355            .filter(|c| {
356                c.get_capability_type() == CapabilityType::Payload
357                    && serde_json::from_str::<Value>(&c.metadata_json)
358                        .ok()
359                        .and_then(|v| v.get("strike_capable").and_then(|s| s.as_bool()))
360                        .unwrap_or(false)
361            })
362            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
363
364        // Find BDA sensor
365        let bda = capabilities
366            .iter()
367            .filter(|c| c.get_capability_type() == CapabilityType::Sensor)
368            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
369
370        if let (Some(isr), Some(strike), Some(bda)) = (isr, strike, bda) {
371            // Confidence is minimum of all components (critical for strike operations)
372            let chain_confidence = isr.confidence.min(strike.confidence).min(bda.confidence);
373
374            let mut composed = Capability::new(
375                format!("emergent_strike_chain_{}", uuid::Uuid::new_v4()),
376                "Strike Chain".to_string(),
377                CapabilityType::Emergent,
378                chain_confidence,
379            );
380            composed.metadata_json = serde_json::to_string(&json!({
381                "composition_type": "emergent",
382                "pattern": "strike_chain",
383                "components": {
384                    "isr": isr.id,
385                    "strike": strike.id,
386                    "bda": bda.id
387                },
388                "description": "Complete targeting cycle with assessment",
389                "requires_human_approval": true // Safety critical
390            }))
391            .unwrap_or_default();
392
393            let contributors = vec![isr.id.clone(), strike.id.clone(), bda.id.clone()];
394
395            return Ok(CompositionResult::new(vec![composed], chain_confidence)
396                .with_contributors(contributors));
397        }
398
399        Ok(CompositionResult::new(vec![], 0.0))
400    }
401}
402
403/// Rule for detecting authorization coverage capability
404///
405/// Checks if the composition has human-in-the-loop authorization coverage.
406/// Requires: Communication capability + Node with bound Operator having sufficient authority
407/// Emergent capability: Authorization/command coverage for the party
408pub struct AuthorizationCoverageRule {
409    /// Minimum authority level required
410    min_authority: crate::models::AuthorityLevel,
411}
412
413impl AuthorizationCoverageRule {
414    /// Create a new authorization coverage rule
415    pub fn new(min_authority: crate::models::AuthorityLevel) -> Self {
416        Self { min_authority }
417    }
418
419    /// Create rule requiring Commander authority (for lethal/critical actions)
420    pub fn commander_required() -> Self {
421        Self::new(crate::models::AuthorityLevel::Commander)
422    }
423
424    /// Create rule requiring Supervisor authority (for general override)
425    pub fn supervisor_required() -> Self {
426        Self::new(crate::models::AuthorityLevel::Supervisor)
427    }
428}
429
430impl Default for AuthorizationCoverageRule {
431    fn default() -> Self {
432        Self::commander_required()
433    }
434}
435
436#[async_trait]
437impl CompositionRule for AuthorizationCoverageRule {
438    fn name(&self) -> &str {
439        "authorization_coverage"
440    }
441
442    fn description(&self) -> &str {
443        "Detects authorization coverage from communication + human operator with sufficient authority"
444    }
445
446    fn applies_to(&self, capabilities: &[Capability]) -> bool {
447        // Requires at least Communication capability
448        // The actual authority check happens in compose() using context.node_configs
449        capabilities
450            .iter()
451            .any(|c| c.get_capability_type() == CapabilityType::Communication)
452    }
453
454    async fn compose(
455        &self,
456        capabilities: &[Capability],
457        context: &CompositionContext,
458    ) -> Result<CompositionResult> {
459        use crate::models::{AuthorityLevelExt, HumanMachinePairExt};
460
461        // Find best communication capability
462        let best_comms = capabilities
463            .iter()
464            .filter(|c| c.get_capability_type() == CapabilityType::Communication)
465            .max_by(|a, b| a.confidence.partial_cmp(&b.confidence).unwrap());
466
467        // Check for operator with sufficient authority
468        let max_authority = context.max_authority();
469
470        let has_sufficient_authority = max_authority
471            .map(|auth| auth >= self.min_authority)
472            .unwrap_or(false);
473
474        if let (Some(comms), true) = (best_comms, has_sufficient_authority) {
475            let authority = max_authority.unwrap();
476            let auth_score = authority.to_score() as f32;
477
478            // Confidence is combination of comms quality and authority level
479            let coverage_confidence = (comms.confidence * 0.4 + auth_score * 0.6).min(1.0);
480
481            let mut composed = Capability::new(
482                format!("emergent_auth_coverage_{}", uuid::Uuid::new_v4()),
483                "Authorization Coverage".to_string(),
484                CapabilityType::Emergent,
485                coverage_confidence,
486            );
487
488            // Find the node with the authorizing operator
489            let authorizing_node = context
490                .node_configs
491                .iter()
492                .find(|config| {
493                    config
494                        .operator_binding
495                        .as_ref()
496                        .and_then(|b| b.max_authority())
497                        .map(|a| a >= self.min_authority)
498                        .unwrap_or(false)
499                })
500                .map(|c| c.id.clone());
501
502            composed.metadata_json = serde_json::to_string(&json!({
503                "composition_type": "emergent",
504                "pattern": "authorization_coverage",
505                "components": {
506                    "communication": comms.id,
507                    "authorizing_node": authorizing_node,
508                },
509                "authority_level": format!("{:?}", authority),
510                "authorization_bonus": context.authorization_bonus(),
511                "can_authorize_strike": authority == crate::models::AuthorityLevel::Commander,
512                "description": "Human-in-the-loop authorization capability"
513            }))
514            .unwrap_or_default();
515
516            let contributors = vec![comms.id.clone()];
517
518            return Ok(CompositionResult::new(vec![composed], coverage_confidence)
519                .with_contributors(contributors));
520        }
521
522        Ok(CompositionResult::new(vec![], 0.0))
523    }
524}
525
526/// Rule for detecting multi-domain coverage capability
527///
528/// Requires: Sensors or capabilities that cover multiple domains (Air, Surface, Subsurface)
529/// Emergent capability: Multi-domain battlespace awareness
530pub struct MultiDomainCoverageRule {
531    /// Minimum number of domains required for the rule to apply
532    min_domains: usize,
533    /// Minimum confidence threshold for sensors
534    min_confidence: f32,
535}
536
537impl MultiDomainCoverageRule {
538    /// Create a new multi-domain coverage rule
539    pub fn new(min_domains: usize, min_confidence: f32) -> Self {
540        Self {
541            min_domains: min_domains.max(2), // At least 2 domains for "multi"
542            min_confidence,
543        }
544    }
545
546    /// Create rule requiring all three domains
547    pub fn full_spectrum() -> Self {
548        Self::new(3, 0.7)
549    }
550
551    /// Create rule requiring any two domains
552    pub fn dual_domain() -> Self {
553        Self::new(2, 0.7)
554    }
555
556    /// Extract sensor type and infer domains
557    fn get_sensor_domains(cap: &Capability) -> crate::models::DomainSet {
558        use crate::models::{DomainSet, SensorType};
559
560        if cap.get_capability_type() != CapabilityType::Sensor {
561            return DomainSet::empty();
562        }
563
564        // Try to get sensor type from metadata
565        let sensor_type = serde_json::from_str::<serde_json::Value>(&cap.metadata_json)
566            .ok()
567            .and_then(|v| {
568                v.get("sensor_type").and_then(|s| s.as_str()).and_then(|s| {
569                    match s.to_lowercase().as_str() {
570                        "electro_optical" | "eo" | "camera" => Some(SensorType::ElectroOptical),
571                        "infrared" | "ir" | "thermal" => Some(SensorType::Infrared),
572                        "radar" | "rad" => Some(SensorType::Radar),
573                        "sonar" | "son" => Some(SensorType::Sonar),
574                        "acoustic" | "aco" => Some(SensorType::Acoustic),
575                        "sigint" | "sig" | "signals_intelligence" => Some(SensorType::Sigint),
576                        "mad" | "magnetic" => Some(SensorType::Mad),
577                        _ => None,
578                    }
579                })
580            });
581
582        if let Some(st) = sensor_type {
583            st.detection_domains()
584        } else {
585            // If sensor type not specified, check for explicit domains
586            serde_json::from_str::<serde_json::Value>(&cap.metadata_json)
587                .ok()
588                .and_then(|v| {
589                    v.get("detection_domains").and_then(|domains| {
590                        if let Some(arr) = domains.as_array() {
591                            let mut set = DomainSet::empty();
592                            for d in arr {
593                                if let Some(s) = d.as_str() {
594                                    if let Some(domain) = crate::models::Domain::parse(s) {
595                                        set.add(domain);
596                                    }
597                                }
598                            }
599                            Some(set)
600                        } else {
601                            None
602                        }
603                    })
604                })
605                .unwrap_or_else(|| {
606                    // Default: assume surface + air for unknown sensors
607                    DomainSet::from_domains(&[
608                        crate::models::Domain::Surface,
609                        crate::models::Domain::Air,
610                    ])
611                })
612        }
613    }
614}
615
616impl Default for MultiDomainCoverageRule {
617    fn default() -> Self {
618        Self::dual_domain()
619    }
620}
621
622#[async_trait]
623impl CompositionRule for MultiDomainCoverageRule {
624    fn name(&self) -> &str {
625        "multi_domain_coverage"
626    }
627
628    fn description(&self) -> &str {
629        "Detects multi-domain coverage from sensors spanning air, surface, and/or subsurface"
630    }
631
632    fn applies_to(&self, capabilities: &[Capability]) -> bool {
633        use crate::models::DomainSet;
634
635        // Aggregate domains from all capabilities
636        let mut covered = DomainSet::empty();
637
638        for cap in capabilities {
639            if cap.confidence < self.min_confidence {
640                continue;
641            }
642
643            // Get domains this capability covers
644            let domains = Self::get_sensor_domains(cap);
645            covered = covered.union(&domains);
646        }
647
648        covered.count() >= self.min_domains
649    }
650
651    async fn compose(
652        &self,
653        capabilities: &[Capability],
654        _context: &CompositionContext,
655    ) -> Result<CompositionResult> {
656        use crate::models::{Domain, DomainSet};
657
658        let mut covered = DomainSet::empty();
659        let mut contributors: Vec<String> = Vec::new();
660        let mut domain_sensors: std::collections::HashMap<Domain, Vec<String>> =
661            std::collections::HashMap::new();
662        let mut min_confidence = 1.0f32;
663
664        for cap in capabilities {
665            if cap.confidence < self.min_confidence {
666                continue;
667            }
668
669            let domains = Self::get_sensor_domains(cap);
670
671            if !domains.is_empty() {
672                contributors.push(cap.id.clone());
673                min_confidence = min_confidence.min(cap.confidence);
674
675                for domain in domains.iter() {
676                    covered.add(domain);
677                    domain_sensors
678                        .entry(domain)
679                        .or_default()
680                        .push(cap.id.clone());
681                }
682            }
683        }
684
685        if covered.count() < self.min_domains {
686            return Ok(CompositionResult::new(vec![], 0.0));
687        }
688
689        // Calculate composition bonus based on coverage
690        let coverage_bonus = match covered.count() {
691            3 => 3, // Full spectrum
692            2 => 2, // Dual domain
693            _ => 1, // Single domain (shouldn't happen but safe)
694        };
695
696        // Confidence is minimum of all contributors, boosted slightly by coverage
697        let coverage_confidence = (min_confidence + (coverage_bonus as f32 * 0.05)).min(1.0);
698
699        let coverage_name = match covered.count() {
700            3 => "Full Spectrum Coverage",
701            2 => "Dual-Domain Coverage",
702            _ => "Domain Coverage",
703        };
704
705        let mut composed = Capability::new(
706            format!("emergent_multi_domain_{}", uuid::Uuid::new_v4()),
707            coverage_name.to_string(),
708            CapabilityType::Emergent,
709            coverage_confidence,
710        );
711
712        // Build domain coverage map for metadata
713        let domain_coverage: serde_json::Map<String, serde_json::Value> = domain_sensors
714            .iter()
715            .map(|(domain, sensors)| {
716                (
717                    domain.name().to_lowercase(),
718                    serde_json::Value::Array(
719                        sensors
720                            .iter()
721                            .map(|s| serde_json::Value::String(s.clone()))
722                            .collect(),
723                    ),
724                )
725            })
726            .collect();
727
728        composed.metadata_json = serde_json::to_string(&json!({
729            "composition_type": "emergent",
730            "pattern": "multi_domain_coverage",
731            "domains_covered": covered.to_vec().iter().map(|d| d.name()).collect::<Vec<_>>(),
732            "domain_count": covered.count(),
733            "coverage_bonus": coverage_bonus,
734            "domain_sensors": domain_coverage,
735            "is_full_spectrum": covered.count() == 3,
736            "can_detect_subsurface": covered.contains(Domain::Subsurface),
737            "description": format!("Multi-domain awareness across {} domains", covered.count())
738        }))
739        .unwrap_or_default();
740
741        Ok(CompositionResult::new(vec![composed], coverage_confidence)
742            .with_contributors(contributors))
743    }
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749    use serde_json::json;
750
751    #[tokio::test]
752    async fn test_isr_chain_detection() {
753        let rule = IsrChainRule::default();
754
755        let mut sensor = Capability::new(
756            "sensor1".to_string(),
757            "EO Camera".to_string(),
758            CapabilityType::Sensor,
759            0.9,
760        );
761        sensor.metadata_json =
762            serde_json::to_string(&json!({"sensor_type": "camera"})).unwrap_or_default();
763
764        let compute = Capability::new(
765            "compute1".to_string(),
766            "Edge Compute".to_string(),
767            CapabilityType::Compute,
768            0.85,
769        );
770
771        let mut comms = Capability::new(
772            "comms1".to_string(),
773            "Tactical Radio".to_string(),
774            CapabilityType::Communication,
775            0.8,
776        );
777        comms.metadata_json =
778            serde_json::to_string(&json!({"bandwidth": 10.0})).unwrap_or_default();
779
780        let caps = vec![sensor, compute, comms];
781        let context = CompositionContext::new(vec!["node1".to_string()]);
782
783        assert!(rule.applies_to(&caps));
784
785        let result = rule.compose(&caps, &context).await.unwrap();
786        assert!(result.has_compositions());
787        assert_eq!(result.composed_capabilities.len(), 1);
788
789        let composed = &result.composed_capabilities[0];
790        assert_eq!(composed.get_capability_type(), CapabilityType::Emergent);
791        assert_eq!(composed.name, "ISR Chain");
792        // Confidence should be minimum of all components (0.8)
793        assert_eq!(composed.confidence, 0.8);
794        assert_eq!(result.contributing_capabilities.len(), 3);
795    }
796
797    #[tokio::test]
798    async fn test_isr_chain_missing_component() {
799        let rule = IsrChainRule::default();
800
801        let sensor = Capability::new(
802            "sensor1".to_string(),
803            "Sensor".to_string(),
804            CapabilityType::Sensor,
805            0.9,
806        );
807
808        let compute = Capability::new(
809            "compute1".to_string(),
810            "Compute".to_string(),
811            CapabilityType::Compute,
812            0.85,
813        );
814
815        // Missing communication capability
816        let caps = vec![sensor, compute];
817
818        assert!(!rule.applies_to(&caps));
819    }
820
821    #[tokio::test]
822    async fn test_3d_mapping_detection() {
823        let rule = Mapping3dRule::default();
824
825        let mut camera = Capability::new(
826            "camera1".to_string(),
827            "RGB Camera".to_string(),
828            CapabilityType::Sensor,
829            0.95,
830        );
831        camera.metadata_json =
832            serde_json::to_string(&json!({"sensor_type": "camera"})).unwrap_or_default();
833
834        let mut lidar = Capability::new(
835            "lidar1".to_string(),
836            "3D Lidar".to_string(),
837            CapabilityType::Sensor,
838            0.9,
839        );
840        lidar.metadata_json =
841            serde_json::to_string(&json!({"sensor_type": "lidar"})).unwrap_or_default();
842
843        let compute = Capability::new(
844            "compute1".to_string(),
845            "GPU Compute".to_string(),
846            CapabilityType::Compute,
847            0.85,
848        );
849
850        let caps = vec![camera, lidar, compute];
851        let context = CompositionContext::new(vec!["node1".to_string()]);
852
853        assert!(rule.applies_to(&caps));
854
855        let result = rule.compose(&caps, &context).await.unwrap();
856        assert!(result.has_compositions());
857
858        let composed = &result.composed_capabilities[0];
859        assert_eq!(composed.name, "3D Mapping");
860        assert_eq!(composed.confidence, 0.85); // Min of components
861        assert_eq!(result.contributing_capabilities.len(), 3);
862    }
863
864    #[tokio::test]
865    async fn test_strike_chain_detection() {
866        let rule = StrikeChainRule::default();
867
868        // Create an ISR capability (would come from IsrChainRule)
869        let mut isr = Capability::new(
870            "isr1".to_string(),
871            "ISR Chain".to_string(),
872            CapabilityType::Emergent,
873            0.9,
874        );
875        isr.metadata_json = serde_json::to_string(&json!({
876            "pattern": "isr_chain"
877        }))
878        .unwrap_or_default();
879
880        let mut strike = Capability::new(
881            "strike1".to_string(),
882            "Precision Munition".to_string(),
883            CapabilityType::Payload,
884            0.95,
885        );
886        strike.metadata_json =
887            serde_json::to_string(&json!({"strike_capable": true})).unwrap_or_default();
888
889        let bda_sensor = Capability::new(
890            "bda1".to_string(),
891            "BDA Camera".to_string(),
892            CapabilityType::Sensor,
893            0.85,
894        );
895
896        let caps = vec![isr, strike, bda_sensor];
897        let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
898
899        assert!(rule.applies_to(&caps));
900
901        let result = rule.compose(&caps, &context).await.unwrap();
902        assert!(result.has_compositions());
903
904        let composed = &result.composed_capabilities[0];
905        assert_eq!(composed.name, "Strike Chain");
906        assert_eq!(composed.confidence, 0.85); // Min of all components
907        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
908        assert!(metadata["requires_human_approval"].as_bool().unwrap());
909        assert_eq!(result.contributing_capabilities.len(), 3);
910    }
911
912    #[tokio::test]
913    async fn test_low_confidence_component_affects_emergent() {
914        let rule = IsrChainRule::default();
915
916        let sensor = Capability::new(
917            "sensor1".to_string(),
918            "Sensor".to_string(),
919            CapabilityType::Sensor,
920            0.95,
921        );
922
923        let compute = Capability::new(
924            "compute1".to_string(),
925            "Compute".to_string(),
926            CapabilityType::Compute,
927            0.9,
928        );
929
930        // Low confidence comms - this should drag down the emergent capability
931        let comms = Capability::new(
932            "comms1".to_string(),
933            "Comms".to_string(),
934            CapabilityType::Communication,
935            0.5,
936        );
937
938        let caps = vec![sensor, compute, comms];
939        let context = CompositionContext::new(vec!["node1".to_string()]);
940
941        let result = rule.compose(&caps, &context).await.unwrap();
942
943        // Emergent confidence should be limited by weakest link
944        let composed = &result.composed_capabilities[0];
945        assert_eq!(composed.confidence, 0.5);
946    }
947
948    #[tokio::test]
949    async fn test_authorization_coverage_with_commander() {
950        use crate::models::{
951            AuthorityLevel, HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt,
952            Operator, OperatorExt, OperatorRank,
953        };
954
955        let rule = AuthorizationCoverageRule::default();
956
957        // Create communication capability
958        let comms = Capability::new(
959            "radio1".to_string(),
960            "Tactical Radio".to_string(),
961            CapabilityType::Communication,
962            0.9,
963        );
964
965        let caps = vec![comms];
966
967        // Create a node with a Commander-level operator
968        let operator = Operator::new(
969            "op1".to_string(),
970            "CPT Smith".to_string(),
971            OperatorRank::O3,
972            AuthorityLevel::Commander,
973            "11A".to_string(),
974        );
975
976        let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
977        let config = NodeConfig::with_operator("Command Post".to_string(), binding);
978
979        let context =
980            CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
981
982        assert!(rule.applies_to(&caps));
983        assert!(context.has_commander());
984        assert_eq!(context.authorization_bonus(), 4); // Commander = 0.8 * 5 = 4
985
986        let result = rule.compose(&caps, &context).await.unwrap();
987        assert!(result.has_compositions());
988
989        let composed = &result.composed_capabilities[0];
990        assert_eq!(composed.name, "Authorization Coverage");
991
992        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
993        assert!(metadata["can_authorize_strike"].as_bool().unwrap());
994        assert_eq!(metadata["authorization_bonus"].as_i64().unwrap(), 4);
995    }
996
997    #[tokio::test]
998    async fn test_authorization_coverage_without_operator() {
999        let rule = AuthorizationCoverageRule::default();
1000
1001        let comms = Capability::new(
1002            "radio1".to_string(),
1003            "Autonomous Radio".to_string(),
1004            CapabilityType::Communication,
1005            0.9,
1006        );
1007
1008        let caps = vec![comms];
1009
1010        // No operator binding
1011        let context = CompositionContext::new(vec!["node1".to_string()]);
1012
1013        assert!(rule.applies_to(&caps));
1014        assert!(!context.has_commander());
1015        assert_eq!(context.authorization_bonus(), 0);
1016
1017        let result = rule.compose(&caps, &context).await.unwrap();
1018        assert!(!result.has_compositions()); // No authorization without human
1019    }
1020
1021    #[tokio::test]
1022    async fn test_authorization_coverage_supervisor_level() {
1023        use crate::models::{
1024            AuthorityLevel, HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt,
1025            Operator, OperatorExt, OperatorRank,
1026        };
1027
1028        // Use supervisor-level rule
1029        let rule = AuthorizationCoverageRule::supervisor_required();
1030
1031        let comms = Capability::new(
1032            "radio1".to_string(),
1033            "Radio".to_string(),
1034            CapabilityType::Communication,
1035            0.85,
1036        );
1037
1038        let caps = vec![comms];
1039
1040        // Create a node with Supervisor authority (not Commander)
1041        let operator = Operator::new(
1042            "op1".to_string(),
1043            "SGT Jones".to_string(),
1044            OperatorRank::E5,
1045            AuthorityLevel::Supervisor,
1046            "11B".to_string(),
1047        );
1048
1049        let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
1050        let config = NodeConfig::with_operator("Control Station".to_string(), binding);
1051
1052        let context =
1053            CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
1054
1055        // Supervisor level rule should find coverage
1056        let result = rule.compose(&caps, &context).await.unwrap();
1057        assert!(result.has_compositions());
1058
1059        let composed = &result.composed_capabilities[0];
1060        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
1061        // Supervisor can't authorize strikes
1062        assert!(!metadata["can_authorize_strike"].as_bool().unwrap());
1063        // Supervisor = 0.5 * 5 = 2.5 rounds to 2 or 3
1064        assert!(metadata["authorization_bonus"].as_i64().unwrap() >= 2);
1065    }
1066
1067    #[tokio::test]
1068    async fn test_authorization_coverage_insufficient_authority() {
1069        use crate::models::{
1070            AuthorityLevel, HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt,
1071            Operator, OperatorExt, OperatorRank,
1072        };
1073
1074        // Commander-level rule (default)
1075        let rule = AuthorizationCoverageRule::default();
1076
1077        let comms = Capability::new(
1078            "radio1".to_string(),
1079            "Radio".to_string(),
1080            CapabilityType::Communication,
1081            0.9,
1082        );
1083
1084        let caps = vec![comms];
1085
1086        // Only Advisor authority - not sufficient for Commander requirement
1087        let operator = Operator::new(
1088            "op1".to_string(),
1089            "SPC Brown".to_string(),
1090            OperatorRank::E4,
1091            AuthorityLevel::Advisor,
1092            "11B".to_string(),
1093        );
1094
1095        let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
1096        let config = NodeConfig::with_operator("Observation Post".to_string(), binding);
1097
1098        let context =
1099            CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
1100
1101        let result = rule.compose(&caps, &context).await.unwrap();
1102        assert!(!result.has_compositions()); // Advisor can't satisfy Commander requirement
1103    }
1104
1105    // MultiDomainCoverageRule tests
1106
1107    #[tokio::test]
1108    async fn test_multi_domain_dual_domain_coverage() {
1109        let rule = MultiDomainCoverageRule::default(); // dual domain
1110
1111        // Radar (air+surface) + Sonar (subsurface+surface)
1112        let mut radar = Capability::new(
1113            "radar1".to_string(),
1114            "Search Radar".to_string(),
1115            CapabilityType::Sensor,
1116            0.9,
1117        );
1118        radar.metadata_json = serde_json::to_string(&json!({
1119            "sensor_type": "radar"
1120        }))
1121        .unwrap();
1122
1123        let mut sonar = Capability::new(
1124            "sonar1".to_string(),
1125            "Hull Sonar".to_string(),
1126            CapabilityType::Sensor,
1127            0.85,
1128        );
1129        sonar.metadata_json = serde_json::to_string(&json!({
1130            "sensor_type": "sonar"
1131        }))
1132        .unwrap();
1133
1134        let caps = vec![radar, sonar];
1135        let context = CompositionContext::new(vec!["ship1".to_string()]);
1136
1137        assert!(rule.applies_to(&caps));
1138
1139        let result = rule.compose(&caps, &context).await.unwrap();
1140        assert!(result.has_compositions());
1141
1142        let composed = &result.composed_capabilities[0];
1143        assert!(composed.name.contains("Coverage"));
1144
1145        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
1146        assert!(metadata["domain_count"].as_i64().unwrap() >= 2);
1147        assert!(metadata["can_detect_subsurface"].as_bool().unwrap());
1148    }
1149
1150    #[tokio::test]
1151    async fn test_multi_domain_full_spectrum() {
1152        let rule = MultiDomainCoverageRule::full_spectrum();
1153
1154        // Need sensors covering all three domains
1155        let mut radar = Capability::new(
1156            "radar1".to_string(),
1157            "Air Search Radar".to_string(),
1158            CapabilityType::Sensor,
1159            0.9,
1160        );
1161        radar.metadata_json = serde_json::to_string(&json!({
1162            "sensor_type": "radar"
1163        }))
1164        .unwrap();
1165
1166        let mut sonar = Capability::new(
1167            "sonar1".to_string(),
1168            "Sonar".to_string(),
1169            CapabilityType::Sensor,
1170            0.85,
1171        );
1172        sonar.metadata_json = serde_json::to_string(&json!({
1173            "sensor_type": "sonar"
1174        }))
1175        .unwrap();
1176
1177        // Together radar (air+surface) + sonar (subsurface+surface) = all 3 domains
1178        let caps = vec![radar, sonar];
1179        let context = CompositionContext::new(vec!["ship1".to_string()]);
1180
1181        assert!(rule.applies_to(&caps));
1182
1183        let result = rule.compose(&caps, &context).await.unwrap();
1184        assert!(result.has_compositions());
1185
1186        let composed = &result.composed_capabilities[0];
1187        assert_eq!(composed.name, "Full Spectrum Coverage");
1188
1189        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
1190        assert!(metadata["is_full_spectrum"].as_bool().unwrap());
1191        assert_eq!(metadata["domain_count"].as_i64().unwrap(), 3);
1192        assert_eq!(metadata["coverage_bonus"].as_i64().unwrap(), 3);
1193    }
1194
1195    #[tokio::test]
1196    async fn test_multi_domain_insufficient_coverage() {
1197        let rule = MultiDomainCoverageRule::full_spectrum();
1198
1199        // Only one sensor type = not full spectrum
1200        let mut radar = Capability::new(
1201            "radar1".to_string(),
1202            "Radar".to_string(),
1203            CapabilityType::Sensor,
1204            0.9,
1205        );
1206        radar.metadata_json = serde_json::to_string(&json!({
1207            "sensor_type": "radar"
1208        }))
1209        .unwrap();
1210
1211        let caps = vec![radar];
1212        let _context = CompositionContext::new(vec!["node1".to_string()]);
1213
1214        // Radar only covers air+surface, not subsurface
1215        assert!(!rule.applies_to(&caps));
1216    }
1217
1218    #[tokio::test]
1219    async fn test_multi_domain_low_confidence_filtered() {
1220        let rule = MultiDomainCoverageRule::new(2, 0.8); // min 0.8 confidence
1221
1222        let mut radar = Capability::new(
1223            "radar1".to_string(),
1224            "Radar".to_string(),
1225            CapabilityType::Sensor,
1226            0.9, // Good
1227        );
1228        radar.metadata_json = serde_json::to_string(&json!({
1229            "sensor_type": "radar"
1230        }))
1231        .unwrap();
1232
1233        let mut sonar = Capability::new(
1234            "sonar1".to_string(),
1235            "Sonar".to_string(),
1236            CapabilityType::Sensor,
1237            0.5, // Too low - should be filtered
1238        );
1239        sonar.metadata_json = serde_json::to_string(&json!({
1240            "sensor_type": "sonar"
1241        }))
1242        .unwrap();
1243
1244        let caps = vec![radar, sonar];
1245        let context = CompositionContext::new(vec!["node1".to_string()]);
1246
1247        // Sonar filtered due to low confidence, so only 2 domains from radar
1248        assert!(rule.applies_to(&caps)); // radar still covers air+surface = 2 domains
1249
1250        let result = rule.compose(&caps, &context).await.unwrap();
1251        assert!(result.has_compositions());
1252
1253        // Only radar should be a contributor
1254        assert_eq!(result.contributing_capabilities.len(), 1);
1255    }
1256
1257    #[tokio::test]
1258    async fn test_multi_domain_explicit_domains_in_metadata() {
1259        let rule = MultiDomainCoverageRule::default();
1260
1261        // Sensor with explicit domain specification
1262        let mut custom_sensor = Capability::new(
1263            "custom1".to_string(),
1264            "Custom Sensor".to_string(),
1265            CapabilityType::Sensor,
1266            0.9,
1267        );
1268        custom_sensor.metadata_json = serde_json::to_string(&json!({
1269            "detection_domains": ["subsurface", "surface", "air"]
1270        }))
1271        .unwrap();
1272
1273        let caps = vec![custom_sensor];
1274        let context = CompositionContext::new(vec!["node1".to_string()]);
1275
1276        // Single sensor covering all domains
1277        assert!(rule.applies_to(&caps));
1278
1279        let result = rule.compose(&caps, &context).await.unwrap();
1280        assert!(result.has_compositions());
1281
1282        let composed = &result.composed_capabilities[0];
1283        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
1284        assert_eq!(metadata["domain_count"].as_i64().unwrap(), 3);
1285    }
1286
1287    #[tokio::test]
1288    async fn test_multi_domain_acoustic_covers_all() {
1289        let rule = MultiDomainCoverageRule::full_spectrum();
1290
1291        // Acoustic sensor covers all domains
1292        let mut acoustic = Capability::new(
1293            "acoustic1".to_string(),
1294            "Acoustic Array".to_string(),
1295            CapabilityType::Sensor,
1296            0.85,
1297        );
1298        acoustic.metadata_json = serde_json::to_string(&json!({
1299            "sensor_type": "acoustic"
1300        }))
1301        .unwrap();
1302
1303        let caps = vec![acoustic];
1304        let context = CompositionContext::new(vec!["node1".to_string()]);
1305
1306        assert!(rule.applies_to(&caps));
1307
1308        let result = rule.compose(&caps, &context).await.unwrap();
1309        assert!(result.has_compositions());
1310
1311        let composed = &result.composed_capabilities[0];
1312        assert_eq!(composed.name, "Full Spectrum Coverage");
1313    }
1314
1315    #[tokio::test]
1316    async fn test_multi_domain_non_sensors_ignored() {
1317        let rule = MultiDomainCoverageRule::default();
1318
1319        // Non-sensor capabilities should be ignored
1320        let compute = Capability::new(
1321            "compute1".to_string(),
1322            "Compute".to_string(),
1323            CapabilityType::Compute,
1324            0.9,
1325        );
1326
1327        let comms = Capability::new(
1328            "comms1".to_string(),
1329            "Radio".to_string(),
1330            CapabilityType::Communication,
1331            0.9,
1332        );
1333
1334        let caps = vec![compute, comms];
1335        let _context = CompositionContext::new(vec!["node1".to_string()]);
1336
1337        // No sensors = no domain coverage
1338        assert!(!rule.applies_to(&caps));
1339    }
1340
1341    #[tokio::test]
1342    async fn test_multi_domain_mad_for_asw() {
1343        let rule = MultiDomainCoverageRule::default();
1344
1345        // MAD operates from air/surface but detects subsurface
1346        let mut mad = Capability::new(
1347            "mad1".to_string(),
1348            "MAD Boom".to_string(),
1349            CapabilityType::Sensor,
1350            0.8,
1351        );
1352        mad.metadata_json = serde_json::to_string(&json!({
1353            "sensor_type": "mad"
1354        }))
1355        .unwrap();
1356
1357        let mut radar = Capability::new(
1358            "radar1".to_string(),
1359            "Surface Radar".to_string(),
1360            CapabilityType::Sensor,
1361            0.9,
1362        );
1363        radar.metadata_json = serde_json::to_string(&json!({
1364            "sensor_type": "radar"
1365        }))
1366        .unwrap();
1367
1368        let caps = vec![mad, radar];
1369        let context = CompositionContext::new(vec!["p3c".to_string()]); // ASW aircraft
1370
1371        assert!(rule.applies_to(&caps));
1372
1373        let result = rule.compose(&caps, &context).await.unwrap();
1374        assert!(result.has_compositions());
1375
1376        let composed = &result.composed_capabilities[0];
1377        let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
1378        // MAD detects subsurface, radar detects air+surface = 3 domains
1379        assert!(metadata["can_detect_subsurface"].as_bool().unwrap());
1380    }
1381}