cadi_core/rehydration/
engine.rs1use std::collections::HashSet;
6
7use super::assembler::Assembler;
8use super::config::ViewConfig;
9use super::view::VirtualView;
10use crate::error::{CadiError, CadiResult};
11use crate::graph::GraphStore;
12
13pub struct RehydrationEngine {
15 graph: GraphStore,
16}
17
18impl RehydrationEngine {
19 pub fn new(graph: GraphStore) -> Self {
21 Self { graph }
22 }
23
24 pub async fn create_view(
26 &self,
27 atom_ids: Vec<String>,
28 config: ViewConfig,
29 ) -> CadiResult<VirtualView> {
30 let expansion_depth = config.expansion_depth;
31
32 let (all_atoms, ghost_atoms) = if expansion_depth > 0 {
34 self.expand_dependencies(&atom_ids, expansion_depth).await?
35 } else {
36 (atom_ids.clone(), Vec::new())
37 };
38
39 let mut atoms_with_content = Vec::new();
41 for atom_id in &all_atoms {
42 if let Some(node) = self.graph.get_node(atom_id)? {
43 if let Some(content) = self.graph.get_content_str(atom_id)? {
44 atoms_with_content.push((node, content));
45 }
46 }
47 }
48
49 if atoms_with_content.is_empty() {
50 return Ok(VirtualView::new("unknown"));
51 }
52
53 let language = atoms_with_content
55 .first()
56 .map(|(n, _)| n.language.clone())
57 .unwrap_or_else(|| "unknown".to_string());
58
59 let assembler = Assembler::new(config.clone());
61 let result = assembler.assemble(atoms_with_content, &language);
62
63 let mut explanation = format!(
65 "Created view with {} atoms",
66 all_atoms.len()
67 );
68 if !ghost_atoms.is_empty() {
69 explanation.push_str(&format!(
70 " ({} added via Ghost Import)",
71 ghost_atoms.len()
72 ));
73 }
74
75 Ok(VirtualView {
76 source: result.source,
77 atoms: all_atoms,
78 ghost_atoms,
79 token_estimate: result.total_tokens,
80 language,
81 symbol_locations: result.symbol_locations,
82 fragments: result.fragments,
83 truncated: result.truncated,
84 explanation,
85 })
86 }
87
88 pub async fn create_expanded_view(
90 &self,
91 atom_ids: Vec<String>,
92 expansion_depth: usize,
93 max_tokens: usize,
94 ) -> CadiResult<VirtualView> {
95 let config = ViewConfig::default()
96 .with_expansion(expansion_depth)
97 .with_max_tokens(max_tokens);
98
99 self.create_view(atom_ids, config).await
100 }
101
102 async fn expand_dependencies(
104 &self,
105 atom_ids: &[String],
106 depth: usize,
107 ) -> CadiResult<(Vec<String>, Vec<String>)> {
108 let mut all_atoms = HashSet::new();
109 let mut ghost_atoms = Vec::new();
110
111 for id in atom_ids {
113 all_atoms.insert(id.clone());
114 }
115
116 let mut frontier: Vec<String> = atom_ids.to_vec();
118
119 for _current_depth in 0..depth {
120 let mut next_frontier = Vec::new();
121
122 for atom_id in &frontier {
123 let deps = self.graph.get_dependencies(atom_id)?;
125
126 for (edge_type, dep_id) in deps {
127 if !edge_type.should_auto_expand() {
129 continue;
130 }
131
132 if !all_atoms.contains(&dep_id) {
133 all_atoms.insert(dep_id.clone());
134 ghost_atoms.push(dep_id.clone());
135 next_frontier.push(dep_id);
136 }
137 }
138 }
139
140 frontier = next_frontier;
141
142 if frontier.is_empty() {
143 break;
144 }
145 }
146
147 Ok((all_atoms.into_iter().collect(), ghost_atoms))
148 }
149
150 pub fn estimate_tokens(&self, atom_ids: &[String]) -> CadiResult<usize> {
152 let mut total = 0;
153 for id in atom_ids {
154 total += self.graph.get_token_estimate(id)?;
155 }
156 Ok(total)
157 }
158
159 pub fn find_defining_atoms(&self, symbol: &str) -> CadiResult<Vec<String>> {
161 match self.graph.find_symbol(symbol)? {
162 Some(chunk_id) => Ok(vec![chunk_id]),
163 None => Ok(Vec::new()),
164 }
165 }
166
167 pub async fn view_for_symbol(
169 &self,
170 symbol: &str,
171 config: ViewConfig,
172 ) -> CadiResult<VirtualView> {
173 let defining_atoms = self.find_defining_atoms(symbol)?;
174
175 if defining_atoms.is_empty() {
176 return Err(CadiError::ChunkNotFound(format!(
177 "No atom defines symbol: {}", symbol
178 )));
179 }
180
181 self.create_view(defining_atoms, config).await
182 }
183
184 pub async fn signatures_view(&self, atom_ids: Vec<String>) -> CadiResult<VirtualView> {
186 let config = ViewConfig {
187 format: super::config::ViewFormat::Signatures,
188 expansion_depth: 0,
189 include_docs: false,
190 ..Default::default()
191 };
192
193 self.create_view(atom_ids, config).await
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::graph::{GraphNode, EdgeType};
201
202 #[test]
203 fn test_create_view() {
204 let graph = GraphStore::in_memory().unwrap();
205
206 let node_a = GraphNode::new("chunk:a", "a")
208 .with_alias("test/a")
209 .with_language("rust")
210 .with_defines(vec!["func_a".to_string()]);
211
212 graph.insert_node(&node_a).unwrap();
213 graph.store_content("chunk:a", b"fn func_a() {}").unwrap();
214
215 let engine = RehydrationEngine::new(graph);
216 let rt = tokio::runtime::Runtime::new().unwrap();
217 let view = rt.block_on(engine.create_view(
218 vec!["chunk:a".to_string()],
219 ViewConfig::default(),
220 )).unwrap();
221
222 assert!(view.source.contains("func_a"));
223 assert_eq!(view.atoms.len(), 1);
224 }
225
226 #[test]
227 fn test_ghost_imports() {
228 let graph = GraphStore::in_memory().unwrap();
229
230 let node_a = GraphNode::new("chunk:a", "a")
232 .with_language("rust")
233 .with_defines(vec!["use_b".to_string()]);
234 let node_b = GraphNode::new("chunk:b", "b")
235 .with_language("rust")
236 .with_defines(vec!["helper".to_string()]);
237
238 graph.insert_node(&node_a).unwrap();
239 graph.insert_node(&node_b).unwrap();
240 graph.store_content("chunk:a", b"fn use_b() { helper(); }").unwrap();
241 graph.store_content("chunk:b", b"fn helper() {}").unwrap();
242 graph.add_dependency("chunk:a", "chunk:b", EdgeType::Imports).unwrap();
243
244 let engine = RehydrationEngine::new(graph);
245 let rt = tokio::runtime::Runtime::new().unwrap();
246 let view = rt.block_on(engine.create_view(
247 vec!["chunk:a".to_string()],
248 ViewConfig::default().with_expansion(1),
249 )).unwrap();
250
251 assert!(view.atoms.contains(&"chunk:b".to_string()));
253 assert!(view.ghost_atoms.contains(&"chunk:b".to_string()));
254 }
255}