Skip to main content

agentic_codebase/graph/
traversal.rs

1//! Graph traversal algorithms for the code graph.
2//!
3//! BFS, DFS, and specialized traversals for dependency analysis,
4//! impact analysis, and concept grouping.
5
6use std::collections::{HashSet, VecDeque};
7
8use crate::types::EdgeType;
9
10use super::code_graph::CodeGraph;
11
12/// Direction of traversal.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Direction {
15    /// Follow edges from source to target.
16    Forward,
17    /// Follow edges from target to source (reverse).
18    Backward,
19}
20
21/// Options for graph traversal.
22#[derive(Debug, Clone)]
23pub struct TraversalOptions {
24    /// Maximum depth to traverse (-1 = unlimited).
25    pub max_depth: i32,
26    /// Only traverse edges of these types (empty = all).
27    pub edge_types: Vec<EdgeType>,
28    /// Direction of traversal.
29    pub direction: Direction,
30}
31
32impl Default for TraversalOptions {
33    fn default() -> Self {
34        Self {
35            max_depth: -1,
36            edge_types: Vec::new(),
37            direction: Direction::Forward,
38        }
39    }
40}
41
42/// Result of a traversal: a list of (unit_id, depth) pairs.
43pub type TraversalResult = Vec<(u64, u32)>;
44
45/// Perform a breadth-first traversal from the given start node.
46///
47/// Returns all reachable nodes with their distance from the start.
48pub fn bfs(graph: &CodeGraph, start_id: u64, options: &TraversalOptions) -> TraversalResult {
49    let mut visited = HashSet::new();
50    let mut queue = VecDeque::new();
51    let mut result = Vec::new();
52
53    visited.insert(start_id);
54    queue.push_back((start_id, 0u32));
55
56    while let Some((current_id, depth)) = queue.pop_front() {
57        result.push((current_id, depth));
58
59        // Check depth limit
60        if options.max_depth >= 0 && depth >= options.max_depth as u32 {
61            continue;
62        }
63
64        let neighbors = match options.direction {
65            Direction::Forward => graph.edges_from(current_id),
66            Direction::Backward => graph.edges_to(current_id),
67        };
68
69        for edge in neighbors {
70            // Filter by edge type if specified
71            if !options.edge_types.is_empty() && !options.edge_types.contains(&edge.edge_type) {
72                continue;
73            }
74
75            let next_id = match options.direction {
76                Direction::Forward => edge.target_id,
77                Direction::Backward => edge.source_id,
78            };
79
80            if visited.insert(next_id) {
81                queue.push_back((next_id, depth + 1));
82            }
83        }
84    }
85
86    result
87}
88
89/// Perform a depth-first traversal from the given start node.
90///
91/// Returns all reachable nodes with their distance from the start.
92pub fn dfs(graph: &CodeGraph, start_id: u64, options: &TraversalOptions) -> TraversalResult {
93    let mut visited = HashSet::new();
94    let mut result = Vec::new();
95    dfs_inner(graph, start_id, 0, options, &mut visited, &mut result);
96    result
97}
98
99fn dfs_inner(
100    graph: &CodeGraph,
101    current_id: u64,
102    depth: u32,
103    options: &TraversalOptions,
104    visited: &mut HashSet<u64>,
105    result: &mut TraversalResult,
106) {
107    if !visited.insert(current_id) {
108        return;
109    }
110
111    result.push((current_id, depth));
112
113    if options.max_depth >= 0 && depth >= options.max_depth as u32 {
114        return;
115    }
116
117    let neighbors = match options.direction {
118        Direction::Forward => graph.edges_from(current_id),
119        Direction::Backward => graph.edges_to(current_id),
120    };
121
122    for edge in neighbors {
123        if !options.edge_types.is_empty() && !options.edge_types.contains(&edge.edge_type) {
124            continue;
125        }
126
127        let next_id = match options.direction {
128            Direction::Forward => edge.target_id,
129            Direction::Backward => edge.source_id,
130        };
131
132        dfs_inner(graph, next_id, depth + 1, options, visited, result);
133    }
134}
135
136/// Find all paths between two nodes up to a maximum depth.
137pub fn find_paths(
138    graph: &CodeGraph,
139    from: u64,
140    to: u64,
141    max_depth: u32,
142    edge_types: &[EdgeType],
143) -> Vec<Vec<u64>> {
144    let mut paths = Vec::new();
145    let mut current_path = vec![from];
146    let mut visited = HashSet::new();
147    visited.insert(from);
148    find_paths_inner(
149        graph,
150        to,
151        max_depth,
152        edge_types,
153        &mut current_path,
154        &mut visited,
155        &mut paths,
156    );
157    paths
158}
159
160fn find_paths_inner(
161    graph: &CodeGraph,
162    target: u64,
163    max_depth: u32,
164    edge_types: &[EdgeType],
165    current_path: &mut Vec<u64>,
166    visited: &mut HashSet<u64>,
167    results: &mut Vec<Vec<u64>>,
168) {
169    let current = *current_path.last().unwrap();
170
171    if current == target && current_path.len() > 1 {
172        results.push(current_path.clone());
173        return;
174    }
175
176    if current_path.len() > max_depth as usize {
177        return;
178    }
179
180    for edge in graph.edges_from(current) {
181        if !edge_types.is_empty() && !edge_types.contains(&edge.edge_type) {
182            continue;
183        }
184
185        if visited.insert(edge.target_id) {
186            current_path.push(edge.target_id);
187            find_paths_inner(
188                graph,
189                target,
190                max_depth,
191                edge_types,
192                current_path,
193                visited,
194                results,
195            );
196            current_path.pop();
197            visited.remove(&edge.target_id);
198        }
199    }
200}
201
202/// Find the shortest path between two nodes using BFS.
203///
204/// Returns `None` if no path exists.
205pub fn shortest_path(
206    graph: &CodeGraph,
207    from: u64,
208    to: u64,
209    edge_types: &[EdgeType],
210) -> Option<Vec<u64>> {
211    if from == to {
212        return Some(vec![from]);
213    }
214
215    let mut visited = HashSet::new();
216    let mut queue = VecDeque::new();
217    let mut parent: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
218
219    visited.insert(from);
220    queue.push_back(from);
221
222    while let Some(current) = queue.pop_front() {
223        for edge in graph.edges_from(current) {
224            if !edge_types.is_empty() && !edge_types.contains(&edge.edge_type) {
225                continue;
226            }
227
228            if visited.insert(edge.target_id) {
229                parent.insert(edge.target_id, current);
230
231                if edge.target_id == to {
232                    // Reconstruct path
233                    let mut path = vec![to];
234                    let mut node = to;
235                    while let Some(&p) = parent.get(&node) {
236                        path.push(p);
237                        node = p;
238                    }
239                    path.reverse();
240                    return Some(path);
241                }
242
243                queue.push_back(edge.target_id);
244            }
245        }
246    }
247
248    None
249}