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