Skip to main content

forgekit_core/knowledge/
mod.rs

1//! Knowledge graph — sqlitegraph native-v3 backed graph for code intelligence.
2
3pub mod nodes;
4pub mod sync;
5pub mod traversal;
6pub mod types;
7
8pub use nodes::SourceSpan;
9pub use types::*;
10
11use std::path::{Path, PathBuf};
12
13use crate::error::{ForgeError, Result};
14use sqlitegraph::backend::GraphBackend;
15use sqlitegraph::config::{open_graph, GraphConfig};
16
17pub struct KnowledgeGraph {
18    pub(crate) backend: Box<dyn GraphBackend>,
19    pub(crate) graph_path: PathBuf,
20    pub(crate) db_path: PathBuf,
21}
22
23impl KnowledgeGraph {
24    pub fn open(graph_path: &Path, db_path: &Path) -> Result<Self> {
25        if let Some(parent) = graph_path.parent() {
26            std::fs::create_dir_all(parent).map_err(|e| {
27                ForgeError::DatabaseError(format!("Failed to create graph directory: {}", e))
28            })?;
29        }
30
31        let config = GraphConfig::native();
32        let backend = open_graph(graph_path, &config).map_err(|e| {
33            ForgeError::DatabaseError(format!("Failed to open knowledge graph: {}", e))
34        })?;
35
36        Ok(Self {
37            backend,
38            graph_path: graph_path.to_path_buf(),
39            db_path: db_path.to_path_buf(),
40        })
41    }
42
43    pub fn graph_path(&self) -> &Path {
44        &self.graph_path
45    }
46
47    pub fn db_path(&self) -> &Path {
48        &self.db_path
49    }
50
51    pub(crate) fn snapshot() -> sqlitegraph::snapshot::SnapshotId {
52        sqlitegraph::snapshot::SnapshotId::current()
53    }
54
55    pub(crate) fn insert_node(
56        &self,
57        kind: &str,
58        name: &str,
59        file_path: Option<&str>,
60        data: serde_json::Value,
61    ) -> Result<i64> {
62        let spec = sqlitegraph::backend::NodeSpec {
63            kind: kind.to_string(),
64            name: name.to_string(),
65            file_path: file_path.map(|s| s.to_string()),
66            data,
67        };
68        self.backend
69            .insert_node(spec)
70            .map_err(|e| ForgeError::DatabaseError(format!("Insert node failed: {}", e)))
71    }
72}
73
74#[cfg(test)]
75pub(crate) fn open_kg() -> (tempfile::TempDir, KnowledgeGraph) {
76    let temp = tempfile::tempdir().expect("invariant: tempdir creation succeeds");
77    let kg = KnowledgeGraph::open(
78        &temp.path().join("kg.graph"),
79        &temp.path().join("magellan.db"),
80    )
81    .expect("invariant: fresh temp paths always open");
82    (temp, kg)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn test_knowledge_graph_open_creates_file() {
91        let temp = tempfile::tempdir().expect("invariant: tempdir creation succeeds");
92        let graph_path = temp.path().join("knowledge.graph");
93        let db_path = temp.path().join("magellan.db");
94
95        let kg = KnowledgeGraph::open(&graph_path, &db_path)
96            .expect("invariant: fresh temp paths always open");
97
98        assert!(graph_path.exists());
99        assert_eq!(kg.graph_path(), graph_path);
100        assert_eq!(kg.db_path(), db_path);
101    }
102
103    #[test]
104    fn test_knowledge_graph_open_creates_parent_dirs() {
105        let temp = tempfile::tempdir().expect("invariant: tempdir creation succeeds");
106        let graph_path = temp.path().join("nested").join("dir").join("kg.graph");
107        let db_path = temp.path().join("magellan.db");
108
109        let _kg = KnowledgeGraph::open(&graph_path, &db_path)
110            .expect("invariant: fresh temp paths always open");
111        assert!(graph_path.exists());
112    }
113
114    #[test]
115    fn test_knowledge_graph_open_existing_file() {
116        let temp = tempfile::tempdir().expect("invariant: tempdir creation succeeds");
117        let graph_path = temp.path().join("kg.graph");
118        let db_path = temp.path().join("magellan.db");
119
120        {
121            let _kg = KnowledgeGraph::open(&graph_path, &db_path)
122                .expect("invariant: fresh temp paths always open");
123        }
124
125        let _kg2 = KnowledgeGraph::open(&graph_path, &db_path)
126            .expect("invariant: fresh temp paths always open");
127        assert!(graph_path.exists());
128    }
129}