1use serde::Serialize;
4
5use crate::graph::KnowledgeGraph;
6use crate::model::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(&item.upstream.refines, "refines", graph, targets);
88 Self::add_targets(&item.upstream.derives_from, "derives_from", graph, targets);
89 Self::add_targets(&item.upstream.satisfies, "satisfies", graph, targets);
90 }
91
92 fn collect_downstream_targets(
94 item: &crate::model::Item,
95 graph: &KnowledgeGraph,
96 targets: &mut Vec<MatrixTarget>,
97 ) {
98 Self::add_targets(
99 &item.downstream.is_refined_by,
100 "is_refined_by",
101 graph,
102 targets,
103 );
104 Self::add_targets(&item.downstream.derives, "derives", graph, targets);
105 Self::add_targets(
106 &item.downstream.is_satisfied_by,
107 "is_satisfied_by",
108 graph,
109 targets,
110 );
111 }
112
113 fn add_targets(
115 ref_ids: &[crate::model::ItemId],
116 relationship: &str,
117 graph: &KnowledgeGraph,
118 targets: &mut Vec<MatrixTarget>,
119 ) {
120 for ref_id in ref_ids {
121 if let Some(target) = graph.get(ref_id) {
122 targets.push(MatrixTarget {
123 id: ref_id.as_str().to_string(),
124 name: target.name.clone(),
125 target_type: target.item_type.display_name().to_string(),
126 relationship: relationship.to_string(),
127 });
128 }
129 }
130 }
131
132 fn sort_rows(rows: &mut [MatrixRow]) {
134 rows.sort_by(|a, b| {
135 let type_order_a = Self::type_order(&a.source_type);
136 let type_order_b = Self::type_order(&b.source_type);
137 type_order_a
138 .cmp(&type_order_b)
139 .then(a.source_id.cmp(&b.source_id))
140 });
141 }
142
143 fn build_columns() -> Vec<String> {
145 ItemType::all()
146 .iter()
147 .map(|t| t.display_name().to_string())
148 .collect()
149 }
150
151 fn type_order(type_name: &str) -> usize {
153 match type_name {
154 "Solution" => 0,
155 "Use Case" => 1,
156 "Scenario" => 2,
157 "System Requirement" => 3,
158 "System Architecture" => 4,
159 "Hardware Requirement" => 5,
160 "Software Requirement" => 6,
161 "Hardware Detailed Design" => 7,
162 "Software Detailed Design" => 8,
163 _ => 9,
164 }
165 }
166
167 pub fn to_csv(&self) -> String {
169 let mut csv = String::new();
170
171 csv.push_str(
173 "Source ID,Source Name,Source Type,Target ID,Target Name,Target Type,Relationship\n",
174 );
175
176 for row in &self.rows {
178 if row.targets.is_empty() {
179 csv.push_str(&format!(
180 "{},{},{},,,, \n",
181 Self::escape_csv(&row.source_id),
182 Self::escape_csv(&row.source_name),
183 Self::escape_csv(&row.source_type),
184 ));
185 } else {
186 for target in &row.targets {
187 csv.push_str(&format!(
188 "{},{},{},{},{},{},{}\n",
189 Self::escape_csv(&row.source_id),
190 Self::escape_csv(&row.source_name),
191 Self::escape_csv(&row.source_type),
192 Self::escape_csv(&target.id),
193 Self::escape_csv(&target.name),
194 Self::escape_csv(&target.target_type),
195 Self::escape_csv(&target.relationship),
196 ));
197 }
198 }
199 }
200
201 csv
202 }
203
204 fn escape_csv(value: &str) -> String {
206 if value.contains(',') || value.contains('"') || value.contains('\n') {
207 format!("\"{}\"", value.replace('"', "\"\""))
208 } else {
209 value.to_string()
210 }
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::graph::GraphBuilder;
218 use crate::model::{Item, ItemBuilder, ItemId, SourceLocation, UpstreamRefs};
219 use std::path::PathBuf;
220
221 fn create_test_item(id: &str, item_type: ItemType) -> Item {
222 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id), 1);
223 let mut builder = ItemBuilder::new()
224 .id(ItemId::new_unchecked(id))
225 .item_type(item_type)
226 .name(format!("Test {}", id))
227 .source(source);
228
229 if item_type.requires_specification() {
230 builder = builder.specification("Test specification");
231 }
232
233 builder.build().unwrap()
234 }
235
236 fn create_test_item_with_upstream(
237 id: &str,
238 item_type: ItemType,
239 upstream: UpstreamRefs,
240 ) -> Item {
241 let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id), 1);
242 let mut builder = ItemBuilder::new()
243 .id(ItemId::new_unchecked(id))
244 .item_type(item_type)
245 .name(format!("Test {}", id))
246 .source(source)
247 .upstream(upstream);
248
249 if item_type.requires_specification() {
250 builder = builder.specification("Test specification");
251 }
252
253 builder.build().unwrap()
254 }
255
256 #[test]
257 fn test_matrix_generation() {
258 let sol = create_test_item("SOL-001", ItemType::Solution);
259 let uc = create_test_item_with_upstream(
260 "UC-001",
261 ItemType::UseCase,
262 UpstreamRefs {
263 refines: vec![ItemId::new_unchecked("SOL-001")],
264 ..Default::default()
265 },
266 );
267
268 let graph = GraphBuilder::new()
269 .add_item(sol)
270 .add_item(uc)
271 .build()
272 .unwrap();
273
274 let matrix = TraceabilityMatrix::generate(&graph);
275 assert_eq!(matrix.rows.len(), 2);
276 assert!(matrix.total_relationships > 0);
277 }
278
279 #[test]
280 fn test_matrix_csv() {
281 let sol = create_test_item("SOL-001", ItemType::Solution);
282
283 let graph = GraphBuilder::new().add_item(sol).build().unwrap();
284
285 let matrix = TraceabilityMatrix::generate(&graph);
286 let csv = matrix.to_csv();
287 assert!(csv.contains("Source ID"));
288 assert!(csv.contains("SOL-001"));
289 }
290}