Skip to main content

forge_core/
lib.rs

1//! ForgeKit - Deterministic Code Intelligence SDK
2//!
3//! This crate provides core SDK for programmatic code intelligence operations.
4//!
5//! # Overview
6//!
7//! ForgeKit unifies several code intelligence tools into a single API:
8//!
9//! - **Graph Module**: Symbol and reference queries (native implementation)
10//! - **Search Module**: Semantic code search (via LLMGrep)
11//! - **CFG Module**: Control flow analysis (via Mirage)
12//! - **Edit Module**: Span-safe code editing (via Splice)
13//!
14//! # Quick Start
15//!
16//! ```rust,no_run
17//! use forge_core::Forge;
18//!
19//! #[tokio::main]
20//! async fn main() -> anyhow::Result<()> {
21//!     let forge = Forge::open("./my-project").await?;
22//!
23//!     // Query code graph
24//!     let symbols = forge.graph().find_symbol("main").await?;
25//!     println!("Found: {:?}", symbols);
26//!
27//!     Ok(())
28//! }
29//! ```
30
31// Core modules
32pub mod error;
33pub mod types;
34
35// Public API modules
36pub mod storage;
37pub mod graph;
38pub mod search;
39pub mod cfg;
40pub mod edit;
41pub mod analysis;
42
43// Runtime layer modules (Phase 2)
44// TODO: Re-enable when dependencies are available
45// pub mod watcher;
46// pub mod indexing;
47// pub mod cache;
48// pub mod pool;
49// pub mod runtime;
50
51// Re-export sqlitegraph types for advanced usage
52pub use sqlitegraph::backend::{NodeSpec, EdgeSpec};
53pub use sqlitegraph::graph::{GraphEntity, SqliteGraph};
54pub use sqlitegraph::config::{BackendKind as SqliteGraphBackendKind, GraphConfig, open_graph};
55
56// Re-export commonly used types
57pub use error::{ForgeError, Result};
58pub use types::{SymbolId, Location};
59pub use storage::{BackendKind, UnifiedGraphStore};
60
61use anyhow::anyhow;
62
63/// Main entry point for ForgeKit SDK.
64///
65/// The `Forge` type provides access to all code intelligence modules.
66
67#[derive(Clone, Debug)]
68pub struct Forge {
69    store: std::sync::Arc<UnifiedGraphStore>,
70}
71
72impl Forge {
73    /// Opens a Forge instance on given codebase path.
74    ///
75    /// This will create or open a graph database at `.forge/graph.db`
76    /// within the codebase directory. Uses SQLite backend by default.
77    ///
78    /// # Arguments
79    ///
80    /// * `path` - Path to codebase directory
81    ///
82    /// # Returns
83    ///
84    /// A `Forge` instance or an error if database cannot be opened.
85    pub async fn open(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
86        Self::open_with_backend(path, BackendKind::default()).await
87    }
88    
89    /// Opens a Forge instance with a specific backend.
90    ///
91    /// # Arguments
92    ///
93    /// * `path` - Path to codebase directory
94    /// * `backend` - Backend kind (SQLite or Native V3)
95    ///
96    /// # Returns
97    ///
98    /// A `Forge` instance or an error if database cannot be opened.
99    pub async fn open_with_backend(
100        path: impl AsRef<std::path::Path>,
101        backend: BackendKind
102    ) -> anyhow::Result<Self> {
103        let store = std::sync::Arc::new(
104            storage::UnifiedGraphStore::open(path, backend).await?
105        );
106        Ok(Forge { store })
107    }
108    
109    /// Returns the backend kind currently in use.
110    pub fn backend_kind(&self) -> BackendKind {
111        self.store.backend_kind()
112    }
113
114    /// Returns the graph module for symbol and reference queries.
115    pub fn graph(&self) -> graph::GraphModule {
116        graph::GraphModule::new(self.store.clone())
117    }
118
119    /// Returns the search module for semantic code queries.
120    pub fn search(&self) -> search::SearchModule {
121        search::SearchModule::new(self.store.clone())
122    }
123
124    /// Returns the CFG module for control flow analysis.
125    pub fn cfg(&self) -> cfg::CfgModule {
126        cfg::CfgModule::new(self.store.clone())
127    }
128
129    /// Returns the edit module for span-safe refactoring.
130    pub fn edit(&self) -> edit::EditModule {
131        edit::EditModule::new(self.store.clone())
132    }
133
134    /// Returns the analysis module for combined operations.
135    pub fn analysis(&self) -> analysis::AnalysisModule {
136        analysis::AnalysisModule::new(
137            self.graph(),
138            self.cfg(),
139            self.edit(),
140        )
141    }
142}
143
144/// Builder for configuring and creating a Forge instance.
145#[derive(Clone, Default)]
146pub struct ForgeBuilder {
147    path: Option<std::path::PathBuf>,
148    database_path: Option<std::path::PathBuf>,
149    backend_kind: Option<BackendKind>,
150    cache_ttl: Option<std::time::Duration>,
151}
152
153impl ForgeBuilder {
154    /// Creates a new builder with default configuration.
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Sets the path to the codebase.
160    pub fn path(mut self, path: impl AsRef<std::path::Path>) -> Self {
161        Self {
162            path: Some(path.as_ref().to_path_buf()),
163            ..self
164        }
165    }
166
167    /// Sets a custom path for the graph database file.
168    pub fn database_path(mut self, db_path: impl AsRef<std::path::Path>) -> Self {
169        Self {
170            database_path: Some(db_path.as_ref().to_path_buf()),
171            ..self
172        }
173    }
174
175    /// Sets the backend kind (SQLite or Native V3).
176    pub fn backend_kind(mut self, kind: BackendKind) -> Self {
177        Self {
178            backend_kind: Some(kind),
179            ..self
180        }
181    }
182
183    /// Sets the cache TTL for query results.
184    pub fn cache_ttl(mut self, ttl: std::time::Duration) -> Self {
185        Self {
186            cache_ttl: Some(ttl),
187            ..self
188        }
189    }
190
191    /// Builds a `Forge` instance with configured options.
192    pub async fn build(self) -> anyhow::Result<Forge> {
193        let path = self.path
194            .ok_or_else(|| anyhow!("path is required"))?;
195
196        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(
197                &path,
198                self.backend_kind.unwrap_or_default()
199            ).await?);
200
201        Ok(Forge { store })
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    // Forge Creation Tests
210
211    #[tokio::test]
212    async fn test_forge_open_creates_database() {
213        let temp_dir = tempfile::tempdir().unwrap();
214        let db_path = temp_dir.path().join(".forge").join("graph.db");
215
216        // Verify database doesn't exist initially
217        assert!(!db_path.exists());
218
219        // Open Forge - this creates database directory and file
220        let forge = Forge::open(temp_dir.path()).await.unwrap();
221
222        // Verify database was created
223        assert!(db_path.exists());
224
225        // Verify Forge instance is valid
226        let _graph = forge.graph();
227        let _search = forge.search();
228
229        drop(forge);
230    }
231
232    // Module Accessor Tests
233
234    #[tokio::test]
235    async fn test_forge_graph_accessor() {
236        let temp_dir = tempfile::tempdir().unwrap();
237        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
238
239        let forge = Forge { store };
240
241        // Graph accessor should return GraphModule
242        let graph = forge.graph();
243        drop(graph);
244    }
245
246    #[tokio::test]
247    async fn test_forge_search_accessor() {
248        let temp_dir = tempfile::tempdir().unwrap();
249        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
250
251        let forge = Forge { store };
252
253        // Search accessor should return SearchModule
254        let search = forge.search();
255        drop(search);
256    }
257
258    #[tokio::test]
259    async fn test_forge_cfg_accessor() {
260        let temp_dir = tempfile::tempdir().unwrap();
261        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
262
263        let forge = Forge { store };
264
265        // CFG accessor should return CfgModule
266        let cfg = forge.cfg();
267        drop(cfg);
268    }
269
270    #[tokio::test]
271    async fn test_forge_edit_accessor() {
272        let temp_dir = tempfile::tempdir().unwrap();
273        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
274
275        let forge = Forge { store };
276
277        // Edit accessor should return EditModule
278        let edit = forge.edit();
279        drop(edit);
280    }
281
282    #[tokio::test]
283    async fn test_forge_analysis_accessor() {
284        let temp_dir = tempfile::tempdir().unwrap();
285        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(temp_dir.path(), BackendKind::default()).await.unwrap());
286
287        let forge = Forge { store };
288
289        // Analysis accessor should return AnalysisModule
290        let analysis = forge.analysis();
291        drop(analysis);
292    }
293
294    // ForgeBuilder Tests
295
296    #[test]
297    fn test_forge_builder_default() {
298        let builder = ForgeBuilder::new();
299
300        // Default builder should have None for all fields
301        assert!(builder.path.is_none());
302        assert!(builder.database_path.is_none());
303        assert!(builder.backend_kind.is_none());
304        assert!(builder.cache_ttl.is_none());
305    }
306
307    #[test]
308    fn test_forge_builder_path() {
309        let temp_dir = tempfile::tempdir().unwrap();
310        let path = temp_dir.path().join("test");
311        let builder = ForgeBuilder::new().path(&path);
312
313        assert_eq!(builder.path, Some(std::path::PathBuf::from(path)));
314    }
315
316    #[test]
317    fn test_forge_builder_database_path() {
318        let builder = ForgeBuilder::new().database_path("custom.db");
319
320        assert_eq!(builder.database_path, Some(std::path::PathBuf::from("custom.db")));
321    }
322
323    #[test]
324    fn test_forge_builder_backend_kind() {
325        let builder = ForgeBuilder::new().backend_kind(BackendKind::NativeV3);
326
327        assert_eq!(builder.backend_kind, Some(BackendKind::NativeV3));
328    }
329
330    #[test]
331    fn test_forge_builder_cache_ttl() {
332        let ttl = std::time::Duration::from_secs(60);
333        let builder = ForgeBuilder::new().cache_ttl(ttl);
334
335        assert_eq!(builder.cache_ttl, Some(ttl));
336    }
337
338    #[tokio::test]
339    async fn test_forge_builder_build_success() {
340        let temp_dir = tempfile::tempdir().unwrap();
341        let builder = ForgeBuilder::new()
342            .path(temp_dir.path())
343            .backend_kind(BackendKind::SQLite);
344
345        let forge = builder.build().await.unwrap();
346
347        assert!(forge.store.is_connected());
348    }
349
350    #[tokio::test]
351    async fn test_forge_builder_missing_path() {
352        let builder = ForgeBuilder::new();
353
354        let result = builder.build().await;
355
356        assert!(result.is_err());
357        assert!(result.unwrap_err().to_string().contains("path"));
358    }
359}