sara_core/graph/
builder.rs

1//! Graph builder for constructing knowledge graphs.
2
3#![allow(clippy::result_large_err)]
4
5use std::path::PathBuf;
6
7use crate::error::SaraError;
8use crate::graph::KnowledgeGraph;
9use crate::model::{Item, ItemId, RelationshipType};
10
11/// Builder for constructing knowledge graphs.
12#[derive(Debug, Default)]
13pub struct GraphBuilder {
14    items: Vec<Item>,
15    strict_mode: bool,
16    repositories: Vec<PathBuf>,
17}
18
19impl GraphBuilder {
20    /// Creates a new graph builder.
21    pub fn new() -> Self {
22        Self::default()
23    }
24
25    /// Sets strict orphan checking mode.
26    pub fn with_strict_mode(mut self, strict: bool) -> Self {
27        self.strict_mode = strict;
28        self
29    }
30
31    /// Adds a repository path.
32    pub fn add_repository(mut self, path: impl Into<PathBuf>) -> Self {
33        self.repositories.push(path.into());
34        self
35    }
36
37    /// Adds an item to the graph.
38    pub fn add_item(mut self, item: Item) -> Self {
39        self.items.push(item);
40        self
41    }
42
43    /// Adds multiple items to the graph.
44    pub fn add_items(mut self, items: impl IntoIterator<Item = Item>) -> Self {
45        self.items.extend(items);
46        self
47    }
48
49    /// Builds the knowledge graph.
50    pub fn build(self) -> Result<KnowledgeGraph, SaraError> {
51        let mut graph = KnowledgeGraph::new(self.strict_mode);
52
53        // First pass: add all items
54        for item in &self.items {
55            graph.add_item(item.clone());
56        }
57
58        // Second pass: add relationships based on item references
59        for item in &self.items {
60            self.add_relationships_for_item(&mut graph, item);
61        }
62
63        Ok(graph)
64    }
65
66    /// Adds relationships for an item based on its references.
67    fn add_relationships_for_item(&self, graph: &mut KnowledgeGraph, item: &Item) {
68        // Add upstream relationships
69        for target_id in &item.upstream.refines {
70            graph.add_relationship(&item.id, target_id, RelationshipType::Refines);
71        }
72        for target_id in &item.upstream.derives_from {
73            graph.add_relationship(&item.id, target_id, RelationshipType::DerivesFrom);
74        }
75        for target_id in &item.upstream.satisfies {
76            graph.add_relationship(&item.id, target_id, RelationshipType::Satisfies);
77        }
78
79        // Add downstream relationships (and their inverse for bidirectional graph queries)
80        for target_id in &item.downstream.is_refined_by {
81            graph.add_relationship(&item.id, target_id, RelationshipType::IsRefinedBy);
82            // Add inverse: target refines this item
83            graph.add_relationship(target_id, &item.id, RelationshipType::Refines);
84        }
85        for target_id in &item.downstream.derives {
86            graph.add_relationship(&item.id, target_id, RelationshipType::Derives);
87            // Add inverse: target derives_from this item
88            graph.add_relationship(target_id, &item.id, RelationshipType::DerivesFrom);
89        }
90        for target_id in &item.downstream.is_satisfied_by {
91            graph.add_relationship(&item.id, target_id, RelationshipType::IsSatisfiedBy);
92            // Add inverse: target satisfies this item
93            graph.add_relationship(target_id, &item.id, RelationshipType::Satisfies);
94        }
95
96        // Add peer dependencies
97        for target_id in &item.attributes.depends_on {
98            graph.add_relationship(&item.id, target_id, RelationshipType::DependsOn);
99        }
100    }
101}
102
103/// Resolves cross-repository references in the graph.
104pub fn resolve_cross_repository_refs(graph: &mut KnowledgeGraph) -> Vec<(ItemId, ItemId)> {
105    let mut unresolved = Vec::new();
106
107    // Collect all referenced IDs
108    let referenced_ids: Vec<ItemId> = graph
109        .items()
110        .flat_map(|item| item.all_references())
111        .cloned()
112        .collect();
113
114    // Check which ones are missing
115    for ref_id in referenced_ids {
116        if !graph.contains(&ref_id) {
117            // Find the item that references this missing ID
118            for item in graph.items() {
119                if item.all_references().iter().any(|id| **id == ref_id) {
120                    unresolved.push((item.id.clone(), ref_id.clone()));
121                    break;
122                }
123            }
124        }
125    }
126
127    unresolved
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::model::{ItemBuilder, ItemType, SourceLocation, UpstreamRefs};
134    use std::path::PathBuf;
135
136    fn create_test_item(id: &str, item_type: ItemType) -> Item {
137        let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
138        let mut builder = ItemBuilder::new()
139            .id(ItemId::new_unchecked(id))
140            .item_type(item_type)
141            .name(format!("Test {}", id))
142            .source(source);
143
144        if item_type.requires_specification() {
145            builder = builder.specification("Test specification");
146        }
147
148        builder.build().unwrap()
149    }
150
151    fn create_test_item_with_upstream(
152        id: &str,
153        item_type: ItemType,
154        upstream: UpstreamRefs,
155    ) -> Item {
156        let source = SourceLocation::new(PathBuf::from("/repo"), format!("{}.md", id));
157        let mut builder = ItemBuilder::new()
158            .id(ItemId::new_unchecked(id))
159            .item_type(item_type)
160            .name(format!("Test {}", id))
161            .source(source)
162            .upstream(upstream);
163
164        if item_type.requires_specification() {
165            builder = builder.specification("Test specification");
166        }
167
168        builder.build().unwrap()
169    }
170
171    #[test]
172    fn test_build_simple_graph() {
173        let graph = GraphBuilder::new()
174            .add_item(create_test_item("SOL-001", ItemType::Solution))
175            .build()
176            .unwrap();
177
178        assert_eq!(graph.item_count(), 1);
179    }
180
181    #[test]
182    fn test_build_graph_with_relationships() {
183        let sol = create_test_item("SOL-001", ItemType::Solution);
184        let uc = create_test_item_with_upstream(
185            "UC-001",
186            ItemType::UseCase,
187            UpstreamRefs {
188                refines: vec![ItemId::new_unchecked("SOL-001")],
189                ..Default::default()
190            },
191        );
192
193        let graph = GraphBuilder::new()
194            .add_item(sol)
195            .add_item(uc)
196            .build()
197            .unwrap();
198
199        assert_eq!(graph.item_count(), 2);
200        assert_eq!(graph.relationship_count(), 1);
201    }
202
203    #[test]
204    fn test_strict_mode() {
205        let graph = GraphBuilder::new()
206            .with_strict_mode(true)
207            .add_item(create_test_item("SOL-001", ItemType::Solution))
208            .build()
209            .unwrap();
210
211        assert!(graph.is_strict_mode());
212    }
213}