1use serde::Serialize;
4
5use crate::graph::KnowledgeGraph;
6use crate::model::{FieldName, ItemType};
7
8#[derive(Debug, Clone, Serialize)]
10pub struct MatrixRow {
11 pub source_id: String,
13 pub source_name: String,
15 pub source_type: String,
17 pub targets: Vec<MatrixTarget>,
19}
20
21#[derive(Debug, Clone, Serialize)]
23pub struct MatrixTarget {
24 pub id: String,
26 pub name: String,
28 pub target_type: String,
30 pub relationship: String,
32}
33
34#[derive(Debug, Clone, Serialize)]
36pub struct TraceabilityMatrix {
37 pub rows: Vec<MatrixRow>,
39 pub columns: Vec<String>,
41 pub total_relationships: usize,
43}
44
45impl TraceabilityMatrix {
46 pub fn generate(graph: &KnowledgeGraph) -> Self {
48 let mut rows: Vec<MatrixRow> = graph
49 .items()
50 .map(|item| Self::build_row(item, graph))
51 .collect();
52
53 let total_relationships = rows.iter().map(|r| r.targets.len()).sum();
54
55 Self::sort_rows(&mut rows);
56
57 let columns = Self::build_columns();
58
59 Self {
60 rows,
61 columns,
62 total_relationships,
63 }
64 }
65
66 fn build_row(item: &crate::model::Item, graph: &KnowledgeGraph) -> MatrixRow {
68 let mut targets = Vec::new();
69
70 Self::collect_upstream_targets(item, graph, &mut targets);
71 Self::collect_downstream_targets(item, graph, &mut targets);
72
73 MatrixRow {
74 source_id: item.id.as_str().to_string(),
75 source_name: item.name.clone(),
76 source_type: item.item_type.display_name().to_string(),
77 targets,
78 }
79 }
80
81 fn collect_upstream_targets(
83 item: &crate::model::Item,
84 graph: &KnowledgeGraph,
85 targets: &mut Vec<MatrixTarget>,
86 ) {
87 Self::add_targets(
88 &item.upstream.refines,
89 FieldName::Refines.as_str(),
90 graph,
91 targets,
92 );
93 Self::add_targets(
94 &item.upstream.derives_from,
95 FieldName::DerivesFrom.as_str(),
96 graph,
97 targets,
98 );
99 Self::add_targets(
100 &item.upstream.satisfies,
101 FieldName::Satisfies.as_str(),
102 graph,
103 targets,
104 );
105 }
106
107 fn collect_downstream_targets(
109 item: &crate::model::Item,
110 graph: &KnowledgeGraph,
111 targets: &mut Vec<MatrixTarget>,
112 ) {
113 Self::add_targets(
114 &item.downstream.is_refined_by,
115 FieldName::IsRefinedBy.as_str(),
116 graph,
117 targets,
118 );
119 Self::add_targets(
120 &item.downstream.derives,
121 FieldName::Derives.as_str(),
122 graph,
123 targets,
124 );
125 Self::add_targets(
126 &item.downstream.is_satisfied_by,
127 FieldName::IsSatisfiedBy.as_str(),
128 graph,
129 targets,
130 );
131 }
132
133 fn add_targets(
135 ref_ids: &[crate::model::ItemId],
136 relationship: &str,
137 graph: &KnowledgeGraph,
138 targets: &mut Vec<MatrixTarget>,
139 ) {
140 for ref_id in ref_ids {
141 if let Some(target) = graph.get(ref_id) {
142 targets.push(MatrixTarget {
143 id: ref_id.as_str().to_string(),
144 name: target.name.clone(),
145 target_type: target.item_type.display_name().to_string(),
146 relationship: relationship.to_string(),
147 });
148 }
149 }
150 }
151
152 fn sort_rows(rows: &mut [MatrixRow]) {
154 rows.sort_by(|a, b| {
155 let type_order_a = Self::type_order(&a.source_type);
156 let type_order_b = Self::type_order(&b.source_type);
157 type_order_a
158 .cmp(&type_order_b)
159 .then(a.source_id.cmp(&b.source_id))
160 });
161 }
162
163 fn build_columns() -> Vec<String> {
165 ItemType::all()
166 .iter()
167 .map(|t| t.display_name().to_string())
168 .collect()
169 }
170
171 fn type_order(type_name: &str) -> usize {
173 match type_name {
174 "Solution" => 0,
175 "Use Case" => 1,
176 "Scenario" => 2,
177 "System Requirement" => 3,
178 "System Architecture" => 4,
179 "Hardware Requirement" => 5,
180 "Software Requirement" => 6,
181 "Hardware Detailed Design" => 7,
182 "Software Detailed Design" => 8,
183 _ => 9,
184 }
185 }
186
187 pub fn to_csv(&self) -> String {
189 let mut csv = String::new();
190
191 csv.push_str(
193 "Source ID,Source Name,Source Type,Target ID,Target Name,Target Type,Relationship\n",
194 );
195
196 for row in &self.rows {
198 if row.targets.is_empty() {
199 csv.push_str(&format!(
200 "{},{},{},,,, \n",
201 Self::escape_csv(&row.source_id),
202 Self::escape_csv(&row.source_name),
203 Self::escape_csv(&row.source_type),
204 ));
205 } else {
206 for target in &row.targets {
207 csv.push_str(&format!(
208 "{},{},{},{},{},{},{}\n",
209 Self::escape_csv(&row.source_id),
210 Self::escape_csv(&row.source_name),
211 Self::escape_csv(&row.source_type),
212 Self::escape_csv(&target.id),
213 Self::escape_csv(&target.name),
214 Self::escape_csv(&target.target_type),
215 Self::escape_csv(&target.relationship),
216 ));
217 }
218 }
219 }
220
221 csv
222 }
223
224 fn escape_csv(value: &str) -> String {
226 if value.contains(',') || value.contains('"') || value.contains('\n') {
227 format!("\"{}\"", value.replace('"', "\"\""))
228 } else {
229 value.to_string()
230 }
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::graph::GraphBuilder;
238 use crate::model::{Item, ItemBuilder, ItemId, SourceLocation, UpstreamRefs};
239 use std::path::PathBuf;
240
241 fn create_test_item(id: &str, item_type: ItemType) -> Item {
242 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
243 let mut builder = ItemBuilder::new()
244 .id(ItemId::new_unchecked(id))
245 .item_type(item_type)
246 .name(format!("Test {}", id))
247 .source(source);
248
249 if item_type.requires_specification() {
250 builder = builder.specification("Test specification");
251 }
252
253 builder.build().unwrap()
254 }
255
256 fn create_test_item_with_upstream(
257 id: &str,
258 item_type: ItemType,
259 upstream: UpstreamRefs,
260 ) -> Item {
261 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
262 let mut builder = ItemBuilder::new()
263 .id(ItemId::new_unchecked(id))
264 .item_type(item_type)
265 .name(format!("Test {}", id))
266 .source(source)
267 .upstream(upstream);
268
269 if item_type.requires_specification() {
270 builder = builder.specification("Test specification");
271 }
272
273 builder.build().unwrap()
274 }
275
276 #[test]
277 fn test_matrix_generation() {
278 let sol = create_test_item("SOL-001", ItemType::Solution);
279 let uc = create_test_item_with_upstream(
280 "UC-001",
281 ItemType::UseCase,
282 UpstreamRefs {
283 refines: vec![ItemId::new_unchecked("SOL-001")],
284 ..Default::default()
285 },
286 );
287
288 let graph = GraphBuilder::new()
289 .add_item(sol)
290 .add_item(uc)
291 .build()
292 .unwrap();
293
294 let matrix = TraceabilityMatrix::generate(&graph);
295 assert_eq!(matrix.rows.len(), 2);
296 assert!(matrix.total_relationships > 0);
297 }
298
299 #[test]
300 fn test_matrix_csv() {
301 let sol = create_test_item("SOL-001", ItemType::Solution);
302
303 let graph = GraphBuilder::new().add_item(sol).build().unwrap();
304
305 let matrix = TraceabilityMatrix::generate(&graph);
306 let csv = matrix.to_csv();
307 assert!(csv.contains("Source ID"));
308 assert!(csv.contains("SOL-001"));
309 }
310}