Skip to main content

forgekit_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 forgekit_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
35pub mod build;
36pub mod dependency;
37pub mod diagnostic;
38pub mod diff;
39pub mod progress;
40pub mod project;
41pub mod workspace;
42
43use std::sync::Arc;
44
45// Public API modules
46pub mod analysis;
47pub mod cfg;
48pub mod edit;
49pub mod graph;
50pub mod search;
51pub mod storage;
52pub mod treesitter;
53
54// Knowledge graph module (sqlitegraph native-v3)
55pub mod knowledge;
56
57// Runtime layer modules
58pub mod cache;
59pub mod indexing;
60pub mod pool;
61pub mod runtime;
62pub mod watcher;
63
64// Re-export sqlitegraph types for advanced usage
65pub use sqlitegraph::backend::{EdgeSpec, NodeSpec};
66pub use sqlitegraph::config::{open_graph, BackendKind as SqliteGraphBackendKind, GraphConfig};
67pub use sqlitegraph::graph::{GraphEntity, SqliteGraph};
68
69// Re-export commonly used types
70pub use error::{ForgeError, Result};
71pub use storage::{BackendKind, UnifiedGraphStore};
72pub use types::{Location, SymbolId};
73
74// Re-export runtime module types
75pub use cache::QueryCache;
76pub use indexing::{FlushStats, IncrementalIndexer, PathFilter};
77pub use pool::{ConnectionPermit, ConnectionPool};
78pub use runtime::Runtime;
79pub use watcher::{WatchEvent, Watcher};
80
81use anyhow::anyhow;
82
83/// Main entry point for ForgeKit SDK.
84///
85/// The `Forge` type provides access to all code intelligence modules.
86
87#[derive(Clone, Debug)]
88pub struct Forge {
89    store: std::sync::Arc<UnifiedGraphStore>,
90    undo_capacity: usize,
91}
92
93impl Forge {
94    /// Opens a Forge instance on given codebase path.
95    ///
96    /// This will create or open a graph database at `.forge/graph.db`
97    /// within the codebase directory. Uses SQLite backend by default.
98    ///
99    /// # Arguments
100    ///
101    /// * `path` - Path to codebase directory
102    ///
103    /// # Returns
104    ///
105    /// A `Forge` instance or an error if database cannot be opened.
106    pub async fn open(path: impl AsRef<std::path::Path>) -> anyhow::Result<Self> {
107        Self::open_with_backend(path, BackendKind::default()).await
108    }
109
110    /// Opens a Forge instance with a specific backend.
111    ///
112    /// # Arguments
113    ///
114    /// * `path` - Path to codebase directory
115    /// * `backend` - Backend kind (SQLite or Native V3)
116    ///
117    /// # Returns
118    ///
119    /// A `Forge` instance or an error if database cannot be opened.
120    pub async fn open_with_backend(
121        path: impl AsRef<std::path::Path>,
122        backend: BackendKind,
123    ) -> anyhow::Result<Self> {
124        let store = std::sync::Arc::new(storage::UnifiedGraphStore::open(path, backend).await?);
125        let forge = Forge {
126            store,
127            undo_capacity: 100,
128        };
129
130        {
131            if forge.store.needs_indexing() {
132                tracing::info!("Graph empty — auto-indexing codebase with magellan");
133                if let Err(e) = forge.graph().index().await {
134                    tracing::warn!("Auto-indexing failed: {}", e);
135                }
136            }
137        }
138
139        Ok(forge)
140    }
141
142    /// Returns the backend kind currently in use.
143    pub fn backend_kind(&self) -> BackendKind {
144        self.store.backend_kind()
145    }
146
147    /// Returns the graph module for symbol and reference queries.
148    pub fn graph(&self) -> graph::GraphModule {
149        graph::GraphModule::new(Arc::clone(&self.store))
150    }
151
152    /// Returns the search module for semantic code queries.
153    pub fn search(&self) -> search::SearchModule {
154        search::SearchModule::new(Arc::clone(&self.store))
155    }
156
157    /// Returns the CFG module for control flow analysis.
158    pub fn cfg(&self) -> cfg::CfgModule {
159        cfg::CfgModule::new(Arc::clone(&self.store))
160    }
161
162    /// Returns the edit module for span-safe refactoring.
163    pub fn edit(&self) -> edit::EditModule {
164        edit::EditModule::new(Arc::clone(&self.store)).with_undo_capacity(self.undo_capacity)
165    }
166
167    /// Returns the analysis module for combined operations.
168    pub fn analysis(&self) -> analysis::AnalysisModule {
169        analysis::AnalysisModule::new(self.graph(), self.cfg(), self.edit(), self.search())
170    }
171
172    /// Returns the build module for build system operations.
173    ///
174    /// Detects the build system from the codebase root. Returns `None` if
175    /// no known build system is detected.
176    pub fn build(&self) -> Option<build::BuildModule> {
177        build::BuildModule::detect(&self.store.codebase_path)
178    }
179
180    /// Returns the workspace view for monorepo awareness.
181    pub fn as_workspace(&self) -> anyhow::Result<workspace::Workspace> {
182        workspace::Workspace::open(&self.store.codebase_path).map_err(|e| anyhow!("{e}"))
183    }
184
185    /// Returns the project module for scaffolding and detection.
186    pub fn project(&self) -> project::ProjectModule {
187        project::ProjectModule::new(Arc::clone(&self.store))
188    }
189
190    /// Returns the dependency module for manifest operations.
191    pub fn dependency(&self) -> dependency::DependencyModule {
192        dependency::DependencyModule::new(self.store.codebase_path.clone())
193    }
194
195    /// Returns the codebase path.
196    pub fn codebase_path(&self) -> &std::path::Path {
197        &self.store.codebase_path
198    }
199
200    /// Returns the resolved database path.
201    ///
202    /// This is where the SQLite or Native V3 database file lives, as determined
203    /// by the magellan registry or the `~/.magellan/<stem>/<stem>.db` fallback.
204    pub fn db_path(&self) -> &std::path::Path {
205        &self.store.db_path
206    }
207
208    /// Returns the knowledge graph module.
209    ///
210    /// Opens or creates the `.magellan/knowledge.graph` file using
211    /// sqlitegraph native-v3 backend.
212    #[cfg(feature = "native-v3")]
213    pub fn knowledge(&self) -> anyhow::Result<knowledge::KnowledgeGraph> {
214        let graph_path = self
215            .store
216            .codebase_path
217            .join(".magellan")
218            .join("knowledge.graph");
219        let db_path = self.store.db_path.clone();
220
221        if let Some(parent) = graph_path.parent() {
222            std::fs::create_dir_all(parent)?;
223        }
224
225        knowledge::KnowledgeGraph::open(&graph_path, &db_path)
226            .map_err(|e| anyhow!("Failed to open knowledge graph: {}", e))
227    }
228}
229
230/// Builder for configuring and creating a Forge instance.
231#[derive(Clone, Default)]
232pub struct ForgeBuilder {
233    path: Option<std::path::PathBuf>,
234    backend_kind: Option<BackendKind>,
235    db_path: Option<std::path::PathBuf>,
236    db_dir: Option<std::path::PathBuf>,
237    undo_capacity: Option<usize>,
238}
239
240impl ForgeBuilder {
241    /// Creates a new builder with default configuration.
242    pub fn new() -> Self {
243        Self::default()
244    }
245
246    /// Sets the path to the codebase.
247    pub fn path(self, path: impl AsRef<std::path::Path>) -> Self {
248        Self {
249            path: Some(path.as_ref().to_path_buf()),
250            ..self
251        }
252    }
253
254    /// Sets the backend kind (SQLite or Native V3).
255    pub fn backend_kind(self, kind: BackendKind) -> Self {
256        Self {
257            backend_kind: Some(kind),
258            ..self
259        }
260    }
261
262    /// Sets an explicit database path, overriding the default ~/.magellan/<stem>.db.
263    pub fn db_path(self, path: std::path::PathBuf) -> Self {
264        Self {
265            db_path: Some(path),
266            ..self
267        }
268    }
269
270    /// Sets the database directory; stem is still derived from the project root.
271    pub fn db_dir(self, dir: std::path::PathBuf) -> Self {
272        Self {
273            db_dir: Some(dir),
274            ..self
275        }
276    }
277
278    /// Sets the undo stack capacity (default: 100).
279    pub fn undo_capacity(self, capacity: usize) -> Self {
280        Self {
281            undo_capacity: Some(capacity),
282            ..self
283        }
284    }
285
286    /// Builds a `Forge` instance with configured options.
287    pub async fn build(self) -> anyhow::Result<Forge> {
288        let path = self.path.ok_or_else(|| anyhow!("path is required"))?;
289        let backend = self.backend_kind.unwrap_or_default();
290
291        let resolved_db = if let Some(explicit) = self.db_path {
292            explicit
293        } else if let Some(dir) = self.db_dir {
294            let stem = path.file_name().and_then(|n| n.to_str()).unwrap_or("graph");
295            dir.join(format!("{}.db", stem))
296        } else {
297            storage::default_db_path(&path)
298        };
299
300        let store = std::sync::Arc::new(
301            storage::UnifiedGraphStore::open_with_path(&path, &resolved_db, backend).await?,
302        );
303
304        Ok(Forge {
305            store,
306            undo_capacity: self.undo_capacity.unwrap_or(100),
307        })
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    // Forge Creation Tests
316
317    #[tokio::test]
318    async fn test_forge_open_creates_database() {
319        let temp_dir = tempfile::tempdir().unwrap();
320        let db_path = temp_dir.path().join("test-graph.db");
321
322        // Verify database doesn't exist initially
323        assert!(!db_path.exists());
324
325        // Open Forge with explicit db_path — never writes to ~/.magellan/ in tests
326        let forge = ForgeBuilder::new()
327            .path(temp_dir.path())
328            .db_path(db_path.clone())
329            .build()
330            .await
331            .unwrap();
332
333        // Verify database was created
334        assert!(db_path.exists());
335
336        // Verify Forge instance is valid
337        let _graph = forge.graph();
338        let _search = forge.search();
339
340        drop(forge);
341    }
342
343    // Module Accessor Tests
344
345    #[tokio::test]
346    async fn test_forge_graph_accessor() {
347        let temp_dir = tempfile::tempdir().unwrap();
348        let store = std::sync::Arc::new(
349            storage::UnifiedGraphStore::open_with_path(
350                temp_dir.path(),
351                temp_dir.path().join("test-graph.db"),
352                BackendKind::default(),
353            )
354            .await
355            .unwrap(),
356        );
357
358        let forge = Forge {
359            store,
360            undo_capacity: 100,
361        };
362
363        // Graph accessor should return GraphModule
364        let graph = forge.graph();
365        drop(graph);
366    }
367
368    #[tokio::test]
369    async fn test_forge_search_accessor() {
370        let temp_dir = tempfile::tempdir().unwrap();
371        let store = std::sync::Arc::new(
372            storage::UnifiedGraphStore::open_with_path(
373                temp_dir.path(),
374                temp_dir.path().join("test-graph.db"),
375                BackendKind::default(),
376            )
377            .await
378            .unwrap(),
379        );
380
381        let forge = Forge {
382            store,
383            undo_capacity: 100,
384        };
385
386        // Search accessor should return SearchModule
387        let search = forge.search();
388        drop(search);
389    }
390
391    #[tokio::test]
392    async fn test_forge_cfg_accessor() {
393        let temp_dir = tempfile::tempdir().unwrap();
394        let store = std::sync::Arc::new(
395            storage::UnifiedGraphStore::open_with_path(
396                temp_dir.path(),
397                temp_dir.path().join("test-graph.db"),
398                BackendKind::default(),
399            )
400            .await
401            .unwrap(),
402        );
403
404        let forge = Forge {
405            store,
406            undo_capacity: 100,
407        };
408
409        // CFG accessor should return CfgModule
410        let cfg = forge.cfg();
411        drop(cfg);
412    }
413
414    #[tokio::test]
415    async fn test_forge_edit_accessor() {
416        let temp_dir = tempfile::tempdir().unwrap();
417        let store = std::sync::Arc::new(
418            storage::UnifiedGraphStore::open_with_path(
419                temp_dir.path(),
420                temp_dir.path().join("test-graph.db"),
421                BackendKind::default(),
422            )
423            .await
424            .unwrap(),
425        );
426
427        let forge = Forge {
428            store,
429            undo_capacity: 100,
430        };
431
432        // Edit accessor should return EditModule
433        let edit = forge.edit();
434        drop(edit);
435    }
436
437    #[tokio::test]
438    async fn test_forge_analysis_accessor() {
439        let temp_dir = tempfile::tempdir().unwrap();
440        let store = std::sync::Arc::new(
441            storage::UnifiedGraphStore::open_with_path(
442                temp_dir.path(),
443                temp_dir.path().join("test-graph.db"),
444                BackendKind::default(),
445            )
446            .await
447            .unwrap(),
448        );
449
450        let forge = Forge {
451            store,
452            undo_capacity: 100,
453        };
454
455        // Analysis accessor should return AnalysisModule
456        let analysis = forge.analysis();
457        drop(analysis);
458    }
459
460    // ForgeBuilder Tests
461
462    #[test]
463    fn test_forge_builder_default() {
464        let builder = ForgeBuilder::new();
465
466        // Default builder should have None for all fields
467        assert!(builder.path.is_none());
468        assert!(builder.backend_kind.is_none());
469    }
470
471    #[test]
472    fn test_forge_builder_path() {
473        let temp_dir = tempfile::tempdir().unwrap();
474        let path = temp_dir.path().join("test");
475        let builder = ForgeBuilder::new().path(&path);
476
477        assert_eq!(builder.path, Some(path));
478    }
479
480    #[test]
481    fn test_forge_builder_backend_kind() {
482        let builder = ForgeBuilder::new().backend_kind(BackendKind::NativeV3);
483
484        assert_eq!(builder.backend_kind, Some(BackendKind::NativeV3));
485    }
486
487    #[tokio::test]
488    async fn test_forge_builder_build_success() {
489        let temp_dir = tempfile::tempdir().unwrap();
490        let builder = ForgeBuilder::new()
491            .path(temp_dir.path())
492            .db_path(temp_dir.path().join("test.db"))
493            .backend_kind(BackendKind::SQLite);
494
495        let forge = builder.build().await.unwrap();
496
497        assert!(forge.store.is_connected());
498    }
499
500    #[tokio::test]
501    async fn test_forge_builder_missing_path() {
502        let builder = ForgeBuilder::new();
503
504        let result = builder.build().await;
505
506        assert!(result.is_err());
507        assert!(result.unwrap_err().to_string().contains("path"));
508    }
509
510    #[cfg(feature = "native-v3")]
511    #[tokio::test]
512    async fn test_forge_knowledge_accessor() {
513        let temp_dir = tempfile::tempdir().unwrap();
514        let forge = Forge::open(temp_dir.path()).await.unwrap();
515
516        let kg = forge.knowledge();
517        assert!(kg.is_ok());
518
519        let kg = kg.unwrap();
520        assert!(kg.graph_path().exists());
521    }
522
523    #[tokio::test]
524    async fn test_forge_builder_db_path_override() {
525        let temp_dir = tempfile::tempdir().unwrap();
526        let custom_db = temp_dir.path().join("custom.db");
527
528        let forge = ForgeBuilder::new()
529            .path(temp_dir.path())
530            .db_path(custom_db.clone())
531            .build()
532            .await
533            .unwrap();
534
535        assert_eq!(forge.store.db_path, custom_db);
536    }
537
538    #[tokio::test]
539    async fn test_forge_builder_db_dir_override() {
540        let temp_dir = tempfile::tempdir().unwrap();
541        let db_dir = temp_dir.path().join("custom_dir");
542        std::fs::create_dir_all(&db_dir).unwrap();
543
544        let project_dir = temp_dir.path().join("my-project");
545        std::fs::create_dir_all(&project_dir).unwrap();
546
547        let forge = ForgeBuilder::new()
548            .path(&project_dir)
549            .db_dir(db_dir.clone())
550            .build()
551            .await
552            .unwrap();
553
554        assert_eq!(forge.store.db_path, db_dir.join("my-project.db"));
555    }
556
557    #[tokio::test]
558    async fn test_connection_pool_exported() {
559        use tempfile::TempDir;
560        let dir = TempDir::new().unwrap();
561        let db_path = dir.path().join("test.db");
562        let pool = crate::ConnectionPool::new(&db_path, 4);
563        assert_eq!(pool.available_connections(), 4);
564    }
565
566    #[tokio::test]
567    async fn test_runtime_exported() {
568        use tempfile::TempDir;
569        let dir = TempDir::new().unwrap();
570        let rt = crate::Runtime::new(dir.path().to_path_buf()).await.unwrap();
571        assert!(!rt.is_watching());
572    }
573}