Skip to main content

lean_ctx/core/
context_artifacts.rs

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}