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 TeamSpeedConstraintRule {
23 min_platforms: usize,
25}
26
27impl TeamSpeedConstraintRule {
28 pub fn new(min_platforms: usize) -> Self {
30 Self { min_platforms }
31 }
32}
33
34impl Default for TeamSpeedConstraintRule {
35 fn default() -> Self {
36 Self::new(2)
37 }
38}
39
40#[async_trait]
41impl CompositionRule for TeamSpeedConstraintRule {
42 fn name(&self) -> &str {
43 "team_speed_constraint"
44 }
45
46 fn description(&self) -> &str {
47 "Determines team movement speed based on slowest member constraint"
48 }
49
50 fn applies_to(&self, capabilities: &[Capability]) -> bool {
51 let mobility_count = capabilities
52 .iter()
53 .filter(|c| {
54 c.get_capability_type() == CapabilityType::Mobility
55 && serde_json::from_str::<Value>(&c.metadata_json)
56 .ok()
57 .and_then(|v| v.get("max_speed").cloned())
58 .is_some()
59 })
60 .count();
61
62 mobility_count >= self.min_platforms
63 }
64
65 async fn compose(
66 &self,
67 capabilities: &[Capability],
68 _context: &CompositionContext,
69 ) -> Result<CompositionResult> {
70 let mobility_caps: Vec<&Capability> = capabilities
71 .iter()
72 .filter(|c| {
73 c.get_capability_type() == CapabilityType::Mobility
74 && serde_json::from_str::<Value>(&c.metadata_json)
75 .ok()
76 .and_then(|v| v.get("max_speed").cloned())
77 .is_some()
78 })
79 .collect();
80
81 if mobility_caps.len() < self.min_platforms {
82 return Ok(CompositionResult::new(vec![], 0.0));
83 }
84
85 let speeds: Vec<f64> = mobility_caps
87 .iter()
88 .filter_map(|c| {
89 serde_json::from_str::<Value>(&c.metadata_json)
90 .ok()
91 .and_then(|v| v.get("max_speed").and_then(|s| s.as_f64()))
92 })
93 .collect();
94
95 let team_speed = speeds.iter().cloned().fold(f64::INFINITY, f64::min);
96
97 let slowest = mobility_caps
99 .iter()
100 .min_by(|a, b| {
101 let speed_a = serde_json::from_str::<Value>(&a.metadata_json)
102 .ok()
103 .and_then(|v| v.get("max_speed").and_then(|s| s.as_f64()))
104 .unwrap_or(0.0);
105 let speed_b = serde_json::from_str::<Value>(&b.metadata_json)
106 .ok()
107 .and_then(|v| v.get("max_speed").and_then(|s| s.as_f64()))
108 .unwrap_or(0.0);
109 speed_a.partial_cmp(&speed_b).unwrap()
110 })
111 .unwrap();
112
113 let team_confidence = slowest.confidence;
115
116 let mut composed = Capability::new(
117 format!("constraint_team_speed_{}", uuid::Uuid::new_v4()),
118 "Team Speed".to_string(),
119 CapabilityType::Emergent,
120 team_confidence,
121 );
122 composed.metadata_json = serde_json::to_string(&json!({
123 "composition_type": "constraint",
124 "pattern": "team_speed",
125 "team_speed": team_speed,
126 "platform_count": mobility_caps.len(),
127 "limiting_platform": slowest.id,
128 "individual_speeds": speeds,
129 "description": "Team movement speed constrained by slowest member"
130 }))
131 .unwrap_or_default();
132
133 let contributor_ids: Vec<String> = mobility_caps.iter().map(|c| c.id.clone()).collect();
134
135 Ok(CompositionResult::new(vec![composed], team_confidence)
136 .with_contributors(contributor_ids))
137 }
138}
139
140pub struct CommunicationRangeConstraintRule {
146 min_nodes: usize,
148 has_mesh: bool,
150}
151
152impl CommunicationRangeConstraintRule {
153 pub fn new(min_nodes: usize, has_mesh: bool) -> Self {
155 Self {
156 min_nodes,
157 has_mesh,
158 }
159 }
160}
161
162impl Default for CommunicationRangeConstraintRule {
163 fn default() -> Self {
164 Self::new(2, false) }
166}
167
168#[async_trait]
169impl CompositionRule for CommunicationRangeConstraintRule {
170 fn name(&self) -> &str {
171 "communication_range_constraint"
172 }
173
174 fn description(&self) -> &str {
175 "Determines effective communication range based on mesh capability"
176 }
177
178 fn applies_to(&self, capabilities: &[Capability]) -> bool {
179 let comm_count = capabilities
180 .iter()
181 .filter(|c| {
182 c.get_capability_type() == CapabilityType::Communication
183 && serde_json::from_str::<Value>(&c.metadata_json)
184 .ok()
185 .and_then(|v| v.get("range").cloned())
186 .is_some()
187 })
188 .count();
189
190 comm_count >= self.min_nodes
191 }
192
193 async fn compose(
194 &self,
195 capabilities: &[Capability],
196 _context: &CompositionContext,
197 ) -> Result<CompositionResult> {
198 let comm_caps: Vec<&Capability> = capabilities
199 .iter()
200 .filter(|c| {
201 c.get_capability_type() == CapabilityType::Communication
202 && serde_json::from_str::<Value>(&c.metadata_json)
203 .ok()
204 .and_then(|v| v.get("range").cloned())
205 .is_some()
206 })
207 .collect();
208
209 if comm_caps.len() < self.min_nodes {
210 return Ok(CompositionResult::new(vec![], 0.0));
211 }
212
213 let ranges: Vec<f64> = comm_caps
215 .iter()
216 .filter_map(|c| {
217 serde_json::from_str::<Value>(&c.metadata_json)
218 .ok()
219 .and_then(|v| v.get("range").and_then(|r| r.as_f64()))
220 })
221 .collect();
222
223 let (effective_range, limiting_factor) = if self.has_mesh {
225 let max_range = ranges.iter().cloned().fold(0.0, f64::max);
227 (max_range, "mesh_enabled".to_string())
228 } else {
229 let min_range = ranges.iter().cloned().fold(f64::INFINITY, f64::min);
231 (min_range, "direct_comms_only".to_string())
232 };
233
234 let limiting_node = if self.has_mesh {
236 comm_caps
237 .iter()
238 .max_by(|a, b| {
239 let range_a = serde_json::from_str::<Value>(&a.metadata_json)
240 .ok()
241 .and_then(|v| v.get("range").and_then(|r| r.as_f64()))
242 .unwrap_or(0.0);
243 let range_b = serde_json::from_str::<Value>(&b.metadata_json)
244 .ok()
245 .and_then(|v| v.get("range").and_then(|r| r.as_f64()))
246 .unwrap_or(0.0);
247 range_a.partial_cmp(&range_b).unwrap()
248 })
249 .unwrap()
250 } else {
251 comm_caps
252 .iter()
253 .min_by(|a, b| {
254 let range_a = serde_json::from_str::<Value>(&a.metadata_json)
255 .ok()
256 .and_then(|v| v.get("range").and_then(|r| r.as_f64()))
257 .unwrap_or(0.0);
258 let range_b = serde_json::from_str::<Value>(&b.metadata_json)
259 .ok()
260 .and_then(|v| v.get("range").and_then(|r| r.as_f64()))
261 .unwrap_or(0.0);
262 range_a.partial_cmp(&range_b).unwrap()
263 })
264 .unwrap()
265 };
266
267 let avg_confidence: f32 =
269 comm_caps.iter().map(|c| c.confidence).sum::<f32>() / comm_caps.len() as f32;
270
271 let mut composed = Capability::new(
272 format!("constraint_comm_range_{}", uuid::Uuid::new_v4()),
273 "Team Communication Range".to_string(),
274 CapabilityType::Emergent,
275 avg_confidence,
276 );
277 composed.metadata_json = serde_json::to_string(&json!({
278 "composition_type": "constraint",
279 "pattern": "communication_range",
280 "effective_range": effective_range,
281 "mesh_enabled": self.has_mesh,
282 "limiting_factor": limiting_factor,
283 "limiting_node": limiting_node.id,
284 "node_count": comm_caps.len(),
285 "individual_ranges": ranges,
286 "description": if self.has_mesh {
287 "Extended range through mesh networking"
288 } else {
289 "Range constrained by weakest link"
290 }
291 }))
292 .unwrap_or_default();
293
294 let contributor_ids: Vec<String> = comm_caps.iter().map(|c| c.id.clone()).collect();
295
296 Ok(CompositionResult::new(vec![composed], avg_confidence)
297 .with_contributors(contributor_ids))
298 }
299}
300
301pub struct MissionDurationConstraintRule {
306 min_platforms: usize,
308}
309
310impl MissionDurationConstraintRule {
311 pub fn new(min_platforms: usize) -> Self {
313 Self { min_platforms }
314 }
315}
316
317impl Default for MissionDurationConstraintRule {
318 fn default() -> Self {
319 Self::new(2)
320 }
321}
322
323#[async_trait]
324impl CompositionRule for MissionDurationConstraintRule {
325 fn name(&self) -> &str {
326 "mission_duration_constraint"
327 }
328
329 fn description(&self) -> &str {
330 "Determines maximum mission duration based on shortest platform endurance"
331 }
332
333 fn applies_to(&self, capabilities: &[Capability]) -> bool {
334 let platforms_with_endurance = capabilities
335 .iter()
336 .filter(|c| {
337 serde_json::from_str::<Value>(&c.metadata_json)
338 .ok()
339 .and_then(|v| v.get("endurance_minutes").cloned())
340 .is_some()
341 })
342 .count();
343
344 platforms_with_endurance >= self.min_platforms
345 }
346
347 async fn compose(
348 &self,
349 capabilities: &[Capability],
350 _context: &CompositionContext,
351 ) -> Result<CompositionResult> {
352 let platforms: Vec<&Capability> = capabilities
353 .iter()
354 .filter(|c| {
355 serde_json::from_str::<Value>(&c.metadata_json)
356 .ok()
357 .and_then(|v| v.get("endurance_minutes").cloned())
358 .is_some()
359 })
360 .collect();
361
362 if platforms.len() < self.min_platforms {
363 return Ok(CompositionResult::new(vec![], 0.0));
364 }
365
366 let endurances: Vec<f64> = platforms
368 .iter()
369 .filter_map(|c| {
370 serde_json::from_str::<Value>(&c.metadata_json)
371 .ok()
372 .and_then(|v| v.get("endurance_minutes").and_then(|e| e.as_f64()))
373 })
374 .collect();
375
376 let mission_duration = endurances.iter().cloned().fold(f64::INFINITY, f64::min);
378
379 let limiting_platform = platforms
381 .iter()
382 .min_by(|a, b| {
383 let endurance_a = serde_json::from_str::<Value>(&a.metadata_json)
384 .ok()
385 .and_then(|v| v.get("endurance_minutes").and_then(|e| e.as_f64()))
386 .unwrap_or(0.0);
387 let endurance_b = serde_json::from_str::<Value>(&b.metadata_json)
388 .ok()
389 .and_then(|v| v.get("endurance_minutes").and_then(|e| e.as_f64()))
390 .unwrap_or(0.0);
391 endurance_a.partial_cmp(&endurance_b).unwrap()
392 })
393 .unwrap();
394
395 let mission_confidence = limiting_platform.confidence;
397
398 let mut composed = Capability::new(
399 format!("constraint_mission_duration_{}", uuid::Uuid::new_v4()),
400 "Team Mission Duration".to_string(),
401 CapabilityType::Emergent,
402 mission_confidence,
403 );
404 composed.metadata_json = serde_json::to_string(&json!({
405 "composition_type": "constraint",
406 "pattern": "mission_duration",
407 "mission_duration_minutes": mission_duration,
408 "platform_count": platforms.len(),
409 "limiting_platform": limiting_platform.id,
410 "individual_endurances": endurances,
411 "description": "Mission duration constrained by shortest endurance"
412 }))
413 .unwrap_or_default();
414
415 let contributor_ids: Vec<String> = platforms.iter().map(|c| c.id.clone()).collect();
416
417 Ok(CompositionResult::new(vec![composed], mission_confidence)
418 .with_contributors(contributor_ids))
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use serde_json::json;
426
427 #[tokio::test]
428 async fn test_team_speed_constraint() {
429 let rule = TeamSpeedConstraintRule::default();
430
431 let mut fast_platform = Capability::new(
432 "fast1".to_string(),
433 "Fast Drone".to_string(),
434 CapabilityType::Mobility,
435 0.9,
436 );
437 fast_platform.metadata_json =
438 serde_json::to_string(&json!({"max_speed": 20.0})).unwrap_or_default();
439
440 let mut slow_platform = Capability::new(
441 "slow1".to_string(),
442 "Slow Ground Vehicle".to_string(),
443 CapabilityType::Mobility,
444 0.85,
445 );
446 slow_platform.metadata_json =
447 serde_json::to_string(&json!({"max_speed": 5.0})).unwrap_or_default();
448
449 let caps = vec![fast_platform, slow_platform];
450 let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
451
452 assert!(rule.applies_to(&caps));
453
454 let result = rule.compose(&caps, &context).await.unwrap();
455 assert!(result.has_compositions());
456
457 let composed = &result.composed_capabilities[0];
458 assert_eq!(composed.name, "Team Speed");
459 let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
461 assert_eq!(metadata["team_speed"].as_f64().unwrap(), 5.0);
462 assert_eq!(metadata["limiting_platform"].as_str().unwrap(), "slow1");
463 assert_eq!(composed.confidence, 0.85);
465 }
466
467 #[tokio::test]
468 async fn test_communication_range_without_mesh() {
469 let rule = CommunicationRangeConstraintRule::new(2, false); let mut long_range = Capability::new(
472 "comm1".to_string(),
473 "Long Range Radio".to_string(),
474 CapabilityType::Communication,
475 0.9,
476 );
477 long_range.metadata_json =
478 serde_json::to_string(&json!({"range": 1000.0})).unwrap_or_default();
479
480 let mut short_range = Capability::new(
481 "comm2".to_string(),
482 "Short Range Radio".to_string(),
483 CapabilityType::Communication,
484 0.85,
485 );
486 short_range.metadata_json =
487 serde_json::to_string(&json!({"range": 200.0})).unwrap_or_default();
488
489 let caps = vec![long_range, short_range];
490 let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
491
492 let result = rule.compose(&caps, &context).await.unwrap();
493 assert!(result.has_compositions());
494
495 let composed = &result.composed_capabilities[0];
496 let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
498 assert_eq!(metadata["effective_range"].as_f64().unwrap(), 200.0);
499 assert!(!metadata["mesh_enabled"].as_bool().unwrap());
500 assert_eq!(metadata["limiting_node"].as_str().unwrap(), "comm2");
501 }
502
503 #[tokio::test]
504 async fn test_communication_range_with_mesh() {
505 let rule = CommunicationRangeConstraintRule::new(2, true); let mut long_range = Capability::new(
508 "comm1".to_string(),
509 "Long Range Radio".to_string(),
510 CapabilityType::Communication,
511 0.9,
512 );
513 long_range.metadata_json =
514 serde_json::to_string(&json!({"range": 1000.0})).unwrap_or_default();
515
516 let mut short_range = Capability::new(
517 "comm2".to_string(),
518 "Short Range Radio".to_string(),
519 CapabilityType::Communication,
520 0.85,
521 );
522 short_range.metadata_json =
523 serde_json::to_string(&json!({"range": 200.0})).unwrap_or_default();
524
525 let caps = vec![long_range, short_range];
526 let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
527
528 let result = rule.compose(&caps, &context).await.unwrap();
529 assert!(result.has_compositions());
530
531 let composed = &result.composed_capabilities[0];
532 let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
534 assert_eq!(metadata["effective_range"].as_f64().unwrap(), 1000.0);
535 assert!(metadata["mesh_enabled"].as_bool().unwrap());
536 assert_eq!(metadata["limiting_node"].as_str().unwrap(), "comm1");
537 }
538
539 #[tokio::test]
540 async fn test_mission_duration_constraint() {
541 let rule = MissionDurationConstraintRule::default();
542
543 let mut long_endurance = Capability::new(
544 "platform1".to_string(),
545 "Fixed-Wing UAV".to_string(),
546 CapabilityType::Mobility,
547 0.95,
548 );
549 long_endurance.metadata_json =
550 serde_json::to_string(&json!({"endurance_minutes": 120.0})).unwrap_or_default(); let mut short_endurance = Capability::new(
553 "platform2".to_string(),
554 "Quadcopter".to_string(),
555 CapabilityType::Mobility,
556 0.8,
557 );
558 short_endurance.metadata_json =
559 serde_json::to_string(&json!({"endurance_minutes": 25.0})).unwrap_or_default(); let caps = vec![long_endurance, short_endurance];
562 let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
563
564 assert!(rule.applies_to(&caps));
565
566 let result = rule.compose(&caps, &context).await.unwrap();
567 assert!(result.has_compositions());
568
569 let composed = &result.composed_capabilities[0];
570 assert_eq!(composed.name, "Team Mission Duration");
571 let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
573 assert_eq!(metadata["mission_duration_minutes"].as_f64().unwrap(), 25.0);
574 assert_eq!(metadata["limiting_platform"].as_str().unwrap(), "platform2");
575 assert_eq!(composed.confidence, 0.8);
577 }
578
579 #[tokio::test]
580 async fn test_constraint_rules_dont_apply_insufficient_platforms() {
581 let speed_rule = TeamSpeedConstraintRule::default();
582 let comm_rule = CommunicationRangeConstraintRule::default();
583 let duration_rule = MissionDurationConstraintRule::default();
584
585 let mut single_platform = Capability::new(
587 "platform1".to_string(),
588 "Solo Platform".to_string(),
589 CapabilityType::Mobility,
590 0.9,
591 );
592 single_platform.metadata_json =
593 serde_json::to_string(&json!({"max_speed": 10.0, "endurance_minutes": 60.0}))
594 .unwrap_or_default();
595
596 let caps = vec![single_platform];
597
598 assert!(!speed_rule.applies_to(&caps));
600 assert!(!comm_rule.applies_to(&caps));
601 assert!(!duration_rule.applies_to(&caps));
602 }
603
604 #[tokio::test]
605 async fn test_team_speed_with_three_platforms() {
606 let rule = TeamSpeedConstraintRule::default();
607
608 let platforms: Vec<Capability> = vec![
609 ("fast", 25.0, 0.95),
610 ("medium", 15.0, 0.9),
611 ("slow", 8.0, 0.85),
612 ]
613 .into_iter()
614 .map(|(name, speed, confidence)| {
615 let mut cap = Capability::new(
616 format!("platform_{}", name),
617 name.to_string(),
618 CapabilityType::Mobility,
619 confidence,
620 );
621 cap.metadata_json =
622 serde_json::to_string(&json!({"max_speed": speed})).unwrap_or_default();
623 cap
624 })
625 .collect();
626
627 let context = CompositionContext::new(vec![
628 "node1".to_string(),
629 "node2".to_string(),
630 "node3".to_string(),
631 ]);
632
633 let result = rule.compose(&platforms, &context).await.unwrap();
634 assert!(result.has_compositions());
635
636 let composed = &result.composed_capabilities[0];
637 let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
639 assert_eq!(metadata["team_speed"].as_f64().unwrap(), 8.0);
640 assert_eq!(metadata["platform_count"].as_u64().unwrap(), 3);
641 }
642
643 #[tokio::test]
644 async fn test_constraint_metadata_accuracy() {
645 let rule = TeamSpeedConstraintRule::default();
646
647 let mut platform1 = Capability::new(
648 "p1".to_string(),
649 "Platform 1".to_string(),
650 CapabilityType::Mobility,
651 0.9,
652 );
653 platform1.metadata_json =
654 serde_json::to_string(&json!({"max_speed": 12.5})).unwrap_or_default();
655
656 let mut platform2 = Capability::new(
657 "p2".to_string(),
658 "Platform 2".to_string(),
659 CapabilityType::Mobility,
660 0.85,
661 );
662 platform2.metadata_json =
663 serde_json::to_string(&json!({"max_speed": 18.3})).unwrap_or_default();
664
665 let caps = vec![platform1, platform2];
666 let context = CompositionContext::new(vec!["node1".to_string(), "node2".to_string()]);
667
668 let result = rule.compose(&caps, &context).await.unwrap();
669 let composed = &result.composed_capabilities[0];
670
671 let metadata: Value = serde_json::from_str(&composed.metadata_json).unwrap();
673 let individual_speeds = metadata["individual_speeds"].as_array().unwrap();
674 assert_eq!(individual_speeds.len(), 2);
675 assert!(individual_speeds.contains(&json!(12.5)));
676 assert!(individual_speeds.contains(&json!(18.3)));
677 }
678}