1use 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
18pub struct IsrChainRule {
23 min_confidence: f32,
25}
26
27impl IsrChainRule {
28 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 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 if let (Some(sensor), Some(compute), Some(comms)) = (best_sensor, best_compute, best_comms)
91 {
92 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
126pub struct Mapping3dRule {
131 min_confidence: f32,
133}
134
135impl Mapping3dRule {
136 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 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 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 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 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
265pub struct StrikeChainRule {
270 min_confidence: f32,
272}
273
274impl StrikeChainRule {
275 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) }
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 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 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 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 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 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 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 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 }))
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
403pub struct AuthorizationCoverageRule {
409 min_authority: crate::models::AuthorityLevel,
411}
412
413impl AuthorizationCoverageRule {
414 pub fn new(min_authority: crate::models::AuthorityLevel) -> Self {
416 Self { min_authority }
417 }
418
419 pub fn commander_required() -> Self {
421 Self::new(crate::models::AuthorityLevel::Commander)
422 }
423
424 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 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 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 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 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 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
526pub struct MultiDomainCoverageRule {
531 min_domains: usize,
533 min_confidence: f32,
535}
536
537impl MultiDomainCoverageRule {
538 pub fn new(min_domains: usize, min_confidence: f32) -> Self {
540 Self {
541 min_domains: min_domains.max(2), min_confidence,
543 }
544 }
545
546 pub fn full_spectrum() -> Self {
548 Self::new(3, 0.7)
549 }
550
551 pub fn dual_domain() -> Self {
553 Self::new(2, 0.7)
554 }
555
556 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 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 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 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 let mut covered = DomainSet::empty();
637
638 for cap in capabilities {
639 if cap.confidence < self.min_confidence {
640 continue;
641 }
642
643 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 let coverage_bonus = match covered.count() {
691 3 => 3, 2 => 2, _ => 1, };
695
696 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 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 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 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); 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 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); 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 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 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 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 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); 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 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()); }
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 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 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 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 assert!(!metadata["can_authorize_strike"].as_bool().unwrap());
1063 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 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 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()); }
1104
1105 #[tokio::test]
1108 async fn test_multi_domain_dual_domain_coverage() {
1109 let rule = MultiDomainCoverageRule::default(); 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 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 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 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 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); let mut radar = Capability::new(
1223 "radar1".to_string(),
1224 "Radar".to_string(),
1225 CapabilityType::Sensor,
1226 0.9, );
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, );
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 assert!(rule.applies_to(&caps)); let result = rule.compose(&caps, &context).await.unwrap();
1251 assert!(result.has_compositions());
1252
1253 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 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 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 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 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 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 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()]); 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 assert!(metadata["can_detect_subsurface"].as_bool().unwrap());
1380 }
1381}