peat_protocol/composition/
rules.rs1use crate::models::capability::Capability;
7use crate::Result;
8use async_trait::async_trait;
9
10use crate::models::{AuthorityLevel, NodeConfig};
11
12#[derive(Debug, Clone)]
16pub struct CompositionContext {
17 pub node_ids: Vec<String>,
19
20 pub cell_id: Option<String>,
22
23 pub timestamp: std::time::SystemTime,
25
26 pub node_configs: Vec<NodeConfig>,
28}
29
30impl CompositionContext {
31 pub fn new(node_ids: Vec<String>) -> Self {
33 Self {
34 node_ids,
35 cell_id: None,
36 timestamp: std::time::SystemTime::now(),
37 node_configs: Vec::new(),
38 }
39 }
40
41 pub fn with_cell_id(mut self, cell_id: String) -> Self {
43 self.cell_id = Some(cell_id);
44 self
45 }
46
47 pub fn with_node_configs(mut self, configs: Vec<NodeConfig>) -> Self {
49 self.node_configs = configs;
50 self
51 }
52
53 pub fn max_authority(&self) -> Option<AuthorityLevel> {
55 use crate::models::HumanMachinePairExt;
56
57 self.node_configs
58 .iter()
59 .filter_map(|config| config.operator_binding.as_ref())
60 .filter_map(|binding| binding.max_authority())
61 .max()
62 }
63
64 pub fn has_commander(&self) -> bool {
66 self.max_authority() == Some(AuthorityLevel::Commander)
67 }
68
69 pub fn authorization_bonus(&self) -> i32 {
71 use crate::models::AuthorityLevelExt;
72
73 match self.max_authority() {
74 Some(auth) => (auth.to_score() * 5.0).round() as i32,
75 None => 0,
76 }
77 }
78}
79
80impl Default for CompositionContext {
81 fn default() -> Self {
82 Self {
83 node_ids: Vec::new(),
84 cell_id: None,
85 timestamp: std::time::SystemTime::now(),
86 node_configs: Vec::new(),
87 }
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct CompositionResult {
94 pub composed_capabilities: Vec<Capability>,
96
97 pub confidence: f32,
99
100 pub contributing_capabilities: Vec<String>, }
103
104impl CompositionResult {
105 pub fn new(composed_capabilities: Vec<Capability>, confidence: f32) -> Self {
107 Self {
108 composed_capabilities,
109 confidence,
110 contributing_capabilities: Vec::new(),
111 }
112 }
113
114 pub fn with_contributors(mut self, capability_ids: Vec<String>) -> Self {
116 self.contributing_capabilities = capability_ids;
117 self
118 }
119
120 pub fn has_compositions(&self) -> bool {
122 !self.composed_capabilities.is_empty()
123 }
124}
125
126#[async_trait]
134pub trait CompositionRule: Send + Sync {
135 fn name(&self) -> &str;
137
138 fn description(&self) -> &str;
140
141 fn applies_to(&self, capabilities: &[Capability]) -> bool;
146
147 async fn compose(
152 &self,
153 capabilities: &[Capability],
154 context: &CompositionContext,
155 ) -> Result<CompositionResult>;
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::models::capability::{CapabilityExt, CapabilityType};
162
163 #[test]
164 fn test_composition_context_creation() {
165 let node_ids = vec!["node1".to_string(), "node2".to_string()];
166 let ctx = CompositionContext::new(node_ids.clone());
167
168 assert_eq!(ctx.node_ids, node_ids);
169 assert_eq!(ctx.cell_id, None);
170 }
171
172 #[test]
173 fn test_composition_context_with_cell() {
174 let ctx = CompositionContext::new(vec!["node1".to_string()])
175 .with_cell_id("cell_alpha".to_string());
176
177 assert_eq!(ctx.cell_id, Some("cell_alpha".to_string()));
178 }
179
180 #[test]
181 fn test_composition_result_creation() {
182 let capability = Capability::new(
183 "test".to_string(),
184 "Test Capability".to_string(),
185 CapabilityType::Emergent,
186 0.9,
187 );
188
189 let result = CompositionResult::new(vec![capability], 0.8);
190
191 assert_eq!(result.composed_capabilities.len(), 1);
192 assert_eq!(result.confidence, 0.8);
193 assert!(result.has_compositions());
194 }
195
196 #[test]
197 fn test_composition_result_with_contributors() {
198 let capability = Capability::new(
199 "emergent".to_string(),
200 "Emergent".to_string(),
201 CapabilityType::Emergent,
202 0.9,
203 );
204
205 let result = CompositionResult::new(vec![capability], 0.8)
206 .with_contributors(vec!["cap1".to_string(), "cap2".to_string()]);
207
208 assert_eq!(result.contributing_capabilities.len(), 2);
209 assert!(result
210 .contributing_capabilities
211 .contains(&"cap1".to_string()));
212 }
213
214 #[test]
215 fn test_empty_composition_result() {
216 let result = CompositionResult::new(vec![], 0.0);
217
218 assert!(!result.has_compositions());
219 assert_eq!(result.composed_capabilities.len(), 0);
220 }
221
222 #[test]
223 fn test_composition_context_with_node_configs() {
224 use crate::models::{NodeConfig, NodeConfigExt};
225
226 let config = NodeConfig::new("UAV".to_string());
227 let ctx =
228 CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
229
230 assert_eq!(ctx.node_configs.len(), 1);
231 }
232
233 #[test]
234 fn test_composition_context_max_authority_none() {
235 let ctx = CompositionContext::new(vec!["node1".to_string()]);
236
237 assert!(ctx.max_authority().is_none());
238 assert!(!ctx.has_commander());
239 assert_eq!(ctx.authorization_bonus(), 0);
240 }
241
242 #[test]
243 fn test_composition_context_max_authority_with_commander() {
244 use crate::models::{
245 HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt, Operator,
246 OperatorExt, OperatorRank,
247 };
248
249 let operator = Operator::new(
250 "op1".to_string(),
251 "CPT Smith".to_string(),
252 OperatorRank::O3,
253 AuthorityLevel::Commander,
254 "11A".to_string(),
255 );
256
257 let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
258 let config = NodeConfig::with_operator("Command Post".to_string(), binding);
259
260 let ctx =
261 CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
262
263 assert_eq!(ctx.max_authority(), Some(AuthorityLevel::Commander));
264 assert!(ctx.has_commander());
265 assert_eq!(ctx.authorization_bonus(), 4); }
267
268 #[test]
269 fn test_composition_context_authorization_bonus_levels() {
270 use crate::models::{
271 HumanMachinePair, HumanMachinePairExt, NodeConfig, NodeConfigExt, Operator,
272 OperatorExt, OperatorRank,
273 };
274
275 let operator = Operator::new(
277 "op1".to_string(),
278 "SGT Jones".to_string(),
279 OperatorRank::E5,
280 AuthorityLevel::Supervisor,
281 "11B".to_string(),
282 );
283
284 let binding = HumanMachinePair::one_to_one(operator, "node1".to_string());
285 let config = NodeConfig::with_operator("Control Station".to_string(), binding);
286
287 let ctx =
288 CompositionContext::new(vec!["node1".to_string()]).with_node_configs(vec![config]);
289
290 assert_eq!(ctx.max_authority(), Some(AuthorityLevel::Supervisor));
291 assert!(!ctx.has_commander());
292 let bonus = ctx.authorization_bonus();
294 assert!((2..=3).contains(&bonus));
295 }
296
297 #[test]
298 fn test_composition_context_default() {
299 let ctx = CompositionContext::default();
300
301 assert!(ctx.node_ids.is_empty());
302 assert!(ctx.cell_id.is_none());
303 assert!(ctx.node_configs.is_empty());
304 }
305}