1pub 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
45pub mod analysis;
47pub mod cfg;
48pub mod edit;
49pub mod graph;
50pub mod search;
51pub mod storage;
52pub mod treesitter;
53
54pub mod knowledge;
56
57pub mod cache;
59pub mod indexing;
60pub mod pool;
61pub mod runtime;
62pub mod watcher;
63
64pub use sqlitegraph::backend::{EdgeSpec, NodeSpec};
66pub use sqlitegraph::config::{open_graph, BackendKind as SqliteGraphBackendKind, GraphConfig};
67pub use sqlitegraph::graph::{GraphEntity, SqliteGraph};
68
69pub use error::{ForgeError, Result};
71pub use storage::{BackendKind, UnifiedGraphStore};
72pub use types::{Location, SymbolId};
73
74pub 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#[derive(Clone, Debug)]
88pub struct Forge {
89 store: std::sync::Arc<UnifiedGraphStore>,
90 undo_capacity: usize,
91}
92
93impl Forge {
94 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 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 pub fn backend_kind(&self) -> BackendKind {
144 self.store.backend_kind()
145 }
146
147 pub fn graph(&self) -> graph::GraphModule {
149 graph::GraphModule::new(Arc::clone(&self.store))
150 }
151
152 pub fn search(&self) -> search::SearchModule {
154 search::SearchModule::new(Arc::clone(&self.store))
155 }
156
157 pub fn cfg(&self) -> cfg::CfgModule {
159 cfg::CfgModule::new(Arc::clone(&self.store))
160 }
161
162 pub fn edit(&self) -> edit::EditModule {
164 edit::EditModule::new(Arc::clone(&self.store)).with_undo_capacity(self.undo_capacity)
165 }
166
167 pub fn analysis(&self) -> analysis::AnalysisModule {
169 analysis::AnalysisModule::new(self.graph(), self.cfg(), self.edit(), self.search())
170 }
171
172 pub fn build(&self) -> Option<build::BuildModule> {
177 build::BuildModule::detect(&self.store.codebase_path)
178 }
179
180 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 pub fn project(&self) -> project::ProjectModule {
187 project::ProjectModule::new(Arc::clone(&self.store))
188 }
189
190 pub fn dependency(&self) -> dependency::DependencyModule {
192 dependency::DependencyModule::new(self.store.codebase_path.clone())
193 }
194
195 pub fn codebase_path(&self) -> &std::path::Path {
197 &self.store.codebase_path
198 }
199
200 pub fn db_path(&self) -> &std::path::Path {
205 &self.store.db_path
206 }
207
208 #[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#[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 pub fn new() -> Self {
243 Self::default()
244 }
245
246 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 pub fn backend_kind(self, kind: BackendKind) -> Self {
256 Self {
257 backend_kind: Some(kind),
258 ..self
259 }
260 }
261
262 pub fn db_path(self, path: std::path::PathBuf) -> Self {
264 Self {
265 db_path: Some(path),
266 ..self
267 }
268 }
269
270 pub fn db_dir(self, dir: std::path::PathBuf) -> Self {
272 Self {
273 db_dir: Some(dir),
274 ..self
275 }
276 }
277
278 pub fn undo_capacity(self, capacity: usize) -> Self {
280 Self {
281 undo_capacity: Some(capacity),
282 ..self
283 }
284 }
285
286 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 #[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 assert!(!db_path.exists());
324
325 let forge = ForgeBuilder::new()
327 .path(temp_dir.path())
328 .db_path(db_path.clone())
329 .build()
330 .await
331 .unwrap();
332
333 assert!(db_path.exists());
335
336 let _graph = forge.graph();
338 let _search = forge.search();
339
340 drop(forge);
341 }
342
343 #[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 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 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 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 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 let analysis = forge.analysis();
457 drop(analysis);
458 }
459
460 #[test]
463 fn test_forge_builder_default() {
464 let builder = ForgeBuilder::new();
465
466 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}