1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct BehaviorManifest {
14 pub system: SystemInfo,
16
17 pub dimensions: Dimensions,
19
20 #[serde(default)]
22 pub weights: Option<CustomWeights>,
23}
24
25impl BehaviorManifest {
26 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 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 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 pub fn calculate_coverage(
80 &self,
81 coverage: &BehaviorCoverage,
82 ) -> Result<BehaviorCoverageReport> {
83 let weights = self.get_weights()?;
84
85 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 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 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 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 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 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 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 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 pub fn find_uncovered(&self, coverage: &BehaviorCoverage) -> UncoveredBehaviors {
176 let mut uncovered = UncoveredBehaviors::new();
177
178 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 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 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 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 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 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 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 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 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#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
318pub struct ApiSurfaceDimension {
319 pub endpoints: Vec<String>,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize, Default)]
324pub struct StateTransitionsDimension {
325 pub entities: Vec<EntityTransitions>,
326}
327
328#[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#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
347pub struct ErrorScenariosDimension {
348 pub scenarios: Vec<ErrorScenario>,
349}
350
351#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
362pub struct DataFlowsDimension {
363 pub flows: Vec<DataFlow>,
364}
365
366#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
377pub struct IntegrationsDimension {
378 pub services: Vec<IntegrationService>,
379}
380
381#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
392pub struct SpanCoverageDimension {
393 pub expected_spans: Vec<String>,
394}
395
396#[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}