cadi_core/rehydration/
engine.rs

1//! Rehydration Engine
2//!
3//! The main engine for creating virtual views from atoms.
4
5use 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
13/// The rehydration engine
14pub struct RehydrationEngine {
15    graph: GraphStore,
16}
17
18impl RehydrationEngine {
19    /// Create a new rehydration engine
20    pub fn new(graph: GraphStore) -> Self {
21        Self { graph }
22    }
23
24    /// Create a virtual view from requested atom IDs
25    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        // Expand dependencies if configured
33        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        // Collect atom data
40        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        // Detect primary language
54        let language = atoms_with_content
55            .first()
56            .map(|(n, _)| n.language.clone())
57            .unwrap_or_else(|| "unknown".to_string());
58
59        // Assemble the view
60        let assembler = Assembler::new(config.clone());
61        let result = assembler.assemble(atoms_with_content, &language);
62
63        // Build explanation
64        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    /// Create a view with explicit control over ghost imports
89    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    /// Expand dependencies recursively
103    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        // Add requested atoms
112        for id in atom_ids {
113            all_atoms.insert(id.clone());
114        }
115
116        // BFS expansion
117        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                // Get dependencies
124                let deps = self.graph.get_dependencies(atom_id)?;
125                
126                for (edge_type, dep_id) in deps {
127                    // Only follow strong dependencies for ghost imports
128                    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    /// Get token estimate for a set of atoms
151    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    /// Find atoms that define a specific symbol
160    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    /// Create a view for a specific symbol with context
168    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    /// Create a minimal view with just type signatures
185    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        // Add test nodes
207        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        // A depends on B
231        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        // B should be included as a ghost import
252        assert!(view.atoms.contains(&"chunk:b".to_string()));
253        assert!(view.ghost_atoms.contains(&"chunk:b".to_string()));
254    }
255}