1use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10pub mod manifest;
11pub mod report;
12pub mod tracker;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct BehaviorCoverage {
17 pub api_endpoints_covered: HashSet<String>,
19
20 pub state_transitions_covered: HashSet<StateTransition>,
22
23 pub error_scenarios_covered: HashSet<String>,
25
26 pub data_flows_covered: HashSet<String>,
28
29 pub integrations_covered: HashMap<String, HashSet<String>>,
31
32 pub spans_observed: HashSet<String>,
34}
35
36impl BehaviorCoverage {
37 pub fn new() -> Self {
39 Self {
40 api_endpoints_covered: HashSet::new(),
41 state_transitions_covered: HashSet::new(),
42 error_scenarios_covered: HashSet::new(),
43 data_flows_covered: HashSet::new(),
44 integrations_covered: HashMap::new(),
45 spans_observed: HashSet::new(),
46 }
47 }
48
49 pub fn record_api_endpoint(&mut self, endpoint: String) {
51 self.api_endpoints_covered.insert(endpoint);
52 }
53
54 pub fn record_state_transition(&mut self, transition: StateTransition) {
56 self.state_transitions_covered.insert(transition);
57 }
58
59 pub fn record_error_scenario(&mut self, scenario: String) {
61 self.error_scenarios_covered.insert(scenario);
62 }
63
64 pub fn record_data_flow(&mut self, flow: String) {
66 self.data_flows_covered.insert(flow);
67 }
68
69 pub fn record_integration(&mut self, service: String, operation: String) {
71 self.integrations_covered
72 .entry(service)
73 .or_default()
74 .insert(operation);
75 }
76
77 pub fn record_span(&mut self, span_name: String) {
79 self.spans_observed.insert(span_name);
80 }
81
82 pub fn merge(&mut self, other: &BehaviorCoverage) {
84 self.api_endpoints_covered
85 .extend(other.api_endpoints_covered.clone());
86 self.state_transitions_covered
87 .extend(other.state_transitions_covered.clone());
88 self.error_scenarios_covered
89 .extend(other.error_scenarios_covered.clone());
90 self.data_flows_covered
91 .extend(other.data_flows_covered.clone());
92 self.spans_observed.extend(other.spans_observed.clone());
93
94 for (service, operations) in &other.integrations_covered {
95 self.integrations_covered
96 .entry(service.clone())
97 .or_default()
98 .extend(operations.clone());
99 }
100 }
101}
102
103impl Default for BehaviorCoverage {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub struct StateTransition {
112 pub entity: String,
114 pub from_state: Option<String>,
116 pub to_state: String,
118}
119
120impl StateTransition {
121 pub fn new(entity: impl Into<String>, from: Option<String>, to: impl Into<String>) -> Self {
123 Self {
124 entity: entity.into(),
125 from_state: from,
126 to_state: to.into(),
127 }
128 }
129
130 pub fn creation(entity: impl Into<String>, initial_state: impl Into<String>) -> Self {
132 Self::new(entity, None, initial_state)
133 }
134
135 pub fn describe(&self) -> String {
137 match &self.from_state {
138 Some(from) => format!("{}: {} → {}", self.entity, from, self.to_state),
139 None => format!("{}: created as {}", self.entity, self.to_state),
140 }
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct BehaviorCoverageReport {
147 pub total_coverage: f64,
149
150 pub dimensions: Vec<DimensionCoverage>,
152
153 pub uncovered_behaviors: UncoveredBehaviors,
155
156 pub total_behaviors: usize,
158
159 pub covered_behaviors: usize,
161}
162
163impl BehaviorCoverageReport {
164 pub fn grade(&self) -> &'static str {
166 match self.total_coverage {
167 c if c >= 90.0 => "A",
168 c if c >= 80.0 => "B",
169 c if c >= 70.0 => "C",
170 c if c >= 60.0 => "D",
171 _ => "F",
172 }
173 }
174
175 pub fn emoji(&self) -> &'static str {
177 match self.total_coverage {
178 c if c >= 90.0 => "🟢",
179 c if c >= 70.0 => "🟡",
180 c if c >= 50.0 => "🟠",
181 _ => "🔴",
182 }
183 }
184
185 pub fn format_text(&self) -> String {
187 let mut output = String::new();
188
189 output.push_str(&format!(
190 "Behavior Coverage Report\n\
191 ========================\n\n\
192 Overall Coverage: {:.1}% {} (Grade: {})\n\n",
193 self.total_coverage,
194 self.emoji(),
195 self.grade()
196 ));
197
198 output.push_str("Dimension Breakdown:\n");
199 output.push_str("┌─────────────────────┬──────────┬─────────┬──────────┐\n");
200 output.push_str("│ Dimension │ Coverage │ Weight │ Score │\n");
201 output.push_str("├─────────────────────┼──────────┼─────────┼──────────┤\n");
202
203 for dim in &self.dimensions {
204 output.push_str(&format!(
205 "│ {:<19} │ {:>6.1}% │ {:>5.0}% │ {:>6.2}% │\n",
206 dim.name,
207 dim.coverage * 100.0,
208 dim.weight * 100.0,
209 dim.weighted_score * 100.0
210 ));
211 }
212
213 output.push_str("└─────────────────────┴──────────┴─────────┴──────────┘\n\n");
214
215 if !self.uncovered_behaviors.is_empty() {
217 output.push_str("Top Uncovered Behaviors:\n");
218 let mut count = 0;
219 for behavior in self.uncovered_behaviors.top_priority(5) {
220 count += 1;
221 output.push_str(&format!(
222 "{}. {} ({})\n",
223 count, behavior.name, behavior.dimension
224 ));
225 }
226 }
227
228 output
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct DimensionCoverage {
235 pub name: String,
237 pub coverage: f64,
239 pub weight: f64,
241 pub weighted_score: f64,
243 pub total: usize,
245 pub covered: usize,
247}
248
249impl DimensionCoverage {
250 pub fn new(name: impl Into<String>, covered: usize, total: usize, weight: f64) -> Self {
252 let coverage = if total > 0 {
253 covered as f64 / total as f64
254 } else {
255 1.0 };
257
258 Self {
259 name: name.into(),
260 coverage,
261 weight,
262 weighted_score: coverage * weight,
263 total,
264 covered,
265 }
266 }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct UncoveredBehaviors {
272 pub api_endpoints: Vec<String>,
274 pub state_transitions: Vec<StateTransition>,
276 pub error_scenarios: Vec<String>,
278 pub data_flows: Vec<String>,
280 pub integrations: HashMap<String, Vec<String>>,
282 pub missing_spans: Vec<String>,
284}
285
286impl UncoveredBehaviors {
287 pub fn new() -> Self {
289 Self {
290 api_endpoints: Vec::new(),
291 state_transitions: Vec::new(),
292 error_scenarios: Vec::new(),
293 data_flows: Vec::new(),
294 integrations: HashMap::new(),
295 missing_spans: Vec::new(),
296 }
297 }
298
299 pub fn is_empty(&self) -> bool {
301 self.api_endpoints.is_empty()
302 && self.state_transitions.is_empty()
303 && self.error_scenarios.is_empty()
304 && self.data_flows.is_empty()
305 && self.integrations.is_empty()
306 && self.missing_spans.is_empty()
307 }
308
309 pub fn count(&self) -> usize {
311 let integration_ops: usize = self.integrations.values().map(|v| v.len()).sum();
312 self.api_endpoints.len()
313 + self.state_transitions.len()
314 + self.error_scenarios.len()
315 + self.data_flows.len()
316 + integration_ops
317 + self.missing_spans.len()
318 }
319
320 pub fn top_priority(&self, limit: usize) -> Vec<UncoveredBehavior> {
322 let mut behaviors = Vec::new();
323
324 for flow in &self.data_flows {
326 behaviors.push(UncoveredBehavior {
327 name: flow.clone(),
328 dimension: "Data Flow".to_string(),
329 priority: 5,
330 });
331 }
332
333 for transition in &self.state_transitions {
334 behaviors.push(UncoveredBehavior {
335 name: transition.describe(),
336 dimension: "State Transition".to_string(),
337 priority: 4,
338 });
339 }
340
341 for endpoint in &self.api_endpoints {
342 behaviors.push(UncoveredBehavior {
343 name: endpoint.clone(),
344 dimension: "API Surface".to_string(),
345 priority: 3,
346 });
347 }
348
349 for scenario in &self.error_scenarios {
350 behaviors.push(UncoveredBehavior {
351 name: scenario.clone(),
352 dimension: "Error Scenario".to_string(),
353 priority: 2,
354 });
355 }
356
357 for (service, ops) in &self.integrations {
358 for op in ops {
359 behaviors.push(UncoveredBehavior {
360 name: format!("{}.{}", service, op),
361 dimension: "Integration".to_string(),
362 priority: 1,
363 });
364 }
365 }
366
367 behaviors.sort_by(|a, b| b.priority.cmp(&a.priority));
369
370 behaviors.into_iter().take(limit).collect()
371 }
372}
373
374impl Default for UncoveredBehaviors {
375 fn default() -> Self {
376 Self::new()
377 }
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct UncoveredBehavior {
383 pub name: String,
385 pub dimension: String,
387 pub priority: u8,
389}
390
391pub const DEFAULT_WEIGHTS: DimensionWeights = DimensionWeights {
393 api_surface: 0.20,
394 state_transitions: 0.20,
395 error_scenarios: 0.15,
396 data_flows: 0.20,
397 integrations: 0.15,
398 span_coverage: 0.10,
399};
400
401#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
403pub struct DimensionWeights {
404 pub api_surface: f64,
405 pub state_transitions: f64,
406 pub error_scenarios: f64,
407 pub data_flows: f64,
408 pub integrations: f64,
409 pub span_coverage: f64,
410}
411
412impl DimensionWeights {
413 pub fn validate(&self) -> Result<()> {
415 let sum = self.api_surface
416 + self.state_transitions
417 + self.error_scenarios
418 + self.data_flows
419 + self.integrations
420 + self.span_coverage;
421
422 let diff = (sum - 1.0).abs();
423 if diff > 0.01 {
424 return Err(CleanroomError::validation_error(format!(
425 "Dimension weights must sum to 1.0, got {}",
426 sum
427 )));
428 }
429
430 Ok(())
431 }
432}
433
434impl Default for DimensionWeights {
435 fn default() -> Self {
436 DEFAULT_WEIGHTS
437 }
438}