lean_ctx/core/
graph_provider.rs1use super::graph_index::ProjectIndex;
2#[cfg(feature = "property-graph")]
3use super::property_graph::CodeGraph;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum GraphProviderSource {
7 PropertyGraph,
8 GraphIndex,
9}
10
11pub enum GraphProvider {
12 #[cfg(feature = "property-graph")]
13 PropertyGraph(CodeGraph),
14 GraphIndex(ProjectIndex),
15}
16
17pub struct OpenGraphProvider {
18 pub source: GraphProviderSource,
19 pub provider: GraphProvider,
20}
21
22impl GraphProvider {
23 pub fn node_count(&self) -> Option<usize> {
24 match self {
25 #[cfg(feature = "property-graph")]
26 GraphProvider::PropertyGraph(g) => g.node_count().ok(),
27 GraphProvider::GraphIndex(i) => Some(i.file_count()),
28 }
29 }
30
31 pub fn edge_count(&self) -> Option<usize> {
32 match self {
33 #[cfg(feature = "property-graph")]
34 GraphProvider::PropertyGraph(g) => g.edge_count().ok(),
35 GraphProvider::GraphIndex(i) => Some(i.edge_count()),
36 }
37 }
38
39 pub fn dependencies(&self, file_path: &str) -> Vec<String> {
40 match self {
41 #[cfg(feature = "property-graph")]
42 GraphProvider::PropertyGraph(g) => g.dependencies(file_path).unwrap_or_default(),
43 GraphProvider::GraphIndex(i) => i
44 .edges
45 .iter()
46 .filter(|e| e.kind == "import" && e.from == file_path)
47 .map(|e| e.to.clone())
48 .collect(),
49 }
50 }
51
52 pub fn dependents(&self, file_path: &str) -> Vec<String> {
53 match self {
54 #[cfg(feature = "property-graph")]
55 GraphProvider::PropertyGraph(g) => g.dependents(file_path).unwrap_or_default(),
56 GraphProvider::GraphIndex(i) => i
57 .edges
58 .iter()
59 .filter(|e| e.kind == "import" && e.to == file_path)
60 .map(|e| e.from.clone())
61 .collect(),
62 }
63 }
64
65 pub fn related(&self, file_path: &str, depth: usize) -> Vec<String> {
66 match self {
67 #[cfg(feature = "property-graph")]
68 GraphProvider::PropertyGraph(g) => g
69 .impact_analysis(file_path, depth)
70 .map(|r| r.affected_files)
71 .unwrap_or_default(),
72 GraphProvider::GraphIndex(i) => i.get_related(file_path, depth),
73 }
74 }
75}
76
77pub fn open_best_effort(project_root: &str) -> Option<OpenGraphProvider> {
78 #[cfg(feature = "property-graph")]
79 {
80 let root = std::path::Path::new(project_root);
81 if let Ok(pg) = CodeGraph::open(root) {
82 if let Ok(n) = pg.node_count() {
83 if n > 0 {
84 return Some(OpenGraphProvider {
85 source: GraphProviderSource::PropertyGraph,
86 provider: GraphProvider::PropertyGraph(pg),
87 });
88 }
89 }
90 }
91 }
92
93 if let Some(idx) = super::index_orchestrator::try_load_graph_index(project_root) {
94 if !idx.files.is_empty() {
95 return Some(OpenGraphProvider {
96 source: GraphProviderSource::GraphIndex,
97 provider: GraphProvider::GraphIndex(idx),
98 });
99 }
100 }
101
102 None
103}
104
105pub fn open_or_build(project_root: &str) -> Option<OpenGraphProvider> {
106 if let Some(p) = open_best_effort(project_root) {
107 return Some(p);
108 }
109 let idx = super::graph_index::load_or_build(project_root);
110 if idx.files.is_empty() {
111 return None;
112 }
113 Some(OpenGraphProvider {
114 source: GraphProviderSource::GraphIndex,
115 provider: GraphProvider::GraphIndex(idx),
116 })
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn best_effort_prefers_graph_index_when_property_graph_empty() {
125 let _lock = crate::core::data_dir::test_env_lock();
126 let tmp = tempfile::tempdir().expect("tempdir");
127 let data = tmp.path().join("data");
128 std::fs::create_dir_all(&data).expect("mkdir data");
129 std::env::set_var("NEBU_CTX_DATA_DIR", data.to_string_lossy().to_string());
130
131 let project_root = tmp.path().join("proj");
132 std::fs::create_dir_all(&project_root).expect("mkdir proj");
133 let root = project_root.to_string_lossy().to_string();
134
135 let mut idx = ProjectIndex::new(&root);
136 idx.files.insert(
137 "src/main.rs".to_string(),
138 super::super::graph_index::FileEntry {
139 path: "src/main.rs".to_string(),
140 hash: "h".to_string(),
141 language: "rs".to_string(),
142 line_count: 1,
143 token_count: 1,
144 exports: vec![],
145 summary: "".to_string(),
146 },
147 );
148 idx.save().expect("save index");
149
150 let open = open_best_effort(&root).expect("open");
151 assert_eq!(open.source, GraphProviderSource::GraphIndex);
152
153 std::env::remove_var("NEBU_CTX_DATA_DIR");
154 }
155
156 #[test]
157 fn best_effort_none_when_no_graphs() {
158 let _lock = crate::core::data_dir::test_env_lock();
159 let tmp = tempfile::tempdir().expect("tempdir");
160 let data = tmp.path().join("data");
161 std::fs::create_dir_all(&data).expect("mkdir data");
162 std::env::set_var("NEBU_CTX_DATA_DIR", data.to_string_lossy().to_string());
163
164 let project_root = tmp.path().join("proj");
165 std::fs::create_dir_all(&project_root).expect("mkdir proj");
166 let root = project_root.to_string_lossy().to_string();
167
168 let open = open_best_effort(&root);
169 assert!(open.is_none());
170
171 std::env::remove_var("NEBU_CTX_DATA_DIR");
172 }
173}