forgekit-core 0.5.0

Deterministic code intelligence SDK - Core library
Documentation
use std::path::{Path, PathBuf};

use sqlitegraph::config::{open_graph, GraphConfig};

use crate::error::{ForgeError, Result};
use crate::types::ReferenceKind;

use super::{default_db_path, BackendKind};

#[derive(Clone, Debug)]
pub(super) struct StoredReference {
    pub(super) to_symbol: String,
    pub(super) kind: ReferenceKind,
    pub(super) file_path: PathBuf,
    pub(super) line_number: usize,
}

pub struct UnifiedGraphStore {
    pub codebase_path: PathBuf,
    pub db_path: PathBuf,
    pub backend_kind: BackendKind,
    pub(super) references: std::sync::Mutex<Vec<StoredReference>>,
}

impl Clone for UnifiedGraphStore {
    fn clone(&self) -> Self {
        Self {
            codebase_path: self.codebase_path.clone(),
            db_path: self.db_path.clone(),
            backend_kind: self.backend_kind,
            references: std::sync::Mutex::new(
                self.references
                    .lock()
                    .expect("invariant: references mutex not poisoned")
                    .clone(),
            ),
        }
    }
}

impl std::fmt::Debug for UnifiedGraphStore {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("UnifiedGraphStore")
            .field("codebase_path", &self.codebase_path)
            .field("db_path", &self.db_path)
            .field("backend_kind", &self.backend_kind)
            .field("connected", &self.is_connected())
            .finish()
    }
}

impl UnifiedGraphStore {
    pub async fn open(codebase_path: impl AsRef<Path>, backend_kind: BackendKind) -> Result<Self> {
        let codebase = codebase_path.as_ref();
        if !codebase.exists() {
            return Err(ForgeError::DatabaseError(format!(
                "Codebase path does not exist: {}",
                codebase.display()
            )));
        }
        let db_path = default_db_path(codebase);

        if let Some(parent) = db_path.parent() {
            tokio::fs::create_dir_all(parent).await.map_err(|e| {
                ForgeError::DatabaseError(format!("Failed to create database directory: {}", e))
            })?;
        }

        let sqlitegraph_path = match backend_kind {
            BackendKind::SQLite => db_path.clone(),
            BackendKind::NativeV3 => {
                let stem = codebase
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("graph");
                let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
                std::path::PathBuf::from(home)
                    .join(".magellan")
                    .join(format!("{}.v3", stem))
            }
        };
        let config = match backend_kind {
            BackendKind::SQLite => GraphConfig::sqlite(),
            BackendKind::NativeV3 => GraphConfig::native(),
        };

        let _graph = open_graph(&sqlitegraph_path, &config)
            .map_err(|e| ForgeError::DatabaseError(format!("Failed to open database: {}", e)))?;

        if matches!(backend_kind, BackendKind::NativeV3) {
            let _ = open_graph(&db_path, &GraphConfig::sqlite()).map_err(|e| {
                ForgeError::DatabaseError(format!("Failed to init magellan SQLite DB: {}", e))
            })?;
        }

        Ok(UnifiedGraphStore {
            codebase_path: codebase.to_path_buf(),
            db_path,
            backend_kind,
            references: std::sync::Mutex::new(Vec::new()),
        })
    }

    pub async fn open_with_path(
        codebase_path: impl AsRef<Path>,
        db_path: impl AsRef<Path>,
        backend_kind: BackendKind,
    ) -> Result<Self> {
        let codebase = codebase_path.as_ref();
        let db = db_path.as_ref();

        if let Some(parent) = db.parent() {
            tokio::fs::create_dir_all(parent).await.map_err(|e| {
                ForgeError::DatabaseError(format!("Failed to create database directory: {}", e))
            })?;
        }

        let config = match backend_kind {
            BackendKind::SQLite => GraphConfig::sqlite(),
            BackendKind::NativeV3 => GraphConfig::native(),
        };

        let _graph = open_graph(db, &config)
            .map_err(|e| ForgeError::DatabaseError(format!("Failed to open database: {}", e)))?;

        Ok(UnifiedGraphStore {
            codebase_path: codebase.to_path_buf(),
            db_path: db.to_path_buf(),
            backend_kind,
            references: std::sync::Mutex::new(Vec::new()),
        })
    }

    #[cfg(test)]
    pub async fn memory() -> Result<Self> {
        use tempfile::tempdir;

        let temp = tempdir().map_err(|e| {
            ForgeError::DatabaseError(format!("Failed to create temp directory: {}", e))
        })?;

        Self::open(temp.path(), BackendKind::SQLite).await
    }

    #[inline]
    pub fn backend_kind(&self) -> BackendKind {
        self.backend_kind
    }

    #[inline]
    pub fn db_path(&self) -> &Path {
        &self.db_path
    }

    pub fn is_connected(&self) -> bool {
        self.db_path.exists()
    }

    pub fn needs_indexing(&self) -> bool {
        if !self.is_connected() {
            return true;
        }
        let config = match self.backend_kind {
            BackendKind::SQLite => GraphConfig::sqlite(),
            BackendKind::NativeV3 => GraphConfig::native(),
        };
        match open_graph(&self.db_path, &config) {
            Ok(backend) => match backend.entity_ids() {
                Ok(ids) => ids.is_empty(),
                Err(_) => true,
            },
            Err(_) => true,
        }
    }
}