sara_core/report/
coverage.rs1use serde::Serialize;
4
5use crate::graph::KnowledgeGraph;
6use crate::model::ItemType;
7
8#[derive(Debug, Clone, Serialize)]
10pub struct TypeCoverage {
11 pub item_type: ItemType,
13 pub type_name: String,
15 pub total: usize,
17 pub complete: usize,
19 pub incomplete: usize,
21 pub coverage_percent: f64,
23}
24
25#[derive(Debug, Clone, Serialize)]
27pub struct IncompleteItem {
28 pub id: String,
30 pub name: String,
32 pub item_type: String,
34 pub reason: String,
36}
37
38#[derive(Debug, Clone, Serialize)]
40pub struct CoverageReport {
41 pub overall_coverage: f64,
43 pub by_type: Vec<TypeCoverage>,
45 pub incomplete_items: Vec<IncompleteItem>,
47 pub total_items: usize,
49 pub complete_items: usize,
51}
52
53impl CoverageReport {
54 pub fn generate(graph: &KnowledgeGraph) -> Self {
56 let mut by_type = Vec::new();
57 let mut incomplete_items = Vec::new();
58 let mut total_items = 0;
59 let mut complete_items = 0;
60
61 for item_type in ItemType::all() {
63 let items = graph.items_by_type(*item_type);
64 let total = items.len();
65
66 if total == 0 {
67 continue;
68 }
69
70 let mut type_complete = 0;
71 let mut type_incomplete = 0;
72
73 for item in items {
74 let is_complete = Self::check_item_complete(item, graph);
75
76 if is_complete {
77 type_complete += 1;
78 } else {
79 type_incomplete += 1;
80 incomplete_items.push(Self::create_incomplete_item(item, graph));
81 }
82 }
83
84 let coverage_percent = if total > 0 {
85 (type_complete as f64 / total as f64) * 100.0
86 } else {
87 100.0
88 };
89
90 by_type.push(TypeCoverage {
91 item_type: *item_type,
92 type_name: item_type.display_name().to_string(),
93 total,
94 complete: type_complete,
95 incomplete: type_incomplete,
96 coverage_percent,
97 });
98
99 total_items += total;
100 complete_items += type_complete;
101 }
102
103 let overall_coverage = if total_items > 0 {
104 (complete_items as f64 / total_items as f64) * 100.0
105 } else {
106 100.0
107 };
108
109 Self {
110 overall_coverage,
111 by_type,
112 incomplete_items,
113 total_items,
114 complete_items,
115 }
116 }
117
118 fn check_item_complete(item: &crate::model::Item, graph: &KnowledgeGraph) -> bool {
120 if item.item_type.is_root() {
122 return !graph.children(&item.id).is_empty();
123 }
124
125 if item.item_type.is_leaf() {
127 return !item.upstream.is_empty();
128 }
129
130 !item.upstream.is_empty()
132 }
133
134 fn create_incomplete_item(item: &crate::model::Item, graph: &KnowledgeGraph) -> IncompleteItem {
136 let reason = if item.item_type.is_root() && graph.children(&item.id).is_empty() {
137 "No downstream items defined".to_string()
138 } else if item.upstream.is_empty() {
139 format!(
140 "Missing parent {}",
141 Self::expected_parent_type(item.item_type)
142 )
143 } else {
144 "Incomplete traceability".to_string()
145 };
146
147 IncompleteItem {
148 id: item.id.as_str().to_string(),
149 name: item.name.clone(),
150 item_type: item.item_type.display_name().to_string(),
151 reason,
152 }
153 }
154
155 fn expected_parent_type(item_type: ItemType) -> &'static str {
157 match item_type {
158 ItemType::Solution => "N/A (root)",
159 ItemType::UseCase => "Solution",
160 ItemType::Scenario => "Use Case",
161 ItemType::SystemRequirement => "Scenario",
162 ItemType::SystemArchitecture => "System Requirement",
163 ItemType::HardwareRequirement => "System Architecture",
164 ItemType::SoftwareRequirement => "System Architecture",
165 ItemType::HardwareDetailedDesign => "Hardware Requirement",
166 ItemType::SoftwareDetailedDesign => "Software Requirement",
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174 use crate::graph::GraphBuilder;
175 use crate::model::{Item, ItemBuilder, ItemId, SourceLocation, UpstreamRefs};
176 use std::path::PathBuf;
177
178 fn create_test_item(id: &str, item_type: ItemType) -> Item {
179 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
180 let mut builder = ItemBuilder::new()
181 .id(ItemId::new_unchecked(id))
182 .item_type(item_type)
183 .name(format!("Test {}", id))
184 .source(source);
185
186 if item_type.requires_specification() {
187 builder = builder.specification("Test specification");
188 }
189
190 builder.build().unwrap()
191 }
192
193 fn create_test_item_with_upstream(
194 id: &str,
195 item_type: ItemType,
196 upstream: UpstreamRefs,
197 ) -> Item {
198 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
199 let mut builder = ItemBuilder::new()
200 .id(ItemId::new_unchecked(id))
201 .item_type(item_type)
202 .name(format!("Test {}", id))
203 .source(source)
204 .upstream(upstream);
205
206 if item_type.requires_specification() {
207 builder = builder.specification("Test specification");
208 }
209
210 builder.build().unwrap()
211 }
212
213 #[test]
214 fn test_coverage_report_complete() {
215 let sol = create_test_item("SOL-001", ItemType::Solution);
216 let uc = create_test_item_with_upstream(
217 "UC-001",
218 ItemType::UseCase,
219 UpstreamRefs {
220 refines: vec![ItemId::new_unchecked("SOL-001")],
221 ..Default::default()
222 },
223 );
224
225 let graph = GraphBuilder::new()
226 .add_item(sol)
227 .add_item(uc)
228 .build()
229 .unwrap();
230
231 let report = CoverageReport::generate(&graph);
232 assert!(report.overall_coverage > 0.0);
233 }
234
235 #[test]
236 fn test_coverage_report_incomplete() {
237 let uc = create_test_item("UC-001", ItemType::UseCase);
239
240 let graph = GraphBuilder::new().add_item(uc).build().unwrap();
241
242 let report = CoverageReport::generate(&graph);
243 assert!(!report.incomplete_items.is_empty());
244 }
245}