forgekit_core/knowledge/
mod.rs1pub 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}