clnrm_core/coverage/
manifest.rs

1//! Behavior Manifest - defines the complete inventory of system behaviors
2
3use crate::coverage::{
4    BehaviorCoverage, BehaviorCoverageReport, DimensionCoverage, DimensionWeights, StateTransition,
5    UncoveredBehaviors, DEFAULT_WEIGHTS,
6};
7use crate::error::{CleanroomError, Result};
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10
11/// Complete behavior manifest for a system
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BehaviorManifest {
14    /// System metadata
15    pub system: SystemInfo,
16
17    /// Dimension definitions
18    pub dimensions: Dimensions,
19
20    /// Optional custom weights
21    #[serde(default)]
22    pub weights: Option<CustomWeights>,
23}
24
25impl BehaviorManifest {
26    /// Load manifest from TOML file
27    pub fn load(path: impl AsRef<Path>) -> Result<Self> {
28        let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
29            CleanroomError::io_error(format!(
30                "Failed to read behavior manifest {}: {}",
31                path.as_ref().display(),
32                e
33            ))
34        })?;
35
36        toml::from_str(&content).map_err(|e| {
37            CleanroomError::validation_error(format!("Failed to parse behavior manifest: {}", e))
38        })
39    }
40
41    /// Save manifest to TOML file
42    pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
43        let content = toml::to_string_pretty(self).map_err(|e| {
44            CleanroomError::validation_error(format!(
45                "Failed to serialize behavior manifest: {}",
46                e
47            ))
48        })?;
49
50        std::fs::write(path.as_ref(), content).map_err(|e| {
51            CleanroomError::io_error(format!(
52                "Failed to write behavior manifest {}: {}",
53                path.as_ref().display(),
54                e
55            ))
56        })
57    }
58
59    /// Get effective weights (custom or default)
60    pub fn get_weights(&self) -> Result<DimensionWeights> {
61        let weights = self
62            .weights
63            .as_ref()
64            .map(|w| DimensionWeights {
65                api_surface: w.api_surface,
66                state_transitions: w.state_transitions,
67                error_scenarios: w.error_scenarios,
68                data_flows: w.data_flows,
69                integrations: w.integrations,
70                span_coverage: w.span_coverage,
71            })
72            .unwrap_or(DEFAULT_WEIGHTS);
73
74        weights.validate()?;
75        Ok(weights)
76    }
77
78    /// Calculate coverage report from tracked coverage
79    pub fn calculate_coverage(
80        &self,
81        coverage: &BehaviorCoverage,
82    ) -> Result<BehaviorCoverageReport> {
83        let weights = self.get_weights()?;
84
85        // Calculate API surface coverage
86        let api_covered = coverage.api_endpoints_covered.len();
87        let api_total = self.dimensions.api_surface.endpoints.len();
88        let api_dim =
89            DimensionCoverage::new("API Surface", api_covered, api_total, weights.api_surface);
90
91        // Calculate state transition coverage
92        let transitions_covered = coverage.state_transitions_covered.len();
93        let transitions_total = self.count_total_transitions();
94        let transitions_dim = DimensionCoverage::new(
95            "State Transitions",
96            transitions_covered,
97            transitions_total,
98            weights.state_transitions,
99        );
100
101        // Calculate error scenario coverage
102        let errors_covered = coverage.error_scenarios_covered.len();
103        let errors_total = self.dimensions.error_scenarios.scenarios.len();
104        let errors_dim = DimensionCoverage::new(
105            "Error Scenarios",
106            errors_covered,
107            errors_total,
108            weights.error_scenarios,
109        );
110
111        // Calculate data flow coverage
112        let flows_covered = coverage.data_flows_covered.len();
113        let flows_total = self.dimensions.data_flows.flows.len();
114        let flows_dim =
115            DimensionCoverage::new("Data Flows", flows_covered, flows_total, weights.data_flows);
116
117        // Calculate integration coverage
118        let (integrations_covered, integrations_total) = self.count_integration_coverage(coverage);
119        let integrations_dim = DimensionCoverage::new(
120            "Integrations",
121            integrations_covered,
122            integrations_total,
123            weights.integrations,
124        );
125
126        // Calculate span coverage
127        let spans_covered = coverage.spans_observed.len();
128        let spans_total = self.dimensions.span_coverage.expected_spans.len();
129        let spans_dim = DimensionCoverage::new(
130            "Span Coverage",
131            spans_covered,
132            spans_total,
133            weights.span_coverage,
134        );
135
136        // Calculate total coverage
137        let dimensions = vec![
138            api_dim,
139            transitions_dim,
140            errors_dim,
141            flows_dim,
142            integrations_dim,
143            spans_dim,
144        ];
145
146        let total_coverage: f64 = dimensions.iter().map(|d| d.weighted_score).sum::<f64>() * 100.0;
147
148        let total_behaviors = api_total
149            + transitions_total
150            + errors_total
151            + flows_total
152            + integrations_total
153            + spans_total;
154
155        let covered_behaviors = api_covered
156            + transitions_covered
157            + errors_covered
158            + flows_covered
159            + integrations_covered
160            + spans_covered;
161
162        // Find uncovered behaviors
163        let uncovered_behaviors = self.find_uncovered(coverage);
164
165        Ok(BehaviorCoverageReport {
166            total_coverage,
167            dimensions,
168            uncovered_behaviors,
169            total_behaviors,
170            covered_behaviors,
171        })
172    }
173
174    /// Find uncovered behaviors
175    pub fn find_uncovered(&self, coverage: &BehaviorCoverage) -> UncoveredBehaviors {
176        let mut uncovered = UncoveredBehaviors::new();
177
178        // Find uncovered API endpoints
179        for endpoint in &self.dimensions.api_surface.endpoints {
180            if !coverage.api_endpoints_covered.contains(endpoint) {
181                uncovered.api_endpoints.push(endpoint.clone());
182            }
183        }
184
185        // Find uncovered state transitions
186        for entity in &self.dimensions.state_transitions.entities {
187            for transition in &entity.transitions {
188                let state_transition = StateTransition::new(
189                    entity.name.clone(),
190                    transition.from.clone(),
191                    transition.to.clone(),
192                );
193                if !coverage
194                    .state_transitions_covered
195                    .contains(&state_transition)
196                {
197                    uncovered.state_transitions.push(state_transition);
198                }
199            }
200        }
201
202        // Find uncovered error scenarios
203        for scenario in &self.dimensions.error_scenarios.scenarios {
204            if !coverage.error_scenarios_covered.contains(&scenario.name) {
205                uncovered.error_scenarios.push(scenario.name.clone());
206            }
207        }
208
209        // Find uncovered data flows
210        for flow in &self.dimensions.data_flows.flows {
211            if !coverage.data_flows_covered.contains(&flow.name) {
212                uncovered.data_flows.push(flow.name.clone());
213            }
214        }
215
216        // Find uncovered integrations
217        for service in &self.dimensions.integrations.services {
218            let covered_ops = coverage
219                .integrations_covered
220                .get(&service.name)
221                .cloned()
222                .unwrap_or_default();
223
224            let missing_ops: Vec<String> = service
225                .operations
226                .iter()
227                .filter(|op| !covered_ops.contains(*op))
228                .cloned()
229                .collect();
230
231            if !missing_ops.is_empty() {
232                uncovered
233                    .integrations
234                    .insert(service.name.clone(), missing_ops);
235            }
236        }
237
238        // Find missing spans
239        for span in &self.dimensions.span_coverage.expected_spans {
240            if !coverage.spans_observed.contains(span) {
241                uncovered.missing_spans.push(span.clone());
242            }
243        }
244
245        uncovered
246    }
247
248    /// Count total state transitions defined
249    fn count_total_transitions(&self) -> usize {
250        self.dimensions
251            .state_transitions
252            .entities
253            .iter()
254            .map(|e| e.transitions.len())
255            .sum()
256    }
257
258    /// Count integration coverage
259    fn count_integration_coverage(&self, coverage: &BehaviorCoverage) -> (usize, usize) {
260        let mut covered = 0;
261        let mut total = 0;
262
263        for service in &self.dimensions.integrations.services {
264            total += service.operations.len();
265            let covered_ops = coverage
266                .integrations_covered
267                .get(&service.name)
268                .map(|ops| ops.len())
269                .unwrap_or(0);
270            covered += covered_ops;
271        }
272
273        (covered, total)
274    }
275
276    /// Create an empty manifest template
277    pub fn template(system_name: impl Into<String>) -> Self {
278        Self {
279            system: SystemInfo {
280                name: system_name.into(),
281                version: "1.0.0".to_string(),
282                description: None,
283            },
284            dimensions: Dimensions::default(),
285            weights: None,
286        }
287    }
288}
289
290/// System information
291#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct SystemInfo {
293    pub name: String,
294    pub version: String,
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub description: Option<String>,
297}
298
299/// All behavior dimensions
300#[derive(Debug, Clone, Serialize, Deserialize, Default)]
301pub struct Dimensions {
302    #[serde(default)]
303    pub api_surface: ApiSurfaceDimension,
304    #[serde(default)]
305    pub state_transitions: StateTransitionsDimension,
306    #[serde(default)]
307    pub error_scenarios: ErrorScenariosDimension,
308    #[serde(default)]
309    pub data_flows: DataFlowsDimension,
310    #[serde(default)]
311    pub integrations: IntegrationsDimension,
312    #[serde(default)]
313    pub span_coverage: SpanCoverageDimension,
314}
315
316/// API surface dimension
317#[derive(Debug, Clone, Serialize, Deserialize, Default)]
318pub struct ApiSurfaceDimension {
319    pub endpoints: Vec<String>,
320}
321
322/// State transitions dimension
323#[derive(Debug, Clone, Serialize, Deserialize, Default)]
324pub struct StateTransitionsDimension {
325    pub entities: Vec<EntityTransitions>,
326}
327
328/// State transitions for an entity
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct EntityTransitions {
331    pub name: String,
332    pub states: Vec<String>,
333    pub transitions: Vec<TransitionDef>,
334}
335
336/// Transition definition
337#[derive(Debug, Clone, Serialize, Deserialize)]
338pub struct TransitionDef {
339    pub from: Option<String>,
340    pub to: String,
341    #[serde(skip_serializing_if = "Option::is_none")]
342    pub trigger: Option<String>,
343}
344
345/// Error scenarios dimension
346#[derive(Debug, Clone, Serialize, Deserialize, Default)]
347pub struct ErrorScenariosDimension {
348    pub scenarios: Vec<ErrorScenario>,
349}
350
351/// Error scenario definition
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ErrorScenario {
354    pub name: String,
355    pub code: u16,
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub description: Option<String>,
358}
359
360/// Data flows dimension
361#[derive(Debug, Clone, Serialize, Deserialize, Default)]
362pub struct DataFlowsDimension {
363    pub flows: Vec<DataFlow>,
364}
365
366/// Data flow definition
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct DataFlow {
369    pub name: String,
370    pub steps: Vec<String>,
371    #[serde(skip_serializing_if = "Option::is_none")]
372    pub description: Option<String>,
373}
374
375/// Integrations dimension
376#[derive(Debug, Clone, Serialize, Deserialize, Default)]
377pub struct IntegrationsDimension {
378    pub services: Vec<IntegrationService>,
379}
380
381/// Integration service definition
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct IntegrationService {
384    pub name: String,
385    pub operations: Vec<String>,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub service_type: Option<String>,
388}
389
390/// Span coverage dimension
391#[derive(Debug, Clone, Serialize, Deserialize, Default)]
392pub struct SpanCoverageDimension {
393    pub expected_spans: Vec<String>,
394}
395
396/// Custom weights configuration
397#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct CustomWeights {
399    pub api_surface: f64,
400    pub state_transitions: f64,
401    pub error_scenarios: f64,
402    pub data_flows: f64,
403    pub integrations: f64,
404    pub span_coverage: f64,
405}