codesynapse_core/
cache.rs1use crate::types::ExtractionFragment;
2use serde_json::Value;
3use sha2::{Digest, Sha256};
4use std::path::{Path, PathBuf};
5
6pub struct FileCache {
7 cache_dir: PathBuf,
8}
9
10impl FileCache {
11 pub fn new(cache_dir: PathBuf) -> Self {
12 Self { cache_dir }
13 }
14
15 pub fn from_output_dir(output_dir: &Path) -> Self {
16 Self {
17 cache_dir: output_dir.join("cache"),
18 }
19 }
20
21 pub fn cache_path(&self, hash: &str) -> PathBuf {
22 self.cache_dir.join(format!("{}.json", hash))
23 }
24
25 pub fn compute_hash(content: &[u8]) -> String {
26 let mut hasher = Sha256::new();
27 hasher.update(content);
28 format!("{:x}", hasher.finalize())
29 }
30
31 pub fn get_cached(&self, hash: &str) -> Option<ExtractionFragment> {
32 let path = self.cache_path(hash);
33 if !path.exists() {
34 return None;
35 }
36 match std::fs::read_to_string(&path) {
37 Ok(json) => serde_json::from_str(&json).ok(),
38 Err(_) => None,
39 }
40 }
41
42 pub fn set_cached(
43 &self,
44 hash: &str,
45 fragment: &ExtractionFragment,
46 ) -> Result<(), Box<dyn std::error::Error>> {
47 std::fs::create_dir_all(&self.cache_dir)?;
48 let path = self.cache_path(hash);
49 let json = serde_json::to_string(fragment)?;
50 std::fs::write(&path, json)?;
51 Ok(())
52 }
53
54 pub fn is_cached(&self, hash: &str) -> bool {
55 self.cache_path(hash).exists()
56 }
57
58 pub fn clear(&self) -> Result<(), std::io::Error> {
59 if self.cache_dir.exists() {
60 std::fs::remove_dir_all(&self.cache_dir)?;
61 }
62 Ok(())
63 }
64}
65
66pub fn check_semantic_cache(
70 files: &[&str],
71 root: &Path,
72) -> (Vec<Value>, Vec<Value>, Vec<Value>, Vec<String>) {
73 let cache_dir = root.join("codesynapse-out").join("cache").join("semantic");
74 let mut cached_nodes: Vec<Value> = Vec::new();
75 let mut cached_edges: Vec<Value> = Vec::new();
76 let mut cached_hyperedges: Vec<Value> = Vec::new();
77 let mut uncached: Vec<String> = Vec::new();
78
79 for &fpath in files {
80 let p = if std::path::Path::new(fpath).is_absolute() {
81 std::path::PathBuf::from(fpath)
82 } else {
83 root.join(fpath)
84 };
85 let content = match std::fs::read(&p) {
86 Ok(c) => c,
87 Err(_) => {
88 uncached.push(fpath.to_string());
89 continue;
90 }
91 };
92 let hash = FileCache::compute_hash(&content);
93 let entry = cache_dir.join(format!("{}.json", hash));
94 let hit = entry
95 .exists()
96 .then(|| std::fs::read_to_string(&entry).ok())
97 .flatten()
98 .and_then(|t| serde_json::from_str::<Value>(&t).ok());
99 match hit {
100 Some(result) => {
101 if let Some(arr) = result.get("nodes").and_then(|v| v.as_array()) {
102 cached_nodes.extend(arr.iter().cloned());
103 }
104 if let Some(arr) = result.get("edges").and_then(|v| v.as_array()) {
105 cached_edges.extend(arr.iter().cloned());
106 }
107 if let Some(arr) = result.get("hyperedges").and_then(|v| v.as_array()) {
108 cached_hyperedges.extend(arr.iter().cloned());
109 }
110 }
111 None => uncached.push(fpath.to_string()),
112 }
113 }
114
115 (cached_nodes, cached_edges, cached_hyperedges, uncached)
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::types::{Edge, Node};
122 use std::collections::HashMap;
123
124 fn test_fragment() -> ExtractionFragment {
125 ExtractionFragment {
126 nodes: vec![Node {
127 id: "test_node".into(),
128 label: "TestNode".into(),
129 file_type: "code".into(),
130 source_file: "test.py".into(),
131 source_location: Some("1:1".into()),
132 community: None,
133 rationale: None,
134 docstring: None,
135 metadata: HashMap::new(),
136 }],
137 edges: vec![Edge {
138 source: "a".into(),
139 target: "b".into(),
140 relation: "calls".into(),
141 confidence: "EXTRACTED".into(),
142 source_file: Some("test.py".into()),
143 weight: 1.0,
144 context: None,
145 }],
146 }
147 }
148
149 #[test]
150 fn test_compute_hash_deterministic() {
151 let h1 = FileCache::compute_hash(b"hello world");
152 let h2 = FileCache::compute_hash(b"hello world");
153 assert_eq!(h1, h2);
154 assert_eq!(h1.len(), 64);
155 }
156
157 #[test]
158 fn test_compute_hash_different_inputs() {
159 let h1 = FileCache::compute_hash(b"hello");
160 let h2 = FileCache::compute_hash(b"world");
161 assert_ne!(h1, h2);
162 }
163
164 #[test]
165 fn test_cache_roundtrip() {
166 let dir = std::env::temp_dir().join("codesynapse-test-cache-roundtrip");
167 let _ = std::fs::remove_dir_all(&dir);
168 let cache = FileCache::from_output_dir(&dir);
169 let fragment = test_fragment();
170 let hash = FileCache::compute_hash(b"test content");
171
172 cache.set_cached(&hash, &fragment).unwrap();
173 assert!(cache.is_cached(&hash));
174
175 let loaded = cache.get_cached(&hash).unwrap();
176 assert_eq!(loaded.nodes.len(), 1);
177 assert_eq!(loaded.nodes[0].id, "test_node");
178 assert_eq!(loaded.edges.len(), 1);
179 assert_eq!(loaded.edges[0].relation, "calls");
180
181 let _ = std::fs::remove_dir_all(&dir);
182 }
183
184 #[test]
185 fn test_cache_miss() {
186 let dir = std::env::temp_dir().join("codesynapse-test-cache-miss");
187 let _ = std::fs::remove_dir_all(&dir);
188 let cache = FileCache::from_output_dir(&dir);
189 assert!(!cache.is_cached("nonexistent_hash"));
190 assert!(cache.get_cached("nonexistent_hash").is_none());
191 let _ = std::fs::remove_dir_all(&dir);
192 }
193
194 #[test]
195 fn test_cache_clear() {
196 let dir = std::env::temp_dir().join("codesynapse-test-cache-clear");
197 let _ = std::fs::remove_dir_all(&dir);
198 let cache = FileCache::from_output_dir(&dir);
199 let fragment = test_fragment();
200 let hash = FileCache::compute_hash(b"clear test");
201
202 cache.set_cached(&hash, &fragment).unwrap();
203 assert!(cache.is_cached(&hash));
204
205 cache.clear().unwrap();
206 assert!(!cache.is_cached(&hash));
207 let _ = std::fs::remove_dir_all(&dir);
208 }
209}