1use petgraph::Direction;
4use petgraph::graph::{DiGraph, NodeIndex};
5use petgraph::visit::EdgeRef;
6use std::collections::HashMap;
7
8use strsim::levenshtein;
9
10use crate::error::SaraError;
11use crate::model::{Item, ItemId, ItemType, RelationshipType};
12
13#[derive(Debug)]
15pub enum LookupResult<'a> {
16 Found(&'a Item),
18 NotFound {
20 suggestions: Vec<&'a ItemId>,
22 },
23}
24
25#[derive(Debug)]
27pub struct KnowledgeGraph {
28 graph: DiGraph<Item, RelationshipType>,
30
31 index: HashMap<ItemId, NodeIndex>,
33}
34
35impl KnowledgeGraph {
36 pub fn new() -> Self {
38 Self {
39 graph: DiGraph::new(),
40 index: HashMap::new(),
41 }
42 }
43
44 pub fn item_count(&self) -> usize {
46 self.graph.node_count()
47 }
48
49 pub fn relationship_count(&self) -> usize {
51 self.graph.edge_count()
52 }
53
54 fn add_item(&mut self, item: Item) -> NodeIndex {
56 let id = item.id.clone();
57 let idx = self.graph.add_node(item);
58 self.index.insert(id, idx);
59 idx
60 }
61
62 fn add_relationship(&mut self, from: &ItemId, to: &ItemId, rel_type: RelationshipType) {
64 if let (Some(from_idx), Some(to_idx)) = (self.index.get(from), self.index.get(to)) {
65 self.graph.add_edge(*from_idx, *to_idx, rel_type);
66 }
67 }
68
69 pub fn get(&self, id: &ItemId) -> Option<&Item> {
71 let idx = self.index.get(id)?;
72 self.graph.node_weight(*idx)
73 }
74
75 pub fn get_mut(&mut self, id: &ItemId) -> Option<&mut Item> {
77 let idx = self.index.get(id)?;
78 self.graph.node_weight_mut(*idx)
79 }
80
81 pub fn contains(&self, id: &ItemId) -> bool {
83 self.index.contains_key(id)
84 }
85
86 pub fn items(&self) -> impl Iterator<Item = &Item> {
88 self.graph.node_weights()
89 }
90
91 pub fn item_ids(&self) -> impl Iterator<Item = &ItemId> {
93 self.index.keys()
94 }
95
96 pub fn items_by_type(&self, item_type: ItemType) -> Vec<&Item> {
98 self.graph
99 .node_weights()
100 .filter(|item| item.item_type == item_type)
101 .collect()
102 }
103
104 pub fn count_by_type(&self) -> HashMap<ItemType, usize> {
106 let mut counts = HashMap::new();
107 for item in self.graph.node_weights() {
108 *counts.entry(item.item_type).or_insert(0) += 1;
109 }
110 counts
111 }
112
113 pub fn parents(&self, id: &ItemId) -> Vec<&Item> {
115 let Some(idx) = self.index.get(id) else {
116 return Vec::new();
117 };
118
119 self.graph
120 .edges_directed(*idx, Direction::Outgoing)
121 .filter(|edge| edge.weight().is_upstream())
122 .filter_map(|edge| self.graph.node_weight(edge.target()))
123 .collect()
124 }
125
126 pub fn children(&self, id: &ItemId) -> Vec<&Item> {
128 let Some(idx) = self.index.get(id) else {
129 return Vec::new();
130 };
131
132 self.graph
133 .edges_directed(*idx, Direction::Incoming)
134 .filter(|edge| edge.weight().is_upstream())
135 .filter_map(|edge| self.graph.node_weight(edge.source()))
136 .collect()
137 }
138
139 pub fn orphans(&self) -> Vec<&Item> {
141 self.graph
142 .node_weights()
143 .filter(|item| {
144 if item.item_type.is_root() {
146 return false;
147 }
148 !item.has_upstream()
150 })
151 .collect()
152 }
153
154 pub fn inner(&self) -> &DiGraph<Item, RelationshipType> {
156 &self.graph
157 }
158
159 pub fn inner_mut(&mut self) -> &mut DiGraph<Item, RelationshipType> {
161 &mut self.graph
162 }
163
164 pub fn node_index(&self, id: &ItemId) -> Option<NodeIndex> {
166 self.index.get(id).copied()
167 }
168
169 pub fn relationships(&self) -> Vec<(ItemId, ItemId, RelationshipType)> {
171 self.graph
172 .edge_references()
173 .filter_map(|edge| {
174 let from = self.graph.node_weight(edge.source())?;
175 let to = self.graph.node_weight(edge.target())?;
176 Some((from.id.clone(), to.id.clone(), *edge.weight()))
177 })
178 .collect()
179 }
180
181 pub fn lookup(&self, id: &str) -> LookupResult<'_> {
185 let item_id = ItemId::new_unchecked(id);
186
187 if let Some(item) = self.get(&item_id) {
188 return LookupResult::Found(item);
189 }
190
191 let suggestions = self
192 .find_similar_ids_scored(id, 5)
193 .into_iter()
194 .map(|(id, _)| id)
195 .collect();
196 LookupResult::NotFound { suggestions }
197 }
198
199 pub fn lookup_or_suggest(&self, id: &str) -> Result<&Item, SaraError> {
204 let item_id = ItemId::new_unchecked(id);
205
206 if let Some(item) = self.get(&item_id) {
207 return Ok(item);
208 }
209
210 let suggestions = self.find_similar_ids(id, 3);
211 Err(SaraError::ItemNotFound {
212 id: id.to_string(),
213 suggestions,
214 })
215 }
216
217 pub fn find_similar_ids(&self, query: &str, max_suggestions: usize) -> Vec<String> {
222 self.find_similar_ids_scored(query, max_suggestions)
223 .into_iter()
224 .map(|(id, _)| id.as_str().to_string())
225 .collect()
226 }
227
228 pub fn check_parent_exists(&self, item_type: ItemType) -> Result<(), SaraError> {
232 let Some(parent_type) = item_type.required_parent_type() else {
233 return Ok(());
234 };
235
236 let has_parents = self.items().any(|item| item.item_type == parent_type);
237
238 if has_parents {
239 Ok(())
240 } else {
241 Err(SaraError::MissingParent {
242 item_type: item_type.display_name().to_string(),
243 parent_type: parent_type.display_name().to_string(),
244 })
245 }
246 }
247
248 fn find_similar_ids_scored(
250 &self,
251 query: &str,
252 max_suggestions: usize,
253 ) -> Vec<(&ItemId, usize)> {
254 let query_lower = query.to_lowercase();
255
256 let mut scored: Vec<_> = self
257 .item_ids()
258 .map(|id| {
259 let id_lower = id.as_str().to_lowercase();
260 let distance = levenshtein(&query_lower, &id_lower);
261 (id, distance)
262 })
263 .collect();
264
265 scored.sort_by_key(|(_, distance)| *distance);
266
267 scored
268 .into_iter()
269 .filter(|(_, distance)| *distance <= query.len().max(3))
270 .take(max_suggestions)
271 .collect()
272 }
273}
274
275impl Default for KnowledgeGraph {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281#[derive(Debug, Default)]
283pub struct KnowledgeGraphBuilder {
284 items: Vec<Item>,
285}
286
287impl KnowledgeGraphBuilder {
288 pub fn new() -> Self {
290 Self::default()
291 }
292
293 pub fn add_item(mut self, item: Item) -> Self {
295 self.items.push(item);
296 self
297 }
298
299 pub fn add_items(mut self, items: impl IntoIterator<Item = Item>) -> Self {
301 self.items.extend(items);
302 self
303 }
304
305 pub fn build(self) -> Result<KnowledgeGraph, SaraError> {
307 let mut graph = KnowledgeGraph::new();
308
309 let edges: Vec<_> = self.items.iter().flat_map(Self::collect_edges).collect();
311
312 for item in self.items {
314 graph.add_item(item);
315 }
316
317 for (from, to, rel_type) in edges {
319 graph.add_relationship(&from, &to, rel_type);
320 }
321
322 Ok(graph)
323 }
324
325 fn collect_edges(item: &Item) -> Vec<(ItemId, ItemId, RelationshipType)> {
327 let mut edges = Vec::new();
328
329 for rel in &item.relationships {
330 edges.push((item.id.clone(), rel.to.clone(), rel.relationship_type));
331
332 let inverse = match rel.relationship_type {
334 RelationshipType::Justifies => Some(RelationshipType::IsJustifiedBy),
335 RelationshipType::IsRefinedBy => Some(RelationshipType::Refines),
336 RelationshipType::Derives => Some(RelationshipType::DerivesFrom),
337 RelationshipType::IsSatisfiedBy => Some(RelationshipType::Satisfies),
338 RelationshipType::IsJustifiedBy => Some(RelationshipType::Justifies),
339 _ => None,
340 };
341 if let Some(inv_type) = inverse {
342 edges.push((rel.to.clone(), item.id.clone(), inv_type));
343 }
344 }
345
346 for target_id in item.attributes.depends_on() {
348 edges.push((
349 item.id.clone(),
350 target_id.clone(),
351 RelationshipType::DependsOn,
352 ));
353 }
354
355 for target_id in item.attributes.supersedes() {
357 edges.push((
358 item.id.clone(),
359 target_id.clone(),
360 RelationshipType::Supersedes,
361 ));
362 edges.push((
363 target_id.clone(),
364 item.id.clone(),
365 RelationshipType::IsSupersededBy,
366 ));
367 }
368
369 edges
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::model::Relationship;
377 use crate::test_utils::{
378 create_test_adr, create_test_item, create_test_item_with_relationships,
379 };
380
381 #[test]
382 fn test_add_and_get_item() {
383 let graph = KnowledgeGraphBuilder::new()
384 .add_item(create_test_item("SOL-001", ItemType::Solution))
385 .build()
386 .unwrap();
387
388 let id = ItemId::new_unchecked("SOL-001");
389 assert!(graph.contains(&id));
390 assert_eq!(graph.get(&id).unwrap().name, "Test SOL-001");
391 }
392
393 #[test]
394 fn test_items_by_type() {
395 let graph = KnowledgeGraphBuilder::new()
396 .add_item(create_test_item("SOL-001", ItemType::Solution))
397 .add_item(create_test_item("UC-001", ItemType::UseCase))
398 .add_item(create_test_item("UC-002", ItemType::UseCase))
399 .build()
400 .unwrap();
401
402 let solutions = graph.items_by_type(ItemType::Solution);
403 assert_eq!(solutions.len(), 1);
404
405 let use_cases = graph.items_by_type(ItemType::UseCase);
406 assert_eq!(use_cases.len(), 2);
407 }
408
409 #[test]
410 fn test_item_count() {
411 let graph = KnowledgeGraphBuilder::new().build().unwrap();
412 assert_eq!(graph.item_count(), 0);
413
414 let graph = KnowledgeGraphBuilder::new()
415 .add_item(create_test_item("SOL-001", ItemType::Solution))
416 .build()
417 .unwrap();
418 assert_eq!(graph.item_count(), 1);
419
420 let graph = KnowledgeGraphBuilder::new()
421 .add_item(create_test_item("SOL-001", ItemType::Solution))
422 .add_item(create_test_item("UC-001", ItemType::UseCase))
423 .build()
424 .unwrap();
425 assert_eq!(graph.item_count(), 2);
426 }
427
428 #[test]
429 fn test_build_simple_graph() {
430 let graph = KnowledgeGraphBuilder::new()
431 .add_item(create_test_item("SOL-001", ItemType::Solution))
432 .build()
433 .unwrap();
434
435 assert_eq!(graph.item_count(), 1);
436 }
437
438 #[test]
439 fn test_build_graph_with_relationships() {
440 let sol = create_test_item("SOL-001", ItemType::Solution);
441 let uc = create_test_item_with_relationships(
442 "UC-001",
443 ItemType::UseCase,
444 vec![Relationship::new(
445 ItemId::new_unchecked("SOL-001"),
446 RelationshipType::Refines,
447 )],
448 );
449
450 let graph = KnowledgeGraphBuilder::new()
451 .add_item(sol)
452 .add_item(uc)
453 .build()
454 .unwrap();
455
456 assert_eq!(graph.item_count(), 2);
457 assert_eq!(graph.relationship_count(), 1);
458 }
459
460 #[test]
461 fn test_adr_justifies_relationship() {
462 let sysarch = create_test_item("SYSARCH-001", ItemType::SystemArchitecture);
464 let adr = create_test_adr("ADR-001", &["SYSARCH-001"], &[]);
466
467 let graph = KnowledgeGraphBuilder::new()
468 .add_item(sysarch)
469 .add_item(adr)
470 .build()
471 .unwrap();
472
473 assert_eq!(graph.item_count(), 2);
474 assert_eq!(graph.relationship_count(), 2);
477 }
478
479 #[test]
480 fn test_adr_supersession_relationship() {
481 let adr_old = create_test_adr("ADR-001", &[], &[]);
483 let adr_new = create_test_adr("ADR-002", &[], &["ADR-001"]);
484
485 let graph = KnowledgeGraphBuilder::new()
486 .add_item(adr_old)
487 .add_item(adr_new)
488 .build()
489 .unwrap();
490
491 assert_eq!(graph.item_count(), 2);
492 assert_eq!(graph.relationship_count(), 2);
495 }
496
497 #[test]
498 fn test_lookup_found() {
499 let graph = KnowledgeGraphBuilder::new()
500 .add_item(create_test_item("SOL-001", ItemType::Solution))
501 .build()
502 .unwrap();
503
504 match graph.lookup("SOL-001") {
505 LookupResult::Found(item) => {
506 assert_eq!(item.id.as_str(), "SOL-001");
507 }
508 LookupResult::NotFound { .. } => panic!("Expected to find item"),
509 }
510 }
511
512 #[test]
513 fn test_lookup_not_found_with_suggestions() {
514 let graph = KnowledgeGraphBuilder::new()
515 .add_item(create_test_item("SOL-001", ItemType::Solution))
516 .add_item(create_test_item("SOL-002", ItemType::Solution))
517 .add_item(create_test_item("UC-001", ItemType::UseCase))
518 .build()
519 .unwrap();
520
521 match graph.lookup("SOL-003") {
522 LookupResult::Found(_) => panic!("Should not find item"),
523 LookupResult::NotFound { suggestions } => {
524 assert!(!suggestions.is_empty());
525 let suggestion_strs: Vec<_> = suggestions.iter().map(|id| id.as_str()).collect();
526 assert!(
527 suggestion_strs.contains(&"SOL-001") || suggestion_strs.contains(&"SOL-002")
528 );
529 }
530 }
531 }
532}