1use std::path::Path;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use serde::Serialize;
5
6use crate::core::bm25_index::BM25Index;
7use crate::core::git_util::{git_dirty, git_out};
8use crate::core::graph_provider::{self, GraphProvider};
9
10#[derive(Debug, Clone, Copy)]
11pub struct ExportOptions {
12 pub include_deps_graph: bool,
13 pub max_nodes: usize,
14 pub max_edges: usize,
15}
16
17#[derive(Debug, Serialize)]
18pub struct ContextArtifacts {
19 pub generated_at_ms: u64,
20 pub project_root: String,
21 pub git: GitInfo,
22 pub index: IndexSummary,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub deps_graph: Option<DepsGraph>,
25}
26
27#[derive(Debug, Serialize)]
28pub struct GitInfo {
29 pub head: Option<String>,
30 pub branch: Option<String>,
31 pub dirty: bool,
32}
33
34#[derive(Debug, Serialize)]
35pub struct IndexSummary {
36 pub graph_index: GraphIndexSummary,
37 pub bm25_index: Bm25IndexSummary,
38 pub property_graph: PropertyGraphSummary,
39}
40
41#[derive(Debug, Serialize)]
42pub struct GraphIndexSummary {
43 pub files: usize,
44 pub symbols: usize,
45 pub edges: usize,
46 pub last_scan: String,
47 pub index_dir: Option<String>,
48}
49
50#[derive(Debug, Serialize)]
51pub struct Bm25IndexSummary {
52 pub files: usize,
53 pub chunks: usize,
54 pub index_file: String,
55}
56
57#[derive(Debug, Serialize)]
58pub struct PropertyGraphSummary {
59 pub exists: bool,
60 pub db_path: String,
61 pub nodes: Option<usize>,
62 pub edges: Option<usize>,
63}
64
65#[derive(Debug, Serialize)]
66pub struct DepsGraph {
67 pub nodes: Vec<String>,
68 pub edges: Vec<DepsEdge>,
69 pub truncated: bool,
70}
71
72#[derive(Debug, Serialize)]
73pub struct DepsEdge {
74 pub from: String,
75 pub to: String,
76 pub kind: String,
77}
78
79pub fn export_json(project_root: &Path, opts: &ExportOptions) -> Result<String, String> {
80 let artifacts = build(project_root, opts)?;
81 serde_json::to_string_pretty(&artifacts).map_err(|e| e.to_string())
82}
83
84pub fn build(project_root: &Path, opts: &ExportOptions) -> Result<ContextArtifacts, String> {
85 let root_s = project_root.to_string_lossy().to_string();
86
87 let git = git_info(project_root);
88
89 let open = graph_provider::open_or_build(&root_s);
90 let (files, symbols, edges, last_scan) = if let Some(ref o) = open {
91 let gp = &o.provider;
92 (
93 gp.file_count(),
94 gp.symbol_count(),
95 gp.edge_count().unwrap_or(0),
96 gp.last_scan(),
97 )
98 } else {
99 (0, 0, 0, String::new())
100 };
101 let graph_summary = GraphIndexSummary {
102 files,
103 symbols,
104 edges,
105 last_scan,
106 index_dir: GraphProvider::index_dir(&root_s).map(|p| p.to_string_lossy().to_string()),
107 };
108
109 let bm25 = BM25Index::load_or_build(project_root);
110 let bm25_summary = Bm25IndexSummary {
111 files: bm25.files.len(),
112 chunks: bm25.doc_count,
113 index_file: BM25Index::index_file_path(project_root)
114 .to_string_lossy()
115 .to_string(),
116 };
117
118 let pg = property_graph_summary(project_root);
119
120 let deps_graph = if opts.include_deps_graph {
121 open.as_ref()
122 .map(|o| build_deps_graph(&o.provider, opts.max_nodes, opts.max_edges))
123 } else {
124 None
125 };
126
127 Ok(ContextArtifacts {
128 generated_at_ms: now_ms(),
129 project_root: root_s,
130 git,
131 index: IndexSummary {
132 graph_index: graph_summary,
133 bm25_index: bm25_summary,
134 property_graph: pg,
135 },
136 deps_graph,
137 })
138}
139
140fn build_deps_graph(gp: &GraphProvider, max_nodes: usize, max_edges: usize) -> DepsGraph {
141 let max_nodes = max_nodes.max(1);
142 let max_edges = max_edges.max(1);
143
144 let mut nodes = gp.file_paths();
145 nodes.sort();
146
147 let truncated_nodes = nodes.len() > max_nodes;
148 if truncated_nodes {
149 nodes.truncate(max_nodes);
150 }
151 let node_set: std::collections::HashSet<&str> = nodes.iter().map(String::as_str).collect();
152
153 let all_edges = gp.edges();
154 let mut edges: Vec<DepsEdge> = Vec::new();
155 for e in &all_edges {
156 if edges.len() >= max_edges {
157 break;
158 }
159 if !node_set.contains(e.from.as_str()) || !node_set.contains(e.to.as_str()) {
160 continue;
161 }
162 edges.push(DepsEdge {
163 from: e.from.clone(),
164 to: e.to.clone(),
165 kind: e.kind.clone(),
166 });
167 }
168
169 let truncated_edges = all_edges.len() > edges.len() && edges.len() >= max_edges;
170 DepsGraph {
171 nodes,
172 edges,
173 truncated: truncated_nodes || truncated_edges,
174 }
175}
176
177fn property_graph_summary(project_root: &Path) -> PropertyGraphSummary {
178 let root_str = project_root.to_string_lossy();
179 let db_path = crate::core::property_graph::graph_dir(&root_str).join("graph.db");
180 let db_path_s = db_path.to_string_lossy().to_string();
181 if !db_path.exists() {
182 return PropertyGraphSummary {
183 exists: false,
184 db_path: db_path_s,
185 nodes: None,
186 edges: None,
187 };
188 }
189
190 match crate::core::property_graph::CodeGraph::open(&root_str) {
191 Ok(g) => PropertyGraphSummary {
192 exists: true,
193 db_path: g.db_path().to_string_lossy().to_string(),
194 nodes: g.node_count().ok(),
195 edges: g.edge_count().ok(),
196 },
197 Err(_) => PropertyGraphSummary {
198 exists: true,
199 db_path: db_path_s,
200 nodes: None,
201 edges: None,
202 },
203 }
204}
205
206fn git_info(project_root: &Path) -> GitInfo {
207 let head = git_out(project_root, &["rev-parse", "--short", "HEAD"]);
208 let branch = git_out(project_root, &["rev-parse", "--abbrev-ref", "HEAD"]);
209 let dirty = git_dirty(project_root);
210 GitInfo {
211 head,
212 branch,
213 dirty,
214 }
215}
216
217fn now_ms() -> u64 {
218 SystemTime::now()
219 .duration_since(UNIX_EPOCH)
220 .unwrap_or_default()
221 .as_millis() as u64
222}